fix: Use anyio for file operations in a few places (#5167)
Use anyio for file operations in a few places
This commit is contained in:
parent
6f5b984a22
commit
e545d12c40
9 changed files with 36 additions and 30 deletions
|
|
@ -11,6 +11,7 @@ from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import validators
|
import validators
|
||||||
|
from aiofile import async_open
|
||||||
|
|
||||||
from langflow.base.curl.parse import parse_context
|
from langflow.base.curl.parse import parse_context
|
||||||
from langflow.custom import Component
|
from langflow.custom import Component
|
||||||
|
|
@ -193,8 +194,8 @@ class APIRequestComponent(Component):
|
||||||
mode = "wb" if is_binary else "w"
|
mode = "wb" if is_binary else "w"
|
||||||
encoding = response.encoding if mode == "w" else None
|
encoding = response.encoding if mode == "w" else None
|
||||||
if file_path:
|
if file_path:
|
||||||
with file_path.open(mode, encoding=encoding) as f:
|
async with async_open(file_path, mode, encoding=encoding) as f:
|
||||||
f.write(response.content if is_binary else response.text)
|
await f.write(response.content if is_binary else response.text)
|
||||||
|
|
||||||
if include_httpx_metadata:
|
if include_httpx_metadata:
|
||||||
metadata.update(
|
metadata.update(
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import asyncio
|
||||||
import zlib
|
import zlib
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import anyio
|
||||||
from aiofile import async_open
|
from aiofile import async_open
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
|
@ -107,18 +108,18 @@ class DirectoryReader:
|
||||||
|
|
||||||
async def aread_file_content(self, file_path):
|
async def aread_file_content(self, file_path):
|
||||||
"""Read and return the content of a file."""
|
"""Read and return the content of a file."""
|
||||||
file_path_ = Path(file_path)
|
file_path_ = anyio.Path(file_path)
|
||||||
if not file_path_.is_file():
|
if not await file_path_.is_file():
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
async with async_open(file_path_, encoding="utf-8") as file:
|
async with async_open(str(file_path_), encoding="utf-8") as file:
|
||||||
# UnicodeDecodeError: 'charmap' codec can't decode byte 0x9d in position 3069:
|
# UnicodeDecodeError: 'charmap' codec can't decode byte 0x9d in position 3069:
|
||||||
# character maps to <undefined>
|
# character maps to <undefined>
|
||||||
return await file.read()
|
return await file.read()
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
# This is happening in Windows, so we need to open the file in binary mode
|
# This is happening in Windows, so we need to open the file in binary mode
|
||||||
# The file is always just a python file, so we can safely read it as utf-8
|
# The file is always just a python file, so we can safely read it as utf-8
|
||||||
async with async_open(file_path_, "rb") as f:
|
async with async_open(str(file_path_), "rb") as f:
|
||||||
return (await f.read()).decode("utf-8")
|
return (await f.read()).decode("utf-8")
|
||||||
|
|
||||||
def get_files(self):
|
def get_files(self):
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
import anyio
|
||||||
import orjson
|
import orjson
|
||||||
from aiofile import async_open
|
from aiofile import async_open
|
||||||
from emoji import demojize, purely_emoji
|
from emoji import demojize, purely_emoji
|
||||||
|
|
@ -544,13 +545,13 @@ async def load_flows_from_directory() -> None:
|
||||||
msg = "Superuser not found in the database"
|
msg = "Superuser not found in the database"
|
||||||
raise NoResultFound(msg)
|
raise NoResultFound(msg)
|
||||||
user_id = user.id
|
user_id = user.id
|
||||||
flows_path_ = Path(flows_path)
|
flows_path_ = anyio.Path(flows_path)
|
||||||
files = [f for f in flows_path_.iterdir() if f.is_file()]
|
files = [f async for f in flows_path_.iterdir() if await f.is_file()]
|
||||||
for file_path in files:
|
for file_path in files:
|
||||||
if file_path.suffix != ".json":
|
if file_path.suffix != ".json":
|
||||||
continue
|
continue
|
||||||
logger.info(f"Loading flow from file: {file_path.name}")
|
logger.info(f"Loading flow from file: {file_path.name}")
|
||||||
async with async_open(file_path, "r", encoding="utf-8") as f:
|
async with async_open(str(file_path), "r", encoding="utf-8") as f:
|
||||||
content = await f.read()
|
content = await f.read()
|
||||||
flow = orjson.loads(content)
|
flow = orjson.loads(content)
|
||||||
no_json_name = file_path.stem
|
no_json_name = file_path.stem
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from http import HTTPStatus
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
import anyio
|
||||||
from fastapi import FastAPI, HTTPException, Request, Response, status
|
from fastapi import FastAPI, HTTPException, Request, Response, status
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import FileResponse, JSONResponse
|
from fastapi.responses import FileResponse, JSONResponse
|
||||||
|
|
@ -260,9 +261,9 @@ def setup_static_files(app: FastAPI, static_files_dir: Path) -> None:
|
||||||
|
|
||||||
@app.exception_handler(404)
|
@app.exception_handler(404)
|
||||||
async def custom_404_handler(_request, _exc):
|
async def custom_404_handler(_request, _exc):
|
||||||
path = static_files_dir / "index.html"
|
path = anyio.Path(static_files_dir) / "index.html"
|
||||||
|
|
||||||
if not path.exists():
|
if not await path.exists():
|
||||||
msg = f"File at path {path} does not exist."
|
msg = f"File at path {path} does not exist."
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
return FileResponse(path)
|
return FileResponse(path)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
from pathlib import Path
|
import anyio
|
||||||
|
|
||||||
from aiofile import async_open
|
from aiofile import async_open
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
|
@ -12,7 +11,7 @@ class LocalStorageService(StorageService):
|
||||||
def __init__(self, session_service, settings_service) -> None:
|
def __init__(self, session_service, settings_service) -> None:
|
||||||
"""Initialize the local storage service with session and settings services."""
|
"""Initialize the local storage service with session and settings services."""
|
||||||
super().__init__(session_service, settings_service)
|
super().__init__(session_service, settings_service)
|
||||||
self.data_dir = Path(settings_service.settings.config_dir)
|
self.data_dir = anyio.Path(settings_service.settings.config_dir)
|
||||||
self.set_ready()
|
self.set_ready()
|
||||||
|
|
||||||
def build_full_path(self, flow_id: str, file_name: str) -> str:
|
def build_full_path(self, flow_id: str, file_name: str) -> str:
|
||||||
|
|
@ -30,11 +29,11 @@ class LocalStorageService(StorageService):
|
||||||
:raises PermissionError: If there is no permission to write the file.
|
:raises PermissionError: If there is no permission to write the file.
|
||||||
"""
|
"""
|
||||||
folder_path = self.data_dir / flow_id
|
folder_path = self.data_dir / flow_id
|
||||||
folder_path.mkdir(parents=True, exist_ok=True)
|
await folder_path.mkdir(parents=True, exist_ok=True)
|
||||||
file_path = folder_path / file_name
|
file_path = folder_path / file_name
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with async_open(file_path, "wb") as f:
|
async with async_open(str(file_path), "wb") as f:
|
||||||
await f.write(data)
|
await f.write(data)
|
||||||
logger.info(f"File {file_name} saved successfully in flow {flow_id}.")
|
logger.info(f"File {file_name} saved successfully in flow {flow_id}.")
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
@ -50,12 +49,12 @@ class LocalStorageService(StorageService):
|
||||||
:raises FileNotFoundError: If the file does not exist.
|
:raises FileNotFoundError: If the file does not exist.
|
||||||
"""
|
"""
|
||||||
file_path = self.data_dir / flow_id / file_name
|
file_path = self.data_dir / flow_id / file_name
|
||||||
if not file_path.exists():
|
if not await file_path.exists():
|
||||||
logger.warning(f"File {file_name} not found in flow {flow_id}.")
|
logger.warning(f"File {file_name} not found in flow {flow_id}.")
|
||||||
msg = f"File {file_name} not found in flow {flow_id}"
|
msg = f"File {file_name} not found in flow {flow_id}"
|
||||||
raise FileNotFoundError(msg)
|
raise FileNotFoundError(msg)
|
||||||
|
|
||||||
async with async_open(file_path, "rb") as f:
|
async with async_open(str(file_path), "rb") as f:
|
||||||
content = await f.read()
|
content = await f.read()
|
||||||
|
|
||||||
logger.debug(f"File {file_name} retrieved successfully from flow {flow_id}.")
|
logger.debug(f"File {file_name} retrieved successfully from flow {flow_id}.")
|
||||||
|
|
@ -69,12 +68,12 @@ class LocalStorageService(StorageService):
|
||||||
:raises FileNotFoundError: If the flow directory does not exist.
|
:raises FileNotFoundError: If the flow directory does not exist.
|
||||||
"""
|
"""
|
||||||
folder_path = self.data_dir / flow_id
|
folder_path = self.data_dir / flow_id
|
||||||
if not folder_path.exists() or not folder_path.is_dir():
|
if not await folder_path.exists() or not await folder_path.is_dir():
|
||||||
logger.warning(f"Flow {flow_id} directory does not exist.")
|
logger.warning(f"Flow {flow_id} directory does not exist.")
|
||||||
msg = f"Flow {flow_id} directory does not exist."
|
msg = f"Flow {flow_id} directory does not exist."
|
||||||
raise FileNotFoundError(msg)
|
raise FileNotFoundError(msg)
|
||||||
|
|
||||||
files = [file.name for file in folder_path.iterdir() if file.is_file()]
|
files = [file.name async for file in folder_path.iterdir() if await file.is_file()]
|
||||||
logger.info(f"Listed {len(files)} files in flow {flow_id}.")
|
logger.info(f"Listed {len(files)} files in flow {flow_id}.")
|
||||||
return files
|
return files
|
||||||
|
|
||||||
|
|
@ -85,8 +84,8 @@ class LocalStorageService(StorageService):
|
||||||
:param file_name: The name of the file to be deleted.
|
:param file_name: The name of the file to be deleted.
|
||||||
"""
|
"""
|
||||||
file_path = self.data_dir / flow_id / file_name
|
file_path = self.data_dir / flow_id / file_name
|
||||||
if file_path.exists():
|
if await file_path.exists():
|
||||||
file_path.unlink()
|
await file_path.unlink()
|
||||||
logger.info(f"File {file_name} deleted successfully from flow {flow_id}.")
|
logger.info(f"File {file_name} deleted successfully from flow {flow_id}.")
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Attempted to delete non-existent file {file_name} in flow {flow_id}.")
|
logger.warning(f"Attempted to delete non-existent file {file_name} in flow {flow_id}.")
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
import anyio
|
||||||
import orjson
|
import orjson
|
||||||
import pytest
|
import pytest
|
||||||
from asgi_lifespan import LifespanManager
|
from asgi_lifespan import LifespanManager
|
||||||
|
|
@ -365,7 +366,7 @@ async def client_fixture(
|
||||||
monkeypatch.undo()
|
monkeypatch.undo()
|
||||||
# clear the temp db
|
# clear the temp db
|
||||||
with suppress(FileNotFoundError):
|
with suppress(FileNotFoundError):
|
||||||
db_path.unlink()
|
await anyio.Path(db_path).unlink()
|
||||||
|
|
||||||
|
|
||||||
# create a fixture for session_getter above
|
# create a fixture for session_getter above
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import anyio
|
||||||
import pytest
|
import pytest
|
||||||
from asgi_lifespan import LifespanManager
|
from asgi_lifespan import LifespanManager
|
||||||
from httpx import ASGITransport, AsyncClient
|
from httpx import ASGITransport, AsyncClient
|
||||||
|
|
@ -76,7 +77,7 @@ async def files_client_fixture(
|
||||||
monkeypatch.undo()
|
monkeypatch.undo()
|
||||||
# clear the temp db
|
# clear the temp db
|
||||||
with suppress(FileNotFoundError):
|
with suppress(FileNotFoundError):
|
||||||
db_path.unlink()
|
await anyio.Path(db_path).unlink()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import asyncio
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import anyio
|
||||||
import pytest
|
import pytest
|
||||||
from langflow.custom.directory_reader.utils import abuild_custom_component_list_from_path
|
from langflow.custom.directory_reader.utils import abuild_custom_component_list_from_path
|
||||||
from langflow.initial_setup.constants import STARTER_FOLDER_NAME
|
from langflow.initial_setup.constants import STARTER_FOLDER_NAME
|
||||||
|
|
@ -132,7 +133,7 @@ def add_edge(source, target, from_output, to_input):
|
||||||
|
|
||||||
|
|
||||||
async def test_refresh_starter_projects():
|
async def test_refresh_starter_projects():
|
||||||
data_path = str(Path(__file__).parent.parent.parent.absolute() / "base" / "langflow" / "components")
|
data_path = str(await anyio.Path(__file__).parent.parent.parent.absolute() / "base" / "langflow" / "components")
|
||||||
components = await abuild_custom_component_list_from_path(data_path)
|
components = await abuild_custom_component_list_from_path(data_path)
|
||||||
|
|
||||||
chat_input = find_component_by_name(components, "ChatInput")
|
chat_input = find_component_by_name(components, "ChatInput")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
import anyio
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -18,21 +18,21 @@ async def test_webhook_endpoint(client, added_webhook_test):
|
||||||
endpoint = f"api/v1/webhook/{endpoint_name}"
|
endpoint = f"api/v1/webhook/{endpoint_name}"
|
||||||
# Create a temporary file
|
# Create a temporary file
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
file_path = Path(tmp) / "test_file.txt"
|
file_path = anyio.Path(tmp) / "test_file.txt"
|
||||||
|
|
||||||
payload = {"path": str(file_path)}
|
payload = {"path": str(file_path)}
|
||||||
|
|
||||||
response = await client.post(endpoint, json=payload)
|
response = await client.post(endpoint, json=payload)
|
||||||
assert response.status_code == 202
|
assert response.status_code == 202
|
||||||
assert file_path.exists()
|
assert await file_path.exists()
|
||||||
|
|
||||||
assert not file_path.exists()
|
assert not await file_path.exists()
|
||||||
|
|
||||||
# Send an invalid payload
|
# Send an invalid payload
|
||||||
payload = {"invalid_key": "invalid_value"}
|
payload = {"invalid_key": "invalid_value"}
|
||||||
response = await client.post(endpoint, json=payload)
|
response = await client.post(endpoint, json=payload)
|
||||||
assert response.status_code == 202
|
assert response.status_code == 202
|
||||||
assert not file_path.exists()
|
assert not await file_path.exists()
|
||||||
|
|
||||||
|
|
||||||
async def test_webhook_flow_on_run_endpoint(client, added_webhook_test, created_api_key):
|
async def test_webhook_flow_on_run_endpoint(client, added_webhook_test, created_api_key):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue