From 5c992eb7ae09eea3386d9f043ad2c3df253db515 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Tue, 22 Jul 2025 17:26:00 +0200 Subject: [PATCH] Python: move all tests under tests and initial work on int tests (#206) * move all tests under tests and initial work on int tests * added updated tests setup and merge tests * without failing step * fixed upload * updated file names for coverage * reenable surface tests * removed package matrix * simplified variables * correct path * removed mistake * fix mistake in path * fix path * windows specific env set * updated merge tests * slight update in marker * added run integration tests settings * updated setup, moved foundry int tests and updated merge test --- .github/workflows/label-issues.yml | 48 ++++ .github/workflows/label-pr.yml | 21 ++ .github/workflows/python-merge-tests.yml | 188 ++++++++++++++ .github/workflows/python-tests.yml | 111 +++++++++ .github/workflows/python-unit-tests.yml | 49 ---- python/packages/azure/tests/conftest.py | 9 +- .../integration/test_azure_chat_client.py | 121 --------- .../{unit => }/test_azure_chat_client.py | 132 ++++++++++ .../tests/{unit => }/test_cross_package.py | 0 python/packages/azure/tests/unit/__init__.py | 0 python/packages/foundry/tests/conftest.py | 7 +- .../integration/test_foundry_chat_client.py | 112 --------- .../tests/{unit => }/test_cross_package.py | 0 .../{unit => }/test_foundry_chat_client.py | 134 +++++++++- python/packages/main/tests/conftest.py | 79 +++--- .../integration/test_openai_chat_client.py | 120 --------- python/packages/main/tests/openai/conftest.py | 51 ++++ .../tests/openai/test_openai_chat_client.py | 233 ++++++++++++++++++ .../test_openai_chat_client_base.py | 0 .../main/tests/{unit => }/test_agents.py | 0 .../main/tests/{unit => }/test_clients.py | 0 .../main/tests/{unit => }/test_logging.py | 0 .../main/tests/{unit => }/test_tool.py | 0 .../main/tests/{unit => }/test_types.py | 0 python/packages/main/tests/unit/__init__.py | 0 python/packages/main/tests/unit/conftest.py | 38 --- .../main/tests/unit/test_cross_package.py | 9 - .../tests/unit/test_openai_chat_client.py | 103 -------- .../packages/main/tests/unit/test_version.py | 8 - python/shared_tasks.toml | 2 +- python/uv.lock | 42 ++-- 31 files changed, 978 insertions(+), 639 deletions(-) create mode 100644 .github/workflows/label-issues.yml create mode 100644 .github/workflows/label-pr.yml create mode 100644 .github/workflows/python-merge-tests.yml create mode 100644 .github/workflows/python-tests.yml delete mode 100644 .github/workflows/python-unit-tests.yml delete mode 100644 python/packages/azure/tests/integration/test_azure_chat_client.py rename python/packages/azure/tests/{unit => }/test_azure_chat_client.py (82%) rename python/packages/azure/tests/{unit => }/test_cross_package.py (100%) delete mode 100644 python/packages/azure/tests/unit/__init__.py delete mode 100644 python/packages/foundry/tests/integration/test_foundry_chat_client.py rename python/packages/foundry/tests/{unit => }/test_cross_package.py (100%) rename python/packages/foundry/tests/{unit => }/test_foundry_chat_client.py (67%) delete mode 100644 python/packages/main/tests/integration/test_openai_chat_client.py create mode 100644 python/packages/main/tests/openai/conftest.py create mode 100644 python/packages/main/tests/openai/test_openai_chat_client.py rename python/packages/main/tests/{unit => openai}/test_openai_chat_client_base.py (100%) rename python/packages/main/tests/{unit => }/test_agents.py (100%) rename python/packages/main/tests/{unit => }/test_clients.py (100%) rename python/packages/main/tests/{unit => }/test_logging.py (100%) rename python/packages/main/tests/{unit => }/test_tool.py (100%) rename python/packages/main/tests/{unit => }/test_types.py (100%) delete mode 100644 python/packages/main/tests/unit/__init__.py delete mode 100644 python/packages/main/tests/unit/conftest.py delete mode 100644 python/packages/main/tests/unit/test_cross_package.py delete mode 100644 python/packages/main/tests/unit/test_openai_chat_client.py delete mode 100644 python/packages/main/tests/unit/test_version.py diff --git a/.github/workflows/label-issues.yml b/.github/workflows/label-issues.yml new file mode 100644 index 0000000000..658f17fe62 --- /dev/null +++ b/.github/workflows/label-issues.yml @@ -0,0 +1,48 @@ +name: Label issues +on: + issues: + types: + - reopened + - opened + +jobs: + label_issues: + name: "Issue: add labels" + if: ${{ github.event.action == 'opened' || github.event.action == 'reopened' }} + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }} + script: | + // Get the issue body and title + const body = context.payload.issue.body + let title = context.payload.issue.title + + // Define the labels array + let labels = ["triage"] + + // Check if the body or the title contains the word 'python' (case-insensitive) + if ((body != null && body.match(/python/i)) || (title != null && title.match(/python/i))) { + // Add the 'python' label to the array + labels.push("python") + } + + // Check if the body or the title contains the words 'dotnet', '.net', 'c#' or 'csharp' (case-insensitive) + if ((body != null && body.match(/.net/i)) || (title != null && title.match(/.net/i)) || + (body != null && body.match(/dotnet/i)) || (title != null && title.match(/dotnet/i)) || + (body != null && body.match(/C#/i)) || (title != null && title.match(/C#/i)) || + (body != null && body.match(/csharp/i)) || (title != null && title.match(/csharp/i))) { + // Add the '.NET' label to the array + labels.push(".NET") + } + + // Add the labels to the issue + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: labels + }); diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml new file mode 100644 index 0000000000..11d0f083a8 --- /dev/null +++ b/.github/workflows/label-pr.yml @@ -0,0 +1,21 @@ +# This workflow will triage pull requests and apply a label based on the +# paths that are modified in the pull request. +# +# To use this workflow, you will need to set up a .github/labeler.yml +# file with configuration. For more information, see: +# https://github.com/actions/labeler + +name: Label pull request +on: [pull_request_target] + +jobs: + add_label: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - uses: actions/labeler@v5 + with: + repo-token: "${{ secrets.GH_ACTIONS_PR_WRITE }}" diff --git a/.github/workflows/python-merge-tests.yml b/.github/workflows/python-merge-tests.yml new file mode 100644 index 0000000000..11fcca8d1f --- /dev/null +++ b/.github/workflows/python-merge-tests.yml @@ -0,0 +1,188 @@ +name: Python - Tests - Merge + +on: + workflow_dispatch: + merge_group: + branches: ["main"] + +permissions: + contents: read + id-token: "write" + +env: + # Configure a constant location for the uv cache + UV_CACHE_DIR: /tmp/.uv-cache + RUN_INTEGRATION_TESTS: "true" + +jobs: + python-tests-main: + name: Python Tests - Main + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + python-version: ["3.10"] + os: [ubuntu-latest] + env: + UV_PYTHON: ${{ matrix.python-version }} + OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }} + OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }} + PACKAGE_NAME: "main" + permissions: + contents: write + defaults: + run: + working-directory: python + steps: + - uses: actions/checkout@v4 + - name: Set up uv + uses: astral-sh/setup-uv@v6 + with: + version: "0.7.x" + enable-cache: true + cache-suffix: ${{ runner.os }}-${{ matrix.python-version }} + cache-dependency-glob: "**/uv.lock" + - name: Install the project + run: | + uv sync --all-packages --all-extras --dev -U --prerelease=if-necessary-or-explicit + - name: Test with pytest + run: uv run poe --directory ./packages/${{ env.PACKAGE_NAME }} test --junitxml=coverage.xml + working-directory: ./python + - name: Move coverage file + run: | + mv ./packages/${{ env.PACKAGE_NAME }}/coverage.xml coverage_${{ env.PACKAGE_NAME }}.xml + working-directory: ./python + - name: Upload coverage artifact + uses: actions/upload-artifact@v4 + with: + name: coverage-${{ env.PACKAGE_NAME }} + path: ./python/coverage_${{ env.PACKAGE_NAME }}.xml + - name: Surface failing tests + if: always() + uses: pmeier/pytest-results-action@v0.7.2 + with: + path: ./python/**.xml + summary: true + display-options: fEX + fail-on-empty: true + title: Test results + + python-tests-azure: + name: Python Tests - Azure + runs-on: ${{ matrix.os }} + environment: "integration" + strategy: + fail-fast: true + matrix: + python-version: ["3.10"] + os: [ubuntu-latest] + env: + UV_PYTHON: ${{ matrix.python-version }} + AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREAI__DEPLOYMENTNAME }} + AZURE_OPENAI_ENDPOINT: ${{ secrets.AZUREAI__ENDPOINT }} + PACKAGE_NAME: "azure" + permissions: + contents: write + defaults: + run: + working-directory: python + steps: + - uses: actions/checkout@v4 + - name: Set up uv + uses: astral-sh/setup-uv@v6 + with: + version: "0.7.x" + enable-cache: true + cache-suffix: ${{ runner.os }}-${{ matrix.python-version }} + cache-dependency-glob: "**/uv.lock" + - name: Install the project + run: | + uv sync --all-packages --all-extras --dev -U --prerelease=if-necessary-or-explicit + - name: Azure CLI Login + if: github.event_name != 'pull_request' + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Test with pytest + run: uv run poe --directory ./packages/${{ env.PACKAGE_NAME }} test --junitxml=coverage.xml + working-directory: ./python + - name: Move coverage file + run: | + mv ./packages/${{ env.PACKAGE_NAME }}/coverage.xml coverage_${{ env.PACKAGE_NAME }}.xml + working-directory: ./python + - name: Upload coverage artifact + uses: actions/upload-artifact@v4 + with: + name: coverage-${{ env.PACKAGE_NAME }} + path: ./python/coverage_${{ env.PACKAGE_NAME }}.xml + - name: Surface failing tests + if: always() + uses: pmeier/pytest-results-action@v0.7.2 + with: + path: ./python/**.xml + summary: true + display-options: fEX + fail-on-empty: true + title: Test results + + python-tests-foundry: + name: Python Tests - Foundry + runs-on: ${{ matrix.os }} + environment: "integration" + strategy: + fail-fast: true + matrix: + python-version: ["3.10"] + os: [ubuntu-latest] + env: + UV_PYTHON: ${{ matrix.python-version }} + FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }} + FOUNDRY_MODEL_DEPLOYMENT_NAME: ${{ vars.FOUNDRY_MODEL_DEPLOYMENT_NAME }} + PACKAGE_NAME: "foundry" + permissions: + contents: write + defaults: + run: + working-directory: python + steps: + - uses: actions/checkout@v4 + - name: Set up uv + uses: astral-sh/setup-uv@v6 + with: + version: "0.7.x" + enable-cache: true + cache-suffix: ${{ runner.os }}-${{ matrix.python-version }} + cache-dependency-glob: "**/uv.lock" + - name: Install the project + run: | + uv sync --all-packages --all-extras --dev -U --prerelease=if-necessary-or-explicit + - name: Azure CLI Login + if: github.event_name != 'pull_request' + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Test with pytest + run: uv run poe --directory ./packages/${{ env.PACKAGE_NAME }} test --junitxml=coverage.xml + working-directory: ./python + - name: Move coverage file + run: | + mv ./packages/${{ env.PACKAGE_NAME }}/coverage.xml coverage_${{ env.PACKAGE_NAME }}.xml + working-directory: ./python + - name: Upload coverage artifact + uses: actions/upload-artifact@v4 + with: + name: coverage-${{ env.PACKAGE_NAME }} + path: ./python/coverage_${{ env.PACKAGE_NAME }}.xml + - name: Surface failing tests + if: always() + uses: pmeier/pytest-results-action@v0.7.2 + with: + path: ./python/**.xml + summary: true + display-options: fEX + fail-on-empty: true + title: Test results diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml new file mode 100644 index 0000000000..3267b08cea --- /dev/null +++ b/.github/workflows/python-tests.yml @@ -0,0 +1,111 @@ +name: Python - Tests + +on: + pull_request: + branches: ["main", "feature*"] + paths: + - "python/**" +env: + # Configure a constant location for the uv cache + UV_CACHE_DIR: /tmp/.uv-cache + +jobs: + python-tests: + name: Python Tests + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + os: [ubuntu-latest, windows-latest, macos-latest] + env: + UV_PYTHON: ${{ matrix.python-version }} + permissions: + contents: write + defaults: + run: + working-directory: python + steps: + - uses: actions/checkout@v4 + - name: Set up uv + uses: astral-sh/setup-uv@v6 + with: + version: "0.7.x" + enable-cache: true + cache-suffix: ${{ runner.os }}-${{ matrix.python-version }} + cache-dependency-glob: "**/uv.lock" + - name: Install the project + run: | + uv sync --all-packages --all-extras --dev -U --prerelease=if-necessary-or-explicit + # Main package tests + - name: Set environment variables - main - win + if: ${{ matrix.os == 'windows-latest' }} + run: | + echo "PACKAGE_NAME=main" | Out-File -FilePath $env:GITHUB_ENV -Append + - name: Set environment variables - main + if: ${{ matrix.os != 'windows-latest' }} + run: | + echo "PACKAGE_NAME=main" >> $GITHUB_ENV + - name: Test with pytest - main + run: uv run poe --directory ./packages/${{ env.PACKAGE_NAME }} test --junitxml=coverage.xml + working-directory: ./python + - name: Move coverage file - main + run: | + mv ./packages/${{ env.PACKAGE_NAME }}/coverage.xml coverage_${{ matrix.OS }}_${{ matrix.python-version }}_${{ env.PACKAGE_NAME }}.xml + working-directory: ./python + - name: Upload coverage artifact - main + uses: actions/upload-artifact@v4 + with: + name: coverage-${{ matrix.OS }}-${{ matrix.python-version }}-${{ env.PACKAGE_NAME }} + path: ./python/coverage_${{ matrix.OS }}_${{ matrix.python-version }}_${{ env.PACKAGE_NAME }}.xml + # Azure package tests + - name: Set environment variables - azure - win + if: ${{ matrix.os == 'windows-latest' }} + run: | + echo "PACKAGE_NAME=azure" | Out-File -FilePath $env:GITHUB_ENV -Append + - name: Set environment variables - azure + if: ${{ matrix.os != 'windows-latest' }} + run: | + echo "PACKAGE_NAME=azure" >> $GITHUB_ENV + - name: Test with pytest - azure + run: uv run poe --directory ./packages/${{ env.PACKAGE_NAME }} test --junitxml=coverage.xml + working-directory: ./python + - name: Move coverage file - azure + run: | + mv ./packages/${{ env.PACKAGE_NAME }}/coverage.xml coverage_${{ matrix.OS }}_${{ matrix.python-version }}_${{ env.PACKAGE_NAME }}.xml + working-directory: ./python + - name: Upload coverage artifact - azure + uses: actions/upload-artifact@v4 + with: + name: coverage-${{ matrix.OS }}-${{ matrix.python-version }}-${{ env.PACKAGE_NAME }} + path: ./python/coverage_${{ matrix.OS }}_${{ matrix.python-version }}_${{ env.PACKAGE_NAME }}.xml + # Foundry package tests + - name: Set environment variables - foundry - win + if: ${{ matrix.os == 'windows-latest' }} + run: | + echo "PACKAGE_NAME=foundry" | Out-File -FilePath $env:GITHUB_ENV -Append + - name: Set environment variables - foundry + if: ${{ matrix.os != 'windows-latest' }} + run: | + echo "PACKAGE_NAME=foundry" >> $GITHUB_ENV + - name: Test with pytest - foundry + run: uv run poe --directory ./packages/${{ env.PACKAGE_NAME }} test --junitxml=coverage.xml + working-directory: ./python + - name: Move coverage file - foundry + run: | + mv ./packages/${{ env.PACKAGE_NAME }}/coverage.xml coverage_${{ matrix.OS }}_${{ matrix.python-version }}_${{ env.PACKAGE_NAME }}.xml + working-directory: ./python + - name: Upload coverage artifact - foundry + uses: actions/upload-artifact@v4 + with: + name: coverage-${{ matrix.OS }}-${{ matrix.python-version }}-${{ env.PACKAGE_NAME }} + path: ./python/coverage_${{ matrix.OS }}_${{ matrix.python-version }}_${{ env.PACKAGE_NAME }}.xml + - name: Surface failing tests + if: always() + uses: pmeier/pytest-results-action@v0.7.2 + with: + path: ./python/**.xml + summary: true + display-options: fEX + fail-on-empty: true + title: Test results diff --git a/.github/workflows/python-unit-tests.yml b/.github/workflows/python-unit-tests.yml deleted file mode 100644 index cfdb162fe1..0000000000 --- a/.github/workflows/python-unit-tests.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Python - Unit Tests - -on: - pull_request: - branches: ["main", "feature*"] - paths: - - "python/**" -env: - # Configure a constant location for the uv cache - UV_CACHE_DIR: /tmp/.uv-cache - -jobs: - python-unit-tests: - name: Python Unit Tests - runs-on: ${{ matrix.os }} - strategy: - fail-fast: true - matrix: - python-version: ["3.10", "3.11", "3.12", "3.13"] - os: [ubuntu-latest, windows-latest, macos-latest] - env: - UV_PYTHON: ${{ matrix.python-version }} - permissions: - contents: write - defaults: - run: - working-directory: python - steps: - - uses: actions/checkout@v4 - - name: Set up uv - uses: astral-sh/setup-uv@v6 - with: - version: "0.7.x" - enable-cache: true - cache-suffix: ${{ runner.os }}-${{ matrix.python-version }} - cache-dependency-glob: "**/uv.lock" - - name: Install the project - run: uv sync --all-packages --all-extras --dev -U --prerelease=if-necessary-or-explicit - - name: Test with pytest - run: uv run --frozen poe test --junitxml=pytest.xml - - name: Surface failing tests - if: always() - uses: pmeier/pytest-results-action@v0.7.2 - with: - path: python/**/pytest.xml - summary: true - display-options: fEX - fail-on-empty: true - title: Test results diff --git a/python/packages/azure/tests/conftest.py b/python/packages/azure/tests/conftest.py index 816f9710ae..f15d0217cb 100644 --- a/python/packages/azure/tests/conftest.py +++ b/python/packages/azure/tests/conftest.py @@ -22,6 +22,7 @@ def override_env_param_dict(request: Any) -> dict[str, str]: @fixture() def azure_openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): # type: ignore """Fixture to set environment variables for AzureOpenAISettings.""" + if exclude_list is None: exclude_list = [] @@ -29,6 +30,7 @@ def azure_openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dic override_env_param_dict = {} env_vars = { + "AZURE_OPENAI_ENDPOINT": "https://test-endpoint.com", "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME": "test_chat_deployment", "AZURE_OPENAI_TEXT_DEPLOYMENT_NAME": "test_text_deployment", "AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME": "test_embedding_deployment", @@ -37,7 +39,6 @@ def azure_openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dic "AZURE_OPENAI_TEXT_TO_AUDIO_DEPLOYMENT_NAME": "test_text_to_audio_deployment", "AZURE_OPENAI_REALTIME_DEPLOYMENT_NAME": "test_realtime_deployment", "AZURE_OPENAI_API_KEY": "test_api_key", - "AZURE_OPENAI_ENDPOINT": "https://test-endpoint.com", "AZURE_OPENAI_API_VERSION": "2023-03-15-preview", "AZURE_OPENAI_BASE_URL": "https://test_text_deployment.test-base-url.com", "AZURE_OPENAI_TOKEN_ENDPOINT": "https://test-token-endpoint.com", @@ -46,10 +47,10 @@ def azure_openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dic env_vars.update(override_env_param_dict) # type: ignore for key, value in env_vars.items(): - if key not in exclude_list: - monkeypatch.setenv(key, value) # type: ignore - else: + if key in exclude_list: monkeypatch.delenv(key, raising=False) # type: ignore + continue + monkeypatch.setenv(key, value) # type: ignore return env_vars diff --git a/python/packages/azure/tests/integration/test_azure_chat_client.py b/python/packages/azure/tests/integration/test_azure_chat_client.py deleted file mode 100644 index e9610b5ee7..0000000000 --- a/python/packages/azure/tests/integration/test_azure_chat_client.py +++ /dev/null @@ -1,121 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -from agent_framework import ChatClient, ChatMessage, ChatResponse, ChatResponseUpdate, TextContent, ai_function - -from agent_framework_azure import AzureChatClient - - -@ai_function -def get_story_text() -> str: - """Returns a story about Emily and David.""" - return ( - "Emily and David, two passionate scientists, met during a research expedition to Antarctica. " - "Bonded by their love for the natural world and shared curiosity, they uncovered a " - "groundbreaking phenomenon in glaciology that could potentially reshape our understanding " - "of climate change." - ) - - -async def test_azure_openai_chat_client_response() -> None: - """Test Azure OpenAI chat completion responses.""" - azure_chat_client = AzureChatClient(deployment_name="gpt-4o") - - assert isinstance(azure_chat_client, ChatClient) - - messages: list[ChatMessage] = [] - messages.append( - ChatMessage( - role="user", - text="Emily and David, two passionate scientists, met during a research expedition to Antarctica. " - "Bonded by their love for the natural world and shared curiosity, they uncovered a " - "groundbreaking phenomenon in glaciology that could potentially reshape our understanding " - "of climate change.", - ) - ) - messages.append(ChatMessage(role="user", text="who are Emily and David?")) - - # Test that the client can be used to get a response - response = await azure_chat_client.get_response(messages=messages) - - assert response is not None - assert isinstance(response, ChatResponse) - assert "scientists" in response.text - - -async def test_azure_openai_chat_client_response_tools() -> None: - """Test AzureOpenAI chat completion responses.""" - azure_chat_client = AzureChatClient(deployment_name="gpt-4o") - - assert isinstance(azure_chat_client, ChatClient) - - messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="who are Emily and David?")) - - # Test that the client can be used to get a response - response = await azure_chat_client.get_response( - messages=messages, - tools=[get_story_text], - tool_choice="auto", - ) - - assert response is not None - assert isinstance(response, ChatResponse) - assert "scientists" in response.text - - -async def test_azure_openai_chat_client_streaming() -> None: - """Test Azure OpenAI chat completion responses.""" - azure_chat_client = AzureChatClient(deployment_name="gpt-4o") - - assert isinstance(azure_chat_client, ChatClient) - - messages: list[ChatMessage] = [] - messages.append( - ChatMessage( - role="user", - text="Emily and David, two passionate scientists, met during a research expedition to Antarctica. " - "Bonded by their love for the natural world and shared curiosity, they uncovered a " - "groundbreaking phenomenon in glaciology that could potentially reshape our understanding " - "of climate change.", - ) - ) - messages.append(ChatMessage(role="user", text="who are Emily and David?")) - - # Test that the client can be used to get a response - response = azure_chat_client.get_streaming_response(messages=messages) - - full_message: str = "" - async for chunk in response: - assert chunk is not None - assert isinstance(chunk, ChatResponseUpdate) - for content in chunk.contents: - if isinstance(content, TextContent) and content.text: - full_message += content.text - - assert "scientists" in full_message - - -async def test_azure_openai_chat_client_streaming_tools() -> None: - """Test AzureOpenAI chat completion responses.""" - azure_chat_client = AzureChatClient(deployment_name="gpt-4o") - - assert isinstance(azure_chat_client, ChatClient) - - messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="who are Emily and David?")) - - # Test that the client can be used to get a response - response = azure_chat_client.get_streaming_response( - messages=messages, - tools=[get_story_text], - tool_choice="auto", - ) - full_message: str = "" - async for chunk in response: - assert chunk is not None - assert isinstance(chunk, ChatResponseUpdate) - for content in chunk.contents: - if isinstance(content, TextContent) and content.text: - full_message += content.text - - assert "scientists" in full_message diff --git a/python/packages/azure/tests/unit/test_azure_chat_client.py b/python/packages/azure/tests/test_azure_chat_client.py similarity index 82% rename from python/packages/azure/tests/unit/test_azure_chat_client.py rename to python/packages/azure/tests/test_azure_chat_client.py index 23c03b75c9..614f7e8471 100644 --- a/python/packages/azure/tests/unit/test_azure_chat_client.py +++ b/python/packages/azure/tests/test_azure_chat_client.py @@ -7,11 +7,15 @@ from unittest.mock import AsyncMock, MagicMock, patch import openai import pytest from agent_framework import ( + ChatClient, ChatClientBase, ChatMessage, + ChatResponse, + ChatResponseUpdate, FunctionCallContent, FunctionResultContent, TextContent, + ai_function, ) from agent_framework.exceptions import ServiceInitializationError, ServiceResponseException from agent_framework.openai import ( @@ -32,6 +36,14 @@ from agent_framework_azure import AzureChatClient # region Service Setup +skip_if_azure_integration_tests_disabled = pytest.mark.skipif( + os.getenv("RUN_INTEGRATION_TESTS", "false").lower() != "true" + or os.getenv("AZURE_OPENAI_ENDPOINT", "") in ("", "https://test-endpoint.com"), + reason="No real AZURE_OPENAI_ENDPOINT provided; skipping integration tests." + if os.getenv("RUN_INTEGRATION_TESTS", "false").lower() == "true" + else "Integration tests are disabled.", +) + def test_init(azure_openai_unit_test_env: dict[str, str]) -> None: # Test successful initialization @@ -618,3 +630,123 @@ async def test_cmc_streaming( # To ensure consistency, we align the arguments here accordingly. stream_options={"include_usage": True}, ) + + +@ai_function +def get_story_text() -> str: + """Returns a story about Emily and David.""" + return ( + "Emily and David, two passionate scientists, met during a research expedition to Antarctica. " + "Bonded by their love for the natural world and shared curiosity, they uncovered a " + "groundbreaking phenomenon in glaciology that could potentially reshape our understanding " + "of climate change." + ) + + +@skip_if_azure_integration_tests_disabled +async def test_azure_openai_chat_client_response() -> None: + """Test Azure OpenAI chat completion responses.""" + azure_chat_client = AzureChatClient(deployment_name="gpt-4o") + + assert isinstance(azure_chat_client, ChatClient) + + messages: list[ChatMessage] = [] + messages.append( + ChatMessage( + role="user", + text="Emily and David, two passionate scientists, met during a research expedition to Antarctica. " + "Bonded by their love for the natural world and shared curiosity, they uncovered a " + "groundbreaking phenomenon in glaciology that could potentially reshape our understanding " + "of climate change.", + ) + ) + messages.append(ChatMessage(role="user", text="who are Emily and David?")) + + # Test that the client can be used to get a response + response = await azure_chat_client.get_response(messages=messages) + + assert response is not None + assert isinstance(response, ChatResponse) + assert "scientists" in response.text + + +@skip_if_azure_integration_tests_disabled +async def test_azure_openai_chat_client_response_tools() -> None: + """Test AzureOpenAI chat completion responses.""" + azure_chat_client = AzureChatClient(deployment_name="gpt-4o") + + assert isinstance(azure_chat_client, ChatClient) + + messages: list[ChatMessage] = [] + messages.append(ChatMessage(role="user", text="who are Emily and David?")) + + # Test that the client can be used to get a response + response = await azure_chat_client.get_response( + messages=messages, + tools=[get_story_text], + tool_choice="auto", + ) + + assert response is not None + assert isinstance(response, ChatResponse) + assert "scientists" in response.text + + +@skip_if_azure_integration_tests_disabled +async def test_azure_openai_chat_client_streaming() -> None: + """Test Azure OpenAI chat completion responses.""" + azure_chat_client = AzureChatClient(deployment_name="gpt-4o") + + assert isinstance(azure_chat_client, ChatClient) + + messages: list[ChatMessage] = [] + messages.append( + ChatMessage( + role="user", + text="Emily and David, two passionate scientists, met during a research expedition to Antarctica. " + "Bonded by their love for the natural world and shared curiosity, they uncovered a " + "groundbreaking phenomenon in glaciology that could potentially reshape our understanding " + "of climate change.", + ) + ) + messages.append(ChatMessage(role="user", text="who are Emily and David?")) + + # Test that the client can be used to get a response + response = azure_chat_client.get_streaming_response(messages=messages) + + full_message: str = "" + async for chunk in response: + assert chunk is not None + assert isinstance(chunk, ChatResponseUpdate) + for content in chunk.contents: + if isinstance(content, TextContent) and content.text: + full_message += content.text + + assert "scientists" in full_message + + +@skip_if_azure_integration_tests_disabled +async def test_azure_openai_chat_client_streaming_tools() -> None: + """Test AzureOpenAI chat completion responses.""" + azure_chat_client = AzureChatClient(deployment_name="gpt-4o") + + assert isinstance(azure_chat_client, ChatClient) + + messages: list[ChatMessage] = [] + messages.append(ChatMessage(role="user", text="who are Emily and David?")) + + # Test that the client can be used to get a response + response = azure_chat_client.get_streaming_response( + messages=messages, + tools=[get_story_text], + tool_choice="auto", + ) + full_message: str = "" + async for chunk in response: + assert chunk is not None + assert isinstance(chunk, ChatResponseUpdate) + for content in chunk.contents: + if isinstance(content, TextContent) and content.text: + full_message += content.text + + assert "scientists" in full_message diff --git a/python/packages/azure/tests/unit/test_cross_package.py b/python/packages/azure/tests/test_cross_package.py similarity index 100% rename from python/packages/azure/tests/unit/test_cross_package.py rename to python/packages/azure/tests/test_cross_package.py diff --git a/python/packages/azure/tests/unit/__init__.py b/python/packages/azure/tests/unit/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/python/packages/foundry/tests/conftest.py b/python/packages/foundry/tests/conftest.py index 6e29fc5595..cb204880ee 100644 --- a/python/packages/foundry/tests/conftest.py +++ b/python/packages/foundry/tests/conftest.py @@ -20,6 +20,7 @@ def override_env_param_dict(request: Any) -> dict[str, str]: @fixture() def foundry_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): # type: ignore """Fixture to set environment variables for FoundrySettings.""" + if exclude_list is None: exclude_list = [] @@ -35,10 +36,10 @@ def foundry_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): env_vars.update(override_env_param_dict) # type: ignore for key, value in env_vars.items(): - if key not in exclude_list: - monkeypatch.setenv(key, value) # type: ignore - else: + if key in exclude_list: monkeypatch.delenv(key, raising=False) # type: ignore + continue + monkeypatch.setenv(key, value) # type: ignore return env_vars diff --git a/python/packages/foundry/tests/integration/test_foundry_chat_client.py b/python/packages/foundry/tests/integration/test_foundry_chat_client.py deleted file mode 100644 index b54968f958..0000000000 --- a/python/packages/foundry/tests/integration/test_foundry_chat_client.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -from typing import Annotated - -from agent_framework import ChatClient, ChatMessage, ChatResponse, ChatResponseUpdate, TextContent -from pydantic import Field - -from agent_framework_foundry import FoundryChatClient - - -def get_weather( - location: Annotated[str, Field(description="The location to get the weather for.")], -) -> str: - """Get the weather for a given location.""" - return f"The weather in {location} is sunny with a high of 25°C." - - -async def test_foundry_chat_client_get_response() -> None: - """Test Foundry Chat Client response.""" - async with FoundryChatClient() as foundry_chat_client: - assert isinstance(foundry_chat_client, ChatClient) - - messages: list[ChatMessage] = [] - messages.append( - ChatMessage( - role="user", - text="The weather in Seattle is currently sunny with a high of 25°C. " - "It's a beautiful day for outdoor activities.", - ) - ) - messages.append(ChatMessage(role="user", text="What's the weather like today?")) - - # Test that the client can be used to get a response - response = await foundry_chat_client.get_response(messages=messages) - - assert response is not None - assert isinstance(response, ChatResponse) - assert any(word in response.text.lower() for word in ["sunny", "25"]) - - -async def test_foundry_chat_client_get_response_tools() -> None: - """Test Foundry Chat Client response with tools.""" - async with FoundryChatClient() as foundry_chat_client: - assert isinstance(foundry_chat_client, ChatClient) - - messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="What's the weather like in Seattle?")) - - # Test that the client can be used to get a response - response = await foundry_chat_client.get_response( - messages=messages, - tools=[get_weather], - tool_choice="auto", - ) - - assert response is not None - assert isinstance(response, ChatResponse) - assert any(word in response.text.lower() for word in ["sunny", "25"]) - - -async def test_foundry_chat_client_streaming() -> None: - """Test Foundry Chat Client streaming response.""" - async with FoundryChatClient() as foundry_chat_client: - assert isinstance(foundry_chat_client, ChatClient) - - messages: list[ChatMessage] = [] - messages.append( - ChatMessage( - role="user", - text="The weather in Seattle is currently sunny with a high of 25°C. " - "It's a beautiful day for outdoor activities.", - ) - ) - messages.append(ChatMessage(role="user", text="What's the weather like today?")) - - # Test that the client can be used to get a response - response = foundry_chat_client.get_streaming_response(messages=messages) - - full_message: str = "" - async for chunk in response: - assert chunk is not None - assert isinstance(chunk, ChatResponseUpdate) - for content in chunk.contents: - if isinstance(content, TextContent) and content.text: - full_message += content.text - - assert any(word in full_message.lower() for word in ["sunny", "25"]) - - -async def test_foundry_chat_client_streaming_tools() -> None: - """Test Foundry Chat Client streaming response with tools.""" - async with FoundryChatClient() as foundry_chat_client: - assert isinstance(foundry_chat_client, ChatClient) - - messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="What's the weather like in Seattle?")) - - # Test that the client can be used to get a response - response = foundry_chat_client.get_streaming_response( - messages=messages, - tools=[get_weather], - tool_choice="auto", - ) - full_message: str = "" - async for chunk in response: - assert chunk is not None - assert isinstance(chunk, ChatResponseUpdate) - for content in chunk.contents: - if isinstance(content, TextContent) and content.text: - full_message += content.text - - assert any(word in full_message.lower() for word in ["sunny", "25"]) diff --git a/python/packages/foundry/tests/unit/test_cross_package.py b/python/packages/foundry/tests/test_cross_package.py similarity index 100% rename from python/packages/foundry/tests/unit/test_cross_package.py rename to python/packages/foundry/tests/test_cross_package.py diff --git a/python/packages/foundry/tests/unit/test_foundry_chat_client.py b/python/packages/foundry/tests/test_foundry_chat_client.py similarity index 67% rename from python/packages/foundry/tests/unit/test_foundry_chat_client.py rename to python/packages/foundry/tests/test_foundry_chat_client.py index 7764b81433..450e657331 100644 --- a/python/packages/foundry/tests/unit/test_foundry_chat_client.py +++ b/python/packages/foundry/tests/test_foundry_chat_client.py @@ -1,13 +1,32 @@ # Copyright (c) Microsoft. All rights reserved. +import os +from typing import Annotated from unittest.mock import MagicMock import pytest -from agent_framework import ChatClient, ChatMessage, ChatOptions, ChatRole +from agent_framework import ( + ChatClient, + ChatMessage, + ChatOptions, + ChatResponse, + ChatResponseUpdate, + ChatRole, + TextContent, +) from agent_framework.exceptions import ServiceInitializationError +from pydantic import Field from agent_framework_foundry import FoundryChatClient, FoundrySettings +skip_if_foundry_integration_tests_disabled = pytest.mark.skipif( + os.getenv("RUN_INTEGRATION_TESTS", "false").lower() != "true" + or os.getenv("FOUNDRY_PROJECT_ENDPOINT", "") in ("", "https://test-project.cognitiveservices.azure.com/"), + reason="No real FOUNDRY_PROJECT_ENDPOINT provided; skipping integration tests." + if os.getenv("RUN_INTEGRATION_TESTS", "false").lower() == "true" + else "Integration tests are disabled.", +) + def create_test_foundry_chat_client( mock_ai_project_client: MagicMock, @@ -18,7 +37,7 @@ def create_test_foundry_chat_client( ) -> FoundryChatClient: """Helper function to create FoundryChatClient instances for testing, bypassing Pydantic validation.""" if foundry_settings is None: - foundry_settings = FoundrySettings() + foundry_settings = FoundrySettings(env_file_path="test.env") return FoundryChatClient.model_construct( client=mock_ai_project_client, @@ -140,8 +159,9 @@ async def test_foundry_chat_client_get_agent_id_or_create_create_new( assert chat_client._should_delete_agent # type: ignore +@pytest.mark.parametrize("exclude_list", [["FOUNDRY_MODEL_DEPLOYMENT_NAME"]], indirect=True) async def test_foundry_chat_client_get_agent_id_or_create_missing_model( - mock_ai_project_client: MagicMock, + mock_ai_project_client: MagicMock, foundry_unit_test_env: dict[str, str] ) -> None: """Test _get_agent_id_or_create when model_deployment_name is missing.""" chat_client = create_test_foundry_chat_client(mock_ai_project_client) @@ -270,3 +290,111 @@ def test_foundry_chat_client_convert_function_results_to_tool_output_none(mock_a assert run_id is None assert tool_outputs is None + + +def get_weather( + location: Annotated[str, Field(description="The location to get the weather for.")], +) -> str: + """Get the weather for a given location.""" + return f"The weather in {location} is sunny with a high of 25°C." + + +@skip_if_foundry_integration_tests_disabled +async def test_foundry_chat_client_get_response() -> None: + """Test Foundry Chat Client response.""" + async with FoundryChatClient() as foundry_chat_client: + assert isinstance(foundry_chat_client, ChatClient) + + messages: list[ChatMessage] = [] + messages.append( + ChatMessage( + role="user", + text="The weather in Seattle is currently sunny with a high of 25°C. " + "It's a beautiful day for outdoor activities.", + ) + ) + messages.append(ChatMessage(role="user", text="What's the weather like today?")) + + # Test that the client can be used to get a response + response = await foundry_chat_client.get_response(messages=messages) + + assert response is not None + assert isinstance(response, ChatResponse) + assert any(word in response.text.lower() for word in ["sunny", "25"]) + + +@skip_if_foundry_integration_tests_disabled +async def test_foundry_chat_client_get_response_tools() -> None: + """Test Foundry Chat Client response with tools.""" + async with FoundryChatClient() as foundry_chat_client: + assert isinstance(foundry_chat_client, ChatClient) + + messages: list[ChatMessage] = [] + messages.append(ChatMessage(role="user", text="What's the weather like in Seattle?")) + + # Test that the client can be used to get a response + response = await foundry_chat_client.get_response( + messages=messages, + tools=[get_weather], + tool_choice="auto", + ) + + assert response is not None + assert isinstance(response, ChatResponse) + assert any(word in response.text.lower() for word in ["sunny", "25"]) + + +@skip_if_foundry_integration_tests_disabled +async def test_foundry_chat_client_streaming() -> None: + """Test Foundry Chat Client streaming response.""" + async with FoundryChatClient() as foundry_chat_client: + assert isinstance(foundry_chat_client, ChatClient) + + messages: list[ChatMessage] = [] + messages.append( + ChatMessage( + role="user", + text="The weather in Seattle is currently sunny with a high of 25°C. " + "It's a beautiful day for outdoor activities.", + ) + ) + messages.append(ChatMessage(role="user", text="What's the weather like today?")) + + # Test that the client can be used to get a response + response = foundry_chat_client.get_streaming_response(messages=messages) + + full_message: str = "" + async for chunk in response: + assert chunk is not None + assert isinstance(chunk, ChatResponseUpdate) + for content in chunk.contents: + if isinstance(content, TextContent) and content.text: + full_message += content.text + + assert any(word in full_message.lower() for word in ["sunny", "25"]) + + +@skip_if_foundry_integration_tests_disabled +async def test_foundry_chat_client_streaming_tools() -> None: + """Test Foundry Chat Client streaming response with tools.""" + async with FoundryChatClient() as foundry_chat_client: + assert isinstance(foundry_chat_client, ChatClient) + + messages: list[ChatMessage] = [] + messages.append(ChatMessage(role="user", text="What's the weather like in Seattle?")) + + # Test that the client can be used to get a response + response = foundry_chat_client.get_streaming_response( + messages=messages, + tools=[get_weather], + tool_choice="auto", + ) + full_message: str = "" + async for chunk in response: + assert chunk is not None + assert isinstance(chunk, ChatResponseUpdate) + for content in chunk.contents: + if isinstance(content, TextContent) and content.text: + full_message += content.text + + assert any(word in full_message.lower() for word in ["sunny", "25"]) diff --git a/python/packages/main/tests/conftest.py b/python/packages/main/tests/conftest.py index e10a44007e..38b81d39c3 100644 --- a/python/packages/main/tests/conftest.py +++ b/python/packages/main/tests/conftest.py @@ -1,57 +1,42 @@ # Copyright (c) Microsoft. All rights reserved. from typing import Any +from pydantic import BaseModel from pytest import fixture -from agent_framework import ChatMessage - - -# region: Connector Settings fixtures -@fixture -def exclude_list(request: Any) -> list[str]: - """Fixture that returns a list of environment variables to exclude.""" - return request.param if hasattr(request, "param") else [] - - -@fixture -def override_env_param_dict(request: Any) -> dict[str, str]: - """Fixture that returns a dict of environment variables to override.""" - return request.param if hasattr(request, "param") else {} - - -@fixture() -def openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): # type: ignore - """Fixture to set environment variables for OpenAISettings.""" - if exclude_list is None: - exclude_list = [] - - if override_env_param_dict is None: - override_env_param_dict = {} - - env_vars = { - "OPENAI_API_KEY": "test_api_key", - "OPENAI_ORG_ID": "test_org_id", - "OPENAI_RESPONSES_MODEL_ID": "test_responses_model_id", - "OPENAI_CHAT_MODEL_ID": "test_chat_model_id", - "OPENAI_TEXT_MODEL_ID": "test_text_model_id", - "OPENAI_EMBEDDING_MODEL_ID": "test_embedding_model_id", - "OPENAI_TEXT_TO_IMAGE_MODEL_ID": "test_text_to_image_model_id", - "OPENAI_AUDIO_TO_TEXT_MODEL_ID": "test_audio_to_text_model_id", - "OPENAI_TEXT_TO_AUDIO_MODEL_ID": "test_text_to_audio_model_id", - "OPENAI_REALTIME_MODEL_ID": "test_realtime_model_id", - } - - env_vars.update(override_env_param_dict) # type: ignore - - for key, value in env_vars.items(): - if key not in exclude_list: - monkeypatch.setenv(key, value) # type: ignore - else: - monkeypatch.delenv(key, raising=False) # type: ignore - - return env_vars +from agent_framework import AITool, ChatMessage, ai_function @fixture(scope="function") def chat_history() -> list[ChatMessage]: return [] + + +@fixture +def ai_tool() -> AITool: + """Returns a generic AITool.""" + + class GenericTool(BaseModel): + name: str + description: str | None = None + additional_properties: dict[str, Any] | None = None + + def parameters(self) -> dict[str, Any]: + """Return the parameters of the tool as a JSON schema.""" + return { + "name": {"type": "string"}, + } + + return GenericTool(name="generic_tool", description="A generic tool") + + +@fixture +def ai_function_tool() -> AITool: + """Returns a executable AITool.""" + + @ai_function + def simple_function(x: int, y: int) -> int: + """A simple function that adds two numbers.""" + return x + y + + return simple_function diff --git a/python/packages/main/tests/integration/test_openai_chat_client.py b/python/packages/main/tests/integration/test_openai_chat_client.py deleted file mode 100644 index 1d57e367c2..0000000000 --- a/python/packages/main/tests/integration/test_openai_chat_client.py +++ /dev/null @@ -1,120 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -from agent_framework import ChatClient, ChatMessage, ChatResponse, ChatResponseUpdate, TextContent, ai_function -from agent_framework.openai import OpenAIChatClient - - -@ai_function -def get_story_text() -> str: - """Returns a story about Emily and David.""" - return ( - "Emily and David, two passionate scientists, met during a research expedition to Antarctica. " - "Bonded by their love for the natural world and shared curiosity, they uncovered a " - "groundbreaking phenomenon in glaciology that could potentially reshape our understanding " - "of climate change." - ) - - -async def test_openai_chat_completion_response() -> None: - """Test OpenAI chat completion responses.""" - openai_chat_client = OpenAIChatClient(ai_model_id="gpt-4.1-mini") - - assert isinstance(openai_chat_client, ChatClient) - - messages: list[ChatMessage] = [] - messages.append( - ChatMessage( - role="user", - text="Emily and David, two passionate scientists, met during a research expedition to Antarctica. " - "Bonded by their love for the natural world and shared curiosity, they uncovered a " - "groundbreaking phenomenon in glaciology that could potentially reshape our understanding " - "of climate change.", - ) - ) - messages.append(ChatMessage(role="user", text="who are Emily and David?")) - - # Test that the client can be used to get a response - response = await openai_chat_client.get_response(messages=messages) - - assert response is not None - assert isinstance(response, ChatResponse) - assert "scientists" in response.text - - -async def test_openai_chat_completion_response_tools() -> None: - """Test OpenAI chat completion responses.""" - openai_chat_client = OpenAIChatClient(ai_model_id="gpt-4.1-mini") - - assert isinstance(openai_chat_client, ChatClient) - - messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="who are Emily and David?")) - - # Test that the client can be used to get a response - response = await openai_chat_client.get_response( - messages=messages, - tools=[get_story_text], - tool_choice="auto", - ) - - assert response is not None - assert isinstance(response, ChatResponse) - assert "scientists" in response.text - - -async def test_openai_chat_client_streaming() -> None: - """Test Azure OpenAI chat completion responses.""" - openai_chat_client = OpenAIChatClient(ai_model_id="gpt-4.1-mini") - - assert isinstance(openai_chat_client, ChatClient) - - messages: list[ChatMessage] = [] - messages.append( - ChatMessage( - role="user", - text="Emily and David, two passionate scientists, met during a research expedition to Antarctica. " - "Bonded by their love for the natural world and shared curiosity, they uncovered a " - "groundbreaking phenomenon in glaciology that could potentially reshape our understanding " - "of climate change.", - ) - ) - messages.append(ChatMessage(role="user", text="who are Emily and David?")) - - # Test that the client can be used to get a response - response = openai_chat_client.get_streaming_response(messages=messages) - - full_message: str = "" - async for chunk in response: - assert chunk is not None - assert isinstance(chunk, ChatResponseUpdate) - for content in chunk.contents: - if isinstance(content, TextContent) and content.text: - full_message += content.text - - assert "scientists" in full_message - - -async def test_openai_chat_client_streaming_tools() -> None: - """Test AzureOpenAI chat completion responses.""" - openai_chat_client = OpenAIChatClient(ai_model_id="gpt-4.1-mini") - - assert isinstance(openai_chat_client, ChatClient) - - messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="who are Emily and David?")) - - # Test that the client can be used to get a response - response = openai_chat_client.get_streaming_response( - messages=messages, - tools=[get_story_text], - tool_choice="auto", - ) - full_message: str = "" - async for chunk in response: - assert chunk is not None - assert isinstance(chunk, ChatResponseUpdate) - for content in chunk.contents: - if isinstance(content, TextContent) and content.text: - full_message += content.text - - assert "scientists" in full_message diff --git a/python/packages/main/tests/openai/conftest.py b/python/packages/main/tests/openai/conftest.py new file mode 100644 index 0000000000..93f671256e --- /dev/null +++ b/python/packages/main/tests/openai/conftest.py @@ -0,0 +1,51 @@ +# Copyright (c) Microsoft. All rights reserved. +from typing import Any + +from pytest import fixture + + +# region: Connector Settings fixtures +@fixture +def exclude_list(request: Any) -> list[str]: + """Fixture that returns a list of environment variables to exclude.""" + return request.param if hasattr(request, "param") else [] + + +@fixture +def override_env_param_dict(request: Any) -> dict[str, str]: + """Fixture that returns a dict of environment variables to override.""" + return request.param if hasattr(request, "param") else {} + + +@fixture() +def openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): # type: ignore + """Fixture to set environment variables for OpenAISettings.""" + + if exclude_list is None: + exclude_list = [] + + if override_env_param_dict is None: + override_env_param_dict = {} + + env_vars = { + "OPENAI_API_KEY": "test-dummy-key", + "OPENAI_ORG_ID": "test_org_id", + "OPENAI_RESPONSES_MODEL_ID": "test_responses_model_id", + "OPENAI_CHAT_MODEL_ID": "test_chat_model_id", + "OPENAI_TEXT_MODEL_ID": "test_text_model_id", + "OPENAI_EMBEDDING_MODEL_ID": "test_embedding_model_id", + "OPENAI_TEXT_TO_IMAGE_MODEL_ID": "test_text_to_image_model_id", + "OPENAI_AUDIO_TO_TEXT_MODEL_ID": "test_audio_to_text_model_id", + "OPENAI_TEXT_TO_AUDIO_MODEL_ID": "test_text_to_audio_model_id", + "OPENAI_REALTIME_MODEL_ID": "test_realtime_model_id", + } + + env_vars.update(override_env_param_dict) # type: ignore + + for key, value in env_vars.items(): + if key in exclude_list: + monkeypatch.delenv(key, raising=False) # type: ignore + continue + monkeypatch.setenv(key, value) # type: ignore + + return env_vars diff --git a/python/packages/main/tests/openai/test_openai_chat_client.py b/python/packages/main/tests/openai/test_openai_chat_client.py new file mode 100644 index 0000000000..3f3f0c4ecb --- /dev/null +++ b/python/packages/main/tests/openai/test_openai_chat_client.py @@ -0,0 +1,233 @@ +# Copyright (c) Microsoft. All rights reserved. + +import os + +import pytest + +from agent_framework import ChatClient, ChatMessage, ChatResponse, ChatResponseUpdate, TextContent, ai_function +from agent_framework.exceptions import ServiceInitializationError +from agent_framework.openai import OpenAIChatClient + +skip_if_openai_integration_tests_disabled = pytest.mark.skipif( + os.getenv("RUN_INTEGRATION_TESTS", "false").lower() != "true" + or os.getenv("OPENAI_API_KEY", "") in ("", "test-dummy-key"), + reason="No real OPENAI_API_KEY provided; skipping integration tests." + if os.getenv("RUN_INTEGRATION_TESTS", "false").lower() == "true" + else "Integration tests are disabled.", +) + + +def test_init(openai_unit_test_env: dict[str, str]) -> None: + # Test successful initialization + open_ai_chat_completion = OpenAIChatClient() + + assert open_ai_chat_completion.ai_model_id == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"] + assert isinstance(open_ai_chat_completion, ChatClient) + + +def test_init_validation_fail() -> None: + # Test successful initialization + with pytest.raises(ServiceInitializationError): + OpenAIChatClient(api_key="34523", ai_model_id={"test": "dict"}) # type: ignore + + +def test_init_ai_model_id_constructor(openai_unit_test_env: dict[str, str]) -> None: + # Test successful initialization + ai_model_id = "test_model_id" + open_ai_chat_completion = OpenAIChatClient(ai_model_id=ai_model_id) + + assert open_ai_chat_completion.ai_model_id == ai_model_id + assert isinstance(open_ai_chat_completion, ChatClient) + + +def test_init_with_default_header(openai_unit_test_env: dict[str, str]) -> None: + default_headers = {"X-Unit-Test": "test-guid"} + + # Test successful initialization + open_ai_chat_completion = OpenAIChatClient( + default_headers=default_headers, + ) + + assert open_ai_chat_completion.ai_model_id == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"] + assert isinstance(open_ai_chat_completion, ChatClient) + + # Assert that the default header we added is present in the client's default headers + for key, value in default_headers.items(): + assert key in open_ai_chat_completion.client.default_headers + assert open_ai_chat_completion.client.default_headers[key] == value + + +@pytest.mark.parametrize("exclude_list", [["OPENAI_CHAT_MODEL_ID"]], indirect=True) +def test_init_with_empty_model_id(openai_unit_test_env: dict[str, str]) -> None: + with pytest.raises(ServiceInitializationError): + OpenAIChatClient( + env_file_path="test.env", + ) + + +@pytest.mark.parametrize("exclude_list", [["OPENAI_API_KEY"]], indirect=True) +def test_init_with_empty_api_key(openai_unit_test_env: dict[str, str]) -> None: + ai_model_id = "test_model_id" + + with pytest.raises(ServiceInitializationError): + OpenAIChatClient( + ai_model_id=ai_model_id, + env_file_path="test.env", + ) + + +def test_serialize(openai_unit_test_env: dict[str, str]) -> None: + default_headers = {"X-Unit-Test": "test-guid"} + + settings = { + "ai_model_id": openai_unit_test_env["OPENAI_CHAT_MODEL_ID"], + "api_key": openai_unit_test_env["OPENAI_API_KEY"], + "default_headers": default_headers, + } + + open_ai_chat_completion = OpenAIChatClient.from_dict(settings) + dumped_settings = open_ai_chat_completion.to_dict() + assert dumped_settings["ai_model_id"] == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"] + assert dumped_settings["api_key"] == openai_unit_test_env["OPENAI_API_KEY"] + # Assert that the default header we added is present in the dumped_settings default headers + for key, value in default_headers.items(): + assert key in dumped_settings["default_headers"] + assert dumped_settings["default_headers"][key] == value + # Assert that the 'User-Agent' header is not present in the dumped_settings default headers + assert "User-Agent" not in dumped_settings["default_headers"] + + +def test_serialize_with_org_id(openai_unit_test_env: dict[str, str]) -> None: + settings = { + "ai_model_id": openai_unit_test_env["OPENAI_CHAT_MODEL_ID"], + "api_key": openai_unit_test_env["OPENAI_API_KEY"], + "org_id": openai_unit_test_env["OPENAI_ORG_ID"], + } + + open_ai_chat_completion = OpenAIChatClient.from_dict(settings) + dumped_settings = open_ai_chat_completion.to_dict() + assert dumped_settings["ai_model_id"] == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"] + assert dumped_settings["api_key"] == openai_unit_test_env["OPENAI_API_KEY"] + assert dumped_settings["org_id"] == openai_unit_test_env["OPENAI_ORG_ID"] + # Assert that the 'User-Agent' header is not present in the dumped_settings default headers + assert "User-Agent" not in dumped_settings["default_headers"] + + +@ai_function +def get_story_text() -> str: + """Returns a story about Emily and David.""" + return ( + "Emily and David, two passionate scientists, met during a research expedition to Antarctica. " + "Bonded by their love for the natural world and shared curiosity, they uncovered a " + "groundbreaking phenomenon in glaciology that could potentially reshape our understanding " + "of climate change." + ) + + +@skip_if_openai_integration_tests_disabled +async def test_openai_chat_completion_response() -> None: + """Test OpenAI chat completion responses.""" + openai_chat_client = OpenAIChatClient(ai_model_id="gpt-4.1-mini") + + assert isinstance(openai_chat_client, ChatClient) + + messages: list[ChatMessage] = [] + messages.append( + ChatMessage( + role="user", + text="Emily and David, two passionate scientists, met during a research expedition to Antarctica. " + "Bonded by their love for the natural world and shared curiosity, they uncovered a " + "groundbreaking phenomenon in glaciology that could potentially reshape our understanding " + "of climate change.", + ) + ) + messages.append(ChatMessage(role="user", text="who are Emily and David?")) + + # Test that the client can be used to get a response + response = await openai_chat_client.get_response(messages=messages) + + assert response is not None + assert isinstance(response, ChatResponse) + assert "scientists" in response.text + + +@skip_if_openai_integration_tests_disabled +async def test_openai_chat_completion_response_tools() -> None: + """Test OpenAI chat completion responses.""" + openai_chat_client = OpenAIChatClient(ai_model_id="gpt-4.1-mini") + + assert isinstance(openai_chat_client, ChatClient) + + messages: list[ChatMessage] = [] + messages.append(ChatMessage(role="user", text="who are Emily and David?")) + + # Test that the client can be used to get a response + response = await openai_chat_client.get_response( + messages=messages, + tools=[get_story_text], + tool_choice="auto", + ) + + assert response is not None + assert isinstance(response, ChatResponse) + assert "scientists" in response.text + + +@skip_if_openai_integration_tests_disabled +async def test_openai_chat_client_streaming() -> None: + """Test Azure OpenAI chat completion responses.""" + openai_chat_client = OpenAIChatClient(ai_model_id="gpt-4.1-mini") + + assert isinstance(openai_chat_client, ChatClient) + + messages: list[ChatMessage] = [] + messages.append( + ChatMessage( + role="user", + text="Emily and David, two passionate scientists, met during a research expedition to Antarctica. " + "Bonded by their love for the natural world and shared curiosity, they uncovered a " + "groundbreaking phenomenon in glaciology that could potentially reshape our understanding " + "of climate change.", + ) + ) + messages.append(ChatMessage(role="user", text="who are Emily and David?")) + + # Test that the client can be used to get a response + response = openai_chat_client.get_streaming_response(messages=messages) + + full_message: str = "" + async for chunk in response: + assert chunk is not None + assert isinstance(chunk, ChatResponseUpdate) + for content in chunk.contents: + if isinstance(content, TextContent) and content.text: + full_message += content.text + + assert "scientists" in full_message + + +@skip_if_openai_integration_tests_disabled +async def test_openai_chat_client_streaming_tools() -> None: + """Test AzureOpenAI chat completion responses.""" + openai_chat_client = OpenAIChatClient(ai_model_id="gpt-4.1-mini") + + assert isinstance(openai_chat_client, ChatClient) + + messages: list[ChatMessage] = [] + messages.append(ChatMessage(role="user", text="who are Emily and David?")) + + # Test that the client can be used to get a response + response = openai_chat_client.get_streaming_response( + messages=messages, + tools=[get_story_text], + tool_choice="auto", + ) + full_message: str = "" + async for chunk in response: + assert chunk is not None + assert isinstance(chunk, ChatResponseUpdate) + for content in chunk.contents: + if isinstance(content, TextContent) and content.text: + full_message += content.text + + assert "scientists" in full_message diff --git a/python/packages/main/tests/unit/test_openai_chat_client_base.py b/python/packages/main/tests/openai/test_openai_chat_client_base.py similarity index 100% rename from python/packages/main/tests/unit/test_openai_chat_client_base.py rename to python/packages/main/tests/openai/test_openai_chat_client_base.py diff --git a/python/packages/main/tests/unit/test_agents.py b/python/packages/main/tests/test_agents.py similarity index 100% rename from python/packages/main/tests/unit/test_agents.py rename to python/packages/main/tests/test_agents.py diff --git a/python/packages/main/tests/unit/test_clients.py b/python/packages/main/tests/test_clients.py similarity index 100% rename from python/packages/main/tests/unit/test_clients.py rename to python/packages/main/tests/test_clients.py diff --git a/python/packages/main/tests/unit/test_logging.py b/python/packages/main/tests/test_logging.py similarity index 100% rename from python/packages/main/tests/unit/test_logging.py rename to python/packages/main/tests/test_logging.py diff --git a/python/packages/main/tests/unit/test_tool.py b/python/packages/main/tests/test_tool.py similarity index 100% rename from python/packages/main/tests/unit/test_tool.py rename to python/packages/main/tests/test_tool.py diff --git a/python/packages/main/tests/unit/test_types.py b/python/packages/main/tests/test_types.py similarity index 100% rename from python/packages/main/tests/unit/test_types.py rename to python/packages/main/tests/test_types.py diff --git a/python/packages/main/tests/unit/__init__.py b/python/packages/main/tests/unit/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/python/packages/main/tests/unit/conftest.py b/python/packages/main/tests/unit/conftest.py deleted file mode 100644 index 905f4d9418..0000000000 --- a/python/packages/main/tests/unit/conftest.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -from typing import Any - -from pydantic import BaseModel -from pytest import fixture - -from agent_framework import AITool, ai_function - - -@fixture -def ai_tool() -> AITool: - """Returns a generic AITool.""" - - class GenericTool(BaseModel): - name: str - description: str | None = None - additional_properties: dict[str, Any] | None = None - - def parameters(self) -> dict[str, Any]: - """Return the parameters of the tool as a JSON schema.""" - return { - "name": {"type": "string"}, - } - - return GenericTool(name="generic_tool", description="A generic tool") - - -@fixture -def ai_function_tool() -> AITool: - """Returns a executable AITool.""" - - @ai_function - def simple_function(x: int, y: int) -> int: - """A simple function that adds two numbers.""" - return x + y - - return simple_function diff --git a/python/packages/main/tests/unit/test_cross_package.py b/python/packages/main/tests/unit/test_cross_package.py deleted file mode 100644 index ad23e42a41..0000000000 --- a/python/packages/main/tests/unit/test_cross_package.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - - -def test_azure(): - try: - from agent_framework.azure import __version__ - except ImportError: - __version__ = None - assert __version__ is not None diff --git a/python/packages/main/tests/unit/test_openai_chat_client.py b/python/packages/main/tests/unit/test_openai_chat_client.py deleted file mode 100644 index 1ee4e9361a..0000000000 --- a/python/packages/main/tests/unit/test_openai_chat_client.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import pytest - -from agent_framework import ChatClient -from agent_framework.exceptions import ServiceInitializationError -from agent_framework.openai import OpenAIChatClient - - -def test_init(openai_unit_test_env: dict[str, str]) -> None: - # Test successful initialization - open_ai_chat_completion = OpenAIChatClient() - - assert open_ai_chat_completion.ai_model_id == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"] - assert isinstance(open_ai_chat_completion, ChatClient) - - -def test_init_validation_fail() -> None: - # Test successful initialization - with pytest.raises(ServiceInitializationError): - OpenAIChatClient(api_key="34523", ai_model_id={"test": "dict"}) # type: ignore - - -def test_init_ai_model_id_constructor(openai_unit_test_env: dict[str, str]) -> None: - # Test successful initialization - ai_model_id = "test_model_id" - open_ai_chat_completion = OpenAIChatClient(ai_model_id=ai_model_id) - - assert open_ai_chat_completion.ai_model_id == ai_model_id - assert isinstance(open_ai_chat_completion, ChatClient) - - -def test_init_with_default_header(openai_unit_test_env: dict[str, str]) -> None: - default_headers = {"X-Unit-Test": "test-guid"} - - # Test successful initialization - open_ai_chat_completion = OpenAIChatClient( - default_headers=default_headers, - ) - - assert open_ai_chat_completion.ai_model_id == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"] - assert isinstance(open_ai_chat_completion, ChatClient) - - # Assert that the default header we added is present in the client's default headers - for key, value in default_headers.items(): - assert key in open_ai_chat_completion.client.default_headers - assert open_ai_chat_completion.client.default_headers[key] == value - - -@pytest.mark.parametrize("exclude_list", [["OPENAI_CHAT_MODEL_ID"]], indirect=True) -def test_init_with_empty_model_id(openai_unit_test_env: dict[str, str]) -> None: - with pytest.raises(ServiceInitializationError): - OpenAIChatClient( - env_file_path="test.env", - ) - - -@pytest.mark.parametrize("exclude_list", [["OPENAI_API_KEY"]], indirect=True) -def test_init_with_empty_api_key(openai_unit_test_env: dict[str, str]) -> None: - ai_model_id = "test_model_id" - - with pytest.raises(ServiceInitializationError): - OpenAIChatClient( - ai_model_id=ai_model_id, - env_file_path="test.env", - ) - - -def test_serialize(openai_unit_test_env: dict[str, str]) -> None: - default_headers = {"X-Unit-Test": "test-guid"} - - settings = { - "ai_model_id": openai_unit_test_env["OPENAI_CHAT_MODEL_ID"], - "api_key": openai_unit_test_env["OPENAI_API_KEY"], - "default_headers": default_headers, - } - - open_ai_chat_completion = OpenAIChatClient.from_dict(settings) - dumped_settings = open_ai_chat_completion.to_dict() - assert dumped_settings["ai_model_id"] == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"] - assert dumped_settings["api_key"] == openai_unit_test_env["OPENAI_API_KEY"] - # Assert that the default header we added is present in the dumped_settings default headers - for key, value in default_headers.items(): - assert key in dumped_settings["default_headers"] - assert dumped_settings["default_headers"][key] == value - # Assert that the 'User-Agent' header is not present in the dumped_settings default headers - assert "User-Agent" not in dumped_settings["default_headers"] - - -def test_serialize_with_org_id(openai_unit_test_env: dict[str, str]) -> None: - settings = { - "ai_model_id": openai_unit_test_env["OPENAI_CHAT_MODEL_ID"], - "api_key": openai_unit_test_env["OPENAI_API_KEY"], - "org_id": openai_unit_test_env["OPENAI_ORG_ID"], - } - - open_ai_chat_completion = OpenAIChatClient.from_dict(settings) - dumped_settings = open_ai_chat_completion.to_dict() - assert dumped_settings["ai_model_id"] == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"] - assert dumped_settings["api_key"] == openai_unit_test_env["OPENAI_API_KEY"] - assert dumped_settings["org_id"] == openai_unit_test_env["OPENAI_ORG_ID"] - # Assert that the 'User-Agent' header is not present in the dumped_settings default headers - assert "User-Agent" not in dumped_settings["default_headers"] diff --git a/python/packages/main/tests/unit/test_version.py b/python/packages/main/tests/unit/test_version.py deleted file mode 100644 index 56bc0d9acb..0000000000 --- a/python/packages/main/tests/unit/test_version.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - - -from agent_framework import __version__ - - -def test_version(): - assert __version__ is not None diff --git a/python/shared_tasks.toml b/python/shared_tasks.toml index ba4c972a25..9448be94d4 100644 --- a/python/shared_tasks.toml +++ b/python/shared_tasks.toml @@ -5,4 +5,4 @@ lint = "ruff check" mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework" pyright = "pyright" build = "uv build" -test = "pytest --cov=agent_framework --cov-report=term-missing:skip-covered tests/unit" \ No newline at end of file +test = "pytest --cov=agent_framework --cov-report=term-missing:skip-covered tests" diff --git a/python/uv.lock b/python/uv.lock index 64e4846db4..ac1a17c89a 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -635,7 +635,7 @@ wheels = [ [[package]] name = "azure-storage-blob" -version = "12.25.1" +version = "12.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -643,9 +643,9 @@ dependencies = [ { name = "isodate", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/f3/f764536c25cc3829d36857167f03933ce9aee2262293179075439f3cd3ad/azure_storage_blob-12.25.1.tar.gz", hash = "sha256:4f294ddc9bc47909ac66b8934bd26b50d2000278b10ad82cc109764fdc6e0e3b", size = 570541, upload-time = "2025-03-27T17:13:05.424Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/95/3e3414491ce45025a1cde107b6ae72bf72049e6021597c201cd6a3029b9a/azure_storage_blob-12.26.0.tar.gz", hash = "sha256:5dd7d7824224f7de00bfeb032753601c982655173061e242f13be6e26d78d71f", size = 583332, upload-time = "2025-07-16T21:34:07.644Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/33/085d9352d416e617993821b9d9488222fbb559bc15c3641d6cbd6d16d236/azure_storage_blob-12.25.1-py3-none-any.whl", hash = "sha256:1f337aab12e918ec3f1b638baada97550673911c4ceed892acc8e4e891b74167", size = 406990, upload-time = "2025-03-27T17:13:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/5b/64/63dbfdd83b31200ac58820a7951ddfdeed1fbee9285b0f3eae12d1357155/azure_storage_blob-12.26.0-py3-none-any.whl", hash = "sha256:8c5631b8b22b4f53ec5fff2f3bededf34cfef111e2af613ad42c9e6de00a77fe", size = 412907, upload-time = "2025-07-16T21:34:09.367Z" }, ] [[package]] @@ -1332,7 +1332,7 @@ wheels = [ [[package]] name = "ipykernel" -version = "6.29.5" +version = "6.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "appnope", marker = "sys_platform == 'darwin'" }, @@ -1350,9 +1350,9 @@ dependencies = [ { name = "tornado", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "traitlets", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/5c/67594cb0c7055dc50814b21731c22a601101ea3b1b50a9a1b090e11f5d0f/ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215", size = 163367, upload-time = "2024-07-01T14:07:22.543Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/27/9e6e30ed92f2ac53d29f70b09da8b2dc456e256148e289678fa0e825f46a/ipykernel-6.30.0.tar.gz", hash = "sha256:b7b808ddb2d261aae2df3a26ff3ff810046e6de3dfbc6f7de8c98ea0a6cb632c", size = 165125, upload-time = "2025-07-21T10:36:09.259Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/5c/368ae6c01c7628438358e6d337c19b05425727fbb221d2a3c4303c372f42/ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5", size = 117173, upload-time = "2024-07-01T14:07:19.603Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/00813c3d9b46e3dcd88bd4530e0a3c63c0509e5d8c9eff34723ea243ab04/ipykernel-6.30.0-py3-none-any.whl", hash = "sha256:fd2936e55c4a1c2ee8b1e5fa6a372b8eecc0ab1338750dee76f48fa5cca1301e", size = 117264, upload-time = "2025-07-21T10:36:06.854Z" }, ] [[package]] @@ -1528,7 +1528,7 @@ wheels = [ [[package]] name = "jsonschema" -version = "4.24.1" +version = "4.25.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -1536,9 +1536,9 @@ dependencies = [ { name = "referencing", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "rpds-py", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/6e/35174c1d3f30560848c82d3c233c01420e047d70925c897a4d6e932b4898/jsonschema-4.24.1.tar.gz", hash = "sha256:fe45a130cc7f67cd0d67640b4e7e3e2e666919462ae355eda238296eafeb4b5d", size = 356635, upload-time = "2025-07-17T14:40:01.05Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/00/a297a868e9d0784450faa7365c2172a7d6110c763e30ba861867c32ae6a9/jsonschema-4.25.0.tar.gz", hash = "sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f", size = 356830, upload-time = "2025-07-18T15:39:45.11Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/7f/ea48ffb58f9791f9d97ccb35e42fea1ebc81c67ce36dc4b8b2eee60e8661/jsonschema-4.24.1-py3-none-any.whl", hash = "sha256:6b916866aa0b61437785f1277aa2cbd63512e8d4b47151072ef13292049b4627", size = 89060, upload-time = "2025-07-17T14:39:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/c86cd8e011fe98803d7e382fd67c0df5ceab8d2b7ad8c5a81524f791551c/jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716", size = 89184, upload-time = "2025-07-18T15:39:42.956Z" }, ] [[package]] @@ -3323,15 +3323,15 @@ wheels = [ [[package]] name = "starlette" -version = "0.47.1" +version = "0.47.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0a/69/662169fdb92fb96ec3eaee218cf540a629d629c86d7993d9651226a6789b/starlette-0.47.1.tar.gz", hash = "sha256:aef012dd2b6be325ffa16698f9dc533614fb1cebd593a906b90dc1025529a79b", size = 2583072, upload-time = "2025-06-21T04:03:17.337Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/57/d062573f391d062710d4088fa1369428c38d51460ab6fedff920efef932e/starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", size = 2583948, upload-time = "2025-07-20T17:31:58.522Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/95/38ef0cd7fa11eaba6a99b3c4f5ac948d8bc6ff199aabd327a29cc000840c/starlette-0.47.1-py3-none-any.whl", hash = "sha256:5e11c9f5c7c3f24959edbf2dffdc01bba860228acf657129467d8a7468591527", size = 72747, upload-time = "2025-06-21T04:03:15.705Z" }, + { url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984, upload-time = "2025-07-20T17:31:56.738Z" }, ] [[package]] @@ -3448,7 +3448,7 @@ wheels = [ [[package]] name = "tox" -version = "4.27.0" +version = "4.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -3463,23 +3463,23 @@ dependencies = [ { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, { name = "virtualenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a5/b7/19c01717747076f63c54d871ada081cd711a7c9a7572f2225675c3858b94/tox-4.27.0.tar.gz", hash = "sha256:b97d5ecc0c0d5755bcc5348387fef793e1bfa68eb33746412f4c60881d7f5f57", size = 198351, upload-time = "2025-06-17T15:17:50.585Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/3d/2efc68c86b2879b0abdf0d07839da47026489dca424a11379277a40fc67c/tox-4.28.0.tar.gz", hash = "sha256:442347b1a415733850f097e7e78b8c5f38b5e1719f8b7205aade5d055f08068c", size = 199516, upload-time = "2025-07-20T18:25:51.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/3a/30889167f41ecaffb957ec4409e1cbc1d5d558a5bbbdfb734a5b9911930f/tox-4.27.0-py3-none-any.whl", hash = "sha256:2b8a7fb986b82aa2c830c0615082a490d134e0626dbc9189986da46a313c4f20", size = 173441, upload-time = "2025-06-17T15:17:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/be/df/7f962b921f4efa414ad06f2abc665eff379c62ee46b959779381209e2e72/tox-4.28.0-py3-none-any.whl", hash = "sha256:3e2f5c0a00523a58666690108b66820150f6435cb6e4dd95caf21bb52133c1d1", size = 173883, upload-time = "2025-07-20T18:25:49.934Z" }, ] [[package]] name = "tox-uv" -version = "1.26.1" +version = "1.26.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "tox", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "uv", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cf/00/98e564731fc361cc2f1e39c58d2feb0b4c9f9a7cb06f0c769cdeb9a98004/tox_uv-1.26.1.tar.gz", hash = "sha256:241cc530b4a80436c4487977c8303d9aace398c6561d5e7d8845606fa7d482ab", size = 21849, upload-time = "2025-06-23T20:17:54.96Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/31/2c02a8b4d85d3538d6e9a7aa55dfaf3ea372b2007496b9235047e18c0953/tox_uv-1.26.2.tar.gz", hash = "sha256:5270d5d49e26c1303d902b90d6143a593b43ae148ccc5107251b79bf5bd4fefd", size = 21895, upload-time = "2025-07-21T17:03:39.196Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/0b/e47c1bb2bc9e20b22a6913ea2162b7bb5729d38924fa2c1d4eaf95d3b36f/tox_uv-1.26.1-py3-none-any.whl", hash = "sha256:edc25b254e5cdbb13fc5d23d6d05b511dee562ab72b0e99da4a874a78018c38e", size = 16661, upload-time = "2025-06-23T20:17:52.492Z" }, + { url = "https://files.pythonhosted.org/packages/73/c9/354b2a28112ce9619616f09a3e8363dae01f9a4c5a2716fa92bcfcf6ccc5/tox_uv-1.26.2-py3-none-any.whl", hash = "sha256:f95c8635b6e046534faf4de88f46c46ac0d644f2dbe0104fc6adac637e0d44b6", size = 16666, upload-time = "2025-07-21T17:03:38.037Z" }, ] [[package]] @@ -3583,16 +3583,16 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.31.2" +version = "20.32.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "filelock", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "platformdirs", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316, upload-time = "2025-05-08T17:58:23.811Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/96/0834f30fa08dca3738614e6a9d42752b6420ee94e58971d702118f7cfd30/virtualenv-20.32.0.tar.gz", hash = "sha256:886bf75cadfdc964674e6e33eb74d787dff31ca314ceace03ca5810620f4ecf0", size = 6076970, upload-time = "2025-07-21T04:09:50.985Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c6/f8f28009920a736d0df434b52e9feebfb4d702ba942f15338cb4a83eafc1/virtualenv-20.32.0-py3-none-any.whl", hash = "sha256:2c310aecb62e5aa1b06103ed7c2977b81e042695de2697d01017ff0f1034af56", size = 6057761, upload-time = "2025-07-21T04:09:48.059Z" }, ] [[package]]