diff --git a/.env.example b/.env.example index b9676d3808..4a1fbabe29 100644 --- a/.env.example +++ b/.env.example @@ -141,3 +141,8 @@ # Grok API key # XAI_API_KEY="Fill your Grok API Key here" # XAI_API_BASE_URL="Fill your Grok API Base URL here" + +# Microsoft Graph API (https://portal.azure.com/) +# MICROSOFT_TENANT_ID="Fill your Tenant ID here (Optional, default is 'common')" +# MICROSOFT_CLIENT_ID="Fill your Client ID here" +# MICROSOFT_CLIENT_SECRET="Fill your Client Secret here" diff --git a/camel/toolkits/__init__.py b/camel/toolkits/__init__.py index b926e8319e..4a0b40fe15 100644 --- a/camel/toolkits/__init__.py +++ b/camel/toolkits/__init__.py @@ -96,6 +96,7 @@ from .notion_mcp_toolkit import NotionMCPToolkit from .vertex_ai_veo_toolkit import VertexAIVeoToolkit from .minimax_mcp_toolkit import MinimaxMCPToolkit +from .microsoft_outlook_mail_toolkit import OutlookMailToolkit from .earth_science_toolkit import EarthScienceToolkit __all__ = [ @@ -183,5 +184,6 @@ 'NotionMCPToolkit', 'VertexAIVeoToolkit', 'MinimaxMCPToolkit', + "OutlookMailToolkit", 'EarthScienceToolkit', ] diff --git a/camel/toolkits/microsoft_outlook_mail_toolkit.py b/camel/toolkits/microsoft_outlook_mail_toolkit.py new file mode 100644 index 0000000000..1a8d24f69c --- /dev/null +++ b/camel/toolkits/microsoft_outlook_mail_toolkit.py @@ -0,0 +1,1918 @@ +# ========= Copyright 2023-2025 @ CAMEL-AI.org. All Rights Reserved. ========= +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========= Copyright 2023-2025 @ CAMEL-AI.org. All Rights Reserved. ========= + +import asyncio +import json +import os +import time +from http.server import BaseHTTPRequestHandler, HTTPServer +from pathlib import Path +from typing import Any, Dict, List, Optional, cast + +import requests +from dotenv import load_dotenv + +from camel.logger import get_logger +from camel.toolkits import FunctionTool +from camel.toolkits.base import BaseToolkit +from camel.utils import MCPServer, api_keys_required + +load_dotenv() +logger = get_logger(__name__) + + +class OAuthHTTPServer(HTTPServer): + code: Optional[str] = None + + +class RedirectHandler(BaseHTTPRequestHandler): + """Handler for OAuth redirect requests.""" + + def do_GET(self): + """Handles GET request and extracts authorization code.""" + from urllib.parse import parse_qs, urlparse + + try: + query = parse_qs(urlparse(self.path).query) + code = query.get("code", [None])[0] + cast(OAuthHTTPServer, self.server).code = code + self.send_response(200) + self.end_headers() + self.wfile.write( + b"Authentication complete. You can close this window." + ) + except Exception as e: + cast(OAuthHTTPServer, self.server).code = None + self.send_response(500) + self.end_headers() + self.wfile.write( + f"Error during authentication: {e}".encode("utf-8") + ) + + def log_message(self, format, *args): + pass + + +class AsyncCustomAzureCredential: + """Creates an async Azure credential to pass into MSGraph client. + + Implements Azure credential interface with automatic token refresh using + a refresh token. Updates the refresh token file whenever Microsoft issues + a new refresh token during the refresh flow. + + Args: + client_id (str): The OAuth client ID. + client_secret (str): The OAuth client secret. + tenant_id (str): The Microsoft tenant ID. + refresh_token (str): The refresh token from OAuth flow. + scopes (List[str]): List of OAuth permission scopes. + refresh_token_file_path (Optional[Path]): File path of json file + with refresh token. + """ + + def __init__( + self, + client_id: str, + client_secret: str, + tenant_id: str, + refresh_token: str, + scopes: List[str], + refresh_token_file_path: Optional[Path], + ): + self.client_id = client_id + self.client_secret = client_secret + self.tenant_id = tenant_id + self.refresh_token = refresh_token + self.scopes = scopes + self.refresh_token_file_path = refresh_token_file_path + + self._access_token = None + self._expires_at = 0 + self._lock = asyncio.Lock() + self._debug_claims_logged = False + + async def _refresh_access_token(self): + """Refreshes the access token using the refresh token. + + Requests a new access token from Microsoft's token endpoint. + If Microsoft returns a new refresh token, updates both in-memory + and refresh token file. + + Raises: + Exception: If token refresh fails or returns an error. + """ + import httpx + + token_url = ( + f"https://login.microsoftonline.com/{self.tenant_id}" + f"/oauth2/v2.0/token" + ) + data = { + "client_id": self.client_id, + "client_secret": self.client_secret, + "grant_type": "refresh_token", + "refresh_token": self.refresh_token, + "scope": " ".join(self.scopes), + } + + async with httpx.AsyncClient(timeout=30) as client: + response = await client.post(token_url, data=data) + result = response.json() + + # Raise exception if error in response + if "error" in result: + error_desc = result.get('error_description', result['error']) + error_msg = f"Token refresh failed: {error_desc}" + logger.error(error_msg) + raise Exception(error_msg) + + # Update access token and expiration (60 second buffer) + self._access_token = result["access_token"] + self._expires_at = int(time.time()) + int(result["expires_in"]) - 60 + + # Save new refresh token if Microsoft provides one + if "refresh_token" in result: + self.refresh_token = result["refresh_token"] + self._save_refresh_token(self.refresh_token) + + def _save_refresh_token(self, refresh_token: str): + """Saves the refresh token to file. + + Args: + refresh_token (str): The refresh token to save. + """ + if not self.refresh_token_file_path: + logger.info("Token file path not set, skipping token save") + return + + token_data = {"refresh_token": refresh_token} + + try: + # Create parent directories if they don't exist + self.refresh_token_file_path.parent.mkdir( + parents=True, exist_ok=True + ) + + # Write new refresh token to file + with open(self.refresh_token_file_path, 'w') as f: + json.dump(token_data, f, indent=2) + except Exception as e: + logger.warning(f"Failed to save refresh token: {e!s}") + + async def get_token(self, *args, **kwargs): + """Gets a valid AccessToken object for msgraph (async). + + Called by Microsoft Graph SDK when making API requests. + Automatically refreshes the token if expired. + + Args: + *args: Positional arguments that msgraph might pass . + **kwargs: Keyword arguments that msgraph might pass . + + Returns: + AccessToken: Azure AccessToken with token and expiration. + + Raises: + Exception: If requested scopes exceed allowed scopes. + """ + from azure.core.credentials import AccessToken + + def _maybe_log_token_claims(token: Optional[str]) -> None: + if not token: + return + if self._debug_claims_logged: + return + if os.getenv("CAMEL_OUTLOOK_DEBUG_TOKEN_CLAIMS") != "1": + return + + try: + import base64 + + _header_b64, payload_b64, _sig_b64 = token.split(".", 2) + payload_b64 += "=" * (-len(payload_b64) % 4) + payload = json.loads( + base64.urlsafe_b64decode(payload_b64.encode("utf-8")) + ) + logger.info( + "Outlook token claims: aud=%s scp=%s roles=%s", + payload.get("aud"), + payload.get("scp"), + payload.get("roles"), + ) + except Exception as e: + logger.warning("Failed to decode token claims: %s", e) + finally: + self._debug_claims_logged = True + + # Check if token needs refresh + now = int(time.time()) + if now >= self._expires_at: + async with self._lock: + # Double-check after lock (another thread may have refreshed) + if now >= self._expires_at: + await self._refresh_access_token() + + _maybe_log_token_claims(self._access_token) + return AccessToken(self._access_token, self._expires_at) + + async def close(self) -> None: + return None + + +@MCPServer() +class OutlookMailToolkit(BaseToolkit): + """A comprehensive toolkit for Microsoft Outlook Mail operations. + + This class provides methods for Outlook Mail operations including sending + emails, managing drafts, replying to mails, deleting mails, fetching + mails and attachments and changing folder of mails. + API keys can be accessed in the Azure portal (https://portal.azure.com/) + """ + + def __init__( + self, + timeout: Optional[float] = None, + refresh_token_file_path: Optional[str] = None, + ): + """Initializes a new instance of the OutlookMailToolkit. + + Args: + timeout (Optional[float]): The timeout value for API requests + in seconds. If None, no timeout is applied. + (default: :obj:`None`) + refresh_token_file_path (Optional[str]): The path of json file + where refresh token is stored. If None, authentication using + web browser will be required on each initialization. If + provided, the refresh token is read from the file, used, and + automatically updated when it nears expiry. + (default: :obj:`None`) + """ + super().__init__(timeout=timeout) + + self.scopes = self._normalize_scopes(["Mail.Send", "Mail.ReadWrite"]) + self.redirect_uri = self._get_dynamic_redirect_uri() + self.refresh_token_file_path = ( + Path(refresh_token_file_path) if refresh_token_file_path else None + ) + self.credentials = self._authenticate() + self.client = self._get_graph_client( + credentials=self.credentials, scopes=self.scopes + ) + + def _get_dynamic_redirect_uri(self) -> str: + """Finds an available port and returns a dynamic redirect URI. + + Returns: + str: A redirect URI with format 'http://localhost:' where + port is an available port on the system. + """ + import socket + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(('127.0.0.1', 0)) + port = s.getsockname()[1] + return f'http://localhost:{port}' + + def _normalize_scopes(self, scopes: List[str]) -> List[str]: + """Normalizes OAuth scopes to what Azure Identity expects. + + Azure Identity credentials (used by Kiota/MSGraph) expect fully + qualified scopes like `https://graph.microsoft.com/Mail.Send`. + For backwards compatibility, this method also accepts short scopes + like `Mail.Send` and prefixes them with Microsoft Graph resource. + """ + graph_resource = "https://graph.microsoft.com" + passthrough = {"offline_access", "openid", "profile"} + + normalized: List[str] = [] + for scope in scopes: + scope = scope.strip() + if not scope: + continue + if scope in passthrough or "://" in scope: + normalized.append(scope) + continue + normalized.append(f"{graph_resource}/{scope.lstrip('/')}") + return normalized + + def _get_auth_url(self, client_id, tenant_id, redirect_uri, scopes): + """Constructs the Microsoft authorization URL. + + Args: + client_id (str): The OAuth client ID. + tenant_id (str): The Microsoft tenant ID. + redirect_uri (str): The redirect URI for OAuth callback. + scopes (List[str]): List of permission scopes. + + Returns: + str: The complete authorization URL. + """ + from urllib.parse import urlencode + + params = { + 'client_id': client_id, + 'response_type': 'code', + 'redirect_uri': redirect_uri, + 'scope': " ".join(scopes), + } + auth_url = ( + f'https://login.microsoftonline.com/{tenant_id}' + f'/oauth2/v2.0/authorize?{urlencode(params)}' + ) + return auth_url + + def _load_token_from_file(self) -> Optional[str]: + """Loads refresh token from disk. + + Returns: + Optional[str]: Refresh token if file exists and valid, else None. + """ + if not self.refresh_token_file_path: + return None + + if not self.refresh_token_file_path.exists(): + return None + + try: + with open(self.refresh_token_file_path, 'r') as f: + token_data = json.load(f) + + refresh_token = token_data.get('refresh_token') + if refresh_token: + logger.info( + f"Refresh token loaded from {self.refresh_token_file_path}" + ) + return refresh_token + + logger.warning("Token file missing 'refresh_token' field") + return None + + except Exception as e: + logger.warning(f"Failed to load token file: {e!s}") + return None + + def _save_token_to_file(self, refresh_token: str): + """Saves refresh token to disk. + + Args: + refresh_token (str): The refresh token to save. + """ + if not self.refresh_token_file_path: + logger.info("Token file path not set, skipping token save") + return + + try: + # Create parent directories if they don't exist + self.refresh_token_file_path.parent.mkdir( + parents=True, exist_ok=True + ) + + with open(self.refresh_token_file_path, 'w') as f: + json.dump({"refresh_token": refresh_token}, f, indent=2) + logger.info( + f"Refresh token saved to {self.refresh_token_file_path}" + ) + except Exception as e: + logger.warning(f"Failed to save token to file: {e!s}") + + def _authenticate_using_refresh_token( + self, + ) -> AsyncCustomAzureCredential: + """Authenticates using a saved refresh token. + + Loads the refresh token from disk and creates a credential object + that will automatically refresh access tokens as needed. + + Returns: + _RefreshableCredential: Credential with auto-refresh capability. + + Raises: + ValueError: If refresh token cannot be loaded or is invalid. + """ + refresh_token = self._load_token_from_file() + + if not refresh_token: + raise ValueError("No valid refresh token found in file") + + # Create credential with automatic refresh capability + credentials = AsyncCustomAzureCredential( + client_id=self.client_id, + client_secret=self.client_secret, + tenant_id=self.tenant_id, + refresh_token=refresh_token, + scopes=self.scopes, + refresh_token_file_path=self.refresh_token_file_path, + ) + + logger.info("Authentication with saved token successful") + return credentials + + def _authenticate_using_browser(self): + """Authenticates using browser-based OAuth flow. + + Opens browser for user authentication, exchanges authorization + code for tokens, and saves refresh token for future use. + + Returns: + AsyncCustomAzureCredential or AuthorizationCodeCredential : + Credential for Microsoft Graph API. + + Raises: + ValueError: If authentication fails or no authorization code. + """ + from azure.identity.aio import AuthorizationCodeCredential + + # offline_access scope is needed so the azure credential can refresh + # internally after access token expires as azure handles it internally + # Do not add offline_access to self.scopes as MSAL does not allow it + scope = [*self.scopes, "offline_access"] + + auth_url = self._get_auth_url( + client_id=self.client_id, + tenant_id=self.tenant_id, + redirect_uri=self.redirect_uri, + scopes=scope, + ) + + authorization_code = self._get_authorization_code_via_browser(auth_url) + + token_result = self._exchange_authorization_code_for_tokens( + authorization_code=authorization_code, + scope=scope, + ) + + refresh_token = token_result.get("refresh_token") + if refresh_token: + self._save_token_to_file(refresh_token) + credentials = AsyncCustomAzureCredential( + client_id=self.client_id, + client_secret=self.client_secret, + tenant_id=self.tenant_id, + refresh_token=refresh_token, + scopes=self.scopes, + refresh_token_file_path=self.refresh_token_file_path, + ) + + access_token = token_result.get("access_token") + expires_in = token_result.get("expires_in") + if access_token and expires_in: + # Prime the credential to avoid an immediate refresh request. + credentials._access_token = access_token + credentials._expires_at = ( + int(time.time()) + int(expires_in) - 60 + ) + return credentials + + logger.warning( + "No refresh_token returned from browser auth; falling back to " + "AuthorizationCodeCredential (token won't be persisted to the " + "provided refresh_token_file_path)." + ) + return AuthorizationCodeCredential( + tenant_id=self.tenant_id, + client_id=self.client_id, + authorization_code=authorization_code, + redirect_uri=self.redirect_uri, + client_secret=self.client_secret, + ) + + def _get_authorization_code_via_browser(self, auth_url: str) -> str: + """Opens a browser and captures the authorization code via localhost. + + Args: + auth_url (str): The authorization URL to open in the browser. + + Returns: + str: The captured authorization code. + + Raises: + ValueError: If the authorization code cannot be captured. + """ + import webbrowser + from urllib.parse import urlparse + + parsed_uri = urlparse(self.redirect_uri) + hostname = parsed_uri.hostname + port = parsed_uri.port + if not hostname or not port: + raise ValueError( + f"Invalid redirect_uri, expected host and port: " + f"{self.redirect_uri}" + ) + + server_address = (hostname, port) + server = OAuthHTTPServer(server_address, RedirectHandler) + server.code = None + + logger.info(f"Opening browser for authentication: {auth_url}") + webbrowser.open(auth_url) + + server.handle_request() + server.server_close() + + authorization_code = server.code + if not authorization_code: + raise ValueError("Failed to get authorization code") + return authorization_code + + def _exchange_authorization_code_for_tokens( + self, authorization_code: str, scope: List[str] + ) -> Dict[str, Any]: + """Exchanges an authorization code for tokens via OAuth token endpoint. + + Args: + authorization_code (str): Authorization code captured from browser. + scope (List[str]): Scopes requested in the authorization flow. + + Returns: + Dict[str, Any]: Token response JSON. + + Raises: + ValueError: If token exchange fails or returns an error payload. + """ + token_url = ( + f"https://login.microsoftonline.com/{self.tenant_id}" + f"/oauth2/v2.0/token" + ) + data = { + "client_id": self.client_id, + "client_secret": self.client_secret, + "grant_type": "authorization_code", + "code": authorization_code, + "redirect_uri": self.redirect_uri, + "scope": " ".join(scope), + } + + response = requests.post(token_url, data=data, timeout=self.timeout) + result = response.json() + + if "error" in result: + error_desc = result.get("error_description", result["error"]) + raise ValueError(f"Token exchange failed: {error_desc}") + + return result + + @api_keys_required( + [ + (None, "MICROSOFT_CLIENT_ID"), + (None, "MICROSOFT_CLIENT_SECRET"), + ] + ) + def _authenticate(self): + """Authenticates and creates credential for Microsoft Graph. + + Implements two-stage authentication: + 1. Attempts to use saved refresh token if refresh_token_file_path is + provided + 2. Falls back to browser OAuth if no token or token invalid + + Returns: + AuthorizationCodeCredential or AsyncCustomAzureCredential + + Raises: + ValueError: If authentication fails through both methods. + """ + from azure.identity.aio import AuthorizationCodeCredential + + try: + self.tenant_id = os.getenv("MICROSOFT_TENANT_ID", "common") + self.client_id = os.getenv("MICROSOFT_CLIENT_ID") + self.client_secret = os.getenv("MICROSOFT_CLIENT_SECRET") + + # Try saved refresh token first if token file path is provided + if ( + self.refresh_token_file_path + and self.refresh_token_file_path.exists() + ): + try: + credentials: AsyncCustomAzureCredential = ( + self._authenticate_using_refresh_token() + ) + return credentials + except Exception as e: + logger.warning( + f"Authentication using refresh token failed: {e!s}. " + f"Falling back to browser authentication" + ) + + # Fall back to browser authentication + credentials: AuthorizationCodeCredential = ( + self._authenticate_using_browser() + ) + return credentials + + except Exception as e: + error_msg = f"Failed to authenticate: {e!s}" + logger.error(error_msg) + raise ValueError(error_msg) + + def _get_graph_client(self, credentials, scopes): + """Creates Microsoft Graph API client. + + Args: + credentials : AuthorizationCodeCredential or + AsyncCustomAzureCredential. + scopes (List[str]): List of permission scopes. + + Returns: + GraphServiceClient: Microsoft Graph API client. + + Raises: + ValueError: If client creation fails. + """ + from msgraph import GraphServiceClient + + try: + client = GraphServiceClient(credentials=credentials, scopes=scopes) + return client + except Exception as e: + error_msg = f"Failed to create Graph client: {e!s}" + logger.error(error_msg) + raise ValueError(error_msg) + + def is_email_valid(self, email: str) -> bool: + """Validates a single email address. + + Args: + email (str): Email address to validate. + + Returns: + bool: True if the email is valid, False otherwise. + """ + import re + from email.utils import parseaddr + + # Extract email address from both formats : "Email" , "Name " + _, addr = parseaddr(email) + + email_pattern = re.compile( + r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + ) + return bool(addr and email_pattern.match(addr)) + + def _get_invalid_emails(self, *lists: Optional[List[str]]) -> List[str]: + """Finds invalid email addresses from multiple email lists. + + Args: + *lists: Variable number of optional email address lists. + + Returns: + List[str]: List of invalid email addresses. Empty list if all + emails are valid. + """ + invalid_emails = [] + for email_list in lists: + if email_list is None: + continue + for email in email_list: + if not self.is_email_valid(email): + invalid_emails.append(email) + return invalid_emails + + def _create_attachments(self, file_paths: List[str]) -> List[Any]: + """Creates Microsoft Graph FileAttachment objects from file paths. + + Args: + file_paths (List[str]): List of local file paths to attach. + + Returns: + List[Any]: List of FileAttachment objects ready for Graph API use. + + Raises: + ValueError: If any file cannot be read or attached. + """ + from msgraph.generated.models.file_attachment import FileAttachment + + attachment_list = [] + + for file_path in file_paths: + try: + if not os.path.isfile(file_path): + raise ValueError( + f"Path does not exist or is not a file: {file_path}" + ) + + with open(file_path, "rb") as file: + file_content = file.read() + + file_name = os.path.basename(file_path) + + # Create attachment with proper properties + attachment_obj = FileAttachment() + attachment_obj.odata_type = "#microsoft.graph.fileAttachment" + attachment_obj.name = file_name + attachment_obj.content_bytes = file_content + + attachment_list.append(attachment_obj) + + except Exception as e: + raise ValueError(f"Failed to attach file {file_path}: {e!s}") + + return attachment_list + + def _create_recipients(self, email_list: Optional[List[str]]) -> List[Any]: + """Creates Microsoft Graph Recipient objects from email addresses. + + Supports both simple email format ("email@example.com") and + name-email format ("John Doe "). + + Args: + email_list (Optional[List[str]]): List of email addresses, + which can include display names. + + Returns: + List[Any]: List of Recipient objects ready for Graph API use. + """ + from email.utils import parseaddr + + from msgraph.generated.models import email_address, recipient + + if not email_list: + return [] + + recipients: List[Any] = [] + for email in email_list: + # Extract email address from both formats: "Email", "Name " + name, addr = parseaddr(email) + address = email_address.EmailAddress(address=addr) + if name: + address.name = name + recp = recipient.Recipient(email_address=address) + recipients.append(recp) + return recipients + + def _create_message( + self, + to_email: Optional[List[str]] = None, + subject: Optional[str] = None, + content: Optional[str] = None, + is_content_html: bool = False, + attachments: Optional[List[str]] = None, + cc_recipients: Optional[List[str]] = None, + bcc_recipients: Optional[List[str]] = None, + reply_to: Optional[List[str]] = None, + ): + """Creates a message object for sending or updating emails. + + This helper method is used internally to construct Microsoft Graph + message objects. It's used by methods like send_email, + create_draft_email, and update_draft_message. All parameters are + optional to allow partial updates when modifying existing messages. + + Args: + to_email (Optional[List[str]]): List of recipient email addresses. + (default: :obj:`None`) + subject (Optional[str]): The subject of the email. + (default: :obj:`None`) + content (Optional[str]): The body content of the email. + (default: :obj:`None`) + is_content_html (bool): If True, the content type will be set to + HTML; otherwise, it will be Text. (default: :obj:`False`) + attachments (Optional[List[str]]): List of file paths to attach + to the email. (default: :obj:`None`) + cc_recipients (Optional[List[str]]): List of CC recipient email + addresses. (default: :obj:`None`) + bcc_recipients (Optional[List[str]]): List of BCC recipient email + addresses. (default: :obj:`None`) + reply_to (Optional[List[str]]): List of email addresses that will + receive replies when recipients use the "Reply" button. This + allows replies to be directed to different addresses than the + sender's address. (default: :obj:`None`) + + Returns: + message.Message: A Microsoft Graph message object with only the + provided fields set. + """ + from msgraph.generated.models import body_type, item_body, message + + # Determine content type + if is_content_html: + content_type = body_type.BodyType.Html + else: + content_type = body_type.BodyType.Text + + mail_message = message.Message() + + # Set body content if provided + if content: + message_body = item_body.ItemBody( + content_type=content_type, content=content + ) + mail_message.body = message_body + + # Set to recipients if provided + if to_email: + mail_message.to_recipients = self._create_recipients(to_email) + + # Set subject if provided + if subject: + mail_message.subject = subject + + # Add CC recipients if provided + if cc_recipients: + mail_message.cc_recipients = self._create_recipients(cc_recipients) + + # Add BCC recipients if provided + if bcc_recipients: + mail_message.bcc_recipients = self._create_recipients( + bcc_recipients + ) + + # Add reply-to addresses if provided + if reply_to: + mail_message.reply_to = self._create_recipients(reply_to) + + # Add attachments if provided + if attachments: + mail_message.attachments = self._create_attachments(attachments) + + return mail_message + + async def outlook_send_email( + self, + to_email: List[str], + subject: str, + content: str, + is_content_html: bool = False, + attachments: Optional[List[str]] = None, + cc_recipients: Optional[List[str]] = None, + bcc_recipients: Optional[List[str]] = None, + reply_to: Optional[List[str]] = None, + save_to_sent_items: bool = True, + ) -> Dict[str, Any]: + """Sends an email via Microsoft Outlook. + + Args: + to_email (List[str]): List of recipient email addresses. + subject (str): The subject of the email. + content (str): The body content of the email. + is_content_html (bool): If True, the content type will be set to + HTML; otherwise, it will be Text. (default: :obj:`False`) + attachments (Optional[List[str]]): List of file paths to attach + to the email. (default: :obj:`None`) + cc_recipients (Optional[List[str]]): List of CC recipient email + addresses. (default: :obj:`None`) + bcc_recipients (Optional[List[str]]): List of BCC recipient email + addresses. (default: :obj:`None`) + reply_to (Optional[List[str]]): List of email addresses that will + receive replies when recipients use the "Reply" button. This + allows replies to be directed to different addresses than the + sender's address. (default: :obj:`None`) + save_to_sent_items (bool): Whether to save the email to sent + items. (default: :obj:`True`) + + Returns: + Dict[str, Any]: A dictionary containing the result of the email + sending operation. + + """ + from msgraph.generated.users.item.send_mail.send_mail_post_request_body import ( # noqa: E501 + SendMailPostRequestBody, + ) + + try: + # Validate all email addresses + invalid_emails = self._get_invalid_emails( + to_email, cc_recipients, bcc_recipients, reply_to + ) + if invalid_emails: + error_msg = ( + f"Invalid email address(es) provided: " + f"{', '.join(invalid_emails)}" + ) + logger.error(error_msg) + return {"error": error_msg} + + mail_message = self._create_message( + to_email=to_email, + subject=subject, + content=content, + is_content_html=is_content_html, + attachments=attachments, + cc_recipients=cc_recipients, + bcc_recipients=bcc_recipients, + reply_to=reply_to, + ) + + request = SendMailPostRequestBody( + message=mail_message, + save_to_sent_items=save_to_sent_items, + ) + + await self.client.me.send_mail.post(request) + + logger.info("Email sent successfully.") + return { + 'status': 'success', + 'message': 'Email sent successfully', + 'recipients': to_email, + 'subject': subject, + } + except Exception as e: + logger.exception("Failed to send email") + return {"error": f"Failed to send email: {e!s}"} + + async def outlook_create_draft_email( + self, + to_email: List[str], + subject: str, + content: str, + is_content_html: bool = False, + attachments: Optional[List[str]] = None, + cc_recipients: Optional[List[str]] = None, + bcc_recipients: Optional[List[str]] = None, + reply_to: Optional[List[str]] = None, + ) -> Dict[str, Any]: + """Creates a draft email in Microsoft Outlook. + + Args: + to_email (List[str]): List of recipient email addresses. + subject (str): The subject of the email. + content (str): The body content of the email. + is_content_html (bool): If True, the content type will be set to + HTML; otherwise, it will be Text. (default: :obj:`False`) + attachments (Optional[List[str]]): List of file paths to attach + to the email. (default: :obj:`None`) + cc_recipients (Optional[List[str]]): List of CC recipient email + addresses. (default: :obj:`None`) + bcc_recipients (Optional[List[str]]): List of BCC recipient email + addresses. (default: :obj:`None`) + reply_to (Optional[List[str]]): List of email addresses that will + receive replies when recipients use the "Reply" button. This + allows replies to be directed to different addresses than the + sender's address. (default: :obj:`None`) + + Returns: + Dict[str, Any]: A dictionary containing the result of the draft + email creation operation, including the draft ID. + + """ + # Validate all email addresses + invalid_emails = self._get_invalid_emails( + to_email, cc_recipients, bcc_recipients, reply_to + ) + if invalid_emails: + error_msg = ( + f"Invalid email address(es) provided: " + f"{', '.join(invalid_emails)}" + ) + logger.error(error_msg) + return {"error": error_msg} + + try: + request_body = self._create_message( + to_email=to_email, + subject=subject, + content=content, + is_content_html=is_content_html, + attachments=attachments, + cc_recipients=cc_recipients, + bcc_recipients=bcc_recipients, + reply_to=reply_to, + ) + + result = await self.client.me.messages.post(request_body) + + logger.info("Draft email created successfully.") + return { + 'status': 'success', + 'message': 'Draft email created successfully', + 'draft_id': result.id, + 'recipients': to_email, + 'subject': subject, + } + except Exception as e: + error_msg = f"Failed to create draft email: {e!s}" + logger.error(error_msg) + return {"error": error_msg} + + async def outlook_send_draft_email(self, draft_id: str) -> Dict[str, Any]: + """Sends a draft email via Microsoft Outlook. + + Args: + draft_id (str): The ID of the draft email to send. Can be + obtained either by creating a draft via + `create_draft_email()` or from the 'message_id' field in + messages returned by `list_messages()`. + + Returns: + Dict[str, Any]: A dictionary containing the result of the draft + email sending operation. + + """ + try: + await self.client.me.messages.by_message_id(draft_id).send.post() + + logger.info(f"Draft email with ID {draft_id} sent successfully.") + return { + 'status': 'success', + 'message': 'Draft email sent successfully', + 'draft_id': draft_id, + } + except Exception as e: + error_msg = f"Failed to send draft email: {e!s}" + logger.error(error_msg) + return {"error": error_msg} + + async def outlook_delete_email(self, message_id: str) -> Dict[str, Any]: + """Deletes an email from Microsoft Outlook. + + Args: + message_id (str): The ID of the email to delete. Can be obtained + from the 'message_id' field in messages returned by + `list_messages()`. + + Returns: + Dict[str, Any]: A dictionary containing the result of the email + deletion operation. + + """ + try: + await self.client.me.messages.by_message_id(message_id).delete() + logger.info(f"Email with ID {message_id} deleted successfully.") + return { + 'status': 'success', + 'message': 'Email deleted successfully', + 'message_id': message_id, + } + except Exception as e: + error_msg = f"Failed to delete email: {e!s}" + logger.error(error_msg) + return {"error": error_msg} + + async def outlook_move_message_to_folder( + self, message_id: str, destination_folder_id: str + ) -> Dict[str, Any]: + """Moves an email to a specified folder in Microsoft Outlook. + + Args: + message_id (str): The ID of the email to move. Can be obtained + from the 'message_id' field in messages returned by + `list_messages()`. + destination_folder_id (str): The destination folder ID, or + a well-known folder name. Supported well-known folder names are + ("inbox", "drafts", "sentitems", "deleteditems", "junkemail", + "archive", "outbox"). + + Returns: + Dict[str, Any]: A dictionary containing the result of the email + move operation. + + """ + from msgraph.generated.users.item.messages.item.move.move_post_request_body import ( # noqa: E501 + MovePostRequestBody, + ) + + try: + request_body = MovePostRequestBody( + destination_id=destination_folder_id, + ) + message = self.client.me.messages.by_message_id(message_id) + await message.move.post(request_body) + + logger.info( + f"Email with ID {message_id} moved to folder " + f"{destination_folder_id} successfully." + ) + return { + 'status': 'success', + 'message': 'Email moved successfully', + 'message_id': message_id, + 'destination_folder_id': destination_folder_id, + } + except Exception as e: + error_msg = f"Failed to move email: {e!s}" + logger.error(error_msg) + return {"error": error_msg} + + async def outlook_get_attachments( + self, + message_id: str, + metadata_only: bool = True, + include_inline_attachments: bool = False, + save_path: Optional[str] = None, + ) -> Dict[str, Any]: + """Retrieves attachments from a Microsoft Outlook email message. + + This method fetches attachments from a specified email message and can + either return metadata only or download the full attachment content. + Inline attachments (like embedded images) can optionally be included + or excluded from the results. + Also, if a save_path is provided, attachments will be saved to disk. + + Args: + message_id (str): The unique identifier of the email message from + which to retrieve attachments. Can be obtained from the + 'message_id' field in messages returned by `list_messages()`. + metadata_only (bool): If True, returns only attachment metadata + (name, size, content type, etc.) without downloading the actual + file content. If False, downloads the full attachment content. + (default: :obj:`True`) + include_inline_attachments (bool): If True, includes inline + attachments (such as embedded images) in the results. If False, + filters them out. (default: :obj:`False`) + save_path (Optional[str]): The local directory path where + attachments should be saved. If provided, attachments are saved + to disk and the file paths are returned. If None, attachment + content is returned as base64-encoded strings (only when + metadata_only=False). (default: :obj:`None`) + + Returns: + Dict[str, Any]: A dictionary containing the attachment retrieval + results + + """ + try: + request_config = None + if metadata_only: + request_config = self._build_attachment_query() + + attachments_response = await self._fetch_attachments( + message_id, request_config + ) + if not attachments_response: + return { + 'status': 'success', + 'message_id': message_id, + 'attachments': [], + 'total_count': 0, + } + + attachments_list = [] + for attachment in attachments_response.value: + if not include_inline_attachments and attachment.is_inline: + continue + info = self._process_attachment( + attachment, + metadata_only, + save_path, + ) + attachments_list.append(info) + + return { + 'status': 'success', + 'message_id': message_id, + 'attachments': attachments_list, + 'total_count': len(attachments_list), + } + + except Exception as e: + error_msg = f"Failed to get attachments: {e!s}" + logger.error(error_msg) + return {"error": error_msg} + + def _build_attachment_query(self): + """Constructs the query configuration for fetching attachments. + + Args: + metadata_only (bool): Whether to fetch only metadata or include + content bytes. + + Returns: + AttachmentsRequestBuilderGetRequestConfiguration: Query config + for the Graph API request. + """ + from msgraph.generated.users.item.messages.item.attachments.attachments_request_builder import ( # noqa: E501 + AttachmentsRequestBuilder, + ) + + query_params = AttachmentsRequestBuilder.AttachmentsRequestBuilderGetQueryParameters( # noqa: E501 + select=[ + "id", + "lastModifiedDateTime", + "name", + "contentType", + "size", + "isInline", + ] + ) + + return AttachmentsRequestBuilder.AttachmentsRequestBuilderGetRequestConfiguration( # noqa: E501 + query_parameters=query_params + ) + + async def _fetch_attachments( + self, message_id: str, request_config: Optional[Any] = None + ): + """Fetches attachments from the Microsoft Graph API. + + Args: + message_id (str): The email message ID. + request_config (Optional[Any]): The request configuration with + query parameters. (default: :obj:`None`) + + + Returns: + Attachments response from the Graph API. + """ + if not request_config: + return await self.client.me.messages.by_message_id( + message_id + ).attachments.get() + return await self.client.me.messages.by_message_id( + message_id + ).attachments.get(request_configuration=request_config) + + def _process_attachment( + self, + attachment, + metadata_only: bool, + save_path: Optional[str], + ): + """Processes a single attachment and extracts its information. + + Args: + attachment: The attachment object from Graph API. + metadata_only (bool): Whether to include content bytes. + save_path (Optional[str]): Path to save attachment file. + + Returns: + Dict: Dictionary containing attachment information. + """ + import base64 + + info = { + 'id': attachment.id, + 'name': attachment.name, + 'content_type': attachment.content_type, + 'size': attachment.size, + 'is_inline': getattr(attachment, 'is_inline', False), + 'last_modified_date_time': ( + attachment.last_modified_date_time.isoformat() + ), + } + + if not metadata_only: + content_bytes = getattr(attachment, 'content_bytes', None) + if content_bytes: + # Decode once because bytes contain Base64 text ' + decoded_bytes = base64.b64decode(content_bytes) + + if save_path: + file_path = self._save_attachment_file( + save_path, attachment.name, decoded_bytes + ) + info['saved_path'] = file_path + logger.info( + f"Attachment {attachment.name} saved to {file_path}" + ) + else: + info['content_bytes'] = content_bytes + + return info + + def _save_attachment_file( + self, + save_path: str, + attachment_name: str, + content_bytes: bytes, + cannot_overwrite: bool = True, + ) -> str: + """Saves attachment content to a file on disk. + + Args: + save_path (str): Directory path where file should be saved. + attachment_name (str): Name of the attachment file. + content_bytes (bytes): The file content as bytes. + cannot_overwrite (bool): If True, appends counter to filename + if file exists. (default: :obj:`True`) + + Returns: + str: The full file path where the attachment was saved. + """ + import os + + os.makedirs(save_path, exist_ok=True) + file_path = os.path.join(save_path, attachment_name) + file_path_already_exists = os.path.exists(file_path) + if cannot_overwrite and file_path_already_exists: + count = 1 + name, ext = os.path.splitext(attachment_name) + while os.path.exists(file_path): + file_path = os.path.join(save_path, f"{name}_{count}{ext}") + count += 1 + with open(file_path, 'wb') as f: + f.write(content_bytes) + return file_path + + def _handle_html_body(self, body_content: str) -> str: + """Converts HTML email body to plain text. + + Note: This method performs client-side HTML-to-text conversion. + + Args: + body_content (str): The HTML content of the email body. This + content is already sanitized by Microsoft Graph API. + + Returns: + str: Plain text version of the email body with cleaned whitespace + and removed HTML tags. + """ + try: + import html2text + + parser = html2text.HTML2Text() + + parser.ignore_links = False + parser.inline_links = True + parser.protect_links = True + parser.skip_internal_links = True + + parser.ignore_images = False + parser.images_as_html = False + parser.images_to_alt = False + parser.images_with_size = False + + parser.ignore_emphasis = False + parser.body_width = 0 + parser.single_line_break = True + + return parser.handle(body_content).strip() + + except Exception as e: + logger.error(f"Failed to parse HTML body: {e!s}") + return body_content + + def _get_recipients(self, recipient_list: Optional[List[Any]]): + """Gets a list of recipients from a recipient list object.""" + recipients: List[Dict[str, str]] = [] + if not recipient_list: + return recipients + for recipient_info in recipient_list: + email = recipient_info.email_address.address + name = recipient_info.email_address.name + recipients.append({'address': email, 'name': name}) + return recipients + + async def _extract_message_details( + self, + message: Any, + return_html_content: bool = False, + include_attachments: bool = False, + attachment_metadata_only: bool = True, + include_inline_attachments: bool = False, + attachment_save_path: Optional[str] = None, + ) -> Dict[str, Any]: + """Extracts detailed information from a message object. + + This function processes a message object (either from a list response + or a direct fetch) and extracts all relevant details. It can + optionally fetch attachments but does not make additional API calls + for basic message information. + + Args: + message (Any): The Microsoft Graph message object to extract + details from. + return_html_content (bool): If True and body content type is HTML, + returns the raw HTML content without converting it to plain + text. If False and body_type is 'text', HTML content will be + converted to plain text. + (default: :obj:`False`) + include_attachments (bool): Whether to include attachment + information. If True, will make an API call to fetch + attachments. (default: :obj:`False`) + attachment_metadata_only (bool): If True, returns only attachment + metadata without downloading content. If False, downloads full + attachment content. Only used when include_attachments=True. + (default: :obj:`True`) + include_inline_attachments (bool): If True, includes inline + attachments in the results. Only used when + include_attachments=True. (default: :obj:`False`) + attachment_save_path (Optional[str]): Directory path where + attachments should be saved. Only used when + include_attachments=True and attachment_metadata_only=False. + (default: :obj:`None`) + + Returns: + Dict[str, Any]: A dictionary containing the message details + including: + - Basic info (message_id, subject, from, received_date_time, + body etc.) + - Recipients (to_recipients, cc_recipients, bcc_recipients) + - Attachment information (if requested) + + """ + try: + # Validate message object + from msgraph.generated.models.message import Message + + if not isinstance(message, Message): + return {'error': 'Invalid message object provided'} + # Extract basic details + details = { + 'message_id': message.id, + 'subject': message.subject, + # Draft messages have from_ as None + 'from': ( + self._get_recipients([message.from_]) + if message.from_ + else None + ), + 'to_recipients': self._get_recipients(message.to_recipients), + 'cc_recipients': self._get_recipients(message.cc_recipients), + 'bcc_recipients': self._get_recipients(message.bcc_recipients), + 'received_date_time': ( + message.received_date_time.isoformat() + if message.received_date_time + else None + ), + 'sent_date_time': ( + message.sent_date_time.isoformat() + if message.sent_date_time + else None + ), + 'has_non_inline_attachments': message.has_attachments, + 'importance': (str(message.importance)), + 'is_read': message.is_read, + 'is_draft': message.is_draft, + 'body_preview': message.body_preview, + } + + body_content = message.body.content if message.body else '' + content_type = message.body.content_type if (message.body) else '' + + # Convert HTML to text if requested and content is HTML + is_content_html = content_type and "html" in str(content_type) + if is_content_html and not return_html_content and body_content: + body_content = self._handle_html_body(body_content) + + details['body'] = body_content + details['body_type'] = content_type + + # Include attachments if requested + if not include_attachments: + return details + + attachments_info = await self.outlook_get_attachments( + message_id=details['message_id'], + metadata_only=attachment_metadata_only, + include_inline_attachments=include_inline_attachments, + save_path=attachment_save_path, + ) + details['attachments'] = attachments_info.get('attachments', []) + return details + + except Exception as e: + error_msg = f"Failed to extract message details: {e!s}" + logger.error(error_msg) + raise ValueError(error_msg) + + async def outlook_get_message( + self, + message_id: str, + return_html_content: bool = False, + include_attachments: bool = False, + attachment_metadata_only: bool = True, + include_inline_attachments: bool = False, + attachment_save_path: Optional[str] = None, + ) -> Dict[str, Any]: + """Retrieves a single email message by ID from Microsoft Outlook. + + This method fetches a specific email message using its unique + identifier and returns detailed information including subject, sender, + recipients, body content, and optionally attachments. + + Args: + message_id (str): The unique identifier of the email message to + retrieve. Can be obtained from the 'message_id' field in + messages returned by `list_messages()`. + return_html_content (bool): If True and body content type is HTML, + returns the raw HTML content without converting it to plain + text. If False and body_type is HTML, content will be converted + to plain text. (default: :obj:`False`) + include_attachments (bool): Whether to include attachment + information in the response. (default: :obj:`False`) + attachment_metadata_only (bool): If True, returns only attachment + metadata without downloading content. If False, downloads full + attachment content. Only used when include_attachments=True. + (default: :obj:`True`) + include_inline_attachments (bool): If True, includes inline + attachments in the results. Only used when + include_attachments=True. (default: :obj:`False`) + attachment_save_path (Optional[str]): Directory path where + attachments should be saved. Only used when + include_attachments=True and attachment_metadata_only=False. + (default: :obj:`None`) + + Returns: + Dict[str, Any]: A dictionary containing the message details + including message_id, subject, from, to_recipients, + cc_recipients, bcc_recipients, received_date_time, + sent_date_time, body, body_type, has_attachments, importance, + is_read, is_draft, body_preview, and optionally attachments. + + """ + try: + message = await self.client.me.messages.by_message_id( + message_id + ).get() + + if not message: + error_msg = f"Message with ID {message_id} not found" + logger.error(error_msg) + return {"error": error_msg} + + details = await self._extract_message_details( + message=message, + return_html_content=return_html_content, + include_attachments=include_attachments, + attachment_metadata_only=attachment_metadata_only, + include_inline_attachments=include_inline_attachments, + attachment_save_path=attachment_save_path, + ) + + logger.info(f"Message with ID {message_id} retrieved successfully") + return { + 'status': 'success', + 'message': details, + } + + except Exception as e: + error_msg = f"Failed to get message: {e!s}" + logger.error(error_msg) + return {"error": error_msg} + + async def _get_messages_from_folder( + self, + folder_id: str, + request_config, + ): + """Fetches messages from a specific folder. + + Args: + folder_id (str): The folder ID or well-known folder name. + request_config: The request configuration with query parameters. + + Returns: + Messages response from the Graph API, or None if folder not found. + """ + try: + messages = await self.client.me.mail_folders.by_mail_folder_id( + folder_id + ).messages.get(request_configuration=request_config) + return messages + except Exception as e: + logger.warning( + f"Failed to get messages from folder {folder_id}: {e!s}" + ) + return None + + async def outlook_list_messages( + self, + folder_ids: Optional[List[str]] = None, + filter_query: Optional[str] = None, + order_by: Optional[List[str]] = None, + top: int = 10, + skip: int = 0, + return_html_content: bool = False, + include_attachment_metadata: bool = False, + ) -> Dict[str, Any]: + """ + Retrieves messages from Microsoft Outlook using Microsoft Graph API. + + Note: Each folder requires a separate API call. Use folder_ids=None + to search the entire mailbox in one call for better performance. + + When using $filter and $orderby in the same query to get messages, + make sure to specify properties in the following ways: + Properties that appear in $orderby must also appear in $filter. + Properties that appear in $orderby are in the same order as in $filter. + Properties that are present in $orderby appear in $filter before any + properties that aren't. + Failing to do this results in the following error: + Error code: InefficientFilter + Error message: The restriction or sort order is too complex for this + operation. + + Args: + folder_ids (Optional[List[str]]): Folder IDs or well-known names + ("inbox", "drafts", "sentitems", "deleteditems", "junkemail", + "archive", "outbox"). None searches the entire mailbox. + filter_query (Optional[str]): OData filter for messages. + Examples: + - Sender: "from/emailAddress/address eq 'john@example.com'" + - Subject: "subject eq 'Meeting Notes'", + "contains(subject, 'urgent')" + - Read status: "isRead eq false", "isRead eq true" + - Attachments: "hasAttachments eq true/false" + - Importance: "importance eq 'high'/'normal'/'low'" + - Date: "receivedDateTime ge 2024-01-01T00:00:00Z" + - Combine: "isRead eq false and hasAttachments eq true" + - Negation: "not(isRead eq true)" + Reference: https://learn.microsoft.com/en-us/graph/filter-query-parameter + order_by (Optional[List[str]]): OData orderBy for sorting messages. + Examples: + - Date: "receivedDateTime desc/asc", "sentDateTime desc" + - Sender: "from/emailAddress/address asc/desc", + - Subject: "subject asc/desc" + - Importance: "importance desc/asc" + - Size: "size desc/asc" + - Multi-field: "importance desc, receivedDateTime desc" + Reference: https://learn.microsoft.com/en-us/graph/query-parameters + top (int): Max messages per folder (default: 10) + skip (int): Messages to skip for pagination (default: 0) + return_html_content (bool): Return raw HTML if True; + else convert to text (default: False) + include_attachment_metadata (bool): Include attachment metadata + (name, size, type); content not included (default: False) + + Returns: + Dict[str, Any]: Dictionary containing messages and + attachment metadata if requested. + """ + + try: + from msgraph.generated.users.item.mail_folders.item.messages.messages_request_builder import ( # noqa: E501 + MessagesRequestBuilder, + ) + + # Build query parameters + if order_by: + query_params = MessagesRequestBuilder.MessagesRequestBuilderGetQueryParameters( # noqa: E501 + top=top, + skip=skip, + orderby=order_by, + ) + else: + query_params = MessagesRequestBuilder.MessagesRequestBuilderGetQueryParameters( # noqa: E501 + top=top, + skip=skip, + ) + + if filter_query: + query_params.filter = filter_query + + request_config = MessagesRequestBuilder.MessagesRequestBuilderGetRequestConfiguration( # noqa: E501 + query_parameters=query_params + ) + if not folder_ids: + # Search entire mailbox in a single API call + messages_response = await self.client.me.messages.get( + request_configuration=request_config + ) + all_messages = [] + if messages_response and messages_response.value: + for message in messages_response.value: + details = await self._extract_message_details( + message=message, + return_html_content=return_html_content, + include_attachments=include_attachment_metadata, + attachment_metadata_only=True, + include_inline_attachments=True, + attachment_save_path=None, + ) + all_messages.append(details) + + logger.info( + f"Retrieved {len(all_messages)} messages from mailbox" + ) + + return { + 'status': 'success', + 'messages': all_messages, + 'total_count': len(all_messages), + 'skip': skip, + 'top': top, + 'folders_searched': ['all'], + } + # Search specific folders (requires multiple API calls) + all_messages = [] + for folder_id in folder_ids: + messages_response = await self._get_messages_from_folder( + folder_id=folder_id, + request_config=request_config, + ) + + if not messages_response or not messages_response.value: + continue + + # Extract details from each message + for message in messages_response.value: + details = await self._extract_message_details( + message=message, + return_html_content=return_html_content, + include_attachments=include_attachment_metadata, + attachment_metadata_only=True, + include_inline_attachments=False, + attachment_save_path=None, + ) + all_messages.append(details) + + logger.info( + f"Retrieved {len(all_messages)} messages from " + f"{len(folder_ids)} folder(s)" + ) + + return { + 'status': 'success', + 'messages': all_messages, + 'total_count': len(all_messages), + 'skip': skip, + 'top': top, + 'folders_searched': folder_ids, + } + + except Exception as e: + error_msg = f"Failed to list messages: {e!s}" + logger.error(error_msg) + return {"error": error_msg} + + async def outlook_reply_to_email( + self, + message_id: str, + content: str, + reply_all: bool = False, + ) -> Dict[str, Any]: + """Replies to an email in Microsoft Outlook. + + Args: + message_id (str): The ID of the email to reply to. + content (str): The body content of the reply email. + reply_all (bool): If True, replies to all recipients of the + original email. If False, replies only to the sender. + (default: :obj:`False`) + + Returns: + Dict[str, Any]: A dictionary containing the result of the email + reply operation. + + Raises: + ValueError: If replying to the email fails. + """ + from msgraph.generated.users.item.messages.item.reply.reply_post_request_body import ( # noqa: E501 + ReplyPostRequestBody, + ) + from msgraph.generated.users.item.messages.item.reply_all.reply_all_post_request_body import ( # noqa: E501 + ReplyAllPostRequestBody, + ) + + try: + message_request = self.client.me.messages.by_message_id(message_id) + if reply_all: + request_body_reply_all = ReplyAllPostRequestBody( + comment=content + ) + await message_request.reply_all.post(request_body_reply_all) + else: + request_body = ReplyPostRequestBody(comment=content) + await message_request.reply.post(request_body) + + reply_type = "Reply All" if reply_all else "Reply" + logger.info( + f"{reply_type} to email with ID {message_id} sent " + "successfully." + ) + + return { + 'status': 'success', + 'message': f'{reply_type} sent successfully', + 'message_id': message_id, + 'reply_type': reply_type.lower(), + } + + except Exception as e: + error_msg = f"Failed to reply to email: {e!s}" + logger.error(error_msg) + return {"error": error_msg} + + async def outlook_update_draft_message( + self, + message_id: str, + subject: Optional[str] = None, + content: Optional[str] = None, + is_content_html: bool = False, + to_email: Optional[List[str]] = None, + cc_recipients: Optional[List[str]] = None, + bcc_recipients: Optional[List[str]] = None, + reply_to: Optional[List[str]] = None, + ) -> Dict[str, Any]: + """Updates an existing draft email message in Microsoft Outlook. + + Important: Any parameter provided will completely replace the original + value. For example, if you want to add a new recipient while keeping + existing ones, you must pass all recipients (both original and new) in + the to_email parameter. + + Note: This method is intended for draft messages only and not for + sent messages. + + Args: + message_id (str): The ID of the draft message to update. + subject (Optional[str]): Change the subject of the email. + Replaces the original subject completely. + (default: :obj:`None`) + content (Optional[str]): Change the body content of the email. + Replaces the original content completely. + (default: :obj:`None`) + is_content_html (bool): Change the content type. If True, sets + content type to HTML; if False, sets to plain text. + (default: :obj:`False`) + to_email (Optional[List[str]]): Change the recipient email + addresses. Replaces all original recipients completely. + (default: :obj:`None`) + cc_recipients (Optional[List[str]]): Change the CC recipient + email addresses. Replaces all original CC recipients + completely. (default: :obj:`None`) + bcc_recipients (Optional[List[str]]): Change the BCC recipient + email addresses. Replaces all original BCC recipients + completely. (default: :obj:`None`) + reply_to (Optional[List[str]]): Change the email addresses that + will receive replies. Replaces all original reply-to addresses + completely. (default: :obj:`None`) + + Returns: + Dict[str, Any]: A dictionary containing the result of the update + operation. + + """ + try: + # Validate all email addresses if provided + invalid_emails = self._get_invalid_emails( + to_email, cc_recipients, bcc_recipients, reply_to + ) + if invalid_emails: + error_msg = ( + f"Invalid email address(es) provided: " + f"{', '.join(invalid_emails)}" + ) + logger.error(error_msg) + return {"error": error_msg} + + # Create message with only the fields to update + mail_message = self._create_message( + to_email=to_email, + subject=subject, + content=content, + is_content_html=is_content_html, + cc_recipients=cc_recipients, + bcc_recipients=bcc_recipients, + reply_to=reply_to, + ) + + # Update the message using PATCH + await self.client.me.messages.by_message_id(message_id).patch( + mail_message + ) + + logger.info( + f"Draft message with ID {message_id} updated successfully." + ) + + # Build dict of updated parameters + updated_params: Dict[str, Any] = dict() + if subject: + updated_params['subject'] = subject + if content: + updated_params['content'] = content + if to_email: + updated_params['to_email'] = to_email + if cc_recipients: + updated_params['cc_recipients'] = cc_recipients + if bcc_recipients: + updated_params['bcc_recipients'] = bcc_recipients + if reply_to: + updated_params['reply_to'] = reply_to + + return { + 'status': 'success', + 'message': 'Draft message updated successfully', + 'message_id': message_id, + 'updated_params': updated_params, + } + + except Exception as e: + error_msg = f"Failed to update draft message: {e!s}" + logger.error(error_msg) + return {"error": error_msg} + + def get_tools(self) -> List[FunctionTool]: + """Returns a list of FunctionTool objects representing the + functions in the toolkit. + + Returns: + List[FunctionTool]: A list of FunctionTool objects + representing the functions in the toolkit. + """ + return [ + FunctionTool(self.outlook_send_email), + FunctionTool(self.outlook_create_draft_email), + FunctionTool(self.outlook_send_draft_email), + FunctionTool(self.outlook_delete_email), + FunctionTool(self.outlook_move_message_to_folder), + FunctionTool(self.outlook_get_attachments), + FunctionTool(self.outlook_get_message), + FunctionTool(self.outlook_list_messages), + FunctionTool(self.outlook_reply_to_email), + FunctionTool(self.outlook_update_draft_message), + ] diff --git a/examples/toolkits/microsoft_outlook_mail_toolkit.py b/examples/toolkits/microsoft_outlook_mail_toolkit.py new file mode 100644 index 0000000000..04ea6478ad --- /dev/null +++ b/examples/toolkits/microsoft_outlook_mail_toolkit.py @@ -0,0 +1,99 @@ +# ========= Copyright 2023-2025 @ CAMEL-AI.org. All Rights Reserved. ========= +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========= Copyright 2023-2025 @ CAMEL-AI.org. All Rights Reserved. ========= +import asyncio + +from camel.agents import ChatAgent +from camel.models import ModelFactory +from camel.toolkits import OutlookMailToolkit +from camel.types import ModelPlatformType +from camel.types.enums import ModelType + +# Create a model instance +model = ModelFactory.create( + model_platform=ModelPlatformType.DEFAULT, + model_type=ModelType.DEFAULT, +) + +# Define system message for the Outlook assistant +sys_msg = ( + "You are a helpful Microsoft Outlook assistant that can help users manage " + "their emails. You have access to all Outlook tools including sending " + "emails, fetching emails, managing drafts, and more." +) + +# Initialize the Outlook toolkit +print("Initializing Outlook toolkit (browser may open for authentication)...") +outlook_toolkit = OutlookMailToolkit() +print("Outlook toolkit initialized!") + +# Get all Outlook tools +all_tools = outlook_toolkit.get_tools() +print(f"Loaded {len(all_tools)} Outlook tools") + +# Initialize a ChatAgent with all Outlook tools +outlook_agent = ChatAgent( + system_message=sys_msg, + model=model, + tools=all_tools, +) + + +async def main(): + print("\nExample: Sending an email") + print("=" * 50) + + user_message = ( + "Send an email to test@example.com with subject " + "'Hello from Outlook Toolkit' and body 'This is a test email " + "sent using the CAMEL Outlook toolkit.'" + ) + + response = await outlook_agent.astep(user_message) + print("Agent Response:") + print(response.msgs[0].content) + print("\nTool calls:") + print(response.info['tool_calls']) + + +asyncio.run(main()) +""" +Example: Sending an email +================================================== +Agent Response: +Done  your message has been sent to test@example.com. + +Tool calls: +[ToolCallingRecord( + tool_name='outlook_send_email', + args={ + 'to_email': ['test@example.com'], + 'subject': 'Hello from Outlook Toolkit', + 'content': 'This is a test email sent using the CAMEL toolkit.', + 'is_content_html': False, + 'attachments': None, + 'cc_recipients': None, + 'bcc_recipients': None, + 'reply_to': None, + 'save_to_sent_items': True + }, + result={ + 'status': 'success', + 'message': 'Email sent successfully', + 'recipients': ['test@example.com'], + 'subject': 'Hello from Outlook Toolkit' + }, + tool_call_id='call_abc123', + images=None +)] +""" diff --git a/pyproject.toml b/pyproject.toml index ec8dc78fbd..85f1c47b0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -151,6 +151,9 @@ communication_tools = [ "notion-client>=2.2.1,<3", "praw>=7.7.1,<8", "resend>=2.0.0,<3", + "azure-identity>=1.25.1,<2", + "msgraph-sdk>=1.46.0,<2", + "msal>=1.34.0,<2" ] data_tools = [ "numpy>=1.2,<=2.2", @@ -354,6 +357,9 @@ all = [ "agentops>=0.3.21,<0.4", "praw>=7.7.1,<8", "resend>=2.0.0,<3", + "azure-identity>=1.25.1,<2", + "msgraph-sdk>=1.46.0,<2", + "msal>=1.34.0,<2", "textblob>=0.17.1,<0.18", "scholarly[tor]==1.7.11", "notion-client>=2.2.1,<3", diff --git a/test/toolkits/test_microsoft_outlook_mail_toolkit.py b/test/toolkits/test_microsoft_outlook_mail_toolkit.py new file mode 100644 index 0000000000..8d5ba32d9c --- /dev/null +++ b/test/toolkits/test_microsoft_outlook_mail_toolkit.py @@ -0,0 +1,840 @@ +# ========= Copyright 2023-2025 @ CAMEL-AI.org. All Rights Reserved. ========= +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========= Copyright 2023-2025 @ CAMEL-AI.org. All Rights Reserved. ========= + +import json +import os +from unittest.mock import AsyncMock, MagicMock, mock_open, patch + +import pytest + +from camel.toolkits import OutlookMailToolkit +from camel.toolkits.microsoft_outlook_mail_toolkit import ( + AsyncCustomAzureCredential, +) + +pytestmark = pytest.mark.asyncio + + +@pytest.fixture +def mock_graph_service(): + """Mock Microsoft Graph API service.""" + with patch("msgraph.GraphServiceClient") as mock_service_client: + mock_client = MagicMock() + mock_service_client.return_value = mock_client + + # Mock me endpoint + mock_me = MagicMock() + mock_client.me = mock_me + + # Mock messages endpoint + mock_messages = MagicMock() + mock_me.messages = mock_messages + + # Mock send_mail endpoint + mock_send_mail = MagicMock() + mock_me.send_mail = mock_send_mail + mock_send_mail.post = AsyncMock() + + # Mock messages.get for listing messages + mock_messages.get = AsyncMock() + + # Mock messages.post for creating drafts + mock_messages.post = AsyncMock() + + # Mock messages.by_message_id for specific message operations + mock_by_message_id = MagicMock() + mock_messages.by_message_id = MagicMock( + return_value=mock_by_message_id + ) + + # Mock get message by ID + mock_by_message_id.get = AsyncMock() + + # Mock send draft + mock_send = MagicMock() + mock_by_message_id.send = mock_send + mock_send.post = AsyncMock() + + # Mock delete message + mock_by_message_id.delete = AsyncMock() + + # Mock move message + mock_move = MagicMock() + mock_by_message_id.move = mock_move + mock_move.post = AsyncMock() + + # Mock attachments + mock_attachments = MagicMock() + mock_by_message_id.attachments = mock_attachments + mock_attachments.get = AsyncMock() + + # Mock reply endpoint + mock_reply = MagicMock() + mock_by_message_id.reply = mock_reply + mock_reply.post = AsyncMock() + + # Mock reply_all endpoint + mock_reply_all = MagicMock() + mock_by_message_id.reply_all = mock_reply_all + mock_reply_all.post = AsyncMock() + + # Mock patch endpoint for updating messages + mock_by_message_id.patch = AsyncMock() + + # Mock mail_folders for folder-specific operations + mock_mail_folders = MagicMock() + mock_me.mail_folders = mock_mail_folders + + # Mock by_mail_folder_id + mock_by_folder_id = MagicMock() + mock_mail_folders.by_mail_folder_id = MagicMock( + return_value=mock_by_folder_id + ) + + # Mock folder messages + mock_folder_messages = MagicMock() + mock_by_folder_id.messages = mock_folder_messages + mock_folder_messages.get = AsyncMock() + + yield mock_client + + +@pytest.fixture +def outlook_toolkit(mock_graph_service): + """Fixture that provides a mocked OutlookMailToolkit instance.""" + # Create a mock credentials object to avoid OAuth authentication + mock_credentials = MagicMock() + mock_credentials.valid = True + + with ( + patch.dict( + 'os.environ', + { + 'MICROSOFT_TENANT_ID': 'mock_tenant_id', + 'MICROSOFT_CLIENT_ID': 'mock_client_id', + 'MICROSOFT_CLIENT_SECRET': 'mock_client_secret', + }, + ), + patch.object( + OutlookMailToolkit, + '_get_dynamic_redirect_uri', + return_value="http://localhost:12345", + ), + patch.object( + OutlookMailToolkit, + '_authenticate', + return_value=mock_credentials, + ), + patch.object( + OutlookMailToolkit, + '_get_graph_client', + return_value=mock_graph_service, + ), + ): + toolkit = OutlookMailToolkit() + toolkit.client = mock_graph_service + yield toolkit + + +async def test_send_email(outlook_toolkit, mock_graph_service): + """Test sending an email successfully.""" + mock_graph_service.me.send_mail.post.return_value = None + + result = await outlook_toolkit.outlook_send_email( + to_email=['test@example.com'], + subject='Test Subject', + content='Test Body', + ) + + assert result['status'] == 'success' + assert result['message'] == 'Email sent successfully' + assert result['recipients'] == ['test@example.com'] + assert result['subject'] == 'Test Subject' + + mock_graph_service.me.send_mail.post.assert_called_once() + + +async def test_send_email_with_attachments( + outlook_toolkit, mock_graph_service +): + """Test sending an email with attachments.""" + mock_graph_service.me.send_mail.post.return_value = None + + with ( + patch('os.path.isfile', return_value=True), + patch('builtins.open', create=True) as mock_open, + ): + mock_open.return_value.__enter__.return_value.read.return_value = ( + b'test content' + ) + + result = await outlook_toolkit.outlook_send_email( + to_email=['test@example.com'], + subject='Test Subject', + content='Test Body', + attachments=['/path/to/file.txt'], + ) + + assert result['status'] == 'success' + mock_graph_service.me.send_mail.post.assert_called_once() + + +async def test_send_email_invalid_email(outlook_toolkit): + """Test sending email with invalid email address.""" + result = await outlook_toolkit.outlook_send_email( + to_email=['invalid-email'], + subject='Test Subject', + content='Test Body', + ) + + assert 'error' in result + assert 'Invalid email address' in result['error'] + + +async def test_send_email_failure(outlook_toolkit, mock_graph_service): + """Test sending email failure.""" + mock_graph_service.me.send_mail.post.side_effect = Exception("API Error") + + result = await outlook_toolkit.outlook_send_email( + to_email=['test@example.com'], + subject='Test Subject', + content='Test Body', + ) + + assert 'error' in result + assert 'Failed to send email' in result['error'] + + +async def test_create_email_draft(outlook_toolkit, mock_graph_service): + """Test creating an email draft.""" + mock_draft_result = MagicMock() + mock_draft_result.id = 'draft123' + mock_graph_service.me.messages.post.return_value = mock_draft_result + + result = await outlook_toolkit.outlook_create_draft_email( + to_email=['test@example.com'], + subject='Test Subject', + content='Test Body', + ) + + assert result['status'] == 'success' + assert result['draft_id'] == 'draft123' + assert result['message'] == 'Draft email created successfully' + assert result['recipients'] == ['test@example.com'] + assert result['subject'] == 'Test Subject' + + mock_graph_service.me.messages.post.assert_called_once() + + +async def test_browser_auth_persists_refresh_token(tmp_path): + toolkit = OutlookMailToolkit.__new__(OutlookMailToolkit) + toolkit.scopes = ["Mail.Send", "Mail.ReadWrite"] + toolkit.redirect_uri = "http://localhost:12345" + toolkit.refresh_token_file_path = tmp_path / "refresh_token.json" + toolkit.timeout = 5 + toolkit.tenant_id = "common" + toolkit.client_id = "mock_client_id" + toolkit.client_secret = "mock_client_secret" + + toolkit._get_auth_url = MagicMock(return_value="https://example.com/auth") + toolkit._get_authorization_code_via_browser = MagicMock( + return_value="mock_auth_code" + ) + + mock_response = MagicMock() + mock_response.json.return_value = { + "access_token": "mock_access_token", + "refresh_token": "mock_refresh_token", + "expires_in": 3600, + } + + with patch( + "camel.toolkits.microsoft_outlook_mail_toolkit.requests.post", + return_value=mock_response, + ): + credentials = toolkit._authenticate_using_browser() + + assert isinstance(credentials, AsyncCustomAzureCredential) + assert credentials.refresh_token == "mock_refresh_token" + assert credentials._access_token == "mock_access_token" + + with open(toolkit.refresh_token_file_path, "r") as f: + token_data = json.load(f) + assert token_data["refresh_token"] == "mock_refresh_token" + + +async def test_create_email_draft_with_attachments( + outlook_toolkit, mock_graph_service +): + """Test creating an email draft with attachments.""" + mock_draft_result = MagicMock() + mock_draft_result.id = 'draft123' + mock_graph_service.me.messages.post.return_value = mock_draft_result + + with ( + patch('os.path.isfile', return_value=True), + patch('builtins.open', create=True) as mock_open, + ): + mock_open.return_value.__enter__.return_value.read.return_value = ( + b'test content' + ) + + result = await outlook_toolkit.outlook_create_draft_email( + to_email=['test@example.com'], + subject='Test Subject', + content='Test Body', + attachments=['/path/to/file.txt'], + ) + + assert result['status'] == 'success' + assert result['draft_id'] == 'draft123' + mock_graph_service.me.messages.post.assert_called_once() + + +async def test_create_email_draft_invalid_email(outlook_toolkit): + """Test creating draft with invalid email address.""" + result = await outlook_toolkit.outlook_create_draft_email( + to_email=['invalid-email'], + subject='Test Subject', + content='Test Body', + ) + + assert 'error' in result + assert 'Invalid email address' in result['error'] + + +async def test_create_email_draft_failure(outlook_toolkit, mock_graph_service): + """Test creating email draft failure.""" + mock_graph_service.me.messages.post.side_effect = Exception("API Error") + + result = await outlook_toolkit.outlook_create_draft_email( + to_email=['test@example.com'], + subject='Test Subject', + content='Test Body', + ) + + assert 'error' in result + assert 'Failed to create draft email' in result['error'] + + +async def test_send_draft_email(outlook_toolkit, mock_graph_service): + """Test sending a draft email.""" + mock_graph_service.me.messages.by_message_id().send.post.return_value = ( + None + ) + + result = await outlook_toolkit.outlook_send_draft_email( + draft_id='draft123' + ) + + assert result['status'] == 'success' + assert result['message'] == 'Draft email sent successfully' + assert result['draft_id'] == 'draft123' + + mock_graph_service.me.messages.by_message_id().send.post.assert_called_once() + + +async def test_send_draft_email_failure(outlook_toolkit, mock_graph_service): + """Test sending draft email failure.""" + mock_graph_service.me.messages.by_message_id().send.post.side_effect = ( + Exception("API Error") + ) + + result = await outlook_toolkit.outlook_send_draft_email( + draft_id='draft123' + ) + + assert 'error' in result + assert 'Failed to send draft email' in result['error'] + + +async def test_delete_email(outlook_toolkit, mock_graph_service): + """Test deleting an email successfully.""" + mock_graph_service.me.messages.by_message_id().delete.return_value = None + + result = await outlook_toolkit.outlook_delete_email(message_id='msg123') + + assert result['status'] == 'success' + assert result['message'] == 'Email deleted successfully' + assert result['message_id'] == 'msg123' + + mock_graph_service.me.messages.by_message_id().delete.assert_called_once() + + +async def test_delete_email_failure(outlook_toolkit, mock_graph_service): + """Test deleting email failure.""" + mock_graph_service.me.messages.by_message_id().delete.side_effect = ( + Exception("API Error") + ) + + result = await outlook_toolkit.outlook_delete_email(message_id='msg123') + + assert 'error' in result + assert 'Failed to delete email' in result['error'] + + +async def test_move_message_to_folder(outlook_toolkit, mock_graph_service): + """Test moving an email to a folder successfully.""" + mock_graph_service.me.messages.by_message_id().move.post.return_value = ( + None + ) + + result = await outlook_toolkit.outlook_move_message_to_folder( + message_id='msg123', destination_folder_id='inbox' + ) + + assert result['status'] == 'success' + assert result['message'] == 'Email moved successfully' + assert result['message_id'] == 'msg123' + assert result['destination_folder_id'] == 'inbox' + + mock_graph_service.me.messages.by_message_id().move.post.assert_called_once() + + +async def test_move_message_to_folder_failure( + outlook_toolkit, mock_graph_service +): + """Test moving email failure.""" + mock_graph_service.me.messages.by_message_id().move.post.side_effect = ( + Exception("API Error") + ) + + result = await outlook_toolkit.outlook_move_message_to_folder( + message_id='msg123', destination_folder_id='inbox' + ) + + assert 'error' in result + assert 'Failed to move email' in result['error'] + + +async def test_get_attachments(outlook_toolkit, mock_graph_service): + """Test getting attachments and saving to disk.""" + import base64 + + mock_attachment = MagicMock() + mock_attachment.name = 'document.pdf' + mock_attachment.is_inline = False + mock_attachment.content_bytes = base64.b64encode(b'test content') + + mock_response = MagicMock() + mock_response.value = [mock_attachment] + mock_attachments = mock_graph_service.me.messages.by_message_id() + mock_attachments.attachments.get.return_value = mock_response + + with ( + patch('os.makedirs'), + patch('os.path.exists', return_value=False), + patch('builtins.open', create=True), + ): + result = await outlook_toolkit.outlook_get_attachments( + message_id='msg123', + ) + + assert result['status'] == 'success' + assert result['total_count'] == 1 + + +async def test_get_attachments_exclude_inline( + outlook_toolkit, mock_graph_service +): + """Test getting attachments excluding inline attachments (default).""" + mock_attachment1 = MagicMock() + mock_attachment1.name = 'image.png' + mock_attachment1.is_inline = True + + mock_response = MagicMock() + mock_response.value = [mock_attachment1] + mock_attachments = mock_graph_service.me.messages.by_message_id() + mock_attachments.attachments.get.return_value = mock_response + + result = await outlook_toolkit.outlook_get_attachments( + message_id='msg123', + include_inline_attachments=False, + ) + + assert result['status'] == 'success' + assert result['total_count'] == 0 # Only non-inline attachment + assert not result['attachments'] + + +async def test_get_attachments_include_inline( + outlook_toolkit, mock_graph_service +): + """Test getting attachments including inline attachments.""" + mock_attachment1 = MagicMock() + mock_attachment1.name = 'document.pdf' + mock_attachment1.is_inline = True + + mock_attachment2 = MagicMock() + mock_attachment2.name = 'image.png' + mock_attachment2.is_inline = True + + mock_response = MagicMock() + mock_response.value = [mock_attachment1, mock_attachment2] + mock_attachments = mock_graph_service.me.messages.by_message_id() + mock_attachments.attachments.get.return_value = mock_response + + result = await outlook_toolkit.outlook_get_attachments( + message_id='msg123', + metadata_only=True, + include_inline_attachments=True, + ) + + assert result['status'] == 'success' + assert result['total_count'] == 2 # Both attachments included + assert result['attachments'][0]['name'] == 'document.pdf' + assert result['attachments'][1]['name'] == 'image.png' + + +async def test_get_attachments_failure(outlook_toolkit, mock_graph_service): + """Test getting attachments failure.""" + mock_attachments = mock_graph_service.me.messages.by_message_id() + mock_attachments.attachments.get.side_effect = Exception("API Error") + + result = await outlook_toolkit.outlook_get_attachments(message_id='msg123') + + assert 'error' in result + assert 'Failed to get attachments' in result['error'] + + +async def test_get_attachments_with_content_and_save_path( + outlook_toolkit, mock_graph_service +): + """Test getting attachments with metadata_only=False and save_path.""" + import base64 + import tempfile + + original_content = b'This is a test PDF file content.' + encoded_content = base64.b64encode(original_content) + + mock_attachment = MagicMock() + mock_attachment.id = 'attachment-id-456' + mock_attachment.name = 'invoice.pdf' + mock_attachment.is_inline = False + mock_attachment.content_bytes = encoded_content + + mock_response = MagicMock() + mock_response.value = [mock_attachment] + mock_attachments = mock_graph_service.me.messages.by_message_id() + mock_attachments.attachments.get.return_value = mock_response + + with tempfile.TemporaryDirectory() as temp_dir: + m_open = mock_open() + with ( + patch('os.makedirs') as mock_makedirs, + patch('os.path.exists', return_value=False), + patch('builtins.open', m_open), + ): + result = await outlook_toolkit.outlook_get_attachments( + message_id='msg456', + metadata_only=False, + save_path=temp_dir, + ) + + assert result['status'] == 'success' + assert result['total_count'] == 1 + attachment_info = result['attachments'][0] + assert attachment_info['name'] == 'invoice.pdf' + assert 'saved_path' in attachment_info + assert 'content_bytes' not in attachment_info + + expected_path = os.path.join(temp_dir, 'invoice.pdf') + assert attachment_info['saved_path'] == expected_path + mock_makedirs.assert_called_once_with(temp_dir, exist_ok=True) + m_open.assert_called_once_with(expected_path, 'wb') + handle = m_open() + handle.write.assert_called_once_with(original_content) + + +@pytest.fixture( + params=[ + ('Plain text email body.', 'text', 'Plain text email body.'), + ( + '

