Enhance Windows Compatibility with Event Loop Policy and Path Resolution (#3986)
Co-authored-by: Suchintan <suchintan@users.noreply.github.com> Co-authored-by: Shuchang Zheng <wintonzheng0325@gmail.com> Co-authored-by: Stanislav Novosad <stas@skyvern.com>
This commit is contained in:
@@ -7,6 +7,7 @@ from ddtrace.ext import http
|
|||||||
from ddtrace.trace import TraceFilter, Span
|
from ddtrace.trace import TraceFilter, Span
|
||||||
|
|
||||||
from skyvern.forge.sdk.forge_log import setup_logger
|
from skyvern.forge.sdk.forge_log import setup_logger
|
||||||
|
from skyvern.utils import setup_windows_event_loop_policy
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from skyvern.library import Skyvern # noqa: E402
|
from skyvern.library import Skyvern # noqa: E402
|
||||||
@@ -24,6 +25,7 @@ class FilterHeartbeat(TraceFilter):
|
|||||||
return trace
|
return trace
|
||||||
|
|
||||||
|
|
||||||
|
setup_windows_event_loop_policy()
|
||||||
tracer.configure(trace_processors=[FilterHeartbeat()])
|
tracer.configure(trace_processors=[FilterHeartbeat()])
|
||||||
setup_logger()
|
setup_logger()
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ def init(
|
|||||||
|
|
||||||
backend_env_path = resolve_backend_env_path()
|
backend_env_path = resolve_backend_env_path()
|
||||||
if backend_env_path.exists():
|
if backend_env_path.exists():
|
||||||
console.print(f"💡 [{backend_env_path}] file already exists.", style="yellow")
|
console.print(f"💡 [{backend_env_path}] file already exists.", style="yellow", markup=False)
|
||||||
redo_llm_setup = Confirm.ask(
|
redo_llm_setup = Confirm.ask(
|
||||||
"Do you want to go through [bold yellow]LLM provider setup again[/bold yellow]?",
|
"Do you want to go through [bold yellow]LLM provider setup again[/bold yellow]?",
|
||||||
default=False,
|
default=False,
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ from typing import Any, Callable, ClassVar, Dict, List, Mapping, Optional, Set,
|
|||||||
|
|
||||||
import pydantic
|
import pydantic
|
||||||
|
|
||||||
|
from .datetime_utils import serialize_datetime
|
||||||
|
from .serialization import convert_and_respect_annotation_metadata
|
||||||
|
from typing_extensions import TypeAlias
|
||||||
|
|
||||||
IS_PYDANTIC_V2 = pydantic.VERSION.startswith("2.")
|
IS_PYDANTIC_V2 = pydantic.VERSION.startswith("2.")
|
||||||
|
|
||||||
if IS_PYDANTIC_V2:
|
if IS_PYDANTIC_V2:
|
||||||
@@ -28,9 +32,6 @@ else:
|
|||||||
from pydantic.typing import is_literal_type as is_literal_type # type: ignore[no-redef]
|
from pydantic.typing import is_literal_type as is_literal_type # type: ignore[no-redef]
|
||||||
from pydantic.typing import is_union as is_union # type: ignore[no-redef]
|
from pydantic.typing import is_union as is_union # type: ignore[no-redef]
|
||||||
|
|
||||||
from .datetime_utils import serialize_datetime
|
|
||||||
from .serialization import convert_and_respect_annotation_metadata
|
|
||||||
from typing_extensions import TypeAlias
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
Model = TypeVar("Model", bound=pydantic.BaseModel)
|
Model = TypeVar("Model", bound=pydantic.BaseModel)
|
||||||
@@ -247,7 +248,7 @@ def _get_model_fields(model: Type["Model"]) -> Mapping[str, PydanticField]:
|
|||||||
def _get_field_default(field: PydanticField) -> Any:
|
def _get_field_default(field: PydanticField) -> Any:
|
||||||
try:
|
try:
|
||||||
value = field.get_default() # type: ignore[union-attr]
|
value = field.get_default() # type: ignore[union-attr]
|
||||||
except:
|
except Exception:
|
||||||
value = field.default
|
value = field.default
|
||||||
if IS_PYDANTIC_V2:
|
if IS_PYDANTIC_V2:
|
||||||
from pydantic_core import PydanticUndefined
|
from pydantic_core import PydanticUndefined
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ def convert_and_respect_annotation_metadata(
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
typing_extensions.get_origin(clean_type) == typing.Dict
|
typing_extensions.get_origin(clean_type) == typing.Dict
|
||||||
or typing_extensions.get_origin(clean_type) == dict
|
or typing_extensions.get_origin(clean_type) is dict
|
||||||
or clean_type == typing.Dict
|
or clean_type == typing.Dict
|
||||||
) and isinstance(object_, typing.Dict):
|
) and isinstance(object_, typing.Dict):
|
||||||
key_type = typing_extensions.get_args(clean_type)[0]
|
key_type = typing_extensions.get_args(clean_type)[0]
|
||||||
@@ -92,7 +92,7 @@ def convert_and_respect_annotation_metadata(
|
|||||||
if not isinstance(object_, str):
|
if not isinstance(object_, str):
|
||||||
if (
|
if (
|
||||||
typing_extensions.get_origin(clean_type) == typing.Set
|
typing_extensions.get_origin(clean_type) == typing.Set
|
||||||
or typing_extensions.get_origin(clean_type) == set
|
or typing_extensions.get_origin(clean_type) is set
|
||||||
or clean_type == typing.Set
|
or clean_type == typing.Set
|
||||||
) and isinstance(object_, typing.Set):
|
) and isinstance(object_, typing.Set):
|
||||||
inner_type = typing_extensions.get_args(clean_type)[0]
|
inner_type = typing_extensions.get_args(clean_type)[0]
|
||||||
@@ -108,14 +108,14 @@ def convert_and_respect_annotation_metadata(
|
|||||||
elif (
|
elif (
|
||||||
(
|
(
|
||||||
typing_extensions.get_origin(clean_type) == typing.List
|
typing_extensions.get_origin(clean_type) == typing.List
|
||||||
or typing_extensions.get_origin(clean_type) == list
|
or typing_extensions.get_origin(clean_type) is list
|
||||||
or clean_type == typing.List
|
or clean_type == typing.List
|
||||||
)
|
)
|
||||||
and isinstance(object_, typing.List)
|
and isinstance(object_, typing.List)
|
||||||
) or (
|
) or (
|
||||||
(
|
(
|
||||||
typing_extensions.get_origin(clean_type) == typing.Sequence
|
typing_extensions.get_origin(clean_type) == typing.Sequence
|
||||||
or typing_extensions.get_origin(clean_type) == collections.abc.Sequence
|
or typing_extensions.get_origin(clean_type) is collections.abc.Sequence
|
||||||
or clean_type == typing.Sequence
|
or clean_type == typing.Sequence
|
||||||
)
|
)
|
||||||
and isinstance(object_, typing.Sequence)
|
and isinstance(object_, typing.Sequence)
|
||||||
@@ -131,7 +131,7 @@ def convert_and_respect_annotation_metadata(
|
|||||||
for item in object_
|
for item in object_
|
||||||
]
|
]
|
||||||
|
|
||||||
if typing_extensions.get_origin(clean_type) == typing.Union:
|
if typing_extensions.get_origin(clean_type) is typing.Union:
|
||||||
# We should be able to ~relatively~ safely try to convert keys against all
|
# We should be able to ~relatively~ safely try to convert keys against all
|
||||||
# member types in the union, the edge case here is if one member aliases a field
|
# member types in the union, the edge case here is if one member aliases a field
|
||||||
# of the same name to a different name from another member
|
# of the same name to a different name from another member
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import logging
|
||||||
|
import platform
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
from skyvern import constants
|
from skyvern import constants
|
||||||
@@ -16,6 +20,9 @@ _DEFAULT_ENV_FILES = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
model_config = SettingsConfigDict(env_file=_DEFAULT_ENV_FILES, extra="ignore")
|
model_config = SettingsConfigDict(env_file=_DEFAULT_ENV_FILES, extra="ignore")
|
||||||
|
|
||||||
@@ -46,7 +53,11 @@ class Settings(BaseSettings):
|
|||||||
LONG_RUNNING_TASK_WARNING_RATIO: float = 0.95
|
LONG_RUNNING_TASK_WARNING_RATIO: float = 0.95
|
||||||
MAX_RETRIES_PER_STEP: int = 5
|
MAX_RETRIES_PER_STEP: int = 5
|
||||||
DEBUG_MODE: bool = False
|
DEBUG_MODE: bool = False
|
||||||
DATABASE_STRING: str = "postgresql+psycopg://skyvern@localhost/skyvern"
|
DATABASE_STRING: str = (
|
||||||
|
"postgresql+asyncpg://skyvern@localhost/skyvern"
|
||||||
|
if platform.system() == "Windows"
|
||||||
|
else "postgresql+psycopg://skyvern@localhost/skyvern"
|
||||||
|
)
|
||||||
DATABASE_STATEMENT_TIMEOUT_MS: int = 60000
|
DATABASE_STATEMENT_TIMEOUT_MS: int = 60000
|
||||||
DISABLE_CONNECTION_POOL: bool = False
|
DISABLE_CONNECTION_POOL: bool = False
|
||||||
PROMPT_ACTION_HISTORY_WINDOW: int = 1
|
PROMPT_ACTION_HISTORY_WINDOW: int = 1
|
||||||
@@ -460,6 +471,29 @@ class Settings(BaseSettings):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def model_post_init(self, __context: Any) -> None: # type: ignore[override]
|
||||||
|
super().model_post_init(__context)
|
||||||
|
if platform.system() != "Windows":
|
||||||
|
return
|
||||||
|
|
||||||
|
scheme, sep, remainder = self.DATABASE_STRING.partition("://")
|
||||||
|
if not sep:
|
||||||
|
return
|
||||||
|
|
||||||
|
dialect, driver_sep, driver = scheme.partition("+")
|
||||||
|
if not driver_sep or driver not in {"psycopg", "psycopg2"}:
|
||||||
|
return
|
||||||
|
|
||||||
|
updated_string = f"{dialect}+asyncpg://{remainder}"
|
||||||
|
if updated_string == self.DATABASE_STRING:
|
||||||
|
return
|
||||||
|
|
||||||
|
LOG.warning(
|
||||||
|
"Detected Windows environment: switching DATABASE_STRING driver from psycopg to asyncpg "
|
||||||
|
"for compatibility with the Proactor event loop policy."
|
||||||
|
)
|
||||||
|
object.__setattr__(self, "DATABASE_STRING", updated_string)
|
||||||
|
|
||||||
def is_cloud_environment(self) -> bool:
|
def is_cloud_environment(self) -> bool:
|
||||||
"""
|
"""
|
||||||
:return: True if env is not local, else False
|
:return: True if env is not local, else False
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import asyncio
|
||||||
import platform
|
import platform
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -8,6 +9,28 @@ from alembic.config import Config
|
|||||||
from skyvern.constants import REPO_ROOT_DIR
|
from skyvern.constants import REPO_ROOT_DIR
|
||||||
|
|
||||||
|
|
||||||
|
def setup_windows_event_loop_policy() -> None:
|
||||||
|
"""
|
||||||
|
Ensure the Windows event loop policy supports subprocesses (required by Playwright).
|
||||||
|
|
||||||
|
Explicitly setting the Proactor event loop policy prevents third-party packages or older
|
||||||
|
Python defaults from forcing the Selector policy, which does not implement subprocess
|
||||||
|
transports and causes runtime failures when Playwright tries to launch a browser.
|
||||||
|
"""
|
||||||
|
if platform.system() != "Windows":
|
||||||
|
return
|
||||||
|
|
||||||
|
windows_policy_cls = getattr(asyncio, "WindowsProactorEventLoopPolicy", None)
|
||||||
|
if windows_policy_cls is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
current_policy = asyncio.get_event_loop_policy()
|
||||||
|
if isinstance(current_policy, windows_policy_cls):
|
||||||
|
return
|
||||||
|
|
||||||
|
asyncio.set_event_loop_policy(windows_policy_cls())
|
||||||
|
|
||||||
|
|
||||||
def migrate_db() -> None:
|
def migrate_db() -> None:
|
||||||
alembic_cfg = Config()
|
alembic_cfg = Config()
|
||||||
path = f"{REPO_ROOT_DIR}/alembic"
|
path = f"{REPO_ROOT_DIR}/alembic"
|
||||||
|
|||||||
Reference in New Issue
Block a user