267 lines
7.6 KiB
Python
267 lines
7.6 KiB
Python
![]() |
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||
|
#
|
||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
# you may not use this file except in compliance with the License.
|
||
|
# You may obtain a copy of the License at
|
||
|
#
|
||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||
|
#
|
||
|
# Unless required by applicable law or agreed to in writing, software
|
||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
# See the License for the specific language governing permissions and
|
||
|
# limitations under the License.
|
||
|
|
||
|
from __future__ import annotations
|
||
|
|
||
|
import contextlib
|
||
|
import errno
|
||
|
import io
|
||
|
import os
|
||
|
from pathlib import Path
|
||
|
from typing import IO, TYPE_CHECKING, Any, Final
|
||
|
|
||
|
from streamlit import env_util, errors
|
||
|
from streamlit.string_util import is_binary_string
|
||
|
|
||
|
if TYPE_CHECKING:
|
||
|
from collections.abc import Generator
|
||
|
|
||
|
# Configuration and credentials are stored inside the ~/.streamlit folder
|
||
|
CONFIG_FOLDER_NAME: Final = ".streamlit"
|
||
|
|
||
|
# If enableStaticServing is enabled, static file served from the ./static folder
|
||
|
APP_STATIC_FOLDER_NAME: Final = "static"
|
||
|
|
||
|
|
||
|
def get_encoded_file_data(
|
||
|
data: bytes, encoding: str = "auto"
|
||
|
) -> io.StringIO | io.BytesIO:
|
||
|
"""Coerce bytes to a BytesIO or a StringIO.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
data : bytes
|
||
|
encoding : str
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
BytesIO or StringIO
|
||
|
If the file's data is in a well-known textual format (or if the encoding
|
||
|
parameter is set), return a StringIO. Otherwise, return BytesIO.
|
||
|
|
||
|
"""
|
||
|
if encoding == "auto":
|
||
|
# If the file does not look like a pure binary file, assume
|
||
|
# it's utf-8. It would be great if we could guess it a little
|
||
|
# more smartly here, but it is what it is!
|
||
|
data_encoding = None if is_binary_string(data) else "utf-8"
|
||
|
else:
|
||
|
data_encoding = encoding
|
||
|
|
||
|
if data_encoding:
|
||
|
return io.StringIO(data.decode(data_encoding))
|
||
|
|
||
|
return io.BytesIO(data)
|
||
|
|
||
|
|
||
|
@contextlib.contextmanager
|
||
|
def streamlit_read(path: str, binary: bool = False) -> Generator[IO[Any], None, None]:
|
||
|
"""Opens a context to read this file relative to the streamlit path.
|
||
|
|
||
|
For example:
|
||
|
|
||
|
with streamlit_read('foo.txt') as foo:
|
||
|
...
|
||
|
|
||
|
opens the file `.streamlit/foo.txt`
|
||
|
|
||
|
path - the path to write to (within the streamlit directory)
|
||
|
binary - set to True for binary IO
|
||
|
"""
|
||
|
filename = get_streamlit_file_path(path)
|
||
|
if os.stat(filename).st_size == 0:
|
||
|
raise errors.Error(f'Read zero byte file: "{filename}"')
|
||
|
|
||
|
mode = "r"
|
||
|
if binary:
|
||
|
mode += "b"
|
||
|
with open(os.path.join(CONFIG_FOLDER_NAME, path), mode) as handle:
|
||
|
yield handle
|
||
|
|
||
|
|
||
|
@contextlib.contextmanager
|
||
|
def streamlit_write(path: str, binary: bool = False) -> Generator[IO[Any], None, None]:
|
||
|
"""Opens a file for writing within the streamlit path, and
|
||
|
ensuring that the path exists.
|
||
|
|
||
|
For example:
|
||
|
|
||
|
with streamlit_write('foo/bar.txt') as bar:
|
||
|
...
|
||
|
|
||
|
opens the file .streamlit/foo/bar.txt for writing,
|
||
|
creating any necessary directories along the way.
|
||
|
|
||
|
path - the path to write to (within the streamlit directory)
|
||
|
binary - set to True for binary IO
|
||
|
"""
|
||
|
mode = "w"
|
||
|
if binary:
|
||
|
mode += "b"
|
||
|
path = get_streamlit_file_path(path)
|
||
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||
|
try:
|
||
|
with open(path, mode) as handle:
|
||
|
yield handle
|
||
|
except OSError as e:
|
||
|
msg = [f"Unable to write file: {os.path.abspath(path)}"]
|
||
|
if e.errno == errno.EINVAL and env_util.IS_DARWIN:
|
||
|
msg.append(
|
||
|
"Python is limited to files below 2GB on OSX. "
|
||
|
"See https://bugs.python.org/issue24658"
|
||
|
)
|
||
|
raise errors.Error("\n".join(msg))
|
||
|
|
||
|
|
||
|
def get_static_dir() -> str:
|
||
|
"""Get the folder where static HTML/JS/CSS files live."""
|
||
|
dirname = os.path.dirname(os.path.normpath(__file__))
|
||
|
return os.path.normpath(os.path.join(dirname, "static"))
|
||
|
|
||
|
|
||
|
def get_app_static_dir(main_script_path: str) -> str:
|
||
|
"""Get the folder where app static files live."""
|
||
|
static_dir = Path(main_script_path).parent / APP_STATIC_FOLDER_NAME
|
||
|
return os.path.abspath(static_dir)
|
||
|
|
||
|
|
||
|
def get_streamlit_file_path(*filepath: str) -> str:
|
||
|
"""Return the full path to a file in ~/.streamlit.
|
||
|
|
||
|
This doesn't guarantee that the file (or its directory) exists.
|
||
|
"""
|
||
|
home = Path.home()
|
||
|
if home is None:
|
||
|
raise RuntimeError("No home directory.")
|
||
|
|
||
|
return str(home / CONFIG_FOLDER_NAME / Path(*filepath))
|
||
|
|
||
|
|
||
|
def get_project_streamlit_file_path(*filepath: str) -> str:
|
||
|
"""Return the full path to a filepath in ${CWD}/.streamlit.
|
||
|
|
||
|
This doesn't guarantee that the file (or its directory) exists.
|
||
|
"""
|
||
|
return str(Path.cwd() / CONFIG_FOLDER_NAME / Path(*filepath))
|
||
|
|
||
|
|
||
|
def get_main_script_streamlit_file_path(main_script_path: str, filename: str) -> str:
|
||
|
"""Return the full path to a file in the .streamlit folder relative to the
|
||
|
main script's path.
|
||
|
|
||
|
This doesn't guarantee that the file (or its directory) exists.
|
||
|
"""
|
||
|
|
||
|
return str(
|
||
|
Path(os.path.abspath(os.path.dirname(main_script_path)))
|
||
|
/ CONFIG_FOLDER_NAME
|
||
|
/ filename
|
||
|
)
|
||
|
|
||
|
|
||
|
def file_is_in_folder_glob(filepath: str, folderpath_glob: str) -> bool:
|
||
|
"""Test whether a file is in some folder with globbing support.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
filepath : str
|
||
|
A file path.
|
||
|
folderpath_glob: str
|
||
|
A path to a folder that may include globbing.
|
||
|
|
||
|
"""
|
||
|
# Make the glob always end with "/*" so we match files inside subfolders of
|
||
|
# folderpath_glob.
|
||
|
if not folderpath_glob.endswith("*"):
|
||
|
if folderpath_glob.endswith("/"):
|
||
|
folderpath_glob += "*"
|
||
|
else:
|
||
|
folderpath_glob += "/*"
|
||
|
|
||
|
import fnmatch
|
||
|
|
||
|
file_dir = os.path.dirname(filepath) + "/"
|
||
|
return fnmatch.fnmatch(file_dir, folderpath_glob)
|
||
|
|
||
|
|
||
|
def get_directory_size(directory: str) -> int:
|
||
|
"""Return the size of a directory in bytes."""
|
||
|
total_size = 0
|
||
|
for dirpath, _, filenames in os.walk(directory):
|
||
|
for f in filenames:
|
||
|
fp = os.path.join(dirpath, f)
|
||
|
total_size += os.path.getsize(fp)
|
||
|
return total_size
|
||
|
|
||
|
|
||
|
def file_in_pythonpath(filepath: str) -> bool:
|
||
|
"""Test whether a filepath is in the same folder of a path specified in the PYTHONPATH env variable.
|
||
|
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
filepath : str
|
||
|
An absolute file path.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
boolean
|
||
|
True if contained in PYTHONPATH, False otherwise. False if PYTHONPATH is not defined or empty.
|
||
|
|
||
|
"""
|
||
|
pythonpath = os.environ.get("PYTHONPATH", "")
|
||
|
if len(pythonpath) == 0:
|
||
|
return False
|
||
|
|
||
|
absolute_paths = [os.path.abspath(path) for path in pythonpath.split(os.pathsep)]
|
||
|
return any(
|
||
|
file_is_in_folder_glob(os.path.normpath(filepath), path)
|
||
|
for path in absolute_paths
|
||
|
)
|
||
|
|
||
|
|
||
|
def normalize_path_join(*args: str) -> str:
|
||
|
"""Return the normalized path of the joined path.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
*args : str
|
||
|
The path components to join.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
str
|
||
|
The normalized path of the joined path.
|
||
|
"""
|
||
|
return os.path.normpath(os.path.join(*args))
|
||
|
|
||
|
|
||
|
def get_main_script_directory(main_script: str) -> str:
|
||
|
"""Return the full path to the main script directory.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
main_script : str
|
||
|
The main script path. The path can be an absolute path or a relative
|
||
|
path.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
str
|
||
|
The full path to the main script directory.
|
||
|
"""
|
||
|
main_script_path = normalize_path_join(os.getcwd(), main_script)
|
||
|
|
||
|
return os.path.dirname(main_script_path)
|