diff --git a/.github/workflows/nightly-throughput-stress.yml b/.github/workflows/nightly-throughput-stress.yml new file mode 100644 index 000000000..ec64a298d --- /dev/null +++ b/.github/workflows/nightly-throughput-stress.yml @@ -0,0 +1,173 @@ +name: Nightly Throughput Stress + +on: + schedule: + # Run at 3 AM PST (11:00 UTC) - offset from existing nightly + - cron: '00 11 * * *' + push: + branches: + - add-nightly-throughput-stress-workflow + workflow_dispatch: + inputs: + duration: + description: 'Test duration (e.g., 6h, 1h)' + required: false + default: '5h' + type: string + timeout: + description: 'Scenario timeout (should always be greater than duration)' + required: false + default: '5h30m' + type: string + job_timeout_minutes: + description: 'GitHub Actions job timeout in minutes' + required: false + default: 360 + type: number + +env: + # Workflow configuration + TEST_DURATION: ${{ inputs.duration || vars.NIGHTLY_TEST_DURATION || '5h' }} + TEST_TIMEOUT: ${{ inputs.timeout || vars.NIGHTLY_TEST_TIMEOUT || '5h30m' }} + + # Logging and artifacts + WORKER_LOG_DIR: /tmp/throughput-stress-logs + + # Omes configuration + OMES_REPO: temporalio/omes + OMES_REF: main + RUN_ID: ${{ github.run_id }}-throughput-stress + +jobs: + throughput-stress: + runs-on: ubuntu-latest-4-cores + timeout-minutes: ${{ fromJSON(inputs.job_timeout_minutes || vars.NIGHTLY_JOB_TIMEOUT_MINUTES || 360) }} + + steps: + - name: Print test configuration + run: | + echo "=== Throughput Stress Test Configuration ===" + echo "Duration: $TEST_DURATION" + echo "Timeout: $TEST_TIMEOUT" + echo "Run ID: $RUN_ID" + echo "==========================================" + + - name: Checkout SDK + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Checkout OMES + uses: actions/checkout@v4 + with: + repository: ${{ env.OMES_REPO }} + ref: ${{ env.OMES_REF }} + path: omes + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: omes/go.mod + cache-dependency-path: omes/go.sum + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Setup Rust cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: temporalio/bridge -> target + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install protoc + uses: arduino/setup-protoc@v3 + with: + version: '23.x' + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup uv + uses: astral-sh/setup-uv@v5 + + - name: Install poethepoet + run: uv tool install poethepoet + + - name: Install dependencies + run: uv sync --all-extras + + - name: Build SDK + run: poe build-develop + + - name: Install Temporal CLI + uses: temporalio/setup-temporal@v0 + + - name: Setup log directory + run: mkdir -p $WORKER_LOG_DIR + + - name: Start Temporal Server + run: | + temporal server start-dev \ + --db-filename temporal-throughput-stress.sqlite \ + --sqlite-pragma journal_mode=WAL \ + --sqlite-pragma synchronous=OFF \ + --headless &> $WORKER_LOG_DIR/temporal-server.log & + + - name: Run throughput stress scenario with local SDK + working-directory: omes + run: | + # This makes the pipeline return the exit code of the first failing command + # Otherwise the output of the `tee` command will be used + # (which is troublesome when the scenario fails but the `tee` command succeeds) + set -o pipefail + + # Use run-scenario-with-worker to build and run in one step + # Pass the SDK directory as --version for local testing + # Note: The hardcoded values below match OMES defaults, except: + # - visibility-count-timeout: 5m (vs 3m default) + # to give CI a bit more time for visibility consistency + go run ./cmd run-scenario-with-worker \ + --scenario throughput_stress \ + --language python \ + --version $(pwd)/.. \ + --run-id $RUN_ID \ + --duration $TEST_DURATION \ + --timeout $TEST_TIMEOUT \ + --max-concurrent 10 \ + --option internal-iterations=10 \ + --option continue-as-new-after-iterations=3 \ + --option sleep-time=1s \ + --option visibility-count-timeout=5m \ + --option min-throughput-per-hour=1000 \ + 2>&1 | tee $WORKER_LOG_DIR/scenario.log + + - name: Upload logs on failure + if: failure() || cancelled() + uses: actions/upload-artifact@v4 + with: + name: throughput-stress-logs + path: ${{ env.WORKER_LOG_DIR }} + retention-days: 30 + + - name: Notify Slack on failure + if: failure() || cancelled() + uses: slackapi/slack-github-action@v2 + with: + webhook-type: incoming-webhook + payload: | + { + "text": "Nightly Python throughput stress test failed", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Nightly Throughput Stress Failed* :x:\n\n*Duration:* ${{ env.TEST_DURATION }}\n*Run:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Logs>\n*Triggered by:* ${{ github.event_name == 'schedule' && 'Scheduled' || github.actor }}" + } + } + ] + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_SDK_ALERTS_WEBHOOK }} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 96a17a60c..bfd8bee67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,8 @@ description = "Temporal.io Python SDK" authors = [{ name = "Temporal Technologies Inc", email = "sdk@temporal.io" }] requires-python = ">=3.10" readme = "README.md" -license = { file = "LICENSE" } +license = "MIT" +license-files = ["LICENSE"] keywords = ["temporal", "workflow"] dependencies = [ "nexus-rpc==1.1.0", @@ -28,7 +29,7 @@ grpc = ["grpcio>=1.48.2,<2"] opentelemetry = ["opentelemetry-api>=1.11.1,<2", "opentelemetry-sdk>=1.11.1,<2"] pydantic = ["pydantic>=2.0.0,<3"] openai-agents = [ - "openai-agents>=0.3,<0.4", + "openai-agents>=0.3,<0.5", "mcp>=1.9.4, <2", ] @@ -58,7 +59,7 @@ dev = [ "pytest-cov>=6.1.1", "httpx>=0.28.1", "pytest-pretty>=1.3.0", - "openai-agents>=0.3,<0.4; python_version >= '3.14'", + "openai-agents>=0.3,<0.5; python_version >= '3.14'", "openai-agents[litellm]>=0.3,<0.4; python_version < '3.14'", "googleapis-common-protos==1.70.0", ] diff --git a/temporalio/contrib/opentelemetry.py b/temporalio/contrib/opentelemetry.py index 9e1542814..351a2e42f 100644 --- a/temporalio/contrib/opentelemetry.py +++ b/temporalio/contrib/opentelemetry.py @@ -26,8 +26,7 @@ import opentelemetry.trace.propagation.tracecontext import opentelemetry.util.types from opentelemetry.context import Context -from opentelemetry.trace import Span, SpanKind, Status, StatusCode, _Links -from opentelemetry.util import types +from opentelemetry.trace import Status, StatusCode from typing_extensions import Protocol, TypeAlias, TypedDict import temporalio.activity @@ -473,7 +472,12 @@ async def handle_query(self, input: temporalio.worker.HandleQueryInput) -> Any: ) return await super().handle_query(input) finally: - opentelemetry.context.detach(token) + # In some exceptional cases this finally is executed with a + # different contextvars.Context than the one the token was created + # on. As such we do a best effort detach to avoid using a mismatched + # token. + if context is opentelemetry.context.get_current(): + opentelemetry.context.detach(token) def handle_update_validator( self, input: temporalio.worker.HandleUpdateInput @@ -545,6 +549,7 @@ def _top_level_workflow_context( exception: Optional[Exception] = None # Run under this context token = opentelemetry.context.attach(context) + try: yield None success = True @@ -561,7 +566,13 @@ def _top_level_workflow_context( exception=exception, kind=opentelemetry.trace.SpanKind.INTERNAL, ) - opentelemetry.context.detach(token) + + # In some exceptional cases this finally is executed with a + # different contextvars.Context than the one the token was created + # on. As such we do a best effort detach to avoid using a mismatched + # token. + if context is opentelemetry.context.get_current(): + opentelemetry.context.detach(token) def _context_to_headers( self, headers: Mapping[str, temporalio.api.common.v1.Payload] diff --git a/temporalio/worker/_worker.py b/temporalio/worker/_worker.py index 9850d32a7..fc2c2241d 100644 --- a/temporalio/worker/_worker.py +++ b/temporalio/worker/_worker.py @@ -203,9 +203,11 @@ def __init__( interceptors already on the client that also implement :py:class:`Interceptor` are prepended to this list and should not be explicitly given here. - build_id: Unique identifier for the current runtime. This is best - set as a hash of all code and should change only when code does. - If unset, a best-effort identifier is generated. + build_id: A unique identifier for the current runtime, ideally provided as a + representation of the complete source code. If not explicitly set, the system + automatically generates a best-effort identifier by traversing and computing + hashes of all modules in the codebase. In very large codebases this automatic + process may significantly increase initialization time. Exclusive with `deployment_config`. WARNING: Deprecated. Use `deployment_config` instead. identity: Identity for this worker client. If unset, the client diff --git a/tests/contrib/test_opentelemetry.py b/tests/contrib/test_opentelemetry.py index be6b17707..9dbdfed93 100644 --- a/tests/contrib/test_opentelemetry.py +++ b/tests/contrib/test_opentelemetry.py @@ -1,13 +1,18 @@ from __future__ import annotations import asyncio +import gc import logging +import queue +import sys +import threading import uuid from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass from datetime import timedelta from typing import Iterable, List, Optional +import opentelemetry.context import pytest from opentelemetry.sdk.trace import ReadableSpan, TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor @@ -17,11 +22,20 @@ from temporalio import activity, workflow from temporalio.client import Client, WithStartWorkflowOperation, WorkflowUpdateStage from temporalio.common import RetryPolicy, WorkflowIDConflictPolicy -from temporalio.contrib.opentelemetry import TracingInterceptor +from temporalio.contrib.opentelemetry import ( + TracingInterceptor, + TracingWorkflowInboundInterceptor, +) from temporalio.contrib.opentelemetry import workflow as otel_workflow from temporalio.exceptions import ApplicationError, ApplicationErrorCategory from temporalio.testing import WorkflowEnvironment from temporalio.worker import UnsandboxedWorkflowRunner, Worker +from tests.helpers import LogCapturer +from tests.helpers.cache_eviction import ( + CacheEvictionTearDownWorkflow, + WaitForeverWorkflow, + wait_forever_activity, +) @dataclass @@ -420,7 +434,10 @@ def dump_spans( span_links: List[str] = [] for link in span.links: for link_span in spans: - if link_span.context.span_id == link.context.span_id: + if ( + link_span.context is not None + and link_span.context.span_id == link.context.span_id + ): span_links.append(link_span.name) span_str += f" (links: {', '.join(span_links)})" # Signals can duplicate in rare situations, so we make sure not to @@ -430,7 +447,7 @@ def dump_spans( ret.append(span_str) ret += dump_spans( spans, - parent_id=span.context.span_id, + parent_id=span.context.span_id if span.context else None, with_attributes=with_attributes, indent_depth=indent_depth + 1, ) @@ -547,3 +564,50 @@ async def test_opentelemetry_benign_exception(client: Client): # * workflow failure and wft failure # * signal with start # * signal failure and wft failure from signal + + +def test_opentelemetry_safe_detach(): + class _fake_self: + def _load_workflow_context_carrier(*args): + return None + + def _set_on_context(self, ctx): + return opentelemetry.context.set_value("test-key", "test-value", ctx) + + def _completed_span(*args, **kwargs): + pass + + # create a context manager and force enter to happen on this thread + context_manager = TracingWorkflowInboundInterceptor._top_level_workflow_context( + _fake_self(), # type: ignore + success_is_complete=True, + ) + context_manager.__enter__() + + # move reference to context manager into queue + q: queue.Queue = queue.Queue() + q.put(context_manager) + del context_manager + + def worker(): + # pull reference from queue and delete the last reference + context_manager = q.get() + del context_manager + # force gc + gc.collect() + + with LogCapturer().logs_captured(opentelemetry.context.logger) as capturer: + # run forced gc on other thread so exit happens there + t = threading.Thread(target=worker) + t.start() + t.join(timeout=5) + + def otel_context_error(record: logging.LogRecord) -> bool: + return ( + record.name == "opentelemetry.context" + and "Failed to detach context" in record.message + ) + + assert ( + capturer.find(otel_context_error) is None + ), "Detach from context message should not be logged" diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py index 79d3687fd..4a3850024 100644 --- a/tests/helpers/__init__.py +++ b/tests/helpers/__init__.py @@ -1,11 +1,25 @@ import asyncio +import logging +import logging.handlers +import queue import socket import time import uuid -from contextlib import closing +from contextlib import closing, contextmanager from dataclasses import dataclass from datetime import datetime, timedelta, timezone -from typing import Any, Awaitable, Callable, Optional, Sequence, Type, TypeVar, Union +from typing import ( + Any, + Awaitable, + Callable, + List, + Optional, + Sequence, + Type, + TypeVar, + Union, + cast, +) from temporalio.api.common.v1 import WorkflowExecution from temporalio.api.enums.v1 import EventType as EventType @@ -401,3 +415,34 @@ def _format_row(items: list[str], truncate: bool = False) -> str: padding = len(f" *: {elapsed_ms:>4} ") summary_row[col_idx] = f"{' ' * padding}[{summary}]"[: col_width - 3] print(_format_row(summary_row)) + + +class LogCapturer: + def __init__(self) -> None: + self.log_queue: queue.Queue[logging.LogRecord] = queue.Queue() + + @contextmanager + def logs_captured(self, *loggers: logging.Logger): + handler = logging.handlers.QueueHandler(self.log_queue) + + prev_levels = [l.level for l in loggers] + for l in loggers: + l.setLevel(logging.INFO) + l.addHandler(handler) + try: + yield self + finally: + for i, l in enumerate(loggers): + l.removeHandler(handler) + l.setLevel(prev_levels[i]) + + def find_log(self, starts_with: str) -> Optional[logging.LogRecord]: + return self.find(lambda l: l.message.startswith(starts_with)) + + def find( + self, pred: Callable[[logging.LogRecord], bool] + ) -> Optional[logging.LogRecord]: + for record in cast(List[logging.LogRecord], self.log_queue.queue): + if pred(record): + return record + return None diff --git a/tests/helpers/cache_eviction.py b/tests/helpers/cache_eviction.py new file mode 100644 index 000000000..191d51078 --- /dev/null +++ b/tests/helpers/cache_eviction.py @@ -0,0 +1,68 @@ +import asyncio +from datetime import timedelta + +from temporalio import activity, workflow + + +@activity.defn +async def wait_forever_activity() -> None: + await asyncio.Future() + + +@workflow.defn +class WaitForeverWorkflow: + @workflow.run + async def run(self) -> None: + await asyncio.Future() + + +@workflow.defn +class CacheEvictionTearDownWorkflow: + def __init__(self) -> None: + self._signal_count = 0 + + @workflow.run + async def run(self) -> None: + # Start several things in background. This is just to show that eviction + # can work even with these things running. + tasks = [ + asyncio.create_task( + workflow.execute_activity( + wait_forever_activity, start_to_close_timeout=timedelta(hours=1) + ) + ), + asyncio.create_task( + workflow.execute_child_workflow(WaitForeverWorkflow.run) + ), + asyncio.create_task(asyncio.sleep(1000)), + asyncio.shield( + workflow.execute_activity( + wait_forever_activity, start_to_close_timeout=timedelta(hours=1) + ) + ), + asyncio.create_task(workflow.wait_condition(lambda: False)), + ] + gather_fut = asyncio.gather(*tasks, return_exceptions=True) + # Let's also start something in the background that we never wait on + asyncio.create_task(asyncio.sleep(1000)) + try: + # Wait for signal count to reach 2 + await asyncio.sleep(0.01) + await workflow.wait_condition(lambda: self._signal_count > 1) + finally: + # This finally, on eviction, is actually called but the command + # should be ignored + await asyncio.sleep(0.01) + await workflow.wait_condition(lambda: self._signal_count > 2) + # Cancel gather tasks and wait on them, but ignore the errors + for task in tasks: + task.cancel() + await gather_fut + + @workflow.signal + async def signal(self) -> None: + self._signal_count += 1 + + @workflow.query + def signal_count(self) -> int: + return self._signal_count diff --git a/tests/worker/test_workflow.py b/tests/worker/test_workflow.py index da335635b..f7735db01 100644 --- a/tests/worker/test_workflow.py +++ b/tests/worker/test_workflow.py @@ -131,6 +131,7 @@ ) from tests import DEV_SERVER_DOWNLOAD_VERSION from tests.helpers import ( + LogCapturer, admitted_update_task, assert_eq_eventually, assert_eventually, @@ -145,6 +146,11 @@ unpause_and_assert, workflow_update_exists, ) +from tests.helpers.cache_eviction import ( + CacheEvictionTearDownWorkflow, + WaitForeverWorkflow, + wait_forever_activity, +) from tests.helpers.external_stack_trace import ( ExternalStackTraceWorkflow, external_wait_cancel, @@ -1992,37 +1998,6 @@ def last_signal(self) -> str: return self._last_signal -class LogCapturer: - def __init__(self) -> None: - self.log_queue: queue.Queue[logging.LogRecord] = queue.Queue() - - @contextmanager - def logs_captured(self, *loggers: logging.Logger): - handler = logging.handlers.QueueHandler(self.log_queue) - - prev_levels = [l.level for l in loggers] - for l in loggers: - l.setLevel(logging.INFO) - l.addHandler(handler) - try: - yield self - finally: - for i, l in enumerate(loggers): - l.removeHandler(handler) - l.setLevel(prev_levels[i]) - - def find_log(self, starts_with: str) -> Optional[logging.LogRecord]: - return self.find(lambda l: l.message.startswith(starts_with)) - - def find( - self, pred: Callable[[logging.LogRecord], bool] - ) -> Optional[logging.LogRecord]: - for record in cast(List[logging.LogRecord], self.log_queue.queue): - if pred(record): - return record - return None - - async def test_workflow_logging(client: Client, env: WorkflowEnvironment): workflow.logger.full_workflow_info_on_extra = True with LogCapturer().logs_captured( @@ -3738,70 +3713,6 @@ async def test_manual_result_type(client: Client): assert res4 == ManualResultType(some_string="from-query") -@activity.defn -async def wait_forever_activity() -> None: - await asyncio.Future() - - -@workflow.defn -class WaitForeverWorkflow: - @workflow.run - async def run(self) -> None: - await asyncio.Future() - - -@workflow.defn -class CacheEvictionTearDownWorkflow: - def __init__(self) -> None: - self._signal_count = 0 - - @workflow.run - async def run(self) -> None: - # Start several things in background. This is just to show that eviction - # can work even with these things running. - tasks = [ - asyncio.create_task( - workflow.execute_activity( - wait_forever_activity, start_to_close_timeout=timedelta(hours=1) - ) - ), - asyncio.create_task( - workflow.execute_child_workflow(WaitForeverWorkflow.run) - ), - asyncio.create_task(asyncio.sleep(1000)), - asyncio.shield( - workflow.execute_activity( - wait_forever_activity, start_to_close_timeout=timedelta(hours=1) - ) - ), - asyncio.create_task(workflow.wait_condition(lambda: False)), - ] - gather_fut = asyncio.gather(*tasks, return_exceptions=True) - # Let's also start something in the background that we never wait on - asyncio.create_task(asyncio.sleep(1000)) - try: - # Wait for signal count to reach 2 - await asyncio.sleep(0.01) - await workflow.wait_condition(lambda: self._signal_count > 1) - finally: - # This finally, on eviction, is actually called but the command - # should be ignored - await asyncio.sleep(0.01) - await workflow.wait_condition(lambda: self._signal_count > 2) - # Cancel gather tasks and wait on them, but ignore the errors - for task in tasks: - task.cancel() - await gather_fut - - @workflow.signal - async def signal(self) -> None: - self._signal_count += 1 - - @workflow.query - def signal_count(self) -> int: - return self._signal_count - - async def test_cache_eviction_tear_down(client: Client): # This test simulates forcing eviction. This used to raise GeneratorExit on # GC which triggered the finally which could run on any thread Python diff --git a/uv.lock b/uv.lock index df0875606..68f2f73bd 100644 --- a/uv.lock +++ b/uv.lock @@ -1311,7 +1311,7 @@ dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.14'" }, { name = "jinja2", marker = "python_full_version < '3.14'" }, { name = "jsonschema", marker = "python_full_version < '3.14'" }, - { name = "openai", marker = "python_full_version < '3.14'" }, + { name = "openai", version = "1.109.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, { name = "pydantic", marker = "python_full_version < '3.14'" }, { name = "python-dotenv", marker = "python_full_version < '3.14'" }, { name = "tiktoken", marker = "python_full_version < '3.14'" }, @@ -1816,33 +1816,61 @@ wheels = [ name = "openai" version = "1.109.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.14'", +] dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "httpx" }, - { name = "jiter" }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "tqdm" }, - { name = "typing-extensions" }, + { name = "anyio", marker = "python_full_version < '3.14'" }, + { name = "distro", marker = "python_full_version < '3.14'" }, + { name = "httpx", marker = "python_full_version < '3.14'" }, + { name = "jiter", marker = "python_full_version < '3.14'" }, + { name = "pydantic", marker = "python_full_version < '3.14'" }, + { name = "sniffio", marker = "python_full_version < '3.14'" }, + { name = "tqdm", marker = "python_full_version < '3.14'" }, + { name = "typing-extensions", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c6/a1/a303104dc55fc546a3f6914c842d3da471c64eec92043aef8f652eb6c524/openai-1.109.1.tar.gz", hash = "sha256:d173ed8dbca665892a6db099b4a2dfac624f94d20a93f46eb0b56aae940ed869", size = 564133, upload-time = "2025-09-24T13:00:53.075Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1d/2a/7dd3d207ec669cacc1f186fd856a0f61dbc255d24f6fdc1a6715d6051b0f/openai-1.109.1-py3-none-any.whl", hash = "sha256:6bcaf57086cf59159b8e27447e4e7dd019db5d29a438072fbd49c290c7e65315", size = 948627, upload-time = "2025-09-24T13:00:50.754Z" }, ] +[[package]] +name = "openai" +version = "2.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.14'" }, + { name = "distro", marker = "python_full_version >= '3.14'" }, + { name = "httpx", marker = "python_full_version >= '3.14'" }, + { name = "jiter", marker = "python_full_version >= '3.14'" }, + { name = "pydantic", marker = "python_full_version >= '3.14'" }, + { name = "sniffio", marker = "python_full_version >= '3.14'" }, + { name = "tqdm", marker = "python_full_version >= '3.14'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/c7/e42bcd89dfd47fec8a30b9e20f93e512efdbfbb3391b05bbb79a2fb295fa/openai-2.6.0.tar.gz", hash = "sha256:f119faf7fc07d7e558c1e7c32c873e241439b01bd7480418234291ee8c8f4b9d", size = 592904, upload-time = "2025-10-20T17:17:24.588Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/0a/58e9dcd34abe273eaeac3807a8483073767b5609d01bb78ea2f048e515a0/openai-2.6.0-py3-none-any.whl", hash = "sha256:f33fa12070fe347b5787a7861c8dd397786a4a17e1c3186e239338dac7e2e743", size = 1005403, upload-time = "2025-10-20T17:17:22.091Z" }, +] + [[package]] name = "openai-agents" version = "0.3.3" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.14'", +] dependencies = [ - { name = "griffe" }, - { name = "mcp" }, - { name = "openai" }, - { name = "pydantic" }, - { name = "requests" }, - { name = "types-requests" }, - { name = "typing-extensions" }, + { name = "griffe", marker = "python_full_version < '3.14'" }, + { name = "mcp", marker = "python_full_version < '3.14'" }, + { name = "openai", version = "1.109.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "pydantic", marker = "python_full_version < '3.14'" }, + { name = "requests", marker = "python_full_version < '3.14'" }, + { name = "types-requests", marker = "python_full_version < '3.14'" }, + { name = "typing-extensions", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a4/37/2b4f828840d3ff32d82b813c3371ec9ee26b3b8dc6b4acbb7a4a579f617a/openai_agents-0.3.3.tar.gz", hash = "sha256:b016381a6890e1cb6879eb23c53c35f8c2312be1117f1cd4e4b5e2463150839f", size = 1816230, upload-time = "2025-09-30T23:20:24.22Z" } wheels = [ @@ -1854,6 +1882,27 @@ litellm = [ { name = "litellm", marker = "python_full_version < '3.14'" }, ] +[[package]] +name = "openai-agents" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", +] +dependencies = [ + { name = "griffe", marker = "python_full_version >= '3.14'" }, + { name = "mcp", marker = "python_full_version >= '3.14'" }, + { name = "openai", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "pydantic", marker = "python_full_version >= '3.14'" }, + { name = "requests", marker = "python_full_version >= '3.14'" }, + { name = "types-requests", marker = "python_full_version >= '3.14'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/76/52398d0416706daa69b7e79d1d86f728bea4a49b60442006e397564d1366/openai_agents-0.4.1.tar.gz", hash = "sha256:ead3ad58fd918dd7bcbfcb5cd43a27bcd9dfca1e47f444afcf7b62c86f0f2634", size = 1924077, upload-time = "2025-10-22T00:47:12.799Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/38/df7ecff75ee67779e5159d0ac34e483ef99e658984b5a0706ccbdd68c1bf/openai_agents-0.4.1-py3-none-any.whl", hash = "sha256:d59fa9545625965b270b4d177b58db013730bf1b8c835b473f1e26ebf78a5eb4", size = 215641, upload-time = "2025-10-22T00:47:10.687Z" }, +] + [[package]] name = "opentelemetry-api" version = "1.37.0" @@ -2362,7 +2411,7 @@ name = "python-dateutil" version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "six" }, + { name = "six", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ @@ -2918,7 +2967,8 @@ grpc = [ ] openai-agents = [ { name = "mcp" }, - { name = "openai-agents" }, + { name = "openai-agents", version = "0.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "openai-agents", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, ] opentelemetry = [ { name = "opentelemetry-api" }, @@ -2937,8 +2987,8 @@ dev = [ { name = "maturin" }, { name = "mypy" }, { name = "mypy-protobuf" }, - { name = "openai-agents" }, - { name = "openai-agents", extra = ["litellm"], marker = "python_full_version < '3.14'" }, + { name = "openai-agents", version = "0.3.3", source = { registry = "https://pypi.org/simple" }, extra = ["litellm"], marker = "python_full_version < '3.14'" }, + { name = "openai-agents", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "psutil" }, { name = "pydocstyle" }, { name = "pydoctor" }, @@ -2958,7 +3008,7 @@ requires-dist = [ { name = "grpcio", marker = "extra == 'grpc'", specifier = ">=1.48.2,<2" }, { name = "mcp", marker = "extra == 'openai-agents'", specifier = ">=1.9.4,<2" }, { name = "nexus-rpc", specifier = "==1.1.0" }, - { name = "openai-agents", marker = "extra == 'openai-agents'", specifier = ">=0.3,<0.4" }, + { name = "openai-agents", marker = "extra == 'openai-agents'", specifier = ">=0.3,<0.5" }, { name = "opentelemetry-api", marker = "extra == 'opentelemetry'", specifier = ">=1.11.1,<2" }, { name = "opentelemetry-sdk", marker = "extra == 'opentelemetry'", specifier = ">=1.11.1,<2" }, { name = "protobuf", specifier = ">=3.20,<7.0.0" }, @@ -2978,7 +3028,7 @@ dev = [ { name = "maturin", specifier = ">=1.8.2" }, { name = "mypy", specifier = "==1.18.2" }, { name = "mypy-protobuf", specifier = ">=3.3.0,<4" }, - { name = "openai-agents", marker = "python_full_version >= '3.14'", specifier = ">=0.3,<0.4" }, + { name = "openai-agents", marker = "python_full_version >= '3.14'", specifier = ">=0.3,<0.5" }, { name = "openai-agents", extras = ["litellm"], marker = "python_full_version < '3.14'", specifier = ">=0.3,<0.4" }, { name = "psutil", specifier = ">=5.9.3,<6" }, { name = "pydocstyle", specifier = ">=6.3.0,<7" },