diff --git a/AGENTS.md b/AGENTS.md index 7831d5f..6ec59b4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,3 +29,11 @@ When adding new azd environment variables, update these files: 6. **infra/main.bicep outputs**: If the value needs to be stored back in azd env after provisioning: - Add an output (note: `@secure()` parameters cannot be outputs) + +## Updating Python dependencies + +When updating or adding Python dependencies: + +1. Edit `pyproject.toml` with the new or updated version constraints. +2. Run `uv lock` to re-resolve dependencies (use `uv lock -P ` to upgrade only a specific package). +3. Run `uv sync` to install the updated lockfile into the virtual environment. diff --git a/README.md b/README.md index fb40957..f4833b9 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ If you're not using one of the above options, then you'll need to: This project includes MCP servers in the [`servers/`](servers/) directory: | File | Description | -|------|-------------| +| ---- | ----------- | | [servers/basic_mcp_stdio.py](servers/basic_mcp_stdio.py) | MCP server with stdio transport for VS Code integration | | [servers/basic_mcp_http.py](servers/basic_mcp_http.py) | MCP server with HTTP transport on port 8000 | | [servers/deployed_mcp.py](servers/deployed_mcp.py) | MCP server for Azure deployment with Cosmos DB and optional Keycloak auth | @@ -201,7 +201,7 @@ You can use the [.NET Aspire Dashboard](https://learn.microsoft.com/dotnet/aspir This project includes example agents in the [`agents/`](agents/) directory that demonstrate how to connect AI agents to MCP servers: | File | Description | -|------|-------------| +| ---- | ----------- | | [agents/agentframework_learn.py](agents/agentframework_learn.py) | Microsoft Agent Framework integration with MCP | | [agents/agentframework_http.py](agents/agentframework_http.py) | Microsoft Agent Framework integration with local Expenses MCP server | | [agents/langchainv1_http.py](agents/langchainv1_http.py) | LangChain agent with MCP integration | @@ -381,10 +381,12 @@ When using VNet configuration, additional Azure resources are provisioned: This project supports deploying with OAuth 2.0 authentication using Keycloak as the identity provider, implementing the [MCP OAuth specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) with Dynamic Client Registration (DCR). +[📺 Watch a demo video of Keycloak integration](https://youtu.be/lpH8PI4JgEY) + ### What gets deployed | Component | Description | -|-----------|-------------| +| --------- | ----------- | | **Keycloak Container App** | Keycloak 26.0 with pre-configured realm | | **HTTP Route Configuration** | Rule-based routing: `/auth/*` → Keycloak, `/*` → MCP Server | | **OAuth-protected MCP Server** | FastMCP with JWT validation against Keycloak's JWKS endpoint | @@ -433,7 +435,6 @@ This project supports deploying with OAuth 2.0 authentication using Keycloak as Login with `admin` and your configured password. - ### Use Keycloak OAuth MCP server with GitHub Copilot The Keycloak deployment supports Dynamic Client Registration (DCR), which allows VS Code to automatically register as an OAuth client. VS Code redirect URIs are pre-configured in the Keycloak realm. @@ -477,7 +478,7 @@ To use the deployed MCP server with GitHub Copilot Chat: ### Known limitations (demo trade-offs) | Item | Current | Production Recommendation | Why | -|------|---------|---------------------------|-----| +| ---- | ------- | ------------------------- | --- | | Keycloak mode | `start-dev` | `start` with proper config | Dev mode has relaxed security defaults | | Database | H2 in-memory | PostgreSQL | H2 doesn't persist data across restarts | | Replicas | 1 (due to H2) | Multiple with shared DB | H2 is in-memory, can't share state | @@ -492,10 +493,12 @@ To use the deployed MCP server with GitHub Copilot Chat: This project supports deploying with Microsoft Entra ID (Azure AD) authentication using FastMCP's built-in Azure OAuth proxy. This is an alternative to Keycloak that uses Microsoft Entra with your Azure tenant for identity management. +[📺 Watch a demo video of Entra integration](https://youtu.be/nOPXUBOXU2M) + ### What gets deployed with Entra OAuth | Component | Description | -|-----------|-------------| +| --------- | ----------- | | **Microsoft Entra App Registration** | Created automatically during provisioning with redirect URIs for local development, VS Code, and production | | **OAuth-protected MCP Server** | FastMCP with AzureProvider for OAuth authentication | | **CosmosDB OAuth Client Storage** | Persists OAuth client registrations across server restarts | @@ -535,10 +538,10 @@ This project supports deploying with Microsoft Entra ID (Azure AD) authenticatio The following environment variables are automatically set by the deployment hooks: -| Variable | Description | -|----------|-------------| -| `ENTRA_PROXY_AZURE_CLIENT_ID` | The App Registration's client ID | -| `ENTRA_PROXY_AZURE_CLIENT_SECRET` | The App Registration's client secret | +| Variable | Description | +| ----------------------------------- | --------------------------------------- | +| `ENTRA_PROXY_AZURE_CLIENT_ID` | The App Registration's client ID | +| `ENTRA_PROXY_AZURE_CLIENT_SECRET` | The App Registration's client secret | These are then written to `.env` by the postprovision hook for local development. @@ -548,7 +551,7 @@ After deployment, you can test locally with OAuth enabled: ```bash # Run the MCP server -cd servers && uvicorn auth_mcp:app --host 0.0.0.0 --port 8000 +cd servers && uvicorn auth_entra_mcp:app --host 0.0.0.0 --port 8000 ``` The server will use the Entra App Registration for OAuth and CosmosDB for client storage. diff --git a/infra/auth_init.py b/infra/auth_init.py index a4197d5..3ed3b1d 100644 --- a/infra/auth_init.py +++ b/infra/auth_init.py @@ -3,19 +3,26 @@ import random import subprocess import uuid +from dataclasses import dataclass from azure.identity.aio import AzureDeveloperCliCredential from dotenv_azd import load_azd_env +from kiota_abstractions.api_error import APIError +from kiota_abstractions.base_request_configuration import RequestConfiguration +from msgraph import GraphServiceClient from msgraph.generated.applications.item.add_password.add_password_post_request_body import ( AddPasswordPostRequestBody, ) from msgraph.generated.models.api_application import ApiApplication from msgraph.generated.models.application import Application +from msgraph.generated.models.o_auth2_permission_grant import OAuth2PermissionGrant from msgraph.generated.models.password_credential import PasswordCredential from msgraph.generated.models.permission_scope import PermissionScope from msgraph.generated.models.service_principal import ServicePrincipal from msgraph.generated.models.web_application import WebApplication -from msgraph.graph_service_client import GraphServiceClient +from msgraph.generated.oauth2_permission_grants.oauth2_permission_grants_request_builder import ( + Oauth2PermissionGrantsRequestBuilder, +) async def get_application(graph_client: GraphServiceClient, app_id: str) -> str | None: @@ -136,7 +143,7 @@ def update_app_with_identifier_uri(client_id: str) -> Application: ) -async def create_or_update_fastmcp_app(graph_client: GraphServiceClient) -> None: +async def create_or_update_fastmcp_app(graph_client: GraphServiceClient): """Create or update a FastMCP app registration.""" app_id_env_var = "ENTRA_PROXY_AZURE_CLIENT_ID" app_secret_env_var = "ENTRA_PROXY_AZURE_CLIENT_SECRET" @@ -171,6 +178,73 @@ async def create_or_update_fastmcp_app(graph_client: GraphServiceClient) -> None update_azd_env(app_secret_env_var, client_secret) print("Client secret created and saved to environment.") + return app_id + + +@dataclass +class GrantDefinition: + principal_id: str + resource_app_id: str + scopes: list[str] + target_label: str + + def scope_string(self) -> str: + return " ".join(self.scopes) + + +async def grant_application_admin_consent(graph_client: GraphServiceClient, server_app_id: str): + server_principal = await graph_client.service_principals_with_app_id(server_app_id).get() + if server_principal is None or server_principal.id is None: + raise ValueError("Unable to locate service principal for server application") + + grant_definitions = [ + GrantDefinition( + principal_id=server_principal.id, + resource_app_id="00000003-0000-0000-c000-000000000000", + scopes=["User.Read", "email", "offline_access", "openid", "profile"], + target_label="server application", + ) + ] + + for grant in grant_definitions: + resource_principal = await graph_client.service_principals_with_app_id(grant.resource_app_id).get() + if resource_principal is None or resource_principal.id is None: + raise ValueError(f"Unable to locate service principal for resource {grant.resource_app_id}") + + desired_scope = grant.scope_string() + filter_query = f"clientId eq '{grant.principal_id}' and resourceId eq '{resource_principal.id}'" + query_params = Oauth2PermissionGrantsRequestBuilder.Oauth2PermissionGrantsRequestBuilderGetQueryParameters( + filter=filter_query + ) + request_config = RequestConfiguration[ + Oauth2PermissionGrantsRequestBuilder.Oauth2PermissionGrantsRequestBuilderGetQueryParameters + ](query_parameters=query_params) + existing_grants = await graph_client.oauth2_permission_grants.get(request_configuration=request_config) + + current_grant = existing_grants.value[0] if existing_grants and existing_grants.value else None + + if current_grant: + print(f"Admin consent already granted for {desired_scope} on the {grant.target_label}") + continue + + try: + await graph_client.oauth2_permission_grants.post( + OAuth2PermissionGrant( + client_id=grant.principal_id, + consent_type="AllPrincipals", + resource_id=resource_principal.id, + scope=desired_scope, + ) + ) + print(f"Granted admin consent for {desired_scope} on the {grant.target_label}") + except APIError as error: + status_code = error.response_status_code + if status_code in {401, 403}: + print(f"Failed to grant admin consent: {error.message}") + return + else: + raise + async def main(): # Configuration - customize these as needed @@ -182,8 +256,12 @@ async def main(): scopes = ["https://graph.microsoft.com/.default"] graph_client = GraphServiceClient(credentials=credential, scopes=scopes) - await create_or_update_fastmcp_app(graph_client) - print("Setup complete!") + server_app_id = await create_or_update_fastmcp_app(graph_client) + + print("Attempting to grant admin consent for the server application...") + await grant_application_admin_consent(graph_client, server_app_id) + + print("✅ Entra app registration setup is complete.") if __name__ == "__main__": diff --git a/infra/main.bicep b/infra/main.bicep index 70e2d6a..5b569d3 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -78,6 +78,9 @@ param keycloakMcpServerAudience string = 'mcp-server' @description('Flag to restrict ACR public network access (requires VPN for local image push when true)') param usePrivateAcr bool = false +@description('Entra ID group ID for admin access to expense statistics (only used when mcpAuthProvider is entra_proxy)') +param entraAdminGroupId string = '' + @description('Flag to restrict Log Analytics public query access for increased security') param usePrivateLogAnalytics bool = false @@ -790,6 +793,7 @@ module server 'server.bicep' = { entraProxyClientSecret: useEntraProxy ? entraProxyClientSecret : '' entraProxyBaseUrl: useEntraProxy ? entraProxyMcpServerBaseUrl : '' tenantId: useEntraProxy ? tenant().tenantId : '' + entraAdminGroupId: useEntraProxy ? entraAdminGroupId : '' mcpAuthProvider: mcpAuthProvider logfireToken: logfireToken } diff --git a/infra/main.parameters.json b/infra/main.parameters.json index e7d274f..f039a9a 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -56,6 +56,9 @@ "entraProxyClientSecret": { "value": "${ENTRA_PROXY_AZURE_CLIENT_SECRET}" }, + "entraAdminGroupId": { + "value": "${ENTRA_ADMIN_GROUP_ID}" + }, "logfireToken": { "value": "${LOGFIRE_TOKEN}" } diff --git a/infra/server.bicep b/infra/server.bicep index bc8084a..1efd6a0 100644 --- a/infra/server.bicep +++ b/infra/server.bicep @@ -29,6 +29,7 @@ param entraProxyClientId string = '' param entraProxyClientSecret string = '' param entraProxyBaseUrl string = '' param tenantId string = '' +param entraAdminGroupId string = '' @secure() param logfireToken string = '' @allowed([ @@ -39,8 +40,13 @@ param logfireToken string = '' param mcpAuthProvider string = 'none' // Base environment variables -// Select MCP entrypoint based on configured auth (Keycloak or FastMCP Azure auth) -var mcpEntry = (!empty(keycloakRealmUrl) || !empty(entraProxyClientId)) ? 'auth' : 'deployed' +// Select MCP entrypoint based on configured auth +// - Keycloak → 'auth_keycloak_mcp' +// - Entra OAuth Proxy → 'auth_entra_mcp' +// - None → 'deployed_mcp' +var mcpEntry = mcpAuthProvider == 'keycloak' + ? 'auth_keycloak_mcp' + : (mcpAuthProvider == 'entra_proxy' ? 'auth_entra_mcp' : 'deployed_mcp') var baseEnv = [ { name: 'AZURE_OPENAI_CHAT_DEPLOYMENT' @@ -139,6 +145,10 @@ var entraProxyEnv = !empty(entraProxyClientId) ? [ name: 'AZURE_TENANT_ID' value: tenantId } + { + name: 'ENTRA_ADMIN_GROUP_ID' + value: entraAdminGroupId + } ] : [] // Secrets for sensitive values diff --git a/infra/write_env.ps1 b/infra/write_env.ps1 index da228d1..cf5c4ed 100644 --- a/infra/write_env.ps1 +++ b/infra/write_env.ps1 @@ -58,6 +58,7 @@ if ($ENTRA_PROXY_AZURE_CLIENT_ID -and $ENTRA_PROXY_AZURE_CLIENT_ID -ne "") { Add-Content -Path $ENV_FILE_PATH -Value "ENTRA_PROXY_AZURE_CLIENT_ID=$ENTRA_PROXY_AZURE_CLIENT_ID" Write-Env ENTRA_PROXY_AZURE_CLIENT_SECRET Write-Env ENTRA_PROXY_MCP_SERVER_BASE_URL + Write-EnvIfSet ENTRA_ADMIN_GROUP_ID } Add-Content -Path $ENV_FILE_PATH -Value "MCP_ENTRY=$(azd env get-value MCP_ENTRY)" Add-Content -Path $ENV_FILE_PATH -Value "MCP_SERVER_URL=$(azd env get-value MCP_SERVER_URL)" diff --git a/infra/write_env.sh b/infra/write_env.sh index 618e02d..2c37b45 100755 --- a/infra/write_env.sh +++ b/infra/write_env.sh @@ -64,6 +64,7 @@ if [ -n "$ENTRA_PROXY_AZURE_CLIENT_ID" ]; then echo "ENTRA_PROXY_AZURE_CLIENT_ID=${ENTRA_PROXY_AZURE_CLIENT_ID}" >> "$ENV_FILE_PATH" write_env ENTRA_PROXY_AZURE_CLIENT_SECRET write_env ENTRA_PROXY_MCP_SERVER_BASE_URL + write_env_if_set ENTRA_ADMIN_GROUP_ID fi echo "MCP_ENTRY=$(azd env get-value MCP_ENTRY)" >> "$ENV_FILE_PATH" echo "MCP_SERVER_URL=$(azd env get-value MCP_SERVER_URL)" >> "$ENV_FILE_PATH" diff --git a/pyproject.toml b/pyproject.toml index dc14aca..104c67c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "Demonstration of Python FastMCP servers" readme = "README.md" requires-python = "==3.13.*" dependencies = [ - "fastmcp>=2.13.3", + "fastmcp>=2.14.2", "debugpy>=1.8.0", "langchain-core>=0.3.0", "mcp>=1.3.0", @@ -18,9 +18,9 @@ dependencies = [ "azure-ai-agents>=1.1.0", "agent-framework>=1.0.0b251016", "azure-cosmos>=4.9.0", - "azure-monitor-opentelemetry>=1.6.4", - "opentelemetry-instrumentation-starlette>=0.49b0", - "opentelemetry-exporter-otlp-proto-grpc>=1.28.0", + "azure-monitor-opentelemetry>=1.8.3", + "opentelemetry-instrumentation-starlette>=0.60b0", + "opentelemetry-exporter-otlp-proto-grpc>=1.39.0", "logfire>=4.15.1", "azure-core-tracing-opentelemetry>=1.0.0b12" ] diff --git a/servers/Dockerfile b/servers/Dockerfile index 253f1bd..c522b16 100644 --- a/servers/Dockerfile +++ b/servers/Dockerfile @@ -37,7 +37,7 @@ ENV PATH="/code/.venv/bin:$PATH" EXPOSE 8000 -ENV MCP_ENTRY=deployed +ENV MCP_ENTRY=deployed_mcp # Copy and use entrypoint script for clean env expansion COPY --chown=app:app servers/entrypoint.sh /usr/local/bin/entrypoint.sh diff --git a/servers/auth_entra_mcp.py b/servers/auth_entra_mcp.py new file mode 100644 index 0000000..715ec12 --- /dev/null +++ b/servers/auth_entra_mcp.py @@ -0,0 +1,313 @@ +""" +Expense tracking MCP server with Entra authentication and Cosmos DB storage. + +Run with: cd servers && uvicorn auth_entra_mcp:app --host 0.0.0.0 --port 8000 +""" + +import logging +import os +import uuid +import warnings +from datetime import date +from enum import Enum +from typing import Annotated + +import httpx +import logfire +from azure.core.settings import settings +from azure.cosmos.aio import CosmosClient +from azure.identity.aio import DefaultAzureCredential, ManagedIdentityCredential +from azure.monitor.opentelemetry import configure_azure_monitor +from cosmosdb_store import CosmosDBStore +from dotenv import load_dotenv +from fastmcp import Context, FastMCP +from fastmcp.server.auth.providers.azure import AzureProvider +from fastmcp.server.dependencies import get_access_token +from fastmcp.server.middleware import Middleware, MiddlewareContext +from key_value.aio.stores.memory import MemoryStore +from msal import ConfidentialClientApplication, TokenCache +from opentelemetry.instrumentation.starlette import StarletteInstrumentor +from rich.console import Console +from rich.logging import RichHandler +from starlette.responses import JSONResponse + +from opentelemetry_middleware import OpenTelemetryMiddleware + +RUNNING_IN_PRODUCTION = os.getenv("RUNNING_IN_PRODUCTION", "false").lower() == "true" + +if not RUNNING_IN_PRODUCTION: + load_dotenv(override=True) + +logging.basicConfig( + level=logging.WARNING, + format="%(name)s: %(message)s", + handlers=[ + RichHandler( + console=Console(stderr=True), + show_path=False, + show_level=False, + rich_tracebacks=True, + ) + ], +) +# Suppress OTEL 1.39 deprecation warnings and noisy logs +warnings.filterwarnings("ignore", category=DeprecationWarning, message=r".*Deprecated since version 1\.39\.0.*") +logging.getLogger("azure.monitor.opentelemetry.exporter._performance_counters._manager").setLevel(logging.ERROR) +logger = logging.getLogger("ExpensesMCP") +logger.setLevel(logging.INFO) + +# Configure Azure SDK OpenTelemetry to use OTEL +settings.tracing_implementation = "opentelemetry" + +# Configure OpenTelemetry exporters based on OPENTELEMETRY_PLATFORM env var +opentelemetry_platform = os.getenv("OPENTELEMETRY_PLATFORM", "none").lower() +if opentelemetry_platform == "appinsights" and os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING"): + logger.info("Setting up Azure Monitor instrumentation") + configure_azure_monitor() +elif opentelemetry_platform == "logfire" and os.getenv("LOGFIRE_TOKEN"): + logger.info("Setting up Logfire instrumentation") + logfire.configure(service_name="expenses-mcp", send_to_logfire=True) + +# Configure Cosmos DB client +if RUNNING_IN_PRODUCTION: + azure_credential = ManagedIdentityCredential(client_id=os.environ["AZURE_CLIENT_ID"]) + logger.info("Using Managed Identity Credential for Azure authentication") +else: + azure_credential = DefaultAzureCredential() + logger.info("Using Default Azure Credential for Azure authentication") +cosmos_client = CosmosClient( + url=f"https://{os.environ['AZURE_COSMOSDB_ACCOUNT']}.documents.azure.com:443/", + credential=azure_credential, +) +cosmos_db = cosmos_client.get_database_client(os.environ["AZURE_COSMOSDB_DATABASE"]) +cosmos_container = cosmos_db.get_container_client(os.environ["AZURE_COSMOSDB_USER_CONTAINER"]) + +# Configure authentication provider +# Azure/Entra ID authentication using AzureProvider +# When running locally, always use localhost for base URL (OAuth redirects need to match) +oauth_client_store = None +if RUNNING_IN_PRODUCTION: + oauth_container = cosmos_db.get_container_client(os.environ["AZURE_COSMOSDB_OAUTH_CONTAINER"]) + oauth_client_store = CosmosDBStore(container=oauth_container, default_collection="oauth-clients") + entra_base_url = os.environ["ENTRA_PROXY_MCP_SERVER_BASE_URL"] +else: + oauth_client_store = MemoryStore() + entra_base_url = "http://localhost:8000" +auth = AzureProvider( + client_id=os.environ["ENTRA_PROXY_AZURE_CLIENT_ID"], + client_secret=os.environ["ENTRA_PROXY_AZURE_CLIENT_SECRET"], + tenant_id=os.environ["AZURE_TENANT_ID"], + base_url=entra_base_url, + required_scopes=["mcp-access"], + client_storage=oauth_client_store, +) +logger.info("Using Entra OAuth Proxy for server %s and %s storage", entra_base_url, type(oauth_client_store).__name__) + +confidential_client = ConfidentialClientApplication( + client_id=os.environ["ENTRA_PROXY_AZURE_CLIENT_ID"], + client_credential=os.environ["ENTRA_PROXY_AZURE_CLIENT_SECRET"], + authority=f"https://login.microsoftonline.com/{os.environ['AZURE_TENANT_ID']}", + token_cache=TokenCache(), +) + + +async def check_user_in_group(graph_token: str, group_id: str) -> bool: + """Check if the authenticated user is a member of the specified group (including transitive membership).""" + async with httpx.AsyncClient() as client: + url = ( + "https://graph.microsoft.com/v1.0/me/transitiveMemberOf/microsoft.graph.group" + f"?$filter=id eq '{group_id}'&$count=true" + ) + logger.info(f"Checking group membership for group ID: {group_id}") + response = await client.get( + url, + headers={ + "Authorization": f"Bearer {graph_token}", + "ConsistencyLevel": "eventual", + }, + ) + response.raise_for_status() + data = response.json() + membership_count = data.get("@odata.count", 0) + logger.info(f"User membership count in group {group_id}: {membership_count}") + return membership_count > 0 + + +# Middleware to populate user_id in per-request context state +class UserAuthMiddleware(Middleware): + def _get_user_id(self): + token = get_access_token() + if not (token and hasattr(token, "claims")): + return None + return token.claims.get("oid") + + async def on_call_tool(self, context: MiddlewareContext, call_next): + user_id = self._get_user_id() + if context.fastmcp_context is not None: + context.fastmcp_context.set_state("user_id", user_id) + return await call_next(context) + + async def on_read_resource(self, context: MiddlewareContext, call_next): + user_id = self._get_user_id() + if context.fastmcp_context is not None: + context.fastmcp_context.set_state("user_id", user_id) + return await call_next(context) + + +# Create the MCP server +mcp = FastMCP("Expenses Tracker", auth=auth, middleware=[OpenTelemetryMiddleware("ExpensesMCP"), UserAuthMiddleware()]) + + +class PaymentMethod(Enum): + AMEX = "amex" + VISA = "visa" + CASH = "cash" + + +class Category(Enum): + FOOD = "food" + TRANSPORT = "transport" + ENTERTAINMENT = "entertainment" + SHOPPING = "shopping" + GADGET = "gadget" + OTHER = "other" + + +@mcp.tool +async def add_user_expense( + date: Annotated[date, "Date of the expense in YYYY-MM-DD format"], + amount: Annotated[float, "Positive numeric amount of the expense"], + category: Annotated[Category, "Category label"], + description: Annotated[str, "Human-readable description of the expense"], + payment_method: Annotated[PaymentMethod, "Payment method used"], + ctx: Context, +): + """Add a new expense to Cosmos DB.""" + if amount <= 0: + return "Error: Amount must be positive" + + date_iso = date.isoformat() + logger.info(f"Adding expense: ${amount} for {description} on {date_iso}") + + try: + # Read user_id stored by middleware + user_id = ctx.get_state("user_id") + if not user_id: + return "Error: Authentication required (no user_id present)" + expense_id = str(uuid.uuid4()) + expense_item = { + "id": expense_id, + "user_id": user_id, + "date": date_iso, + "amount": amount, + "category": category.value, + "description": description, + "payment_method": payment_method.value, + } + await cosmos_container.create_item(body=expense_item) + return f"Successfully added expense: ${amount} for {description} on {date_iso}" + + except Exception as e: + logger.error(f"Error adding expense: {str(e)}") + return f"Error: Unable to add expense - {str(e)}" + + +@mcp.tool +async def get_user_expenses(ctx: Context): + """Get the authenticated user's expense data from Cosmos DB.""" + + try: + user_id = ctx.get_state("user_id") + if not user_id: + return "Error: Authentication required (no user_id present)" + query = "SELECT * FROM c WHERE c.user_id = @uid ORDER BY c.date DESC" + parameters = [{"name": "@uid", "value": user_id}] + expenses_data = [] + + async for item in cosmos_container.query_items(query=query, parameters=parameters, partition_key=user_id): + expenses_data.append(item) + + if not expenses_data: + return "No expenses found." + + expense_summary = f"Expense data ({len(expenses_data)} entries):\n\n" + for expense in expenses_data: + expense_summary += ( + f"Date: {expense.get('date', 'N/A')}, " + f"Amount: ${expense.get('amount', 0)}, " + f"Category: {expense.get('category', 'N/A')}, " + f"Description: {expense.get('description', 'N/A')}, " + f"Payment: {expense.get('payment_method', 'N/A')}\n" + ) + + return expense_summary + + except Exception as e: + logger.error(f"Error reading expenses: {str(e)}") + return f"Error: Unable to retrieve expense data - {str(e)}" + + +@mcp.tool +async def get_expense_stats(ctx: Context): + """Get a statistical summary of expenses (count per category) for all users. + Only accessible to users in the authorized admin group. + """ + access_token = get_access_token() + if not access_token: + return "Error: Authentication required" + + auth_token = access_token.token + try: + graph_resource_access_token = confidential_client.acquire_token_on_behalf_of( + user_assertion=auth_token, scopes=["https://graph.microsoft.com/.default"] + ) + if "error" in graph_resource_access_token: + logger.error( + "OBO token acquisition failed: %s", + graph_resource_access_token.get("error_description", "Unknown error"), + ) + return "Error: Unable to verify permissions. Please try again later." + + graph_auth_token = graph_resource_access_token["access_token"] + + # Check for the specific admin group ID using transitive membership + admin_group_id = os.environ.get("ENTRA_ADMIN_GROUP_ID", "") + if not admin_group_id: + return "Error: Admin group ID not configured. Set ENTRA_ADMIN_GROUP_ID environment variable." + is_admin = await check_user_in_group(graph_auth_token, admin_group_id) + + if not is_admin: + return "Error: Unauthorized. You do not have permission to access expense statistics." + + # Query Cosmos DB for stats across all users + # We fetch categories and aggregate in Python to avoid cross-partition GROUP BY limitations + query = "SELECT c.category FROM c" + stats = {} + async for item in cosmos_container.query_items(query=query): + category = item.get("category", "Unknown") + stats[category] = stats.get(category, 0) + 1 + + if not stats: + return "No expense data found to summarize." + + summary = "Expense Statistics (Count per Category):\n" + for category, count in stats.items(): + summary += f"- Category {category}: {count} expenses\n" + + return summary + + except Exception: + logger.error("Error retrieving expense stats", exc_info=True) + return "Error: Unable to retrieve expense statistics. Please try again later." + + +@mcp.custom_route("/health", methods=["GET"]) +async def health_check(_request): + """Health check endpoint for service availability.""" + return JSONResponse({"status": "healthy", "service": "mcp-server"}) + + +# Configure Starlette middleware for OpenTelemetry +# We must do this *after* defining all the MCP server routes +app = mcp.http_app() +StarletteInstrumentor.instrument_app(app) diff --git a/servers/auth_mcp.py b/servers/auth_keycloak_mcp.py similarity index 73% rename from servers/auth_mcp.py rename to servers/auth_keycloak_mcp.py index fc58ae3..bd871e5 100644 --- a/servers/auth_mcp.py +++ b/servers/auth_keycloak_mcp.py @@ -1,4 +1,8 @@ -"""Run with: cd servers && uvicorn auth_mcp:app --host 0.0.0.0 --port 8000""" +""" +Expense tracking MCP server with KeyCloak authentication and Cosmos DB storage. + +Run with: cd servers && uvicorn auth_keycloak_mcp:app --host 0.0.0.0 --port 8000 +""" import logging import os @@ -12,13 +16,10 @@ from azure.cosmos.aio import CosmosClient from azure.identity.aio import DefaultAzureCredential, ManagedIdentityCredential from azure.monitor.opentelemetry import configure_azure_monitor -from cosmosdb_store import CosmosDBStore from dotenv import load_dotenv from fastmcp import Context, FastMCP -from fastmcp.server.auth.providers.azure import AzureProvider from fastmcp.server.dependencies import get_access_token from fastmcp.server.middleware import Middleware, MiddlewareContext -from key_value.aio.stores.memory import MemoryStore from keycloak_provider import KeycloakAuthProvider from opentelemetry.instrumentation.starlette import StarletteInstrumentor from rich.console import Console @@ -73,56 +74,27 @@ cosmos_db = cosmos_client.get_database_client(os.environ["AZURE_COSMOSDB_DATABASE"]) cosmos_container = cosmos_db.get_container_client(os.environ["AZURE_COSMOSDB_USER_CONTAINER"]) -# Configure authentication provider -auth = None -mcp_auth_provider = os.getenv("MCP_AUTH_PROVIDER", "none").lower() -if mcp_auth_provider == "entra_proxy": - # Azure/Entra ID authentication using AzureProvider - # When running locally, always use localhost for base URL (OAuth redirects need to match) - oauth_client_store = None - if RUNNING_IN_PRODUCTION: - oauth_container = cosmos_db.get_container_client(os.environ["AZURE_COSMOSDB_OAUTH_CONTAINER"]) - oauth_client_store = CosmosDBStore(container=oauth_container, default_collection="oauth-clients") - entra_base_url = os.environ["ENTRA_PROXY_MCP_SERVER_BASE_URL"] - else: - oauth_client_store = MemoryStore() - entra_base_url = "http://localhost:8000" - auth = AzureProvider( - client_id=os.environ["ENTRA_PROXY_AZURE_CLIENT_ID"], - client_secret=os.environ["ENTRA_PROXY_AZURE_CLIENT_SECRET"], - tenant_id=os.environ["AZURE_TENANT_ID"], - base_url=entra_base_url, - required_scopes=["mcp-access"], - client_storage=oauth_client_store, - ) - logger.info( - "Using Entra OAuth Proxy for server %s and %s storage", entra_base_url, type(oauth_client_store).__name__ - ) -elif mcp_auth_provider == "keycloak": - # Keycloak authentication using KeycloakAuthProvider with DCR support - KEYCLOAK_REALM_URL = os.environ["KEYCLOAK_REALM_URL"] - if RUNNING_IN_PRODUCTION: - keycloak_base_url = os.environ["KEYCLOAK_MCP_SERVER_BASE_URL"] - else: - keycloak_base_url = "http://localhost:8000" - - keycloak_audience = os.getenv("KEYCLOAK_MCP_SERVER_AUDIENCE") or "mcp-server" - - auth = KeycloakAuthProvider( - realm_url=KEYCLOAK_REALM_URL, - base_url=keycloak_base_url, - required_scopes=["openid", "mcp:access"], - audience=keycloak_audience, - ) - logger.info( - "Using Keycloak DCR auth for server %s and realm %s (audience=%s)", - keycloak_base_url, - KEYCLOAK_REALM_URL, - keycloak_audience, - ) +# Configure Keycloak authentication using KeycloakAuthProvider with DCR support +KEYCLOAK_REALM_URL = os.environ["KEYCLOAK_REALM_URL"] +if RUNNING_IN_PRODUCTION: + keycloak_base_url = os.environ["KEYCLOAK_MCP_SERVER_BASE_URL"] else: - logger.error("No authentication configured for MCP server, exiting.") - raise SystemExit(1) + keycloak_base_url = "http://localhost:8000" + +keycloak_audience = os.getenv("KEYCLOAK_MCP_SERVER_AUDIENCE") or "mcp-server" + +auth = KeycloakAuthProvider( + realm_url=KEYCLOAK_REALM_URL, + base_url=keycloak_base_url, + required_scopes=["openid", "mcp:access"], + audience=keycloak_audience, +) +logger.info( + "Using Keycloak DCR auth for server %s and realm %s (audience=%s)", + keycloak_base_url, + KEYCLOAK_REALM_URL, + keycloak_audience, +) # Middleware to populate user_id in per-request context state @@ -131,8 +103,7 @@ def _get_user_id(self): token = get_access_token() if not (token and hasattr(token, "claims")): return None - # Return 'oid' claim if present (for Entra), otherwise fallback to 'sub' (for KeyCloak) - return token.claims.get("oid", token.claims.get("sub")) + return token.claims.get("sub") async def on_call_tool(self, context: MiddlewareContext, call_next): user_id = self._get_user_id() @@ -150,8 +121,6 @@ async def on_read_resource(self, context: MiddlewareContext, call_next): # Create the MCP server mcp = FastMCP("Expenses Tracker", auth=auth, middleware=[OpenTelemetryMiddleware("ExpensesMCP"), UserAuthMiddleware()]) -"""Expense tracking MCP server with authentication and Cosmos DB storage.""" - class PaymentMethod(Enum): AMEX = "amex" diff --git a/servers/entrypoint.sh b/servers/entrypoint.sh index 12109a6..d5ca02b 100644 --- a/servers/entrypoint.sh +++ b/servers/entrypoint.sh @@ -1,11 +1,8 @@ #!/bin/sh set -e -# Default to deployed if not provided by runtime -MCP_ENTRY_VAL="${MCP_ENTRY:-deployed}" - -# Build ASGI target module path -APP_MODULE="${MCP_ENTRY_VAL}_mcp:app" +: "${MCP_ENTRY:=deployed_mcp}" +APP_MODULE="${MCP_ENTRY}:app" echo "Starting uvicorn with module: ${APP_MODULE}" exec uvicorn "${APP_MODULE}" --host 0.0.0.0 --port 8000 diff --git a/spanish/README.md b/spanish/README.md index ba4ec92..0c403fe 100644 --- a/spanish/README.md +++ b/spanish/README.md @@ -16,7 +16,7 @@ Un proyecto de demostración que muestra implementaciones del Model Context Prot - [Correr Agentes <-> MCP](#correr-agentes---mcp) - [Desplegar en Azure](#desplegar-en-azure) - [Desplegar en Azure con red privada](#desplegar-en-azure-con-red-privada) -- [Desplegar en Azure con autenticación Keycloak](#desplegar-en-azure-con-autenticacion-keycloak) +- [Desplegar en Azure con autenticación Keycloak](#desplegar-en-azure-con-autenticación-keycloak) - [Desplegar en Azure con Entra OAuth Proxy](#desplegar-en-azure-con-entra-oauth-proxy) ## Empezar @@ -77,7 +77,7 @@ Si no usas una de las opciones anteriores, necesitas: Este proyecto incluye servidores MCP en el directorio [`servers/`](../servers/): | Archivo | Descripción | -|------|-------------| +| ------- | ----------- | | [servers/basic_mcp_stdio.py](../servers/basic_mcp_stdio.py) | Servidor MCP con transporte stdio para integración con VS Code | | [servers/basic_mcp_http.py](../servers/basic_mcp_http.py) | Servidor MCP con transporte HTTP en el puerto 8000 | | [servers/deployed_mcp.py](../servers/deployed_mcp.py) | Servidor MCP para despliegue en Azure con Cosmos DB y autenticación opcional con Keycloak | @@ -200,7 +200,7 @@ Puedes usar el [.NET Aspire Dashboard](https://learn.microsoft.com/dotnet/aspire Este proyecto incluye agentes de ejemplo en el directorio [`agents/`](../agents/) que demuestran cómo conectar agentes de IA a servidores MCP: | Archivo | Descripción | -|------|-------------| +| ------- | ----------- | | [agents/agentframework_learn.py](../agents/agentframework_learn.py) | Integración del Microsoft Agent Framework con MCP | | [agents/agentframework_http.py](../agents/agentframework_http.py) | Integración del Microsoft Agent Framework con el servidor MCP de gastos local | | [agents/langchainv1_http.py](../agents/langchainv1_http.py) | Agente LangChain con integración MCP | @@ -382,7 +382,7 @@ Este proyecto soporta desplegar con autenticación OAuth 2.0 usando Keycloak com ### Qué se despliega | Componente | Descripción | -|-----------|-------------| +| ---------- | ----------- | | **Container App de Keycloak** | Keycloak 26.0 con realm preconfigurado | | **Configuración de rutas HTTP** | Enrutamiento basado en reglas: `/auth/*` → Keycloak, `/*` → Servidor MCP | | **Servidor MCP protegido con OAuth** | FastMCP con validación JWT contra el endpoint JWKS de Keycloak | @@ -492,7 +492,7 @@ Para usar el servidor MCP desplegado con GitHub Copilot Chat: ### Limitaciones conocidas (trade-offs de la demo) | Ítem | Actual | Recomendación para producción | Por qué | -|------|--------|-------------------------------|--------| +| ---- | ------ | ----------------------------- | ------- | | Modo de Keycloak | `start-dev` | `start` con configuración adecuada | El modo dev tiene defaults de seguridad relajados | | Base de datos | H2 en memoria | PostgreSQL | H2 no persiste datos entre reinicios | | Réplicas | 1 (por H2) | Múltiples con DB compartida | H2 es en memoria, no comparte estado | @@ -510,7 +510,7 @@ Este proyecto soporta desplegar con Microsoft Entra ID (Azure AD) usando el prox ### Qué se despliega con Entra OAuth | Componente | Descripción | -|-----------|-------------| +| ---------- | ----------- | | **App Registration de Microsoft Entra** | Se crea automáticamente durante el aprovisionamiento con URIs de redirección para desarrollo local, VS Code y producción | | **Servidor MCP protegido con OAuth** | FastMCP con AzureProvider para autenticación OAuth | | **Almacenamiento de clientes OAuth en CosmosDB** | Persiste registros de clientes OAuth entre reinicios del servidor | @@ -550,10 +550,10 @@ Este proyecto soporta desplegar con Microsoft Entra ID (Azure AD) usando el prox Las siguientes variables de entorno se definen automáticamente por los hooks de despliegue: -| Variable | Descripción | -|----------|-------------| -| `ENTRA_PROXY_AZURE_CLIENT_ID` | ID de cliente de la App Registration | -| `ENTRA_PROXY_AZURE_CLIENT_SECRET` | Secreto de cliente de la App Registration | +| Variable | Descripción | +| ----------------------------------- | ----------------------------------------- | +| `ENTRA_PROXY_AZURE_CLIENT_ID` | ID de cliente de la App Registration | +| `ENTRA_PROXY_AZURE_CLIENT_SECRET` | Secreto de cliente de la App Registration | Estas luego se escriben a `.env` por el hook de postprovision para desarrollo local. @@ -563,7 +563,7 @@ Después del despliegue, puedes probar localmente con OAuth habilitado: ```bash # Ejecuta el servidor MCP -cd servers && uvicorn auth_mcp:app --host 0.0.0.0 --port 8000 +cd servers && uvicorn auth_entra_mcp:app --host 0.0.0.0 --port 8000 ``` El servidor usará la App Registration de Entra para OAuth y CosmosDB para almacenamiento de clientes. diff --git a/uv.lock b/uv.lock index 2dd831e..abdb12d 100644 --- a/uv.lock +++ b/uv.lock @@ -361,7 +361,7 @@ wheels = [ [[package]] name = "azure-monitor-opentelemetry" -version = "1.8.1" +version = "1.8.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, @@ -377,27 +377,26 @@ dependencies = [ { name = "opentelemetry-resource-detector-azure" }, { name = "opentelemetry-sdk" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/ae/eae89705498c975b1cfcc2ce0e5bfbe784a47ffd54cef6fbebe31fdb2295/azure_monitor_opentelemetry-1.8.1.tar.gz", hash = "sha256:9b93b62868775d74db60d9e997cfccc5898260c5de23278d7e99cce3764e9fda", size = 53471 } +sdist = { url = "https://files.pythonhosted.org/packages/8d/a9/f335c32e76e3bac3fbbd7977980f62a7deec5191e984517bdbb38539dfd1/azure_monitor_opentelemetry-1.8.3.tar.gz", hash = "sha256:4aa10f6712db653f618e14e3701de7a2f96669a8f2fea6fb22125077da4ea91c", size = 55177 } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/ab/d063f5d0debbb01ef716789f5b4b315d58f657dd5dbf15e47ca6648a557b/azure_monitor_opentelemetry-1.8.1-py3-none-any.whl", hash = "sha256:bebca6af9d81ddc52df59b281a5acc84182bbf1cbccd6f843a2074f6e283947e", size = 27169 }, + { url = "https://files.pythonhosted.org/packages/57/4f/138e2f1eddce9b8dda1cccccb5eb3819c1d4d5ea843fbf09ecc3d810641b/azure_monitor_opentelemetry-1.8.3-py3-none-any.whl", hash = "sha256:647248328bb03f8044918411d57c661230277958559f067892bd79f98ce8f69c", size = 27687 }, ] [[package]] name = "azure-monitor-opentelemetry-exporter" -version = "1.0.0b44" +version = "1.0.0b46" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, { name = "azure-identity" }, - { name = "fixedint" }, { name = "msrest" }, { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, { name = "psutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3e/9a/acb253869ef59482c628f4dc7e049323d0026a9374adf7b398d0b04b6094/azure_monitor_opentelemetry_exporter-1.0.0b44.tar.gz", hash = "sha256:9b0f430a6a46a78bf757ae301488c10c1996f1bd6c5c01a07b9d33583cc4fa4b", size = 271712 } +sdist = { url = "https://files.pythonhosted.org/packages/63/57/4dd223fcded4955f85ecfae721802cf4bc5a9a95efd8a9a00271f80d4e6b/azure_monitor_opentelemetry_exporter-1.0.0b46.tar.gz", hash = "sha256:a2fd5837c41b5b10316b089ccbe694fc8a69c23db92a5555b298b1eec3eb38bd", size = 277957 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/46/31809698a0d50559fde108a4f4cb2d9532967ae514a113dba39763e048b7/azure_monitor_opentelemetry_exporter-1.0.0b44-py2.py3-none-any.whl", hash = "sha256:82d23081bf007acab8d4861229ab482e4666307a29492fbf0bf19981b4d37024", size = 198516 }, + { url = "https://files.pythonhosted.org/packages/9b/f9/5b1273d134743b59a271000c08ea3b686f184bfbd73e5ab3a7feae2d0e5f/azure_monitor_opentelemetry_exporter-1.0.0b46-py2.py3-none-any.whl", hash = "sha256:12935e72dcad4a162636eaa5f861e106fcdc3c19928e79cd58b52653fe15625a", size = 200542 }, ] [[package]] @@ -520,6 +519,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295 }, ] +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228 }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -695,6 +703,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317 }, ] +[[package]] +name = "fakeredis" +version = "2.33.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "redis" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/f9/57464119936414d60697fcbd32f38909bb5688b616ae13de6e98384433e0/fakeredis-2.33.0.tar.gz", hash = "sha256:d7bc9a69d21df108a6451bbffee23b3eba432c21a654afc7ff2d295428ec5770", size = 175187 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/78/a850fed8aeef96d4a99043c90b818b2ed5419cd5b24a4049fd7cfb9f1471/fakeredis-2.33.0-py3-none-any.whl", hash = "sha256:de535f3f9ccde1c56672ab2fdd6a8efbc4f2619fc2f1acc87b8737177d71c965", size = 119605 }, +] + +[package.optional-dependencies] +lua = [ + { name = "lupa" }, +] + [[package]] name = "fastapi" version = "0.119.1" @@ -711,7 +737,7 @@ wheels = [ [[package]] name = "fastmcp" -version = "2.13.3" +version = "2.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "authlib" }, @@ -722,17 +748,18 @@ dependencies = [ { name = "mcp" }, { name = "openapi-pydantic" }, { name = "platformdirs" }, - { name = "py-key-value-aio", extra = ["disk", "memory"] }, + { name = "py-key-value-aio", extra = ["disk", "keyring", "memory"] }, { name = "pydantic", extra = ["email"] }, + { name = "pydocket" }, { name = "pyperclip" }, { name = "python-dotenv" }, { name = "rich" }, { name = "uvicorn" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/21/a1/a507bfb73f51983759cbbc3702b6f4780128cff68ebbc51db2f10170c950/fastmcp-2.13.3.tar.gz", hash = "sha256:ebca59e99412c596dd75ebdd5147800f6abc2490d025af76fa8ea4fc5f68781d", size = 8185958 } +sdist = { url = "https://files.pythonhosted.org/packages/d1/1e/e3528227688c248283f6d86869b1e900563ffc223eff00f4f923d2750365/fastmcp-2.14.2.tar.gz", hash = "sha256:bd23d1b808b6f446444f10114dac468b11bfb9153ed78628f5619763d0cf573e", size = 8272966 } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/bc/56925f1202357dbfcfdfd0c75afc6c27ec1e6ef1d89b7e7410df3945ceb4/fastmcp-2.13.3-py3-none-any.whl", hash = "sha256:5173d335f4e6aabcfb5a5131af3fa092f604b303130fd3a49226b7a844a48e65", size = 385644 }, + { url = "https://files.pythonhosted.org/packages/0d/67/8456d39484fcb7afd0defed21918e773ed59a98b39e5b633328527c88367/fastmcp-2.14.2-py3-none-any.whl", hash = "sha256:e33cd622e1ebd5110af6a981804525b6cd41072e3c7d68268ed69ef3be651aca", size = 413279 }, ] [[package]] @@ -744,15 +771,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054 }, ] -[[package]] -name = "fixedint" -version = "0.1.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/32/c6/b1b9b3f69915d51909ef6ebe6352e286ec3d6f2077278af83ec6e3cc569c/fixedint-0.1.6.tar.gz", hash = "sha256:703005d090499d41ce7ce2ee7eae8f7a5589a81acdc6b79f1728a56495f2c799", size = 12750 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/6d/8f5307d26ce700a89e5a67d1e1ad15eff977211f9ed3ae90d7b0d67f4e66/fixedint-0.1.6-py3-none-any.whl", hash = "sha256:b8cf9f913735d2904deadda7a6daa9f57100599da1de57a7448ea1be75ae8c9c", size = 12702 }, -] - [[package]] name = "frozenlist" version = "1.8.0" @@ -1012,6 +1030,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320 }, ] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777 }, +] + +[[package]] +name = "jaraco-context" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/7d/41acf8e22d791bde812cb6c2c36128bb932ed8ae066bcb5e39cb198e8253/jaraco_context-6.0.2.tar.gz", hash = "sha256:953ae8dddb57b1d791bf72ea1009b32088840a7dd19b9ba16443f62be919ee57", size = 14994 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/0c/1e0096ced9c55f9c6c6655446798df74165780375d3f5ab5f33751e087ae/jaraco_context-6.0.2-py3-none-any.whl", hash = "sha256:55fc21af4b4f9ca94aa643b6ee7fe13b1e4c01abf3aeb98ca4ad9c80b741c786", size = 6988 }, +] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481 }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010 }, +] + [[package]] name = "jiter" version = "0.11.1" @@ -1121,6 +1181,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437 }, ] +[[package]] +name = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160 }, +] + [[package]] name = "langchain" version = "1.0.0a5" @@ -1270,7 +1347,7 @@ wheels = [ [[package]] name = "logfire" -version = "4.15.1" +version = "4.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "executing" }, @@ -1281,9 +1358,28 @@ dependencies = [ { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/26/7b/3cdbfbd8fc085912d7322afc5f16ffd396197574fc9c786422b0ce3f5232/logfire-4.15.1.tar.gz", hash = "sha256:fac8463c0319af6d1bf66788802ff0a04b481dac006564f6837f64c7404f474a", size = 549319 } +sdist = { url = "https://files.pythonhosted.org/packages/7d/74/88e8fec6e3fe247a7cdaeba777cbf7aced11de33db423693690e20ab8356/logfire-4.17.0.tar.gz", hash = "sha256:693d47d6b8b0a8f9fd8112d958eb9e3ae00fe5d323e1eee468e4bc6379bb2c4f", size = 558897 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/68/c49baec77c56dcf5698daaa66a097523063d55b2feaecc968000518521a1/logfire-4.17.0-py3-none-any.whl", hash = "sha256:1ed1064a4126f48503f7832ce6039425dff70128d145552d7981ac9ce2151b17", size = 232456 }, +] + +[[package]] +name = "lupa" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/1c/191c3e6ec6502e3dbe25a53e27f69a5daeac3e56de1f73c0138224171ead/lupa-2.6.tar.gz", hash = "sha256:9a770a6e89576be3447668d7ced312cd6fd41d3c13c2462c9dc2c2ab570e45d9", size = 7240282 } wheels = [ - { url = "https://files.pythonhosted.org/packages/21/44/45c39998cb3920a11498455f3f62f173bd3f5d9a15bd0db40f88bedb9e1f/logfire-4.15.1-py3-none-any.whl", hash = "sha256:b931d2becf937c08d7c89f1ab68ab05298095b010dfaf29cbd22f7bcacbaa2bb", size = 228716 }, + { url = "https://files.pythonhosted.org/packages/28/1d/21176b682ca5469001199d8b95fa1737e29957a3d185186e7a8b55345f2e/lupa-2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:663a6e58a0f60e7d212017d6678639ac8df0119bc13c2145029dcba084391310", size = 947232 }, + { url = "https://files.pythonhosted.org/packages/ce/4c/d327befb684660ca13cf79cd1f1d604331808f9f1b6fb6bf57832f8edf80/lupa-2.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:d1f5afda5c20b1f3217a80e9bc1b77037f8a6eb11612fd3ada19065303c8f380", size = 1908625 }, + { url = "https://files.pythonhosted.org/packages/66/8e/ad22b0a19454dfd08662237a84c792d6d420d36b061f239e084f29d1a4f3/lupa-2.6-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:26f2b3c085fe76e9119e48c1013c1cccdc1f51585d456858290475aa38e7089e", size = 981057 }, + { url = "https://files.pythonhosted.org/packages/5c/48/74859073ab276bd0566c719f9ca0108b0cfc1956ca0d68678d117d47d155/lupa-2.6-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:60d2f902c7b96fb8ab98493dcff315e7bb4d0b44dc9dd76eb37de575025d5685", size = 1156227 }, + { url = "https://files.pythonhosted.org/packages/09/6c/0e9ded061916877253c2266074060eb71ed99fb21d73c8c114a76725bce2/lupa-2.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a02d25dee3a3250967c36590128d9220ae02f2eda166a24279da0b481519cbff", size = 1035752 }, + { url = "https://files.pythonhosted.org/packages/dd/ef/f8c32e454ef9f3fe909f6c7d57a39f950996c37a3deb7b391fec7903dab7/lupa-2.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eae1ee16b886b8914ff292dbefbf2f48abfbdee94b33a88d1d5475e02423203", size = 2069009 }, + { url = "https://files.pythonhosted.org/packages/53/dc/15b80c226a5225815a890ee1c11f07968e0aba7a852df41e8ae6fe285063/lupa-2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0edd5073a4ee74ab36f74fe61450148e6044f3952b8d21248581f3c5d1a58be", size = 1056301 }, + { url = "https://files.pythonhosted.org/packages/31/14/2086c1425c985acfb30997a67e90c39457122df41324d3c179d6ee2292c6/lupa-2.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0c53ee9f22a8a17e7d4266ad48e86f43771951797042dd51d1494aaa4f5f3f0a", size = 1170673 }, + { url = "https://files.pythonhosted.org/packages/10/e5/b216c054cf86576c0191bf9a9f05de6f7e8e07164897d95eea0078dca9b2/lupa-2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:de7c0f157a9064a400d828789191a96da7f4ce889969a588b87ec80de9b14772", size = 2162227 }, + { url = "https://files.pythonhosted.org/packages/59/2f/33ecb5bedf4f3bc297ceacb7f016ff951331d352f58e7e791589609ea306/lupa-2.6-cp313-cp313-win32.whl", hash = "sha256:ee9523941ae0a87b5b703417720c5d78f72d2f5bc23883a2ea80a949a3ed9e75", size = 1419558 }, + { url = "https://files.pythonhosted.org/packages/f9/b4/55e885834c847ea610e111d87b9ed4768f0afdaeebc00cd46810f25029f6/lupa-2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b1335a5835b0a25ebdbc75cf0bda195e54d133e4d994877ef025e218c2e59db9", size = 1683424 }, ] [[package]] @@ -1300,7 +1396,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.22.0" +version = "1.25.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1318,9 +1414,9 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/a2/c5ec0ab38b35ade2ae49a90fada718fbc76811dc5aa1760414c6aaa6b08a/mcp-1.22.0.tar.gz", hash = "sha256:769b9ac90ed42134375b19e777a2858ca300f95f2e800982b3e2be62dfc0ba01", size = 471788 } +sdist = { url = "https://files.pythonhosted.org/packages/d5/2d/649d80a0ecf6a1f82632ca44bec21c0461a9d9fc8934d38cb5b319f2db5e/mcp-1.25.0.tar.gz", hash = "sha256:56310361ebf0364e2d438e5b45f7668cbb124e158bb358333cd06e49e83a6802", size = 605387 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/bb/711099f9c6bb52770f56e56401cdfb10da5b67029f701e0df29362df4c8e/mcp-1.22.0-py3-none-any.whl", hash = "sha256:bed758e24df1ed6846989c909ba4e3df339a27b4f30f1b8b627862a4bade4e98", size = 175489 }, + { url = "https://files.pythonhosted.org/packages/e2/fc/6dc7659c2ae5ddf280477011f4213a74f806862856b796ef08f028e664bf/mcp-1.25.0-py3-none-any.whl", hash = "sha256:b37c38144a666add0862614cc79ec276e97d72aa8ca26d622818d4e278b9721a", size = 233076 }, ] [package.optional-dependencies] @@ -1507,6 +1603,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/38/6266604dffb43378055394ea110570cf261a49876fc48f548dfe876f34cc/ml_dtypes-0.5.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdf40d2aaabd3913dec11840f0d0ebb1b93134f99af6a0a4fd88ffe924928ab4", size = 5285422 }, ] +[[package]] +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667 }, +] + [[package]] name = "msal" version = "1.34.0" @@ -1707,32 +1812,32 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.38.0" +version = "1.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/d8/0f354c375628e048bd0570645b310797299754730079853095bf000fba69/opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12", size = 65242 } +sdist = { url = "https://files.pythonhosted.org/packages/c0/0b/e5428c009d4d9af0515b0a8371a8aaae695371af291f45e702f7969dce6b/opentelemetry_api-1.39.0.tar.gz", hash = "sha256:6130644268c5ac6bdffaf660ce878f10906b3e789f7e2daa5e169b047a2933b9", size = 65763 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/a2/d86e01c28300bd41bab8f18afd613676e2bd63515417b77636fc1add426f/opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582", size = 65947 }, + { url = "https://files.pythonhosted.org/packages/05/85/d831a9bc0a9e0e1a304ff3d12c1489a5fbc9bf6690a15dcbdae372bbca45/opentelemetry_api-1.39.0-py3-none-any.whl", hash = "sha256:3c3b3ca5c5687b1b5b37e5c5027ff68eacea8675241b29f13110a8ffbb8f0459", size = 66357 }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.38.0" +version = "1.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/83/dd4660f2956ff88ed071e9e0e36e830df14b8c5dc06722dbde1841accbe8/opentelemetry_exporter_otlp_proto_common-1.38.0.tar.gz", hash = "sha256:e333278afab4695aa8114eeb7bf4e44e65c6607d54968271a249c180b2cb605c", size = 20431 } +sdist = { url = "https://files.pythonhosted.org/packages/11/cb/3a29ce606b10c76d413d6edd42d25a654af03e73e50696611e757d2602f3/opentelemetry_exporter_otlp_proto_common-1.39.0.tar.gz", hash = "sha256:a135fceed1a6d767f75be65bd2845da344dd8b9258eeed6bc48509d02b184409", size = 20407 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/9e/55a41c9601191e8cd8eb626b54ee6827b9c9d4a46d736f32abc80d8039fc/opentelemetry_exporter_otlp_proto_common-1.38.0-py3-none-any.whl", hash = "sha256:03cb76ab213300fe4f4c62b7d8f17d97fcfd21b89f0b5ce38ea156327ddda74a", size = 18359 }, + { url = "https://files.pythonhosted.org/packages/ef/c6/215edba62d13a3948c718b289539f70e40965bc37fc82ecd55bb0b749c1a/opentelemetry_exporter_otlp_proto_common-1.39.0-py3-none-any.whl", hash = "sha256:3d77be7c4bdf90f1a76666c934368b8abed730b5c6f0547a2ec57feb115849ac", size = 18367 }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.38.0" +version = "1.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -1743,14 +1848,14 @@ dependencies = [ { name = "opentelemetry-sdk" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/c0/43222f5b97dc10812bc4f0abc5dc7cd0a2525a91b5151d26c9e2e958f52e/opentelemetry_exporter_otlp_proto_grpc-1.38.0.tar.gz", hash = "sha256:2473935e9eac71f401de6101d37d6f3f0f1831db92b953c7dcc912536158ebd6", size = 24676 } +sdist = { url = "https://files.pythonhosted.org/packages/7e/62/4db083ee9620da3065eeb559e9fc128f41a1d15e7c48d7c83aafbccd354c/opentelemetry_exporter_otlp_proto_grpc-1.39.0.tar.gz", hash = "sha256:7e7bb3f436006836c0e0a42ac619097746ad5553ad7128a5bd4d3e727f37fc06", size = 24650 } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/f0/bd831afbdba74ca2ce3982142a2fad707f8c487e8a3b6fef01f1d5945d1b/opentelemetry_exporter_otlp_proto_grpc-1.38.0-py3-none-any.whl", hash = "sha256:7c49fd9b4bd0dbe9ba13d91f764c2d20b0025649a6e4ac35792fb8d84d764bc7", size = 19695 }, + { url = "https://files.pythonhosted.org/packages/56/e8/d420b94ffddfd8cff85bb4aa5d98da26ce7935dc3cf3eca6b83cd39ab436/opentelemetry_exporter_otlp_proto_grpc-1.39.0-py3-none-any.whl", hash = "sha256:758641278050de9bb895738f35ff8840e4a47685b7e6ef4a201fe83196ba7a05", size = 19765 }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-http" -version = "1.38.0" +version = "1.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -1761,14 +1866,28 @@ dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/0a/debcdfb029fbd1ccd1563f7c287b89a6f7bef3b2902ade56797bfd020854/opentelemetry_exporter_otlp_proto_http-1.38.0.tar.gz", hash = "sha256:f16bd44baf15cbe07633c5112ffc68229d0edbeac7b37610be0b2def4e21e90b", size = 17282 } +sdist = { url = "https://files.pythonhosted.org/packages/81/dc/1e9bf3f6a28e29eba516bc0266e052996d02bc7e92675f3cd38169607609/opentelemetry_exporter_otlp_proto_http-1.39.0.tar.gz", hash = "sha256:28d78fc0eb82d5a71ae552263d5012fa3ebad18dfd189bf8d8095ba0e65ee1ed", size = 17287 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/46/e4a102e17205bb05a50dbf24ef0e92b66b648cd67db9a68865af06a242fd/opentelemetry_exporter_otlp_proto_http-1.39.0-py3-none-any.whl", hash = "sha256:5789cb1375a8b82653328c0ce13a054d285f774099faf9d068032a49de4c7862", size = 19639 }, +] + +[[package]] +name = "opentelemetry-exporter-prometheus" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "prometheus-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/da/8b81ff9d045fae7cac1e8bbf7ad59bf113d1008e483fc03e2cd3a0e620a3/opentelemetry_exporter_prometheus-0.60b0.tar.gz", hash = "sha256:c6ae33e52cdd1dbfed1f7436935df94eb03c725b57322026d04e6fbc37108e6e", size = 14975 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/77/154004c99fb9f291f74aa0822a2f5bbf565a72d8126b3a1b63ed8e5f83c7/opentelemetry_exporter_otlp_proto_http-1.38.0-py3-none-any.whl", hash = "sha256:84b937305edfc563f08ec69b9cb2298be8188371217e867c1854d77198d0825b", size = 19579 }, + { url = "https://files.pythonhosted.org/packages/8b/18/18b662a6ecb8252db9e7457fd3c836729bf28b055b60505cbd4763ea9300/opentelemetry_exporter_prometheus-0.60b0-py3-none-any.whl", hash = "sha256:4f616397040257fae4c5e5272b57b47c13372e3b7f0f2db2427fd4dbe69c60b5", size = 13017 }, ] [[package]] name = "opentelemetry-instrumentation" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1776,14 +1895,14 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/ed/9c65cd209407fd807fa05be03ee30f159bdac8d59e7ea16a8fe5a1601222/opentelemetry_instrumentation-0.59b0.tar.gz", hash = "sha256:6010f0faaacdaf7c4dff8aac84e226d23437b331dcda7e70367f6d73a7db1adc", size = 31544 } +sdist = { url = "https://files.pythonhosted.org/packages/55/3c/bd53dbb42eff93d18e3047c7be11224aa9966ce98ac4cc5bfb860a32c95a/opentelemetry_instrumentation-0.60b0.tar.gz", hash = "sha256:4e9fec930f283a2677a2217754b40aaf9ef76edae40499c165bc7f1d15366a74", size = 31707 } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/f5/7a40ff3f62bfe715dad2f633d7f1174ba1a7dd74254c15b2558b3401262a/opentelemetry_instrumentation-0.59b0-py3-none-any.whl", hash = "sha256:44082cc8fe56b0186e87ee8f7c17c327c4c2ce93bdbe86496e600985d74368ee", size = 33020 }, + { url = "https://files.pythonhosted.org/packages/5c/7b/5b5b9f8cfe727a28553acf9cd287b1d7f706f5c0a00d6e482df55b169483/opentelemetry_instrumentation-0.60b0-py3-none-any.whl", hash = "sha256:aaafa1483543a402819f1bdfb06af721c87d60dd109501f9997332862a35c76a", size = 33096 }, ] [[package]] name = "opentelemetry-instrumentation-asgi" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, @@ -1792,14 +1911,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/a4/cfbb6fc1ec0aa9bf5a93f548e6a11ab3ac1956272f17e0d399aa2c1f85bc/opentelemetry_instrumentation_asgi-0.59b0.tar.gz", hash = "sha256:2509d6fe9fd829399ce3536e3a00426c7e3aa359fc1ed9ceee1628b56da40e7a", size = 25116 } +sdist = { url = "https://files.pythonhosted.org/packages/b0/0a/715ea7044708d3c215385fb2a1c6ffe429aacb3cd23a348060aaeda52834/opentelemetry_instrumentation_asgi-0.60b0.tar.gz", hash = "sha256:928731218050089dca69f0fe980b8bfe109f384be8b89802d7337372ddb67b91", size = 26083 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/88/fe02d809963b182aafbf5588685d7a05af8861379b0ec203d48e360d4502/opentelemetry_instrumentation_asgi-0.59b0-py3-none-any.whl", hash = "sha256:ba9703e09d2c33c52fa798171f344c8123488fcd45017887981df088452d3c53", size = 16797 }, + { url = "https://files.pythonhosted.org/packages/9b/8c/c6c59127fd996107243ca45669355665a7daff578ddafb86d6d2d3b01428/opentelemetry_instrumentation_asgi-0.60b0-py3-none-any.whl", hash = "sha256:9d76a541269452c718a0384478f3291feb650c5a3f29e578fdc6613ea3729cf3", size = 16907 }, ] [[package]] name = "opentelemetry-instrumentation-dbapi" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1807,14 +1926,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/aa/36a09652c98c65b42408d40f222fba031a3a281f1b6682e1b141b20b508d/opentelemetry_instrumentation_dbapi-0.59b0.tar.gz", hash = "sha256:c50112ae1cdb7f55bddcf57eca96aaa0f2dd78732be2b00953183439a4740493", size = 16308 } +sdist = { url = "https://files.pythonhosted.org/packages/12/7f/b4c1fbce01b29daad5ef1396427c9cd3c7a55ee68e75f8c11089c7e2533d/opentelemetry_instrumentation_dbapi-0.60b0.tar.gz", hash = "sha256:2b7eb38e46890cebe5bc1a1c03d2ab07fc159b0b7b91342941ee33dd73876d84", size = 16311 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/9b/1739b5b7926cbae342880d7a56d59a847313e6568a96ba7d4873ce0c0996/opentelemetry_instrumentation_dbapi-0.59b0-py3-none-any.whl", hash = "sha256:672d59caa06754b42d4e722644d9fcd00a1f9f862e9ea5cef6d4da454515ac67", size = 13970 }, + { url = "https://files.pythonhosted.org/packages/23/0a/65e100c6d803de59a9113a993dcd371a4027453ba15ce4dabdb0343ca154/opentelemetry_instrumentation_dbapi-0.60b0-py3-none-any.whl", hash = "sha256:429d8ca34a44a4296b9b09a1bd373fff350998d200525c6e79883c3328559b03", size = 13966 }, ] [[package]] name = "opentelemetry-instrumentation-django" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1823,14 +1942,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/cf/a329abb33a9f7934cfd9e5645e69550a4d5dcdd6d1970283854460e11f9d/opentelemetry_instrumentation_django-0.59b0.tar.gz", hash = "sha256:469c2d973619355645ec696bbc4afab836ce22cbc83236a0382c3090588f7772", size = 25008 } +sdist = { url = "https://files.pythonhosted.org/packages/7c/d2/8ddd9a5c61cd5048d422be8d22fac40f603aa82f0babf9f7c40db871080c/opentelemetry_instrumentation_django-0.60b0.tar.gz", hash = "sha256:461e6fca27936ba97eec26da38bb5f19310783370478c7ca3a3e40faaceac9cc", size = 26596 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/c0/c8980bcb1ef1263fe0f4bbe52b74a1442c29b35eca4a9cb4ab4bb1028a3c/opentelemetry_instrumentation_django-0.59b0-py3-none-any.whl", hash = "sha256:a0a9eb74afc3870e72eaaa776054fbfd4d83ae306d0c5995f14414bcef2d830e", size = 19595 }, + { url = "https://files.pythonhosted.org/packages/18/d6/28684547bf6c699582e998a172ba8bb08405cf6706729b0d6a16042e998f/opentelemetry_instrumentation_django-0.60b0-py3-none-any.whl", hash = "sha256:95495649c8c34ce9217c6873cdd10fc4fcaa67c25f8329adc54f5b286999e40b", size = 21169 }, ] [[package]] name = "opentelemetry-instrumentation-fastapi" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1839,14 +1958,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/a7/7a6ce5009584ce97dbfd5ce77d4f9d9570147507363349d2cb705c402bcf/opentelemetry_instrumentation_fastapi-0.59b0.tar.gz", hash = "sha256:e8fe620cfcca96a7d634003df1bc36a42369dedcdd6893e13fb5903aeeb89b2b", size = 24967 } +sdist = { url = "https://files.pythonhosted.org/packages/fe/51/a021a7c929b5103fcb6bfdfa5a99abcaeb3b505faf9e3ee3ec14612c1ef9/opentelemetry_instrumentation_fastapi-0.60b0.tar.gz", hash = "sha256:5d34d67eb634a08bfe9e530680d6177521cd9da79285144e6d5a8f42683ed1b3", size = 24960 } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/27/5914c8bf140ffc70eff153077e225997c7b054f0bf28e11b9ab91b63b18f/opentelemetry_instrumentation_fastapi-0.59b0-py3-none-any.whl", hash = "sha256:0d8d00ff7d25cca40a4b2356d1d40a8f001e0668f60c102f5aa6bb721d660c4f", size = 13492 }, + { url = "https://files.pythonhosted.org/packages/b1/5a/e238c108eb65a726d75184439377a87d532050036b54e718e4c789b26d1a/opentelemetry_instrumentation_fastapi-0.60b0-py3-none-any.whl", hash = "sha256:415c6602db01ee339276ea4cabe3e80177c9e955631c087f2ef60a75e31bfaee", size = 13478 }, ] [[package]] name = "opentelemetry-instrumentation-flask" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1856,28 +1975,28 @@ dependencies = [ { name = "opentelemetry-util-http" }, { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/42/afccc8414f85108d41bb73155d0e828bf07102068ef03396bd1ef4296544/opentelemetry_instrumentation_flask-0.59b0.tar.gz", hash = "sha256:8b379d331b61f40a7c72c9ae8e0fca72c72ffeb6db75908811217196c9544b9b", size = 19587 } +sdist = { url = "https://files.pythonhosted.org/packages/30/cc/e0758c23d66fd49956169cb24b5b06130373da2ce8d49945abce82003518/opentelemetry_instrumentation_flask-0.60b0.tar.gz", hash = "sha256:560f08598ef40cdcf7ca05bfb2e3ea74fab076e676f4c18bb36bb379bf5c4a1b", size = 20336 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/5e/99db8cedd745d989f860a8c9544c6d5c47c79117251088927e98c7167f85/opentelemetry_instrumentation_flask-0.59b0-py3-none-any.whl", hash = "sha256:5e97fde228f66d7bf9512a86383c0d30a869e2d3b424b51a2781ca40d0287cdc", size = 14741 }, + { url = "https://files.pythonhosted.org/packages/9b/b5/387ce11f59e5ce65b890adc3f9c457877143b8a6d107a3a0b305397933a1/opentelemetry_instrumentation_flask-0.60b0-py3-none-any.whl", hash = "sha256:106e5774f79ac9b86dd0d949c1b8f46c807a8af16184301e10d24fc94e680d04", size = 15189 }, ] [[package]] name = "opentelemetry-instrumentation-psycopg2" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-dbapi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/76/d4adf1b9e811ee6af19b074d80cff1026f3074f78d2d915846aecbab29d9/opentelemetry_instrumentation_psycopg2-0.59b0.tar.gz", hash = "sha256:ba440b15543a7e8c6ffd1f20a30e6062cbf34cc42e61c602b8587b512704588b", size = 10735 } +sdist = { url = "https://files.pythonhosted.org/packages/f8/68/5ae8a3b9a28c2fdf8d3d050e451ddb2612ca963679b08a2959f01f6dda4b/opentelemetry_instrumentation_psycopg2-0.60b0.tar.gz", hash = "sha256:59e527fd97739440380634ffcf9431aa7f2965d939d8d5829790886e2b54ede9", size = 11266 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/70/3ac33f00c928725fb52bb9eaf2b51ac57370dfd9eb8ddb60d6fd6e9fab95/opentelemetry_instrumentation_psycopg2-0.59b0-py3-none-any.whl", hash = "sha256:c96e1f5d91320166173af4ca8f4735ec2de61b7d99810bd23dd44644334514bd", size = 10731 }, + { url = "https://files.pythonhosted.org/packages/d4/24/66b5a41a2b0d1d07cc9b0fbd80f8b5c66b46a4d4731743505891da8b3cbe/opentelemetry_instrumentation_psycopg2-0.60b0-py3-none-any.whl", hash = "sha256:ea136a32babd559aa717c04dddf6aa78aa94b816fb4e10dfe06751727ef306d4", size = 11284 }, ] [[package]] name = "opentelemetry-instrumentation-requests" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1885,14 +2004,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/49/01/31282a46b09684dfc636bc066deb090bae6973e71e85e253a8c74e727b1f/opentelemetry_instrumentation_requests-0.59b0.tar.gz", hash = "sha256:9af2ffe3317f03074d7f865919139e89170b6763a0251b68c25e8e64e04b3400", size = 15186 } +sdist = { url = "https://files.pythonhosted.org/packages/26/0f/94c6181e95c867f559715887c418170a9eadd92ea6090122d464e375ff56/opentelemetry_instrumentation_requests-0.60b0.tar.gz", hash = "sha256:5079ed8df96d01dab915a0766cd28a49be7c33439ce43d6d39843ed6dee3204f", size = 16173 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/ea/c282ba418b2669e4f730cb3f68b02a0ca65f4baf801e971169a4cc449ffb/opentelemetry_instrumentation_requests-0.59b0-py3-none-any.whl", hash = "sha256:d43121532877e31a46c48649279cec2504ee1e0ceb3c87b80fe5ccd7eafc14c1", size = 12966 }, + { url = "https://files.pythonhosted.org/packages/f1/e1/2f13b41c5679243ba8eae651170c4ce2f532349877819566ae4a89a2b47f/opentelemetry_instrumentation_requests-0.60b0-py3-none-any.whl", hash = "sha256:e9957f3a650ae55502fa227b29ff985b37d63e41c85e6e1555d48039f092ea83", size = 13122 }, ] [[package]] name = "opentelemetry-instrumentation-starlette" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1901,14 +2020,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/39/95e5868e731bcf08a642483afaa34c455bfa4f61d6cfdd096626e9ee40d6/opentelemetry_instrumentation_starlette-0.59b0.tar.gz", hash = "sha256:3f033fd92d6a8e4122ebcb3d83afc5c64d6be7930e9094876eb02b8afbd08ba5", size = 14657 } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8f/8113a6cfcf928c1175c7c5ae0266ea2a9aa9814aca304d5555202a137a6c/opentelemetry_instrumentation_starlette-0.60b0.tar.gz", hash = "sha256:f446c1a928cbc46178a90a351c6d6d78629777760ac132c2b4342d1c5aebe1f3", size = 14643 } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/fc/4727e11d7c442ba8bb79ba261303c9e061c235eabcf633ddefc10230c8c4/opentelemetry_instrumentation_starlette-0.59b0-py3-none-any.whl", hash = "sha256:a833b97d297e4b2aaf58041612663dbabb8380e3993c76a4e32b3e351470f321", size = 11778 }, + { url = "https://files.pythonhosted.org/packages/de/8e/3b3cb695b9d6e53f996cedb6d2873391baf452c784573791f9fcc02edf41/opentelemetry_instrumentation_starlette-0.60b0-py3-none-any.whl", hash = "sha256:efdbcf71a44e4c4d2aa506f891c98e966e09c507fa4a5a33f2e45b5424b55f36", size = 11764 }, ] [[package]] name = "opentelemetry-instrumentation-urllib" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1916,14 +2035,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/85/70cc79162aa778179520b82234e3a8668f0aea67a279bd81a2522868687d/opentelemetry_instrumentation_urllib-0.59b0.tar.gz", hash = "sha256:1e2bb3427ce13854453777d8dccf3b0144640b03846f00fc302bdb6e1f2f8c7a", size = 13931 } +sdist = { url = "https://files.pythonhosted.org/packages/95/db/be895de04bd56d7a2b2ef6d267a4c52f6cd325b6647d1c15ae888b1b0f6a/opentelemetry_instrumentation_urllib-0.60b0.tar.gz", hash = "sha256:89b8796f9ab64d0ea0833cfea98745963baa0d7e4a775b3d2a77791aa97cf3f9", size = 13931 } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/94/0e87ffe1edfdda27e401d8ebab71ee3dd9ceaac11f98b8f5c190820a317f/opentelemetry_instrumentation_urllib-0.59b0-py3-none-any.whl", hash = "sha256:ed2bd1a02e4334c13c13033681ff8cf10d5dfcd5b0e6d7514f94a00e7f7bd671", size = 12672 }, + { url = "https://files.pythonhosted.org/packages/2b/e0/178914d5cec77baef797c6d47412da478ff871b05eb8732d64037b87c868/opentelemetry_instrumentation_urllib-0.60b0-py3-none-any.whl", hash = "sha256:80e3545d02505dc0ea61b3a0a141ec2828e11bee6b7dedfd3ee7ed9a7adbf862", size = 12673 }, ] [[package]] name = "opentelemetry-instrumentation-urllib3" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1932,14 +2051,14 @@ dependencies = [ { name = "opentelemetry-util-http" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/53/ff93665911808933b1af6fbbb1be2eb83c0c46e3b5f24b0b04c094b5b719/opentelemetry_instrumentation_urllib3-0.59b0.tar.gz", hash = "sha256:2de8d53a746bba043be1bc8f3246e1b131ebb6e94fe73601edd8b2bd91fe35b8", size = 15788 } +sdist = { url = "https://files.pythonhosted.org/packages/25/a8/16a32239e84741fae1a2932badeade5e72b73bfc331b53f7049a648ca00b/opentelemetry_instrumentation_urllib3-0.60b0.tar.gz", hash = "sha256:6ae1640a993901bae8eda5496d8b1440fb326a29e4ba1db342738b8868174aad", size = 15789 } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/3d/673cbea7aafb93a4613abf3d9c920d7c65a8cad79c910719dc286169bac8/opentelemetry_instrumentation_urllib3-0.59b0-py3-none-any.whl", hash = "sha256:a68c363092cf5db8c67c5778dbb2e4a14554e77baf7d276c374ea75ec926e148", size = 13187 }, + { url = "https://files.pythonhosted.org/packages/16/b2/ca27479eaf1f3f4825481769eb0cb200cad839040b8d5f42662d0398a256/opentelemetry_instrumentation_urllib3-0.60b0-py3-none-any.whl", hash = "sha256:9a07504560feae650a9205b3e2a579a835819bb1d55498d26a5db477fe04bba0", size = 13187 }, ] [[package]] name = "opentelemetry-instrumentation-wsgi" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1947,21 +2066,21 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/1d/595907631263e0e4a9e3d5b2958b9ecfe3872938c706e6c842d0767c798c/opentelemetry_instrumentation_wsgi-0.59b0.tar.gz", hash = "sha256:ff0c3df043bd3653ad6a543cb2a1e666fbd4d63efffa04fa9d9090cef462e798", size = 18377 } +sdist = { url = "https://files.pythonhosted.org/packages/10/ad/ae04e35f3b96d9c20d5d3df94a4c296eabf7a54d35d6c831179471128270/opentelemetry_instrumentation_wsgi-0.60b0.tar.gz", hash = "sha256:5815195b1b9890f55c4baafec94ff98591579a7d9b16256064adea8ee5784651", size = 19104 } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/06/ef769a4f6fde97ff58bc4e38a12b6ae4be1d5fe0f76e69c19b0fd2e10405/opentelemetry_instrumentation_wsgi-0.59b0-py3-none-any.whl", hash = "sha256:f271076e56c22da1d0d3404519ba4a1891b39ee3d470ca7ece7332d57cbaa6b9", size = 14447 }, + { url = "https://files.pythonhosted.org/packages/73/0e/1ed4d3cdce7b2e00a24f79933b3472e642d4db98aaccc09769be5cbe5296/opentelemetry_instrumentation_wsgi-0.60b0-py3-none-any.whl", hash = "sha256:0ff80614c1e73f7e94a5860c7e6222a51195eebab3dc5f50d89013db3d5d2f13", size = 14553 }, ] [[package]] name = "opentelemetry-proto" -version = "1.38.0" +version = "1.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/14/f0c4f0f6371b9cb7f9fa9ee8918bfd59ac7040c7791f1e6da32a1839780d/opentelemetry_proto-1.38.0.tar.gz", hash = "sha256:88b161e89d9d372ce723da289b7da74c3a8354a8e5359992be813942969ed468", size = 46152 } +sdist = { url = "https://files.pythonhosted.org/packages/48/b5/64d2f8c3393cd13ea2092106118f7b98461ba09333d40179a31444c6f176/opentelemetry_proto-1.39.0.tar.gz", hash = "sha256:c1fa48678ad1a1624258698e59be73f990b7fc1f39e73e16a9d08eef65dd838c", size = 46153 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/6a/82b68b14efca5150b2632f3692d627afa76b77378c4999f2648979409528/opentelemetry_proto-1.38.0-py3-none-any.whl", hash = "sha256:b6ebe54d3217c42e45462e2a1ae28c3e2bf2ec5a5645236a490f55f45f1a0a18", size = 72535 }, + { url = "https://files.pythonhosted.org/packages/e3/4d/d500e1862beed68318705732d1976c390f4a72ca8009c4983ff627acff20/opentelemetry_proto-1.39.0-py3-none-any.whl", hash = "sha256:1e086552ac79acb501485ff0ce75533f70f3382d43d0a30728eeee594f7bf818", size = 72534 }, ] [[package]] @@ -1978,29 +2097,29 @@ wheels = [ [[package]] name = "opentelemetry-sdk" -version = "1.38.0" +version = "1.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/85/cb/f0eee1445161faf4c9af3ba7b848cc22a50a3d3e2515051ad8628c35ff80/opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe", size = 171942 } +sdist = { url = "https://files.pythonhosted.org/packages/51/e3/7cd989003e7cde72e0becfe830abff0df55c69d237ee7961a541e0167833/opentelemetry_sdk-1.39.0.tar.gz", hash = "sha256:c22204f12a0529e07aa4d985f1bca9d6b0e7b29fe7f03e923548ae52e0e15dde", size = 171322 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/2e/e93777a95d7d9c40d270a371392b6d6f1ff170c2a3cb32d6176741b5b723/opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b", size = 132349 }, + { url = "https://files.pythonhosted.org/packages/a4/b4/2adc8bc83eb1055ecb592708efb6f0c520cc2eb68970b02b0f6ecda149cf/opentelemetry_sdk-1.39.0-py3-none-any.whl", hash = "sha256:90cfb07600dfc0d2de26120cebc0c8f27e69bf77cd80ef96645232372709a514", size = 132413 }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/bc/8b9ad3802cd8ac6583a4eb7de7e5d7db004e89cb7efe7008f9c8a537ee75/opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0", size = 129861 } +sdist = { url = "https://files.pythonhosted.org/packages/71/0e/176a7844fe4e3cb5de604212094dffaed4e18b32f1c56b5258bcbcba85c2/opentelemetry_semantic_conventions-0.60b0.tar.gz", hash = "sha256:227d7aa73cbb8a2e418029d6b6465553aa01cf7e78ec9d0bc3255c7b3ac5bf8f", size = 137935 } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/7d/c88d7b15ba8fe5c6b8f93be50fc11795e9fc05386c44afaf6b76fe191f9b/opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed", size = 207954 }, + { url = "https://files.pythonhosted.org/packages/d0/56/af0306666f91bae47db14d620775604688361f0f76a872e0005277311131/opentelemetry_semantic_conventions-0.60b0-py3-none-any.whl", hash = "sha256:069530852691136018087b52688857d97bba61cd641d0f8628d2d92788c4f78a", size = 219981 }, ] [[package]] @@ -2014,11 +2133,11 @@ wheels = [ [[package]] name = "opentelemetry-util-http" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/34/f7/13cd081e7851c42520ab0e96efb17ffbd901111a50b8252ec1e240664020/opentelemetry_util_http-0.59b0.tar.gz", hash = "sha256:ae66ee91be31938d832f3b4bc4eb8a911f6eddd38969c4a871b1230db2a0a560", size = 9412 } +sdist = { url = "https://files.pythonhosted.org/packages/38/0d/786a713445cf338131fef3a84fab1378e4b2ef3c3ea348eeb0c915eb804a/opentelemetry_util_http-0.60b0.tar.gz", hash = "sha256:e42b7bb49bba43b6f34390327d97e5016eb1c47949ceaf37c4795472a4e3a82d", size = 10576 } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/56/62282d1d4482061360449dacc990c89cad0fc810a2ed937b636300f55023/opentelemetry_util_http-0.59b0-py3-none-any.whl", hash = "sha256:6d036a07563bce87bf521839c0671b507a02a0d39d7ea61b88efa14c6e25355d", size = 7648 }, + { url = "https://files.pythonhosted.org/packages/53/5d/a448862f6d10c95685ed0e703596b6bd1784074e7ad90bffdc550abb7b68/opentelemetry_util_http-0.60b0-py3-none-any.whl", hash = "sha256:4f366f1a48adb74ffa6f80aee26f96882e767e01b03cd1cfb948b6e1020341fe", size = 8742 }, ] [[package]] @@ -2151,6 +2270,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429 }, ] +[[package]] +name = "prometheus-client" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/53/3edb5d68ecf6b38fcbcc1ad28391117d2a322d9a1a3eff04bfdb184d8c3b/prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce", size = 80481 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145 }, +] + [[package]] name = "propcache" version = "0.4.1" @@ -2250,9 +2378,15 @@ disk = [ { name = "diskcache" }, { name = "pathvalidate" }, ] +keyring = [ + { name = "keyring" }, +] memory = [ { name = "cachetools" }, ] +redis = [ + { name = "redis" }, +] [[package]] name = "py-key-value-shared" @@ -2369,6 +2503,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c", size = 48608 }, ] +[[package]] +name = "pydocket" +version = "0.16.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, + { name = "fakeredis", extra = ["lua"] }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-prometheus" }, + { name = "opentelemetry-instrumentation" }, + { name = "prometheus-client" }, + { name = "py-key-value-aio", extra = ["memory", "redis"] }, + { name = "python-json-logger" }, + { name = "redis" }, + { name = "rich" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/c5/61dcfce4d50b66a3f09743294d37fab598b81bb0975054b7f732da9243ec/pydocket-0.16.3.tar.gz", hash = "sha256:78e9da576de09e9f3f410d2471ef1c679b7741ddd21b586c97a13872b69bd265", size = 297080 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/94/93b7f5981aa04f922e0d9ce7326a4587866ec7e39f7c180ffcf408e66ee8/pydocket-0.16.3-py3-none-any.whl", hash = "sha256:e2b50925356e7cd535286255195458ac7bba15f25293356651b36d223db5dd7c", size = 67087 }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -2422,6 +2579,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556 }, ] +[[package]] +name = "python-json-logger" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548 }, +] + [[package]] name = "python-mcp-demos" version = "0.1.0" @@ -2460,10 +2626,10 @@ requires-dist = [ { name = "azure-core-tracing-opentelemetry", specifier = ">=1.0.0b12" }, { name = "azure-cosmos", specifier = ">=4.9.0" }, { name = "azure-identity", specifier = ">=1.25.1" }, - { name = "azure-monitor-opentelemetry", specifier = ">=1.6.4" }, + { name = "azure-monitor-opentelemetry", specifier = ">=1.8.3" }, { name = "debugpy", specifier = ">=1.8.0" }, { name = "dotenv-azd", specifier = ">=0.1.0" }, - { name = "fastmcp", specifier = ">=2.13.3" }, + { name = "fastmcp", specifier = ">=2.14.2" }, { name = "langchain", specifier = "==1.0.0a5" }, { name = "langchain-core", specifier = ">=0.3.0" }, { name = "langchain-mcp-adapters", specifier = ">=0.1.11" }, @@ -2471,8 +2637,8 @@ requires-dist = [ { name = "logfire", specifier = ">=4.15.1" }, { name = "mcp", specifier = ">=1.3.0" }, { name = "msgraph-sdk", specifier = ">=1.0.0" }, - { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = ">=1.28.0" }, - { name = "opentelemetry-instrumentation-starlette", specifier = ">=0.49b0" }, + { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = ">=1.39.0" }, + { name = "opentelemetry-instrumentation-starlette", specifier = ">=0.60b0" }, ] [package.metadata.requires-dev] @@ -2518,6 +2684,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318 }, ] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -2772,6 +2947,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1d/d2/1637f4360ada6a368d3265bf39f2cf737a0aaab15ab520fc005903e883f8/ruff-0.14.7-py3-none-win_arm64.whl", hash = "sha256:be4d653d3bea1b19742fcc6502354e32f65cd61ff2fbdb365803ef2c2aec6228", size = 13609215 }, ] +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554 }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, +] + [[package]] name = "six" version = "1.17.0" @@ -2790,6 +2987,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 }, +] + [[package]] name = "sqlalchemy" version = "2.0.44" @@ -2891,6 +3097,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, ] +[[package]] +name = "typer" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381 }, +] + [[package]] name = "typing-extensions" version = "4.15.0"