Skip to content

Commit 7de9678

Browse files
authored
chore: Add support for python version 3.14 (#444)
1 parent 6745bd6 commit 7de9678

File tree

8 files changed

+381
-259
lines changed

8 files changed

+381
-259
lines changed

.github/workflows/test.yaml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ jobs:
1212
strategy:
1313
matrix:
1414
os: [ubuntu-latest, macos-latest, windows-latest]
15-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
15+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
16+
exclude:
17+
# lxml doesn't have pre-built wheels for Python 3.14 on Windows yet
18+
- os: windows-latest
19+
python-version: "3.14"
1620

1721
runs-on: ${{ matrix.os }}
1822

@@ -33,6 +37,14 @@ jobs:
3337
- name: Install invoke
3438
run: pipx install invoke
3539

40+
- name: Install lxml build dependencies (Linux)
41+
if: matrix.python-version == '3.14' && runner.os == 'Linux'
42+
run: sudo apt-get install -y libxml2-dev libxslt-dev
43+
44+
- name: Install lxml build dependencies (macOS)
45+
if: matrix.python-version == '3.14' && runner.os == 'macOS'
46+
run: brew install libxml2 libxslt
47+
3648
- name: Install Dependencies
3749
run: invoke install
3850

poetry.lock

Lines changed: 273 additions & 216 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ version = "1.39.0"
44
description = "Client library for the Galileo platform."
55
authors = [{ name = "Galileo Technologies Inc.", email = "team@galileo.ai" }]
66
readme = "README.md"
7-
requires-python = ">=3.9,<3.14"
7+
requires-python = ">=3.9,<3.15"
88
dynamic = ["dependencies"]
99
license = "Apache-2.0"
1010

1111
[project.urls]
1212
Repository = "https://github.com/rungalileo/galileo-python"
1313

1414
[tool.poetry.dependencies]
15-
python = "^3.9,<3.14"
16-
pydantic = "^2.11.7"
15+
python = "^3.9,<3.15"
16+
pydantic = "^2.12.0"
1717
pyjwt = "^2.8.0"
1818
wrapt = "^1.14"
1919
attrs = ">=22.2.0"
@@ -50,7 +50,7 @@ pre-commit = "^4.0.1"
5050
mypy = "^1.16.0"
5151
openai = "^1.95.1"
5252
fastapi = "^0.115.0"
53-
codeflash = ">=0.12.3"
53+
codeflash = { version = ">=0.12.3", python = "<3.14" }
5454
ruff = "^0.12.3"
5555
openapi-python-client = "^0.26.1"
5656
yq = "^3.4.3"
@@ -59,11 +59,14 @@ pyyaml = "^6.0.2"
5959

6060
[tool.pytest.ini_options]
6161
pythonpath = ["./src/"]
62+
# Note: Some env vars are also set in conftest.py for pytest-xdist compatibility
63+
# on Python 3.14+. This section remains for documentation and older Python support.
6264
env = [
6365
"GALILEO_CONSOLE_URL=http://localtest:8088",
6466
"GALILEO_API_KEY=api-1234567890",
6567
"GALILEO_PROJECT=test-project",
6668
"GALILEO_LOG_STREAM=test-log-stream",
69+
"OPENAI_API_KEY=sk-test",
6770
]
6871
addopts = [
6972
# Run tests in parallel.
@@ -321,10 +324,10 @@ formatter-cmds = ["ruff check --exit-zero --fix $file", "ruff format $file"]
321324
[project.optional-dependencies]
322325
langchain = ["langchain-core"]
323326
openai = ["openai", "packaging (>=24.2,<25.0)", "openai-agents"]
324-
crewai = ["crewai (>=0.152.0,<=0.201.1)"]
327+
crewai = ["crewai (>=0.152.0,<=0.201.1); python_version < '3.14'"]
325328
middleware = ["starlette"]
326329
otel = ["opentelemetry-sdk (>=1.38.0,<2.0.0)", "opentelemetry-api (>=1.38.0,<2.0.0)", "opentelemetry-exporter-otlp (>=1.38.0,<2.0.0)"]
327-
all = ["langchain-core", "openai", "packaging (>=24.2,<25.0)", "openai-agents", "opentelemetry-sdk (>=1.38.0,<2.0.0)", "opentelemetry-api (>=1.38.0,<2.0.0)", "opentelemetry-exporter-otlp (>=1.38.0,<2.0.0)", "crewai (>=0.152.0,<=0.201.1)", "starlette"]
330+
all = ["langchain-core", "openai", "packaging (>=24.2,<25.0)", "openai-agents", "opentelemetry-sdk (>=1.38.0,<2.0.0)", "opentelemetry-api (>=1.38.0,<2.0.0)", "opentelemetry-exporter-otlp (>=1.38.0,<2.0.0)", "crewai (>=0.152.0,<=0.201.1); python_version < '3.14'", "starlette"]
328331

329332

330333
[build-system]

src/galileo/handlers/langchain/tool.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from langchain_core.runnables.base import Runnable
55
from langchain_core.tools import BaseTool
66
from pydantic import UUID4, BaseModel, ConfigDict, Field
7-
from pydantic.v1 import BaseModel as BaseModelV1
87

98
from galileo.constants.protect import TIMEOUT_SECS
109
from galileo.protect import ainvoke_protect, invoke_protect
@@ -17,7 +16,9 @@
1716
logger = get_logger(__name__)
1817

1918

20-
class ProtectToolInputSchema(BaseModelV1):
19+
class ProtectToolInputSchema(BaseModel):
20+
"""Input schema for the ProtectTool."""
21+
2122
input: Optional[str] = None
2223
output: Optional[str] = None
2324

@@ -58,8 +59,7 @@ class ProtectTool(BaseTool):
5859
"The tool can be used on the input text or output text, and can be configured "
5960
"with a set of rulesets to evaluate on."
6061
)
61-
# langchain requires pydantic v1
62-
args_schema: type[BaseModelV1] = ProtectToolInputSchema
62+
args_schema: type[BaseModel] = ProtectToolInputSchema
6363

6464
prioritized_rulesets: Optional[Sequence[Ruleset]] = None
6565
project_id: Optional[UUID4] = None

tests/conftest.py

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,43 @@
1-
import datetime
2-
import logging
3-
from collections.abc import Generator
4-
from typing import Callable
5-
from unittest.mock import MagicMock, patch
6-
from uuid import uuid4
7-
8-
import pytest
9-
from openai.types import CompletionUsage
10-
from openai.types.chat import ChatCompletionMessage
11-
from openai.types.chat.chat_completion import ChatCompletion, Choice
12-
13-
from galileo.config import GalileoPythonConfig
14-
from galileo.resources.models import DatasetContent, DatasetRow, DatasetRowValuesDict
15-
from galileo_core.constants.request_method import RequestMethod
16-
from galileo_core.constants.routes import Routes as CoreRoutes
17-
from galileo_core.schemas.core.user import User
18-
from galileo_core.schemas.core.user_role import UserRole
19-
from galileo_core.schemas.protect.rule import Rule, RuleOperator
20-
from galileo_core.schemas.protect.ruleset import Ruleset
21-
from tests.testutils.setup import setup_thread_pool_request_capture
1+
# fmt: off
2+
# CRITICAL: Set test environment variables BEFORE any other imports.
3+
# This MUST be at the absolute top of conftest.py before any import statements.
4+
# Required for pytest-xdist compatibility on Python 3.14+.
5+
#
6+
# NOTE: We unconditionally override these vars (not setdefault) to ensure:
7+
# 1. Tests never accidentally use real credentials from developer's environment
8+
# 2. Test isolation - all tests see the same predictable values
9+
# 3. Security - prevents real API keys from leaking into test logs
10+
import os as _os
11+
12+
_os.environ["GALILEO_CONSOLE_URL"] = "http://localtest:8088"
13+
_os.environ["GALILEO_API_KEY"] = "api-1234567890"
14+
_os.environ["GALILEO_PROJECT"] = "test-project"
15+
_os.environ["GALILEO_LOG_STREAM"] = "test-log-stream"
16+
_os.environ["OPENAI_API_KEY"] = "sk-test"
17+
del _os # Clean up temporary import
18+
# fmt: on
19+
20+
import datetime # noqa: E402
21+
import logging # noqa: E402
22+
from collections.abc import Generator # noqa: E402
23+
from typing import Callable # noqa: E402
24+
from unittest.mock import MagicMock, patch # noqa: E402
25+
from uuid import uuid4 # noqa: E402
26+
27+
import pytest # noqa: E402
28+
from openai.types import CompletionUsage # noqa: E402
29+
from openai.types.chat import ChatCompletionMessage # noqa: E402
30+
from openai.types.chat.chat_completion import ChatCompletion, Choice # noqa: E402
31+
32+
from galileo.config import GalileoPythonConfig # noqa: E402
33+
from galileo.resources.models import DatasetContent, DatasetRow, DatasetRowValuesDict # noqa: E402
34+
from galileo_core.constants.request_method import RequestMethod # noqa: E402
35+
from galileo_core.constants.routes import Routes as CoreRoutes # noqa: E402
36+
from galileo_core.schemas.core.user import User # noqa: E402
37+
from galileo_core.schemas.core.user_role import UserRole # noqa: E402
38+
from galileo_core.schemas.protect.rule import Rule, RuleOperator # noqa: E402
39+
from galileo_core.schemas.protect.ruleset import Ruleset # noqa: E402
40+
from tests.testutils.setup import setup_thread_pool_request_capture # noqa: E402
2241

2342
# Note: The mock_request fixture is automatically provided by galileo_core[testing] extras
2443

@@ -62,7 +81,13 @@ def set_validated_config(
6281
mock_healthcheck: None, mock_login_api_key: None, mock_get_current_user: None, mock_decode_jwt: MagicMock
6382
) -> Generator[None, None, None]:
6483
"""Automatically set up validated config for tests."""
65-
config = GalileoPythonConfig.get()
84+
# Reset any existing config to ensure fresh initialization
85+
# This is needed for pytest-xdist compatibility on Python 3.14+
86+
if GalileoPythonConfig._instance is not None:
87+
GalileoPythonConfig._instance.reset()
88+
# Initialize config with EXPLICIT values to avoid env var timing issues with pytest-xdist
89+
# This ensures correct config even if env vars weren't set before module imports
90+
config = GalileoPythonConfig.get(console_url="http://localtest:8088", api_key="api-1234567890")
6691
yield
6792
config.reset()
6893

tests/future/conftest.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
to prevent interactive prompts and properly isolate Configuration tests.
66
77
These tests are completely isolated from the parent conftest.py fixtures.
8+
9+
NOTE: Unlike the main tests/conftest.py, we do NOT set default env vars here
10+
because future tests need to test "missing env var" scenarios (e.g.,
11+
test_connect_fails_without_console_url, test_connect_fails_without_api_key).
812
"""
913

1014
import logging
@@ -36,18 +40,25 @@ def set_validated_config() -> Generator[None, None, None]:
3640
"""
3741
yield
3842
# Clean up any config that might have been created
39-
try:
40-
config = GalileoPythonConfig.get()
41-
config.reset()
42-
except Exception:
43-
pass
43+
# Only reset if already initialized to avoid triggering config initialization
44+
# with potentially wrong values (no mocks available during teardown)
45+
if GalileoPythonConfig._instance is not None:
46+
GalileoPythonConfig._instance.reset()
4447

4548

46-
@pytest.fixture
49+
@pytest.fixture(autouse=True)
4750
def clean_env(monkeypatch: pytest.MonkeyPatch) -> Generator[None, None, None]:
48-
"""Clean environment fixture that removes all Galileo-related env vars."""
51+
"""Clean environment fixture that removes all Galileo-related env vars.
52+
53+
This is autouse=True to ensure future tests start with a clean environment,
54+
especially important because the parent tests/conftest.py sets env vars at
55+
import time for pytest-xdist compatibility. Future tests need to test
56+
scenarios like "missing console URL" which require these vars to be absent.
57+
"""
4958
for key in _CONFIGURATION_KEYS:
5059
monkeypatch.delenv(key.env_var, raising=False)
60+
# Also clear OPENAI_API_KEY which parent conftest sets
61+
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
5162
yield
5263

5364

tests/test_crewai_handler.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
1+
import sys
12
import uuid
23
from datetime import datetime
34
from typing import Any
45
from unittest.mock import Mock, patch
56

67
import pytest
78

8-
from galileo.handlers.crewai.handler import CrewAIEventListener
9-
from galileo.schema.handlers import NodeType
10-
from tests.testutils.setup import setup_mock_logstreams_client, setup_mock_projects_client, setup_mock_traces_client
9+
# Skip all tests in this module on Python 3.14+ (crewai doesn't support it yet)
10+
pytestmark = pytest.mark.skipif(sys.version_info >= (3, 14), reason="crewai does not support Python 3.14+")
11+
12+
from galileo.handlers.crewai.handler import CrewAIEventListener # noqa: E402
13+
from galileo.schema.handlers import NodeType # noqa: E402
14+
from tests.testutils.setup import ( # noqa: E402
15+
setup_mock_logstreams_client,
16+
setup_mock_projects_client,
17+
setup_mock_traces_client,
18+
)
1119

1220

1321
class MockEvent:

tests/test_openai.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@
1313
from tests.testutils.streaming import EventStream
1414

1515

16+
@pytest.fixture(autouse=True)
17+
def ensure_openai_api_key(monkeypatch):
18+
"""Ensure OPENAI_API_KEY is set for OpenAI tests."""
19+
monkeypatch.setenv("OPENAI_API_KEY", "sk-test")
20+
21+
1622
def openai_incorrect_api_key_error() -> bytes:
1723
return b"{'error': {'message': 'Incorrect API key provided: sk-galil********. You can find your API key at https://platform.openai.com/account/api-keys.', 'type': 'invalid_request_error', 'param': None, 'code': 'invalid_api_key'}}"
1824

0 commit comments

Comments
 (0)