HTML email body.

', + 'html', + 'HTML email body.', + ), + ], + ids=['plain_text', 'html_to_text'], +) +def create_mock_message(request): + """Parametrized fixture that creates mock message objects.""" + from datetime import datetime, timezone + + body_content, body_type, expected_body = request.param + + mock_msg = MagicMock() + mock_msg.id = 'msg123' + mock_msg.subject = 'Test Subject' + mock_msg.body_preview = body_content[:25] + '...' + mock_msg.is_read = False + mock_msg.is_draft = False + mock_msg.has_attachments = False + mock_msg.importance = 'normal' + mock_msg.received_date_time = datetime( + 2024, 1, 15, 10, 30, tzinfo=timezone.utc + ) + mock_msg.sent_date_time = datetime( + 2024, 1, 15, 10, 29, tzinfo=timezone.utc + ) + + # Mock body + mock_body = MagicMock() + mock_body.content = body_content + mock_body.content_type = body_type + mock_msg.body = mock_body + + # Mock from address + mock_from = MagicMock() + mock_from.email_address.address = 'sender@example.com' + mock_from.email_address.name = 'Sender Name' + mock_msg.from_ = mock_from + + # Mock recipients + mock_to = MagicMock() + mock_to.email_address.address = 'recipient@example.com' + mock_to.email_address.name = 'Recipient Name' + mock_msg.to_recipients = [mock_to] + mock_msg.cc_recipients = [] + mock_msg.bcc_recipients = [] + + # Add expected body for assertions + mock_msg.expected_body = expected_body + + return mock_msg + + +async def test_get_message( + outlook_toolkit, mock_graph_service, create_mock_message +): + """Test getting messages with different content types.""" + with patch( + 'camel.toolkits.microsoft_outlook_mail_toolkit.isinstance', + return_value=True, + ): + mock_graph_service.me.messages.by_message_id().get.return_value = ( + create_mock_message + ) + + result = await outlook_toolkit.outlook_get_message(message_id='msg123') + + assert result['status'] == 'success' + assert 'message' in result + + message = result['message'] + assert message['message_id'] == 'msg123' + assert message['subject'] == 'Test Subject' + assert message['body'] == create_mock_message.expected_body + assert message['is_read'] is False + assert message['from'][0]['address'] == 'sender@example.com' + assert message['to_recipients'][0]['address'] == 'recipient@example.com' + + mock_graph_service.me.messages.by_message_id().get.assert_called_once() + + +async def test_get_message_failure(outlook_toolkit, mock_graph_service): + """Test get_message handles API errors correctly.""" + mock_graph_service.me.messages.by_message_id().get.side_effect = Exception( + "API Error" + ) + + result = await outlook_toolkit.outlook_get_message(message_id='msg123') + + assert 'error' in result + assert 'Failed to get message' in result['error'] + + +async def test_list_messages( + outlook_toolkit, mock_graph_service, create_mock_message +): + """Test listing messages with different content types.""" + with patch( + 'camel.toolkits.microsoft_outlook_mail_toolkit.isinstance', + return_value=True, + ): + mock_response = MagicMock() + mock_response.value = [create_mock_message] + mock_graph_service.me.messages.get.return_value = mock_response + + result = await outlook_toolkit.outlook_list_messages() + + assert result['status'] == 'success' + assert 'messages' in result + assert result['total_count'] == 1 + assert len(result['messages']) == 1 + + message = result['messages'][0] + assert message['message_id'] == 'msg123' + assert message['subject'] == 'Test Subject' + assert message['body'] == create_mock_message.expected_body + assert message['is_read'] is False + assert message['from'][0]['address'] == 'sender@example.com' + assert message['to_recipients'][0]['address'] == 'recipient@example.com' + + mock_graph_service.me.messages.get.assert_called_once() + + +async def test_list_messages_failure(outlook_toolkit, mock_graph_service): + """Test list_messages handles API errors correctly.""" + mock_graph_service.me.messages.get.side_effect = Exception("API Error") + + result = await outlook_toolkit.outlook_list_messages() + + assert 'error' in result + assert 'Failed to list messages' in result['error'] + + +async def test_reply_to_email(outlook_toolkit, mock_graph_service): + """Test replying to an email (reply to sender only).""" + mock_graph_service.me.messages.by_message_id().reply.post.return_value = ( + None + ) + + result = await outlook_toolkit.outlook_reply_to_email( + message_id='msg123', + content='This is my reply', + reply_all=False, + ) + + assert result['status'] == 'success' + assert result['message'] == 'Reply sent successfully' + assert result['message_id'] == 'msg123' + assert result['reply_type'] == 'reply' + + mock_graph_service.me.messages.by_message_id().reply.post.assert_called_once() + mock_graph_service.me.messages.by_message_id().reply_all.post.assert_not_called() + + +async def test_reply_to_email_all(outlook_toolkit, mock_graph_service): + """Test replying to all recipients of an email.""" + mock_reply_all = mock_graph_service.me.messages.by_message_id().reply_all + mock_reply_all.post.return_value = None + + result = await outlook_toolkit.outlook_reply_to_email( + message_id='msg456', + content='This is my reply to all', + reply_all=True, + ) + + assert result['status'] == 'success' + assert result['message'] == 'Reply All sent successfully' + assert result['message_id'] == 'msg456' + assert result['reply_type'] == 'reply all' + + mock_reply_all.post.assert_called_once() + mock_graph_service.me.messages.by_message_id().reply.post.assert_not_called() + + +async def test_reply_to_email_failure(outlook_toolkit, mock_graph_service): + """Test reply to email failure when using simple reply.""" + mock_graph_service.me.messages.by_message_id().reply.post.side_effect = ( + Exception("API Error: Unable to send reply") + ) + + result = await outlook_toolkit.outlook_reply_to_email( + message_id='msg123', + content='This reply will fail', + reply_all=False, + ) + + assert 'error' in result + assert 'Failed to reply to email' in result['error'] + assert 'API Error: Unable to send reply' in result['error'] + + +async def test_reply_to_email_all_failure(outlook_toolkit, mock_graph_service): + """Test reply to email failure when using reply all.""" + mock_reply_all = mock_graph_service.me.messages.by_message_id().reply_all + mock_reply_all.post.side_effect = Exception( + "API Error: Unable to send reply all" + ) + + result = await outlook_toolkit.outlook_reply_to_email( + message_id='msg456', + content='This reply all will fail', + reply_all=True, + ) + + assert 'error' in result + assert 'Failed to reply to email' in result['error'] + assert 'API Error: Unable to send reply all' in result['error'] + + +async def test_update_draft_message(outlook_toolkit, mock_graph_service): + """Test updating a draft message with all parameters.""" + mock_by_message_id = mock_graph_service.me.messages.by_message_id() + mock_by_message_id.patch.return_value = None + + result = await outlook_toolkit.outlook_update_draft_message( + message_id='draft123', + subject='Updated Subject', + content='Updated content', + is_content_html=True, + to_email=['new@example.com'], + cc_recipients=['cc@example.com'], + bcc_recipients=['bcc@example.com'], + reply_to=['reply@example.com'], + ) + + assert result['status'] == 'success' + assert result['message'] == 'Draft message updated successfully' + assert result['message_id'] == 'draft123' + assert result['updated_params']['subject'] == 'Updated Subject' + assert result['updated_params']['content'] == 'Updated content' + assert result['updated_params']['to_email'] == ['new@example.com'] + assert result['updated_params']['cc_recipients'] == ['cc@example.com'] + assert result['updated_params']['bcc_recipients'] == ['bcc@example.com'] + assert result['updated_params']['reply_to'] == ['reply@example.com'] + + mock_by_message_id.patch.assert_called_once() + + +async def test_update_draft_message_subject_only( + outlook_toolkit, mock_graph_service +): + """Test updating only the subject of a draft message.""" + mock_by_message_id = mock_graph_service.me.messages.by_message_id() + mock_by_message_id.patch.return_value = None + + result = await outlook_toolkit.outlook_update_draft_message( + message_id='draft456', + subject='New Subject Only', + ) + + assert result['status'] == 'success' + assert result['message_id'] == 'draft456' + assert result['updated_params']['subject'] == 'New Subject Only' + assert 'content' not in result['updated_params'] + assert 'to_email' not in result['updated_params'] + + mock_by_message_id.patch.assert_called_once() + + +async def test_update_draft_message_failure( + outlook_toolkit, mock_graph_service +): + """Test update draft message failure.""" + mock_by_message_id = mock_graph_service.me.messages.by_message_id() + mock_by_message_id.patch.side_effect = Exception( + "API Error: Unable to update draft" + ) + + result = await outlook_toolkit.outlook_update_draft_message( + message_id='draft123', + subject='This update will fail', + ) + + assert 'error' in result + assert 'Failed to update draft message' in result['error'] + assert 'API Error: Unable to update draft' in result['error'] diff --git a/uv.lock b/uv.lock index 8a2df0a584..f447e72488 100644 --- a/uv.lock +++ b/uv.lock @@ -62,9 +62,12 @@ name = "agentops" version = "0.3.26" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-sdk" }, + { name = "opentelemetry-api", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-api", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-exporter-otlp-proto-http", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-exporter-otlp-proto-http", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-sdk", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-sdk", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, { name = "packaging" }, { name = "psutil" }, { name = "pyyaml" }, @@ -593,11 +596,11 @@ name = "azure-identity" version = "1.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "azure-core", marker = "python_full_version < '3.13'" }, - { name = "cryptography", marker = "python_full_version < '3.13'" }, - { name = "msal", marker = "python_full_version < '3.13'" }, - { name = "msal-extensions", marker = "python_full_version < '3.13'" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "azure-core" }, + { name = "cryptography" }, + { name = "msal" }, + { name = "msal-extensions" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/8d/1a6c41c28a37eab26dc85ab6c86992c700cd3f4a597d9ed174b0e9c69489/azure_identity-1.25.1.tar.gz", hash = "sha256:87ca8328883de6036443e1c37b40e8dc8fb74898240f61071e09d2e369361456", size = 279826, upload-time = "2025-10-06T20:30:02.194Z" } wheels = [ @@ -766,16 +769,16 @@ wheels = [ [[package]] name = "botocore" -version = "1.42.12" +version = "1.42.13" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a0/b6/9b7988a8476712cdbfeeb68c733933005465c85ebf0ee469a6ea5ca3415c/botocore-1.42.12.tar.gz", hash = "sha256:1f9f63c3d6bb1f768519da30d6018706443c5d8af5472274d183a4945f3d81f8", size = 14879004, upload-time = "2025-12-17T20:30:29.542Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/28/c4cc2c697227752535488527ca057a1c41fbe32d0a81f15b25a7c738bc75/botocore-1.42.13.tar.gz", hash = "sha256:7e4cf14bd5719b60600fb45d2bb3ae140feb3c182a863b93093aafce7f93cfee", size = 14885136, upload-time = "2025-12-18T20:28:44.722Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/73/22764d0a17130b7d95b2a4104607e6db5487a0e5afb68f5691260ae9c3dc/botocore-1.42.12-py3-none-any.whl", hash = "sha256:4f163880350f6d831857ce5d023875b7c6534be862e5affd9fcf82b8d1ab3537", size = 14552878, upload-time = "2025-12-17T20:30:24.671Z" }, + { url = "https://files.pythonhosted.org/packages/b1/52/b4235bd6cd9b86fa73be92bad1039fd533b666921c32d0d94ffdb220a871/botocore-1.42.13-py3-none-any.whl", hash = "sha256:b750b2de4a2478db9718a02395cb9da8698901ba02378d60037d6369ecb6bb88", size = 14558402, upload-time = "2025-12-18T20:28:40.532Z" }, ] [[package]] @@ -910,6 +913,7 @@ all = [ { name = "apify-client" }, { name = "arxiv" }, { name = "arxiv2text" }, + { name = "azure-identity" }, { name = "azure-storage-blob" }, { name = "beautifulsoup4" }, { name = "botocore" }, @@ -963,8 +967,11 @@ all = [ { name = "mcp" }, { name = "mem0ai" }, { name = "microsandbox" }, - { name = "mistralai" }, + { name = "mistralai", version = "1.9.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "mistralai", version = "1.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, { name = "mock" }, + { name = "msal" }, + { name = "msgraph-sdk" }, { name = "mypy" }, { name = "nebula3-python" }, { name = "neo4j" }, @@ -1048,7 +1055,10 @@ all = [ { name = "yt-dlp" }, ] communication-tools = [ + { name = "azure-identity" }, { name = "discord-py" }, + { name = "msal" }, + { name = "msgraph-sdk" }, { name = "notion-client" }, { name = "praw" }, { name = "pygithub" }, @@ -1207,7 +1217,8 @@ model-platforms = [ { name = "ibm-watsonx-ai", version = "1.4.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "litellm", version = "1.80.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, { name = "litellm", version = "1.80.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "mistralai" }, + { name = "mistralai", version = "1.9.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "mistralai", version = "1.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, { name = "reka-api" }, ] owl = [ @@ -1355,6 +1366,8 @@ requires-dist = [ { name = "arxiv2text", marker = "extra == 'all'", specifier = ">=0.1.14,<0.2" }, { name = "arxiv2text", marker = "extra == 'research-tools'", specifier = ">=0.1.14,<0.2" }, { name = "astor", specifier = ">=0.8.1" }, + { name = "azure-identity", marker = "extra == 'all'", specifier = ">=1.25.1,<2" }, + { name = "azure-identity", marker = "extra == 'communication-tools'", specifier = ">=1.25.1,<2" }, { name = "azure-storage-blob", marker = "extra == 'all'", specifier = ">=12.21.0,<13" }, { name = "azure-storage-blob", marker = "extra == 'storage'", specifier = ">=12.21.0,<13" }, { name = "beautifulsoup4", marker = "extra == 'all'", specifier = ">=4,<5" }, @@ -1504,6 +1517,10 @@ requires-dist = [ { name = "mistralai", marker = "extra == 'model-platforms'", specifier = ">=1.1.0,<2" }, { name = "mock", marker = "extra == 'all'", specifier = ">=5,<6" }, { name = "mock", marker = "extra == 'dev'", specifier = ">=5,<6" }, + { name = "msal", marker = "extra == 'all'", specifier = ">=1.34.0,<2" }, + { name = "msal", marker = "extra == 'communication-tools'", specifier = ">=1.34.0,<2" }, + { name = "msgraph-sdk", marker = "extra == 'all'", specifier = ">=1.46.0,<2" }, + { name = "msgraph-sdk", marker = "extra == 'communication-tools'", specifier = ">=1.46.0,<2" }, { name = "mypy", marker = "extra == 'all'", specifier = ">=1.5.1,<2" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.5.1,<2" }, { name = "myst-parser", marker = "extra == 'docs'" }, @@ -1985,10 +2002,14 @@ dependencies = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, { name = "numpy", version = "2.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "onnxruntime" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-grpc" }, - { name = "opentelemetry-instrumentation-fastapi" }, - { name = "opentelemetry-sdk" }, + { name = "opentelemetry-api", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-api", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-exporter-otlp-proto-grpc", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-exporter-otlp-proto-grpc", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-instrumentation-fastapi", version = "0.59b0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-instrumentation-fastapi", version = "0.60b1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-sdk", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-sdk", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, { name = "orjson" }, { name = "overrides" }, { name = "posthog" }, @@ -2071,12 +2092,11 @@ wheels = [ [[package]] name = "cohere" -version = "5.20.0" +version = "5.20.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastavro" }, { name = "httpx" }, - { name = "httpx-sse" }, { name = "pydantic" }, { name = "pydantic-core" }, { name = "requests" }, @@ -2084,9 +2104,9 @@ dependencies = [ { name = "types-requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/0e5dcfa9d111b82de4f3c7d83fbc92f478d229c8a004cc63c321fe44bb42/cohere-5.20.0.tar.gz", hash = "sha256:fb5ad5afa47447dd7eb090ad29bdb3a8181b0e758a3b03ba6ed8ca48d68d11a7", size = 168600, upload-time = "2025-10-24T20:24:05.903Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/ed/bb02083654bdc089ae4ef1cd7691fd2233f1fd9f32bcbfacc80ff57d9775/cohere-5.20.1.tar.gz", hash = "sha256:50973f63d2c6138ff52ce37d8d6f78ccc539af4e8c43865e960d68e0bf835b6f", size = 180820, upload-time = "2025-12-18T16:39:50.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/5c/e312678fb4dff827c748980ec18918307d25e39ce006c84f7c6b32bc5641/cohere-5.20.0-py3-none-any.whl", hash = "sha256:a95f17ed22be3f978363703beb6008b55000ce0e85124ddb976fa5b688014fea", size = 303306, upload-time = "2025-10-24T20:24:04.237Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e3/94eb11ac3ebaaa3a6afb5d2ff23db95d58bc468ae538c388edf49f2f20b5/cohere-5.20.1-py3-none-any.whl", hash = "sha256:d230fd13d95ba92ae927fce3dd497599b169883afc7954fe29b39fb8d5df5fc7", size = 318973, upload-time = "2025-12-18T16:39:49.504Z" }, ] [[package]] @@ -2685,7 +2705,7 @@ wheels = [ [[package]] name = "ddgs" -version = "9.9.3" +version = "9.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -2694,9 +2714,9 @@ dependencies = [ { name = "lxml" }, { name = "primp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/e5/93cb56402815f86f29fccc1beae0e879e991273c2731dcd4743c299df260/ddgs-9.9.3.tar.gz", hash = "sha256:367b4b055790a44c11e96c2f85ca570e65dbeb59c7399817e00c5eaa0b7076db", size = 36103, upload-time = "2025-12-05T12:25:21.102Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/76/8dc0323d1577037abad7a679f8af150ebb73a94995d3012de71a8898e6e6/ddgs-9.10.0.tar.gz", hash = "sha256:d9381ff75bdf1ad6691d3d1dc2be12be190d1d32ecd24f1002c492143c52c34f", size = 31491, upload-time = "2025-12-17T23:30:15.021Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/4d/0ab756d83e06e18f4a7ef48adc2940577bf308bfdd19c6cea6dab00baec1/ddgs-9.9.3-py3-none-any.whl", hash = "sha256:60a1d5d4b72cf23991495ea6b87d9389640d5fb3452224ecaad8e2ff17b93466", size = 41635, upload-time = "2025-12-05T12:25:19.47Z" }, + { url = "https://files.pythonhosted.org/packages/b5/0e/d4b7d6a8df5074cf67bc14adead39955b0bf847c947ff6cad0bb527887f4/ddgs-9.10.0-py3-none-any.whl", hash = "sha256:81233d79309836eb03e7df2a0d2697adc83c47c342713132c0ba618f1f2c6eee", size = 40311, upload-time = "2025-12-17T23:30:13.606Z" }, ] [[package]] @@ -4933,9 +4953,12 @@ dependencies = [ { name = "backoff" }, { name = "httpx" }, { name = "openai" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-sdk" }, + { name = "opentelemetry-api", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-api", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-exporter-otlp-proto-http", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-exporter-otlp-proto-http", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-sdk", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-sdk", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, { name = "packaging" }, { name = "pydantic" }, { name = "requests" }, @@ -5708,24 +5731,165 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/89/2d6653e4c6bfa535da59d84d7c8bcc1678b35299ed43c1d11fb1c07a2179/microsandbox-0.1.8-py3-none-any.whl", hash = "sha256:b4503f6efd0f58e1acbac782399d3020cc704031279637fe5c60bdb5da267cd8", size = 12112, upload-time = "2025-05-22T13:07:13.176Z" }, ] +[[package]] +name = "microsoft-kiota-abstractions" +version = "1.9.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-api", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-sdk", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-sdk", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "std-uritemplate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/6c/fd855a03545ae261b28d179b206e5f80a0e7c95fac5a580514c4dabedca0/microsoft_kiota_abstractions-1.9.7.tar.gz", hash = "sha256:731ed60c2df74ca80d1bf36d40a4c390aab353db3a76796c63ea9e9a220ce65c", size = 24447, upload-time = "2025-09-09T13:53:42.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/d8/d699a2cb209c72f1258af5f582a7868d1b006e57cc4394b68b0f996ba370/microsoft_kiota_abstractions-1.9.7-py3-none-any.whl", hash = "sha256:8add66c38d05ab9a496c1c843bb16e04b70edc4651dc290b9629b14009f5c0c0", size = 44404, upload-time = "2025-09-09T13:53:41.312Z" }, +] + +[[package]] +name = "microsoft-kiota-authentication-azure" +version = "1.9.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "azure-core" }, + { name = "microsoft-kiota-abstractions" }, + { name = "opentelemetry-api", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-api", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-sdk", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-sdk", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/9a/3deb5d951e55e059fbde93deaf1b3fdd1ec3a6e8bdac01280c640dac7b8c/microsoft_kiota_authentication_azure-1.9.7.tar.gz", hash = "sha256:1ecef94097ca8029e5b903bfef8dbbf47ba75bc1521907164a84b6617226696b", size = 4987, upload-time = "2025-09-09T13:53:52.47Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/49/d12e7eabd6fc7039bfa301dfff26f0fced9bd164564b96b6d99fffcb020b/microsoft_kiota_authentication_azure-1.9.7-py3-none-any.whl", hash = "sha256:a2d776bef22d10be65df1ea9e8f1737e46981bd14cdb70e3fe4f4a066e92b139", size = 6908, upload-time = "2025-09-09T13:53:51.735Z" }, +] + +[[package]] +name = "microsoft-kiota-http" +version = "1.9.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", extra = ["http2"] }, + { name = "microsoft-kiota-abstractions" }, + { name = "opentelemetry-api", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-api", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-sdk", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-sdk", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/a9/7efe67311902394a208545ae067dfc7e957383939b0ee6ff43e1955afbe7/microsoft_kiota_http-1.9.7.tar.gz", hash = "sha256:abcacca784649308ab93d8578c2afb581a42deed048b183d7bbdc48c325dd6a1", size = 21249, upload-time = "2025-09-09T13:54:00.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/99/1d625b9353cabb3aaddb468c379b1e1fc726795281e94437096846b434b1/microsoft_kiota_http-1.9.7-py3-none-any.whl", hash = "sha256:14ce6b14c4fa93608f535f2c6ae21d35b1d0e2635ab70501fa3a3afc90135261", size = 31577, upload-time = "2025-09-09T13:53:59.616Z" }, +] + +[[package]] +name = "microsoft-kiota-serialization-form" +version = "1.9.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "microsoft-kiota-abstractions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/d7/dc6d782f75608be4b1733df6592e4c7e819b6e32b290ac45304f74c0c0cf/microsoft_kiota_serialization_form-1.9.7.tar.gz", hash = "sha256:d3297a60778c0437513334b703225ce108fd109f13c1993afea599b85dc5a528", size = 8999, upload-time = "2025-09-09T13:54:08.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/62/9929fc1fe0ff76af5ff6dd8179b71c58105675465536141cd491e03f5a1d/microsoft_kiota_serialization_form-1.9.7-py3-none-any.whl", hash = "sha256:72d2dc5e57a993145702870ad89c85cebe3336d4d34f231d951ee1bc83ad11b9", size = 10671, upload-time = "2025-09-09T13:54:08.169Z" }, +] + +[[package]] +name = "microsoft-kiota-serialization-json" +version = "1.9.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "microsoft-kiota-abstractions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/f8/e13c48e610a00f2abfa9fa19f03e2cf21fe98486dfc5a453ce6c0490d3f2/microsoft_kiota_serialization_json-1.9.7.tar.gz", hash = "sha256:1e54ff90b185fe21cca94ebbf8468bf44a2ca5f082c4cf04dbd2d42a9472837a", size = 9416, upload-time = "2025-09-09T13:54:18.299Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/6b/761e45c91086fb45e69ee1b85d538f6e5fc89b86f6ade148e8c5575259ce/microsoft_kiota_serialization_json-1.9.7-py3-none-any.whl", hash = "sha256:6f44012f00cf7c4c4d8b9195e7f8a691d186021b5d9a20e791a77c800b5be531", size = 11056, upload-time = "2025-09-09T13:54:17.395Z" }, +] + +[[package]] +name = "microsoft-kiota-serialization-multipart" +version = "1.9.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "microsoft-kiota-abstractions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/23/31b11fd0e44bb79923ea8310c2f3d0bc1e16f56d35d2fc73203a260a0a73/microsoft_kiota_serialization_multipart-1.9.7.tar.gz", hash = "sha256:1a13d193d078dea86711d8c6e89ac142aff5033079c7be4061279b2da5c83ef8", size = 5150, upload-time = "2025-09-09T13:54:35.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/ca/98efd66c8e7180928fe2901f4c766799991836e536a8a0aca9186b5d7c7a/microsoft_kiota_serialization_multipart-1.9.7-py3-none-any.whl", hash = "sha256:cd72ee004039ee64a35bd5254afd3f8bc89877e948282ab0fe0a7efab75f68bb", size = 6651, upload-time = "2025-09-09T13:54:34.811Z" }, +] + +[[package]] +name = "microsoft-kiota-serialization-text" +version = "1.9.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "microsoft-kiota-abstractions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/5c/479378981c7b8fb22d6ba693f07db457a18d3efc86dd083ebe31d6192d37/microsoft_kiota_serialization_text-1.9.7.tar.gz", hash = "sha256:d57a082d5c6ea1e650286314cac9a9e7a2662aa4beb80635bf4addd33d252bd5", size = 7306, upload-time = "2025-09-09T13:54:26.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/8b/b8b6482719d9ecc4d87f07aa8726d33c18004e0630ef5cd2891ee8bf2ada/microsoft_kiota_serialization_text-1.9.7-py3-none-any.whl", hash = "sha256:47c4d774883bec269a6eb077a5ca2f26ae6715986c8defa374d536a9664dc43e", size = 8840, upload-time = "2025-09-09T13:54:25.642Z" }, +] + [[package]] name = "mistralai" version = "1.9.11" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] dependencies = [ - { name = "eval-type-backport" }, - { name = "httpx" }, - { name = "invoke" }, - { name = "pydantic" }, - { name = "python-dateutil" }, - { name = "pyyaml" }, - { name = "typing-inspection" }, + { name = "eval-type-backport", marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "httpx", marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "invoke", marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "pydantic", marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "python-dateutil", marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "pyyaml", marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "typing-inspection", marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5a/8d/d8b7af67a966b6f227024e1cb7287fc19901a434f87a5a391dcfe635d338/mistralai-1.9.11.tar.gz", hash = "sha256:3df9e403c31a756ec79e78df25ee73cea3eb15f86693773e16b16adaf59c9b8a", size = 208051, upload-time = "2025-10-02T15:53:40.473Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fe/76/4ce12563aea5a76016f8643eff30ab731e6656c845e9e4d090ef10c7b925/mistralai-1.9.11-py3-none-any.whl", hash = "sha256:7a3dc2b8ef3fceaa3582220234261b5c4e3e03a972563b07afa150e44a25a6d3", size = 442796, upload-time = "2025-10-02T15:53:39.134Z" }, ] +[[package]] +name = "mistralai" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "eval-type-backport", marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "httpx", marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "invoke", marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-api", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-exporter-otlp-proto-http", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-sdk", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-semantic-conventions", version = "0.59b0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "pydantic", marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "python-dateutil", marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "pyyaml", marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "typing-inspection", marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/b6/ab0f6ca229be1c78a2918327da5c0efc964d006e9e4d94689798ac42249f/mistralai-1.10.0.tar.gz", hash = "sha256:c92e9a5ec7057577b326d47a4b1c186f42660bccbe95167fc25c686fe658ad23", size = 219585, upload-time = "2025-12-17T09:34:50.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/49/ff78671bbd0a678ce0a4d0b0a8f86b0a63c7489d6288cc7636ad14bd1f28/mistralai-1.10.0-py3-none-any.whl", hash = "sha256:fd37d15f077375f77cbfbbb57abed6b2c6ae0a3db39cf4815400742441b3b60a", size = 460994, upload-time = "2025-12-17T09:34:49.214Z" }, +] + [[package]] name = "mistune" version = "3.1.4" @@ -5893,9 +6057,9 @@ name = "msal" version = "1.34.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography", marker = "python_full_version < '3.13'" }, - { name = "pyjwt", extra = ["crypto"], marker = "python_full_version < '3.13'" }, - { name = "requests", marker = "python_full_version < '3.13'" }, + { name = "cryptography" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cf/0e/c857c46d653e104019a84f22d4494f2119b4fe9f896c92b4b864b3b045cc/msal-1.34.0.tar.gz", hash = "sha256:76ba83b716ea5a6d75b0279c0ac353a0e05b820ca1f6682c0eb7f45190c43c2f", size = 153961, upload-time = "2025-09-22T23:05:48.989Z" } wheels = [ @@ -5907,13 +6071,45 @@ name = "msal-extensions" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "msal", marker = "python_full_version < '3.13'" }, + { name = "msal" }, ] sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315, upload-time = "2025-03-14T23:51:03.902Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583, upload-time = "2025-03-14T23:51:03.016Z" }, ] +[[package]] +name = "msgraph-core" +version = "1.3.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", extra = ["http2"] }, + { name = "microsoft-kiota-abstractions" }, + { name = "microsoft-kiota-authentication-azure" }, + { name = "microsoft-kiota-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/4e/123f9530ec43b306c597bb830c62bedab830ffa76e0edf33ea88a26f756e/msgraph_core-1.3.8.tar.gz", hash = "sha256:6e883f9d4c4ad57501234749e07b010478c1a5f19550ef4cf005bbcac4a63ae7", size = 25506, upload-time = "2025-09-11T22:46:57.267Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/4d/01432f60727ae452787014cad0d5bc9e035c6e11a670f12c23f7fc926d90/msgraph_core-1.3.8-py3-none-any.whl", hash = "sha256:86d83edcf62119946f201d13b7e857c947ef67addb088883940197081de85bea", size = 34473, upload-time = "2025-09-11T22:46:56.026Z" }, +] + +[[package]] +name = "msgraph-sdk" +version = "1.51.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-identity" }, + { name = "microsoft-kiota-serialization-form" }, + { name = "microsoft-kiota-serialization-json" }, + { name = "microsoft-kiota-serialization-multipart" }, + { name = "microsoft-kiota-serialization-text" }, + { name = "msgraph-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/d6/f7a310c9f284366ab317f32eb2d8d9b7d4ac0cabcda0e9821ac4704df898/msgraph_sdk-1.51.0.tar.gz", hash = "sha256:6f28ffabf587ac9f04c3603b2b1e876b52b417d86feea59d56b3f3c0371f457e", size = 6210564, upload-time = "2025-12-18T00:03:29.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/3f/59a5f396080d6908a0ecfabd1258cbc003f12f2ce1d060ce9adeca9fcf40/msgraph_sdk-1.51.0-py3-none-any.whl", hash = "sha256:014152eb001fcc5623f5729e967c75e39560f41438aaffb0e0633e938cf2f1fc", size = 25431026, upload-time = "2025-12-18T00:03:26.723Z" }, +] + [[package]] name = "multidict" version = "6.7.0" @@ -6582,7 +6778,7 @@ wheels = [ [[package]] name = "openai" -version = "2.13.0" +version = "2.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -6594,9 +6790,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/39/8e347e9fda125324d253084bb1b82407e5e3c7777a03dc398f79b2d95626/openai-2.13.0.tar.gz", hash = "sha256:9ff633b07a19469ec476b1e2b5b26c5ef700886524a7a72f65e6f0b5203142d5", size = 626583, upload-time = "2025-12-16T18:19:44.387Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/b1/12fe1c196bea326261718eb037307c1c1fe1dedc2d2d4de777df822e6238/openai-2.14.0.tar.gz", hash = "sha256:419357bedde9402d23bf8f2ee372fca1985a73348debba94bddff06f19459952", size = 626938, upload-time = "2025-12-19T03:28:45.742Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/d5/eb52edff49d3d5ea116e225538c118699ddeb7c29fa17ec28af14bc10033/openai-2.13.0-py3-none-any.whl", hash = "sha256:746521065fed68df2f9c2d85613bb50844343ea81f60009b60e6a600c9352c79", size = 1066837, upload-time = "2025-12-16T18:19:43.124Z" }, + { url = "https://files.pythonhosted.org/packages/27/4b/7c1a00c2c3fbd004253937f7520f692a9650767aa73894d7a34f0d65d3f4/openai-2.14.0-py3-none-any.whl", hash = "sha256:7ea40aca4ffc4c4a776e77679021b47eec1160e341f42ae086ba949c9dcc9183", size = 1067558, upload-time = "2025-12-19T03:28:43.727Z" }, ] [[package]] @@ -6694,157 +6890,533 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, ] +[[package]] +name = "opentelemetry-api" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "importlib-metadata", marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/d8/0f354c375628e048bd0570645b310797299754730079853095bf000fba69/opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12", size = 65242, upload-time = "2025-10-16T08:35:50.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/a2/d86e01c28300bd41bab8f18afd613676e2bd63515417b77636fc1add426f/opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582", size = 65947, upload-time = "2025-10-16T08:35:30.23Z" }, +] + [[package]] name = "opentelemetry-api" version = "1.39.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] dependencies = [ - { name = "importlib-metadata" }, - { name = "typing-extensions" }, + { name = "importlib-metadata", marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, ] +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "opentelemetry-proto", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, +] +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, upload-time = "2025-10-16T08:35:53.285Z" } +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, upload-time = "2025-10-16T08:35:34.099Z" }, +] + [[package]] name = "opentelemetry-exporter-otlp-proto-common" version = "1.39.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] dependencies = [ - { name = "opentelemetry-proto" }, + { name = "opentelemetry-proto", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409, upload-time = "2025-12-11T13:32:40.885Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde", size = 18366, upload-time = "2025-12-11T13:32:20.2Z" }, ] +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "googleapis-common-protos", marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "grpcio", marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-api", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-exporter-otlp-proto-common", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-proto", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-sdk", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, +] +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, upload-time = "2025-10-16T08:35:53.83Z" } +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, upload-time = "2025-10-16T08:35:35.053Z" }, +] + [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" version = "1.39.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] dependencies = [ - { name = "googleapis-common-protos" }, - { name = "grpcio" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-common" }, - { name = "opentelemetry-proto" }, - { name = "opentelemetry-sdk" }, - { name = "typing-extensions" }, + { name = "googleapis-common-protos", marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "grpcio", marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-api", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-exporter-otlp-proto-common", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-proto", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-sdk", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/53/48/b329fed2c610c2c32c9366d9dc597202c9d1e58e631c137ba15248d8850f/opentelemetry_exporter_otlp_proto_grpc-1.39.1.tar.gz", hash = "sha256:772eb1c9287485d625e4dbe9c879898e5253fea111d9181140f51291b5fec3ad", size = 24650, upload-time = "2025-12-11T13:32:41.429Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/81/a3/cc9b66575bd6597b98b886a2067eea2693408d2d5f39dad9ab7fc264f5f3/opentelemetry_exporter_otlp_proto_grpc-1.39.1-py3-none-any.whl", hash = "sha256:fa1c136a05c7e9b4c09f739469cbdb927ea20b34088ab1d959a849b5cc589c18", size = 19766, upload-time = "2025-12-11T13:32:21.027Z" }, ] +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "googleapis-common-protos", marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-api", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-exporter-otlp-proto-common", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-proto", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-sdk", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "requests", marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, +] +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, upload-time = "2025-10-16T08:35:54.422Z" } +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, upload-time = "2025-10-16T08:35:36.269Z" }, +] + [[package]] name = "opentelemetry-exporter-otlp-proto-http" version = "1.39.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] dependencies = [ - { name = "googleapis-common-protos" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-common" }, - { name = "opentelemetry-proto" }, - { name = "opentelemetry-sdk" }, - { name = "requests" }, - { name = "typing-extensions" }, + { name = "googleapis-common-protos", marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-api", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-exporter-otlp-proto-common", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-proto", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-sdk", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "requests", marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/80/04/2a08fa9c0214ae38880df01e8bfae12b067ec0793446578575e5080d6545/opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb", size = 17288, upload-time = "2025-12-11T13:32:42.029Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985", size = 19641, upload-time = "2025-12-11T13:32:22.248Z" }, ] +[[package]] +name = "opentelemetry-instrumentation" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "opentelemetry-api", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-semantic-conventions", version = "0.59b0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "packaging", marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "wrapt", marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/ed/9c65cd209407fd807fa05be03ee30f159bdac8d59e7ea16a8fe5a1601222/opentelemetry_instrumentation-0.59b0.tar.gz", hash = "sha256:6010f0faaacdaf7c4dff8aac84e226d23437b331dcda7e70367f6d73a7db1adc", size = 31544, upload-time = "2025-10-16T08:39:31.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/f5/7a40ff3f62bfe715dad2f633d7f1174ba1a7dd74254c15b2558b3401262a/opentelemetry_instrumentation-0.59b0-py3-none-any.whl", hash = "sha256:44082cc8fe56b0186e87ee8f7c17c327c4c2ce93bdbe86496e600985d74368ee", size = 33020, upload-time = "2025-10-16T08:38:31.463Z" }, +] + [[package]] name = "opentelemetry-instrumentation" version = "0.60b1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "packaging" }, - { name = "wrapt" }, + { name = "opentelemetry-api", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-semantic-conventions", version = "0.60b1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "packaging", marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "wrapt", marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706, upload-time = "2025-12-11T13:36:42.515Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096, upload-time = "2025-12-11T13:35:33.067Z" }, ] +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "asgiref", marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-api", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-instrumentation", version = "0.59b0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-semantic-conventions", version = "0.59b0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-util-http", version = "0.59b0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/a4/cfbb6fc1ec0aa9bf5a93f548e6a11ab3ac1956272f17e0d399aa2c1f85bc/opentelemetry_instrumentation_asgi-0.59b0.tar.gz", hash = "sha256:2509d6fe9fd829399ce3536e3a00426c7e3aa359fc1ed9ceee1628b56da40e7a", size = 25116, upload-time = "2025-10-16T08:39:36.092Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/88/fe02d809963b182aafbf5588685d7a05af8861379b0ec203d48e360d4502/opentelemetry_instrumentation_asgi-0.59b0-py3-none-any.whl", hash = "sha256:ba9703e09d2c33c52fa798171f344c8123488fcd45017887981df088452d3c53", size = 16797, upload-time = "2025-10-16T08:38:37.214Z" }, +] + [[package]] name = "opentelemetry-instrumentation-asgi" version = "0.60b1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] dependencies = [ - { name = "asgiref" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, + { name = "asgiref", marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-api", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-instrumentation", version = "0.60b1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-semantic-conventions", version = "0.60b1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-util-http", version = "0.60b1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/77/db/851fa88db7441da82d50bd80f2de5ee55213782e25dc858e04d0c9961d60/opentelemetry_instrumentation_asgi-0.60b1.tar.gz", hash = "sha256:16bfbe595cd24cda309a957456d0fc2523f41bc7b076d1f2d7e98a1ad9876d6f", size = 26107, upload-time = "2025-12-11T13:36:47.015Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/76/76/1fb94367cef64420d2171157a6b9509582873bd09a6afe08a78a8d1f59d9/opentelemetry_instrumentation_asgi-0.60b1-py3-none-any.whl", hash = "sha256:d48def2dbed10294c99cfcf41ebbd0c414d390a11773a41f472d20000fcddc25", size = 16933, upload-time = "2025-12-11T13:35:40.462Z" }, ] +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "opentelemetry-api", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-instrumentation", version = "0.59b0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-instrumentation-asgi", version = "0.59b0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-semantic-conventions", version = "0.59b0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-util-http", version = "0.59b0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/a7/7a6ce5009584ce97dbfd5ce77d4f9d9570147507363349d2cb705c402bcf/opentelemetry_instrumentation_fastapi-0.59b0.tar.gz", hash = "sha256:e8fe620cfcca96a7d634003df1bc36a42369dedcdd6893e13fb5903aeeb89b2b", size = 24967, upload-time = "2025-10-16T08:39:46.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/27/5914c8bf140ffc70eff153077e225997c7b054f0bf28e11b9ab91b63b18f/opentelemetry_instrumentation_fastapi-0.59b0-py3-none-any.whl", hash = "sha256:0d8d00ff7d25cca40a4b2356d1d40a8f001e0668f60c102f5aa6bb721d660c4f", size = 13492, upload-time = "2025-10-16T08:38:52.312Z" }, +] + [[package]] name = "opentelemetry-instrumentation-fastapi" version = "0.60b1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-instrumentation-asgi" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, + { name = "opentelemetry-api", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-instrumentation", version = "0.60b1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-instrumentation-asgi", version = "0.60b1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-semantic-conventions", version = "0.60b1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-util-http", version = "0.60b1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9c/e7/e7e5e50218cf488377209d85666b182fa2d4928bf52389411ceeee1b2b60/opentelemetry_instrumentation_fastapi-0.60b1.tar.gz", hash = "sha256:de608955f7ff8eecf35d056578346a5365015fd7d8623df9b1f08d1c74769c01", size = 24958, upload-time = "2025-12-11T13:36:59.35Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7d/cc/6e808328ba54662e50babdcab21138eae4250bc0fddf67d55526a615a2ca/opentelemetry_instrumentation_fastapi-0.60b1-py3-none-any.whl", hash = "sha256:af94b7a239ad1085fc3a820ecf069f67f579d7faf4c085aaa7bd9b64eafc8eaf", size = 13478, upload-time = "2025-12-11T13:36:00.811Z" }, ] +[[package]] +name = "opentelemetry-proto" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "protobuf", marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/14/f0c4f0f6371b9cb7f9fa9ee8918bfd59ac7040c7791f1e6da32a1839780d/opentelemetry_proto-1.38.0.tar.gz", hash = "sha256:88b161e89d9d372ce723da289b7da74c3a8354a8e5359992be813942969ed468", size = 46152, upload-time = "2025-10-16T08:36:01.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/6a/82b68b14efca5150b2632f3692d627afa76b77378c4999f2648979409528/opentelemetry_proto-1.38.0-py3-none-any.whl", hash = "sha256:b6ebe54d3217c42e45462e2a1ae28c3e2bf2ec5a5645236a490f55f45f1a0a18", size = 72535, upload-time = "2025-10-16T08:35:45.749Z" }, +] + [[package]] name = "opentelemetry-proto" version = "1.39.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] dependencies = [ - { name = "protobuf" }, + { name = "protobuf", marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535, upload-time = "2025-12-11T13:32:33.866Z" }, ] +[[package]] +name = "opentelemetry-sdk" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "opentelemetry-api", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "opentelemetry-semantic-conventions", version = "0.59b0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/cb/f0eee1445161faf4c9af3ba7b848cc22a50a3d3e2515051ad8628c35ff80/opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe", size = 171942, upload-time = "2025-10-16T08:36:02.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/2e/e93777a95d7d9c40d270a371392b6d6f1ff170c2a3cb32d6176741b5b723/opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b", size = 132349, upload-time = "2025-10-16T08:35:46.995Z" }, +] + [[package]] name = "opentelemetry-sdk" version = "1.39.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "typing-extensions" }, + { name = "opentelemetry-api", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "opentelemetry-semantic-conventions", version = "0.60b1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" }, ] +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "opentelemetry-api", version = "1.38.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.11' and python_full_version < '3.14') or (python_full_version >= '3.11' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/bc/8b9ad3802cd8ac6583a4eb7de7e5d7db004e89cb7efe7008f9c8a537ee75/opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0", size = 129861, upload-time = "2025-10-16T08:36:03.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/7d/c88d7b15ba8fe5c6b8f93be50fc11795e9fc05386c44afaf6b76fe191f9b/opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed", size = 207954, upload-time = "2025-10-16T08:35:48.054Z" }, +] + [[package]] name = "opentelemetry-semantic-conventions" version = "0.60b1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] dependencies = [ - { name = "opentelemetry-api" }, - { name = "typing-extensions" }, + { name = "opentelemetry-api", version = "1.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (python_full_version >= '3.14' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, ] +[[package]] +name = "opentelemetry-util-http" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +sdist = { url = "https://files.pythonhosted.org/packages/34/f7/13cd081e7851c42520ab0e96efb17ffbd901111a50b8252ec1e240664020/opentelemetry_util_http-0.59b0.tar.gz", hash = "sha256:ae66ee91be31938d832f3b4bc4eb8a911f6eddd38969c4a871b1230db2a0a560", size = 9412, upload-time = "2025-10-16T08:40:11.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/56/62282d1d4482061360449dacc990c89cad0fc810a2ed937b636300f55023/opentelemetry_util_http-0.59b0-py3-none-any.whl", hash = "sha256:6d036a07563bce87bf521839c0671b507a02a0d39d7ea61b88efa14c6e25355d", size = 7648, upload-time = "2025-10-16T08:39:25.706Z" }, +] + [[package]] name = "opentelemetry-util-http" version = "0.60b1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] sdist = { url = "https://files.pythonhosted.org/packages/50/fc/c47bb04a1d8a941a4061307e1eddfa331ed4d0ab13d8a9781e6db256940a/opentelemetry_util_http-0.60b1.tar.gz", hash = "sha256:0d97152ca8c8a41ced7172d29d3622a219317f74ae6bb3027cfbdcf22c3cc0d6", size = 11053, upload-time = "2025-12-11T13:37:25.115Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/16/5c/d3f1733665f7cd582ef0842fb1d2ed0bc1fba10875160593342d22bba375/opentelemetry_util_http-0.60b1-py3-none-any.whl", hash = "sha256:66381ba28550c91bee14dcba8979ace443444af1ed609226634596b4b0faf199", size = 8947, upload-time = "2025-12-11T13:36:37.151Z" }, @@ -10011,11 +10583,11 @@ wheels = [ [[package]] name = "soupsieve" -version = "2.8" +version = "2.8.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" } +sdist = { url = "https://files.pythonhosted.org/packages/89/23/adf3796d740536d63a6fbda113d07e60c734b6ed5d3058d1e47fc0495e47/soupsieve-2.8.1.tar.gz", hash = "sha256:4cf733bc50fa805f5df4b8ef4740fc0e0fa6218cf3006269afd3f9d6d80fd350", size = 117856, upload-time = "2025-12-18T13:50:34.655Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, + { url = "https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl", hash = "sha256:a11fe2a6f3d76ab3cf2de04eb339c1be5b506a8a47f2ceb6d139803177f85434", size = 36710, upload-time = "2025-12-18T13:50:33.267Z" }, ] [[package]] @@ -10214,11 +10786,11 @@ wheels = [ [[package]] name = "sqlglot" -version = "28.4.1" +version = "28.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/84/4ad36c983e054ff6582f7c042ae6c560064201d366cb8ccd71914a89cc38/sqlglot-28.4.1.tar.gz", hash = "sha256:16de5a1708e79aab4e252d55dd1b0762511ff28d4ef65ecc8f2383b963401633", size = 5651480, upload-time = "2025-12-16T22:19:52.601Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/8c/a4d24b6103305467506c1dea9c3ca8dc92773a91bae246c2517c256a0cf9/sqlglot-28.5.0.tar.gz", hash = "sha256:b3213b3e867dcc306074f1c90480aeee89a0e635cf0dfe70eb4a3af7b61972e6", size = 5652688, upload-time = "2025-12-17T23:38:00.121Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/bb/dfa9d01fff2c1e955efdda1800e65fefbf517bd2be8621e2c0a6f6c80569/sqlglot-28.4.1-py3-none-any.whl", hash = "sha256:e62083b3835054b72e20b554bf46fbb5190734ed9573372f52654023f082bc34", size = 560091, upload-time = "2025-12-16T22:19:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f7/6a7effd2526f64bcb0d2264c0dbebc7f8508add3f2c0748540d1448a24a3/sqlglot-28.5.0-py3-none-any.whl", hash = "sha256:5798bfdb6e9bc36c964e6c64d7222624d98b2631cc20f44628a82eba7cf7b4bf", size = 561086, upload-time = "2025-12-17T23:37:57.972Z" }, ] [[package]] @@ -10321,6 +10893,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/33/f1652d0c59fa51de18492ee2345b65372550501ad061daa38f950be390b6/statsmodels-0.14.6-cp314-cp314-win_amd64.whl", hash = "sha256:151b73e29f01fe619dbce7f66d61a356e9d1fe5e906529b78807df9189c37721", size = 9588010, upload-time = "2025-12-05T23:14:07.28Z" }, ] +[[package]] +name = "std-uritemplate" +version = "2.0.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/93/62/61866776cd32df3f984ff2f79b1428e10700e0a33ca7a7536e3fcba3cf2a/std_uritemplate-2.0.8.tar.gz", hash = "sha256:138ceff2c5bfef18a650372a5e8c82fe7f780c87235513de6c342fb5f7e18347", size = 6018, upload-time = "2025-10-16T15:51:29.774Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/97/b4f2f442fee92a1406f08b4fbc990bd7d02dc84b3b5e6315a59fa9b2a9f4/std_uritemplate-2.0.8-py3-none-any.whl", hash = "sha256:839807a7f9d07f0bad1a88977c3428bd97b9ff0d229412a0bf36123d8c724257", size = 6512, upload-time = "2025-10-16T15:51:28.713Z" }, +] + [[package]] name = "stem" version = "1.8.2" @@ -11226,7 +11807,7 @@ wheels = [ [[package]] name = "weaviate-client" -version = "4.18.3" +version = "4.19.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "authlib" }, @@ -11237,9 +11818,9 @@ dependencies = [ { name = "pydantic" }, { name = "validators" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/76/14e07761c5fb7e8573e3cff562e2d9073c65f266db0e67511403d10435b1/weaviate_client-4.18.3.tar.gz", hash = "sha256:9d889246d62be36641a7f2b8cedf5fb665b804d46f7a53ae37e02d297a11f119", size = 783634, upload-time = "2025-12-03T09:38:28.261Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/3e/2ab8f5c2b59851da4c42b8f13e9c517f511e9f3e398b2b7364710158860a/weaviate_client-4.19.0.tar.gz", hash = "sha256:ca92b952c656f1f27a28907caad7aa557e17c576ee05354935901aebb350d62f", size = 787060, upload-time = "2025-12-18T15:27:32.688Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/ab/f1c2bef56199505bcd07a6747e7705d84f2d40f20c757237323d13d219d0/weaviate_client-4.18.3-py3-none-any.whl", hash = "sha256:fc6ef510dd7b63ab0b673a35a7de9573abbd0626fc80de54633f0ccfd52772b7", size = 599877, upload-time = "2025-12-03T09:38:26.487Z" }, + { url = "https://files.pythonhosted.org/packages/68/1e/2477c185f3721e6b7769c6f6edaaa5fb1464da6cd92ef5658f128273aec6/weaviate_client-4.19.0-py3-none-any.whl", hash = "sha256:9f8ca062b8d4fce67bf7d208a16a201c93e8d087fc5324e7540c90f879768b4c", size = 603522, upload-time = "2025-12-18T15:27:31.048Z" }, ] [[package]]