diff --git a/chatbees.ts b/chatbees.ts new file mode 100644 index 0000000..682314c --- /dev/null +++ b/chatbees.ts @@ -0,0 +1,479 @@ +import { redirect } from "next/navigation"; + + +//const baseurl = '.preprod.aws.chatbees.ai' +const baseurl = + process.env.NEXT_PUBLIC_CHATBEES_BASEURL ?? ".us-west-2.aws.chatbees.ai"; + +export function getServiceUrl(aid: string): string { + if (baseurl == "localhost") { + return "http://localhost:8080"; + } + return "https://" + aid + baseurl; +} + +export function getHeaders( + aid: string, + apiKey: string, + upload: boolean = false, +): { [key: string]: string } { + let headers: { [key: string]: string } = { + "Content-Type": "application/json", + "api-key": apiKey, + }; + if (baseurl == "localhost" || window.location.origin.includes("localhost")) { + headers["x-org-url"] = aid; + } + + if (upload) { + delete headers["Content-Type"]; + } + return headers; +} + +function redirectToLogin( + reason: string = "Session expired, please sign-in again", +) { + const reasonStr = reason == "" ? "" : `?why=${reason}`; + if (typeof window !== "undefined") { + // Client redirect + window.location.href = `/auth/signin${reasonStr}`; + } else { + // Server redirect + redirect(`/auth/signin${reasonStr}`); + } +} + +async function fetch_url( + aid: string, + shortliveApiKey: string, + url_suffix: string, + body: BodyInit, +): Promise { + if (aid == "") { + throw new Error("Account ID not found", { cause: 404 }); + } + + const url: string = getServiceUrl(aid) + url_suffix; + + return await do_fetch_url(aid, shortliveApiKey, url, body); +} + +async function do_fetch_url( + aid: string, + shortliveApiKey: string, + url: string, + body: BodyInit, +): Promise { + const headers: HeadersInit = getHeaders(aid, shortliveApiKey); + + try { + const response = await fetch(url, { + method: "POST", + headers: headers, + body: body, + }); + + if (response.ok) { + return await response.json(); + } else if (response.status != 401) { + throw new Error( + `status: ${response.status}, error: ${response.statusText}`, + { cause: response.status }, + ); + } + + // 401 errors bypass error check. We'll redirect users to login page + } catch (error) { + console.log("Caught error ", error); + throw error; + } + + // If try/catch block did not return or throw, redirect to login (we got 401) + redirectToLogin(); +} + +// Function to process errors +export function processError( + error: Error, + prefix: string, + redirect_href: string = "", +): void { + // Handle 401s first + if (error.message.startsWith("status: 401") || error.cause == 401) { + redirectToLogin(); + } + + console.error(`${prefix} error: ${error.message}`); + if (typeof window !== "undefined") { + //window.alert(`${prefix} error: ${error.message}`); + if (redirect_href !== "") { + console.log("redirect_href", redirect_href); + window.location.href = redirect_href; + } + } + // Non-browser env. TODO how to return error? or redirect? +} + + +// Collection related APIs + +export type Collection = { + name: string; + description?: string; + publicRead?: boolean; + persona?: string; + negativeResponse?: string; +}; + +export interface CollectionDescription { + collection: Collection; +}; + +export async function CreateChatBeesCollection( + aid: string, + apiKey: string, + collectionName: string, +): Promise { + const url_suffix = "/collections/create"; + const json_body = JSON.stringify({ + collection_name: collectionName, + namespace_name: "public", + public_read: false, + }); + + return await fetch_url(aid, apiKey, url_suffix, json_body); +} + +export async function DeleteCollection( + aid: string, + apiKey: string, + collectionName: string, +): Promise { + const url_suffix = "/collections/delete"; + const json_body = JSON.stringify({ + collection_name: collectionName, + namespace_name: "public", + }); + + return await fetch_url(aid, apiKey, url_suffix, json_body); +} + +export async function ListCollections( + aid: string, + apiKey: string, +): Promise<{ collections: Collection[] }> { + const url_suffix = "/collections/list"; + const json_body = JSON.stringify({ namespace_name: "public" }); + + const data = await fetch_url(aid, apiKey, url_suffix, json_body); + if (data == null) { + return { collections: [] }; + } + let collections: Collection[] = []; + for (let colName of data["names"]) { + collections.push({ name: colName }); + } + + return { collections: collections }; +} + +export async function DescribeCollection( + aid: string, + apiKey: string, + collectionName: string, +): Promise { + const url_suffix = "/collections/describe"; + const json_body = JSON.stringify({ + namespace_name: "public", + collection_name: collectionName, + }); + + const data = await fetch_url(aid, apiKey, url_suffix, json_body); + /** + * { + * "description": null, + * "chat_attributes": null, + * "public_read": null, + * } + **/ + + let persona: string = "You are an AI assistant."; + let negativeResponse: string = "I'm sorry, I don't have relevant information to answer your question. If you have any other question, feel free to ask!"; + if (data["chat_attributes"] != null) { + if (data["chat_attributes"]["persona"] != null) { + persona = data["chat_attributes"]["persona"]; + } + if (data["chat_attributes"]["negative_response"] != null) { + negativeResponse = data["chat_attributes"]["negative_response"]; + } + } + + return { + collection: { + name: collectionName, + description: data["description"] ?? "", + publicRead: data["public_read"] ?? false, + persona: persona, + negativeResponse: negativeResponse, + }, + }; +} + +export async function ConfigureChat( + aid: string, + apiKey: string, + collectionName: string, + persona: string | null, + negativeResponse: string | null, +): Promise { + const url_suffix = "/docs/configure_chat"; + const json_body = JSON.stringify({ + namespace_name: "public", + collection_name: collectionName, + chat_attributes: { + persona: persona, + negative_response: negativeResponse, + }, + }); + + return await fetch_url(aid, apiKey, url_suffix, json_body); +} + +export async function ShareOrUnshareCollection( + aid: string, + apiKey: string, + collectionName: string, + publicRead: boolean, +): Promise { + const url_suffix = "/collections/configure"; + const json_body = JSON.stringify({ + namespace_name: "public", + collection_name: collectionName, + public_read: publicRead, + }); + + return await fetch_url(aid, apiKey, url_suffix, json_body); +} + + +// Document related APIs + +export async function DeleteDocument( + aid: string, + apiKey: string, + collectionName: string, + fileName: string, +): Promise { + const url_suffix = "/docs/delete"; + const json_body = JSON.stringify({ + namespace_name: "public", + collection_name: collectionName, + doc_name: fileName, + }); + + return await fetch_url(aid, apiKey, url_suffix, json_body); +} + +export async function ListDocFiles( + aid: string, + apiKey: string, + collectionName: string, +): Promise<{ docs: string[] }> { + const url_suffix = "/docs/list"; + const json_body = JSON.stringify({ + namespace_name: "public", + collection_name: collectionName, + }); + + const data = await fetch_url(aid, apiKey, url_suffix, json_body); + if (data == null) { + return { docs: [] }; + } + + let docs: Document[] = []; + for (let doc of data["documents"]) { + docs.push(doc["name"]); + } + + return { docs: docs }; +} + +export type AnswerRefs = { + docName: string; + pageNum: number; + sampleText: string; +}; + +export async function Ask( + aid: string, + apiKey: string, + collectionName: string, + question: string, + historyMessages: string[][], + conversation_id: string | null, +): Promise<{ + answer: string; + refs: AnswerRefs[]; + conversation_id: string; + request_id: string; +}> { + const url_suffix = "/docs/ask"; + const json_body = + historyMessages.length == 0 + ? JSON.stringify({ + collection_name: collectionName, + namespace_name: "public", + question: question, + conversation_id: conversation_id, + }) + : JSON.stringify({ + collection_name: collectionName, + namespace_name: "public", + question: question, + history_messages: historyMessages, + conversation_id: conversation_id, + }); + + const respData = await fetch_url(aid, apiKey, url_suffix, json_body); + const answer = respData["answer"]; + let refs: AnswerRefs[] = []; + + for (let ref of respData["refs"]) { + refs.push({ + docName: ref["doc_name"], + pageNum: ref["page_num"], + sampleText: ref["sample_text"], + }); + } + + return { + answer: answer, + refs: refs, + conversation_id: respData["conversation_id"], + request_id: respData["request_id"], + }; +} + + +// APIKey related APIs + +export type ApiKey = { + name: string; + value: string; +}; + +export async function CreateChatBeesApiKey( + aid: string, + apiKey: string, + apiKeyName: string, +): Promise { + const url_suffix = "/apikey/create"; + const json_body = JSON.stringify({ name: apiKeyName }); + + return await fetch_url(aid, apiKey, url_suffix, json_body); +} + +export async function DeleteChatBeesApiKey( + aid: string, + apiKey: string, + apiKeyName: string, +): Promise { + const url_suffix = "/apikey/delete"; + const json_body = JSON.stringify({ name: apiKeyName }); + + return await fetch_url(aid, apiKey, url_suffix, json_body); +} + +export async function ListApiKeys( + aid: string, + apiKey: string, +): Promise<{ apiKeys: ApiKey[] }> { + const url_suffix = "/apikey/list"; + const json_body = JSON.stringify({}); + + const data = await fetch_url(aid, apiKey, url_suffix, json_body); + + let apiKeys: ApiKey[] = []; + for (let apikey of data["api_keys"]) { + apiKeys.push({ name: apikey["name"], value: apikey["masked_api_key"] }); + } + + return { apiKeys: apiKeys }; +} + + +// Application related APIs + +export enum ApplicationType { + COLLECTION = "COLLECTION", // RAG chat + GPT = "GPT", // directly talk to model +} + +export interface CollectionTarget { + namespace_name: string; + collection_name: string; +} + +export interface GPTTarget { + provider: string; + model: string; +} + +export interface Application { + application_name: string; + application_type: ApplicationType; + + // Application target could be one of the supported targets. + application_target: CollectionTarget | GPTTarget; +} + +export async function CreateApplication( + aid: string, + apiKey: string, + application: Application, +): Promise { + const url_suffix = "/applications/create"; + const json_body = JSON.stringify({ application: application }); + + return await fetch_url(aid, apiKey, url_suffix, json_body); +} + +export async function DeleteApplication( + aid: string, + apiKey: string, + applicationName: string, +): Promise { + const url_suffix = "/applications/delete"; + const json_body = JSON.stringify({ application_name: applicationName }); + + return await fetch_url(aid, apiKey, url_suffix, json_body); +} + +export async function ListApplications( + aid: string, + apiKey: string, +): Promise<{ applications: Application[] }> { + const url_suffix = "/applications/list"; + const json_body = JSON.stringify({}); + + const data = await fetch_url(aid, apiKey, url_suffix, json_body); + return data; +} + + +export function ValidateName(input: string): boolean { + // Regular expression pattern to match the criteria + const pattern = /^[a-zA-Z_][a-zA-Z0-9_-]{0,254}$/; + + // Check if the input matches the pattern + return pattern.test(input); +} + +export function ValidateEmail(input: string): boolean { + // Regular expression pattern to match the criteria + const pattern = /^.+@.+\..+$/; + + // Check if the input matches the pattern + return pattern.test(input); +} diff --git a/chatbees/__init__.py b/chatbees/__init__.py index f056b66..4b5fcb9 100644 --- a/chatbees/__init__.py +++ b/chatbees/__init__.py @@ -1,5 +1,6 @@ from .client.collection_management import * from .client.admin_management import * +from .client.application_management import * from .client_models.collection import * from .client_models.chat import * from .server_models.doc_api import * diff --git a/chatbees/client/admin_management.py b/chatbees/client/admin_management.py index a050a7f..942e8bf 100644 --- a/chatbees/client/admin_management.py +++ b/chatbees/client/admin_management.py @@ -1,5 +1,9 @@ from typing import List -from chatbees.server_models.admin_api import CreateApiKeyRequest, CreateApiKeyResponse +from chatbees.server_models.admin_api import ( + CreateApiKeyRequest, + CreateApiKeyResponse, + EmailAccountRequest, AccountLoginResponse, +) from chatbees.server_models.ingestion_api import ( ConnectorReference, ListConnectorsRequest, @@ -7,7 +11,7 @@ ) from chatbees.utils.config import Config -__all__ = ["init", "list_connectors"] +__all__ = ["init", "list_connectors", "email_login"] def init( @@ -30,6 +34,33 @@ def init( Config.namespace = namespace Config.validate_setup() +def email_login( + email: str, + password: str, +): + """ + Initialize the ChatBees client. + + Args: + api_key (str): The API key to authenticate requests. + account_id (str): The account ID. + namespace (str, optional): The namespace to use. + Raises: + ValueError: If the provided config is invalid + """ + url = f"{Config.get_base_url()}/account/email" + req = EmailAccountRequest(email=email, password=password) + resp = Config.post( + url=url, + data=req.model_dump_json(), + enforce_api_key=False, + ) + resp = AccountLoginResponse.model_validate(resp.json()) + + Config.api_key = resp.shortlive_api_key + Config.account_id = resp.account_id + Config.namespace = resp.default_namespace + Config.validate_setup() def list_connectors() -> List[ConnectorReference]: url = f'{Config.get_base_url()}/connectors/list' diff --git a/chatbees/client/application_management.py b/chatbees/client/application_management.py new file mode 100644 index 0000000..3445166 --- /dev/null +++ b/chatbees/client/application_management.py @@ -0,0 +1,85 @@ +__all__ = ["create_gpt_application", "create_collection_application", "delete_application", "list_applications"] + +from typing import Optional, List + +from chatbees.server_models.application import ( + Application, + ApplicationType, + CollectionTarget, + GPTTarget, +) +from chatbees.server_models.application_api import ( + CreateApplicationRequest, + DeleteApplicationRequest, ListApplicationsResponse, ListApplicationsRequest, +) +from chatbees.server_models.collection_api import ChatAttributes +from chatbees.utils.config import Config + +def create_gpt_application( + application_name: str, + provider: str, + model: str, + description: Optional[str] = None, + chat_attrs: Optional[ChatAttributes] = None +) -> Application: + """ + Create a new collection application in ChatBees. + + """ + url = f'{Config.get_base_url()}/applications/create' + application = Application( + application_name=application_name, + application_desc=description, + application_type=ApplicationType.GPT, + chat_attrs=chat_attrs, + application_target=GPTTarget( + provider=provider, model=model).model_dump_json()) + req = CreateApplicationRequest(namespace_name=Config.namespace, application=application) + Config.post(url=url, data=req.model_dump_json()) + return application + +def create_collection_application( + application_name: str, + collection_name: str, + description: Optional[str] = None, + chat_attrs: Optional[ChatAttributes] = None +) -> Application: + """ + Create a new collection application in ChatBees. + + """ + url = f'{Config.get_base_url()}/applications/create' + application = Application( + application_name=application_name, + application_desc=description, + application_type=ApplicationType.COLLECTION, + chat_attrs=chat_attrs, + application_target=CollectionTarget(collection_name=collection_name).model_dump_json()) + req = CreateApplicationRequest(namespace_name=Config.namespace, application=application) + Config.post(url=url, data=req.model_dump_json()) + return application + + +def delete_application(application_name: str): + """ + Deletes an application + + Args: + application_name (str): The name of the application. + """ + url = f'{Config.get_base_url()}/applications/delete' + req = DeleteApplicationRequest(namespace_name=Config.namespace, application_name=application_name) + Config.post(url=url, data=req.model_dump_json()) + + +def list_applications() -> List[Application]: + """ + List all applications in account. + + Returns: + List[Application]: A list of application objects. + """ + url = f'{Config.get_base_url()}/applications/list' + req = ListApplicationsRequest(namespace_name=Config.namespace) + resp = Config.post(url=url, data=req.model_dump_json()) + return ListApplicationsResponse.model_validate(resp.json()).applications diff --git a/chatbees/client_models/chat.py b/chatbees/client_models/chat.py index ae39014..2e82767 100644 --- a/chatbees/client_models/chat.py +++ b/chatbees/client_models/chat.py @@ -2,8 +2,13 @@ from pydantic import BaseModel +from chatbees.server_models.conversation import ( + ConversationMeta, + ListConversationsRequest, + ListConversationsResponse, GetConversationRequest, GetConversationResponse, +) from chatbees.server_models.doc_api import AskResponse -from chatbees.utils.ask import ask +from chatbees.utils.ask import ask, ask_application from chatbees.utils.config import Config __all__ = ["Chat"] @@ -13,22 +18,82 @@ class Chat(BaseModel): """ A new chatbot instance that supports conversational Q and A. """ - namespace_name: str - collection_name: str + namespace_name: Optional[str] = None + collection_name: Optional[str] = None + application_name: Optional[str] = None doc_name: Optional[str] = None history_messages: Optional[List[Tuple[str, str]]] = None conversation_id: Optional[str] = None - def ask(self, question: str, top_k: int = 5) -> AskResponse: - resp = ask( - Config.namespace, - self.collection_name, - question, - top_k, - doc_name=self.doc_name, - history_messages=self.history_messages, - conversation_id=self.conversation_id, + def _validate_names(self): + if self.application_name is not None and self.namespace_name is None and self.collection_name is None: + return + if self.application_name is None and self.namespace_name is not None and self.collection_name is not None: + return + raise ValueError(f"Chat must specify either both namespace and collection, or application") + + @classmethod + def list_conversations(cls, collection_name=None, application_name=None) -> List[ConversationMeta]: + url = f'{Config.get_base_url()}/conversations/list' + namespace = Config.namespace if collection_name is not None else None + req = ListConversationsRequest( + namespace_name=namespace, + collection_name=collection_name, + application_name=application_name + ) + resp = Config.post( + url=url, + data=req.model_dump_json(), + ) + return ListConversationsResponse.model_validate(resp.json()).conversations + + @classmethod + def from_conversation(cls, conversation_id, collection_name=None, application_name=None) -> "Chat": + url = f'{Config.get_base_url()}/conversations/get' + req = GetConversationRequest( + conversation_id=conversation_id, + ) + resp = Config.post( + url=url, + data=req.model_dump_json(), ) + chat = Chat() + convos = GetConversationResponse.model_validate(resp.json()).conversation + chat.conversation_id = conversation_id + chat.collection_name = collection_name + chat.application_name = application_name + + # TODO: Fix this convoluted logic...openai accepts a plain array of + # {role, content}. Simply preserve this and pass it over to openai + paired_content = [(convos.messages[i].content, convos.messages[i + 1].content) for i in + range(0, len(convos.messages) - 1, 2)] + chat.history_messages = paired_content + return chat + + + def ask(self, question: str, top_k: int = 5) -> AskResponse: + self._validate_names() + + if self.application_name is None: + resp = ask( + Config.namespace, + self.collection_name, + question, + top_k, + doc_name=self.doc_name, + history_messages=self.history_messages, + conversation_id=self.conversation_id, + ) + else: + resp = ask_application( + self.application_name, + question, + top_k, + doc_name=self.doc_name, + history_messages=self.history_messages, + conversation_id=self.conversation_id, + ) + if self.history_messages is None: self.history_messages = [] self.history_messages.append((question, resp.answer)) diff --git a/chatbees/client_models/collection.py b/chatbees/client_models/collection.py index 17a71e1..c95cd2f 100644 --- a/chatbees/client_models/collection.py +++ b/chatbees/client_models/collection.py @@ -1,3 +1,4 @@ +import logging import os from typing import List, Dict, Tuple, Any, Union, Optional from urllib import request @@ -8,7 +9,7 @@ from chatbees.server_models.doc_api import ( CrawlStatus, AskResponse, - SearchReference, + SearchReference, GetDocRequest, AsyncAddDocResponse, PendingDocumentMetadata, ) from chatbees.server_models.chat import ConfigureChatRequest from chatbees.server_models.ingestion_type import ( @@ -90,6 +91,30 @@ class Collection(BaseModel): periodic_ingests: Optional[List[PeriodicIngest]] = None + + def download_document(self, doc_name: str, save_path = '.'): + """ + Uploads a local or web document into this collection. + + :param path_or_url: Local file path or the URL of a document. URL must + contain scheme (http or https) prefix. + :return: + """ + url = f'{Config.get_base_url()}/docs/get' + req = GetDocRequest(namespace_name=Config.namespace, + collection_name=self.name, doc_name=doc_name) + + response = Config.post(url=url, data=req.model_dump_json()) + response.raise_for_status() + print(f'GET file response is {response}') + + # Write the file to the local save path + filename = os.path.join(save_path, doc_name) + with open(filename, 'wb') as file: + file.write(response.content) + print(f"Document '{doc_name}' downloaded successfully to '{save_path}'.") + return filename + def upload_document(self, path_or_url: str): """ Uploads a local or web document into this collection. @@ -118,6 +143,41 @@ def upload_document(self, path_or_url: str): url=url, files={'file': (fname, f)}, data={'request': req.model_dump_json()}) + def upload_documents_async(self, path_or_urls: List[str]) -> List[str]: + """ + Uploads a local or web document into this collection. + + :param path_or_urls: Local file path or the URL of a document. URL must + contain scheme (http or https) prefix. + :return: + """ + url = f'{Config.get_base_url()}/docs/add_async' + req = AddDocRequest(namespace_name=Config.namespace, + collection_name=self.name) + #files = [('files', open(f"{current_dir}/{fname}", "rb")) for fname in fnames] + files = [] + + for path_or_url in path_or_urls: + if is_url(path_or_url): + validate_url_file(path_or_url) + files.append(('files', request.urlopen(path_or_url) )) + else: + # Handle tilde "~/blah" + path_or_url = os.path.expanduser(path_or_url) + validate_file(path_or_url) + files.append(('files', open(path_or_url, 'rb'))) + resp = Config.post( + url=url, files=files, + data={'request': req.model_dump_json()}) + for _, file in files: + try: + file.close() + logging.info(f"Closed file") + except Exception: + pass + add_resp = AsyncAddDocResponse.model_validate(resp.json()) + return add_resp.task_ids + def delete_document(self, doc_name: str): """ Deletes the document. @@ -147,6 +207,21 @@ def list_documents(self) -> List[str]: list_resp = ListDocsResponse.model_validate(resp.json()) return list_resp.doc_names + def list_pending_documents(self) -> List[PendingDocumentMetadata]: + """ + List the documents. + + :return: A list of the documents + """ + url = f'{Config.get_base_url()}/docs/list' + req = ListDocsRequest( + namespace_name=Config.namespace, + collection_name=self.name, + ) + resp = Config.post(url=url, data=req.model_dump_json()) + list_resp = ListDocsResponse.model_validate(resp.json()) + return list_resp.pending_documents + def summarize_document(self, doc_name: str) -> str: """ Returns a summary of the document. diff --git a/chatbees/server_models/admin_api.py b/chatbees/server_models/admin_api.py index b8aa923..395a340 100644 --- a/chatbees/server_models/admin_api.py +++ b/chatbees/server_models/admin_api.py @@ -1,3 +1,6 @@ +from enum import Enum +from typing import Optional, List + from pydantic import BaseModel @@ -7,3 +10,53 @@ class CreateApiKeyRequest(BaseModel): class CreateApiKeyResponse(BaseModel): api_key: str + +class RoleType(str, Enum): + # Built-in roles with fixed permissions + AccountAdmin = "AccountAdmin" + AccountUser = "AccountUser" + +class Names(BaseModel): + name: str + given_name: Optional[str] = None + middle_name: Optional[str] = None + family_name: Optional[str] = None + +class UserMetadata(BaseModel): + # User creation time in nanoseconds since epoch + ctime: int + email: str + names: Optional[Names] = None + +class ClientUser(BaseModel): + """ + Client-facing user model + + ID corresponds to "email" filed in the internal user model + """ + id: str + + # System role + role: RoleType + + # Custom roles + custom_roles: List[str] = [] + metadata: UserMetadata + +class AccountLoginResponse(BaseModel): + # the unique account id. The rest requests for the account will be like + # account_id.us-west-2.aws.chatbees.ai + account_id: str + # a short-live api key that UI can use to send the rest requests. + shortlive_api_key: str + # Stripe customer ID + stripe_customer_id: str + # user info + user: Optional[ClientUser] = None + # Default namespace + default_namespace: str + +class EmailAccountRequest(BaseModel): + email: str + password: str + diff --git a/chatbees/server_models/application.py b/chatbees/server_models/application.py new file mode 100644 index 0000000..0567793 --- /dev/null +++ b/chatbees/server_models/application.py @@ -0,0 +1,61 @@ +import enum +from typing import Optional, Set + +from pydantic import BaseModel + +from chatbees.server_models.collection_api import ChatAttributes + + +class ApplicationType(str, enum.Enum): + COLLECTION = 'COLLECTION' + GPT = 'GPT' + + +class CollectionTarget(BaseModel): + collection_name: str + + +class GPTTarget(BaseModel): + provider: str + model: str + +class Application(BaseModel): + application_name: str + + application_type: ApplicationType + + # Application target could be one of the supported targets + application_target: str + + application_desc: Optional[str] = None + + # Chat-related attributes + chat_attrs: Optional[ChatAttributes] = None + + """ + System-assigned fields below + """ + + # User ID of owner + owner: str = '' + + # Whether this application is enabled. + # An application is automatically disabled if application_target becomes + # invalid + enabled: bool = True + + # Server-assigned ID for this application + application_id: str = '' + + # Whether this application is public + public: bool = False + + # Whether this application is a shared publication + shared_roles: Set[str] = set() + + created_on_ms: int = 0 + + # This is the last-access timestamp. + # Different users will see different timestamps, based on their own access history + last_accessed_ms: int = 0 + diff --git a/chatbees/server_models/application_api.py b/chatbees/server_models/application_api.py new file mode 100644 index 0000000..f1920ef --- /dev/null +++ b/chatbees/server_models/application_api.py @@ -0,0 +1,32 @@ +import enum +from typing import List, Set, Optional + +from pydantic import BaseModel + +from chatbees.server_models.application import Application + + +class CreateApplicationRequest(BaseModel): + namespace_name: str + application: Application + +class ListApplicationsRequest(BaseModel): + namespace_name: str + +class ListApplicationsResponse(BaseModel): + applications: List[Application] + + +class DeleteApplicationRequest(BaseModel): + namespace_name: str + application_name: str + +class ShareApplicationRequest(BaseModel): + namespace_name: str + application_name: str + + # Roles to share, unshare and public setting + roles_to_share: Set[str] = set() + roles_to_unshare: Set[str] = set() + public: Optional[bool] = None + diff --git a/chatbees/server_models/collection_api.py b/chatbees/server_models/collection_api.py index 1f95623..04858e3 100644 --- a/chatbees/server_models/collection_api.py +++ b/chatbees/server_models/collection_api.py @@ -5,8 +5,10 @@ class CollectionBaseRequest(BaseModel): - namespace_name: str - collection_name: str + namespace_name: Optional[str] = None + collection_name: Optional[str] = None + application_name: Optional[str] = None + class CreateCollectionRequest(CollectionBaseRequest): @@ -35,11 +37,13 @@ class ListCollectionsResponse(BaseModel): class ChatAttributes(BaseModel): - # Configure chatbot role, personality and style. For example: + """ + Chat config + """ + # Configure chatbot personality and style. For example: # - # - You are an AI assistant. You will talk like a 1600s pirate. - # - You are an AI assistant. - # - You are an AI customer support agent. + # - a 1600s pirate, your name is 'Capital Morgan'. + # - a helpful AI assistant persona: Optional[str] = None # Configure chatbot response when no relevant result is found. For example: @@ -47,6 +51,24 @@ class ChatAttributes(BaseModel): # - I do not have that information. negative_response: Optional[str] = None + # Welcome message a user sees when starting a conversation. + welcome_msg: Optional[str] = None + + """ + Model config + """ + # Configures whether the chatbot gives conservative or creative answers. + # Must be between 0 and 1, inclusive of both ends. + temperature: Optional[float] = None + + # TODO: Implement these model configs + top_p: Optional[float] = None + # [-2.0, 2.0] + presence_penalty: Optional[float] = None + # [-2.0, 2.0] + frequency_penalty: Optional[float] = None + max_completion_tokens: Optional[int] = None + class PeriodicIngest(BaseModel): type: IngestionType diff --git a/chatbees/server_models/conversation.py b/chatbees/server_models/conversation.py new file mode 100644 index 0000000..d1840ca --- /dev/null +++ b/chatbees/server_models/conversation.py @@ -0,0 +1,58 @@ +from typing import List + +from pydantic import BaseModel + +from chatbees.server_models.collection_api import CollectionBaseRequest + + +# OpenAI: https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command +# Llama3:https://www.llama.com/docs/model-cards-and-prompt-formats/llama3_1/#prompt-template +class Message(BaseModel): + timestamp: int + request_id: str + + # Openai: user, assistant, tool etc + # Llama: system, user, assistant, ipython, etc + role: str + content: str + + +class ConversationMeta(BaseModel): + conversation_id: str + title: str + start_ts: int + + # Source of the conversation. + # e.g. clid, app_id, clid/doc_id + source_id: str + +class Conversation(BaseModel): + meta: ConversationMeta + messages: List[Message] + + def append(self, messages: List[Message]): + # In most cases, we'll be inserting 2 messages at the end, so + # efficiency does not matter + for msg in messages: + insert_index = len(self.messages) + + # Ensure messages are in chronological order + while insert_index > 0 and msg.timestamp < self.messages[insert_index - 1].timestamp: + insert_index -= 1 + + self.messages.insert(insert_index, msg) + +class ListConversationsRequest(CollectionBaseRequest): + pass + + +class ListConversationsResponse(BaseModel): + conversations: List[ConversationMeta] + + +class GetConversationRequest(BaseModel): + conversation_id: str + + +class GetConversationResponse(BaseModel): + conversation: Conversation diff --git a/chatbees/server_models/doc_api.py b/chatbees/server_models/doc_api.py index ebe7f86..6fab1e9 100644 --- a/chatbees/server_models/doc_api.py +++ b/chatbees/server_models/doc_api.py @@ -1,3 +1,4 @@ +import enum import json from enum import Enum from typing import Any, List, Optional, Tuple, Dict @@ -15,6 +16,19 @@ "ExtractType", ] +class GetDocRequest(CollectionBaseRequest): + doc_name: str + + # fastapi server expects "property name enclosed in double quotes" when + # using with UploadFile. pydantic.model_dump_json() uses single quote. + # explicitly uses json.loads() and dumps(). + @classmethod + def validate_to_json(cls, value): + return cls(**json.loads(value)) if isinstance(value, str) else value + + def to_json_string(self) -> str: + return json.dumps(self.__dict__) + class AddDocRequest(CollectionBaseRequest): # fastapi server expects "property name enclosed in double quotes" when # using with UploadFile. pydantic.model_dump_json() uses single quote. @@ -26,6 +40,9 @@ def validate_to_json(cls, value): def to_json_string(self) -> str: return json.dumps(self.__dict__) +class AsyncAddDocResponse(BaseModel): + task_ids: List[str] + class DeleteDocRequest(CollectionBaseRequest): doc_name: str @@ -50,6 +67,17 @@ class DocumentMetadata(BaseModel): url: Optional[str] = None type: DocumentType +class UploadStatus(str, enum.Enum): + IN_QUEUE = 'IN_QUEUE' # In queue + VECTORIZING = 'VECTORIZING' # Processing file -> vectors + INDEXING = 'INDEXING' # Processing file -> vectors + SUCCEEDED = 'SUCCEEDED' # Processing file -> vectors + FAILED = 'FAILED' # Processing file -> vectors + +class PendingDocumentMetadata(DocumentMetadata): + task_id: str + status: UploadStatus + error: str class ListDocsResponse(BaseModel): documents: List[DocumentMetadata] = [] @@ -57,6 +85,8 @@ class ListDocsResponse(BaseModel): # To be deprecated doc_names: List[str] + pending_documents: List[PendingDocumentMetadata] = [] + class AskRequest(CollectionBaseRequest): question: str @@ -196,3 +226,10 @@ class GetCrawlResponse(BaseModel): class IndexCrawlRequest(CollectionBaseRequest): crawl_id: str + +class AskApplicationRequest(BaseModel): + application_name: str + app_request: str + +class AskApplicationResponse(BaseModel): + app_response: str diff --git a/chatbees/tests/data/realistic.txt b/chatbees/tests/data/realistic.txt new file mode 100644 index 0000000..111c9d0 --- /dev/null +++ b/chatbees/tests/data/realistic.txt @@ -0,0 +1,2 @@ +Future Works +This work is our first attempt effort to achieve knowledge fusion of System-II reasoning LLMs through a model merging approach, which is limited to LLMs with identical scale and architecture. In future work, we plan to employ our explicit model fusion method, based on multi-teacher knowledge distillation, and our implici model fusion method, which utilizes weighted-reward preference optimization for LLMs with different scales and architectures. Furthermore, we intend to explore the combination of knowledge fusion with reinforcement learning (RL) methods, which have been demonstrated as the most effective approach for enhancing reasoning abilities. Stay tuned for the next version of FuseO1! diff --git a/chatbees/tests/localfs_impl_test.py b/chatbees/tests/localfs_impl_test.py new file mode 100644 index 0000000..9a59d79 --- /dev/null +++ b/chatbees/tests/localfs_impl_test.py @@ -0,0 +1,335 @@ +import hashlib +import logging +import os +import time +import unittest +from typing import List + +import chatbees as cb +from chatbees import Chat +from chatbees.server_models.collection_api import ChatAttributes +from chatbees.server_models.doc_api import AnswerReference +from chatbees.utils.ask import ask_application + +TEST_ACCOUNT = os.environ.get('ENV_TEST_ACCOUNT') +TEST_PASSWORD = os.environ.get('ENV_TEST_PASSWORD') + +class LocalfsImplTest(unittest.TestCase): + def setUp(self): + cb.email_login(TEST_ACCOUNT, TEST_PASSWORD) + + def tearDown(self): + cls = cb.list_collections() + apps = cb.list_applications() + for app in apps: + cb.delete_collection(app.application_name) + for cl in cls: + cb.delete_collection(cl) + + def ask(self, clname: str, q: str, top_k: int = 5): + col = cb.Collection(name=clname) + resp = col.ask(q, top_k) + logging.info(f"{clname} q={q} a={resp.answer}") + doc_names = [ref.doc_name for ref in resp.refs] + logging.info(f"refs={doc_names}") + + def ask_app(self, app_name: str, q: str, top_k: int = 5): + resp = ask_application(application_name=app_name, question=q, top_k=top_k) + logging.info(f"{app_name} q={q} a={resp.answer}") + doc_names = [ref.doc_name for ref in resp.refs] + logging.info(f"refs={doc_names}") + + def test_conversations(self): + clname = 'test_conversations' + appname = 'test_app' + try: + col = cb.Collection(name=clname) + cb.create_collection(col) + app = cb.create_collection_application( + application_name=appname, + description='testdesc', + collection_name=clname, + chat_attrs=ChatAttributes(welcome_msg='TEST welcome!', + negative_response='TEST negative')) + + # Upload a test file. + test_file = f'{os.path.dirname(os.path.abspath(__file__))}/data/text_file.txt' + col.upload_document(test_file) + + col.chat().ask('first_cl_question') + Chat(application_name=appname).ask('first_app_question') + + # Test collection conversation + col_chat = col.chat() + cl_foo_answer = col_chat.ask('cl_foo').answer + cl_bar_answer = col_chat.ask('cl_bar').answer + + app_chat = Chat(application_name=appname) + app_foo_answer = app_chat.ask('app_foo').answer + app_bar_answer = app_chat.ask('app_bar').answer + + cl_convos = Chat.list_conversations(collection_name=clname) + app_convos = Chat.list_conversations(application_name=appname) + assert len(cl_convos) == 2, f"Found cl convos {cl_convos}" + assert len(app_convos) == 2, f"Found app convos {app_convos}" + + cl_convo = Chat.from_conversation(col_chat.conversation_id, collection_name=clname) + app_convo = Chat.from_conversation(app_chat.conversation_id, application_name=appname) + + assert cl_convo.history_messages == [('cl_foo', cl_foo_answer), ('cl_bar', cl_bar_answer)], f"Actual convo {cl_convo}" + assert app_convo.history_messages == [('app_foo', app_foo_answer), ('app_bar', app_bar_answer)], f"Actual convo {app_convo}" + + print(f"convo: {cl_convo.history_messages}") + print(f"convo: {app_convo.history_messages}") + finally: + cb.delete_application(appname) + cb.delete_collection(clname) + + def test_applications(self): + clname = 'test_applications' + try: + col = cb.Collection(name=clname) + cb.create_collection(col) + cb.create_collection_application( + application_name='test', + description='testdesc', + collection_name=clname, + chat_attrs=ChatAttributes(welcome_msg='TEST welcome!', + negative_response='TEST negative')) + cb.create_gpt_application(application_name='test2', provider='openai', model='4o') + applications = cb.list_applications() + assert set([app.application_name for app in applications]) == {'test', 'test2'} + + app = applications[0] if applications[0].application_name == 'test' else applications[1] + + # Application info is persisted + assert app.application_desc == 'testdesc' + assert app.chat_attrs.welcome_msg == 'TEST welcome!' + assert app.chat_attrs.negative_response == 'TEST negative' + + # Upload a test file, then ask an unrelated question. should get configured negative + # response back + test_file = f'{os.path.dirname(os.path.abspath(__file__))}/data/text_file.txt' + col.upload_document(test_file) + + # Asking through APP should respect negative_response. + # It is not configured through cl + app_answer = Chat(application_name=app.application_name).ask('what is openai?').answer + cl_answer = col.chat().ask('what is openai?').answer + assert app_answer == 'TEST negative', f'App answer {app_answer}' + assert cl_answer != 'TEST negative', f"Cl answer {cl_answer}" + + cb.delete_application('test') + cb.delete_application('test2') + finally: + cb.delete_collection(clname) + + def test_async_upload(self): + clname = 'test_async_doc_apis' + + # Create a collection and an application + col = cb.Collection(name=clname) + cb.create_collection(col) + + files = [ + f'{os.path.dirname(os.path.abspath(__file__))}/data/text_file.txt', + f'{os.path.dirname(os.path.abspath(__file__))}/data/española.txt', + f'{os.path.dirname(os.path.abspath(__file__))}/data/française.txt', + f'{os.path.dirname(os.path.abspath(__file__))}/data/中文.txt', + ] + doc_names = {'text_file.txt', 'española.txt', 'française.txt', '中文.txt'} + + try: + tasks = col.upload_documents_async(files) + print(f"Uploaded docs for async processing, tasks {tasks}") + while True: + docs = col.list_pending_documents() + if len(docs) == 0: + break + print(f"Async doc upload status {docs}") + time.sleep(2) + all_docs = col.list_documents() + print(f"Got {all_docs}") + assert sorted(all_docs) == sorted(doc_names) + finally: + cb.delete_collection(col.name) + def test_realistic(self): + clname = 'test_realistic' + + # create a collection and an application + col = cb.collection(clname) + cb.create_collection(col) + + files = [ + f'{os.path.dirname(os.path.abspath(__file__))}/data/realistic.txt', + ] + + # add and summarize + for file in files: + col.upload_document(file) + fname = os.path.basename(file) + col.summarize_document(fname) + + # ask + print(col.ask('what are the future work?')) + + def test_doc_apis(self): + clname = 'test_doc_apis' + + # create a collection and an application + col = cb.collection(clname) + cb.create_collection(col) + + app = cb.create_collection_application('testapp', collection_name=clname) + + files = [ + f'{os.path.dirname(os.path.abspath(__file__))}/data/text_file.txt', + f'{os.path.dirname(os.path.abspath(__file__))}/data/española.txt', + f'{os.path.dirname(os.path.abspath(__file__))}/data/française.txt', + f'{os.path.dirname(os.path.abspath(__file__))}/data/中文.txt', + ] + doc_names = {'text_file.txt', 'española.txt', 'française.txt', '中文.txt'} + + try: + # add and summarize + for file in files: + col.upload_document(file) + fname = os.path.basename(file) + col.summarize_document(fname) + + # list + print("list_documents") + list_doc_names = col.list_documents() + assert doc_names == set(list_doc_names) + + # ask + print("ask") + resp = col.ask('question?') + assert len(resp.refs) > 0 + + # delete, then list and ask again + col.delete_document('española.txt') + doc_names = {'text_file.txt', 'française.txt', '中文.txt'} + + list_doc_names = col.list_documents() + assert doc_names == set(list_doc_names) + + resp = col.ask('question?') + assert len(resp.refs) > 0 + + # chat + chat1 = col.chat() + app_chat1 = Chat(application_name=app.application_name) + chat2 = col.chat(doc_name="text_file.txt") + app_chat2 = Chat(application_name=app.application_name, doc_name='text_file.txt') + + chat1.ask("q1") + chat1.ask("q2") + app_chat1.ask("q1") + app_chat1.ask("q2") + + resp = chat2.ask("q1") + self.assertRefsAreFromDoc(resp.refs, "text_file.txt") + resp = chat2.ask("q2") + self.assertRefsAreFromDoc(resp.refs, "text_file.txt") + + resp = app_chat2.ask("q1") + self.assertRefsAreFromDoc(resp.refs, "text_file.txt") + resp = app_chat2.ask("q2") + self.assertRefsAreFromDoc(resp.refs, "text_file.txt") + + print(f"Convo {resp.conversation_id}") + + # ensure we can configure chat attrs + col.configure_chat('a pirate from 1600s', 'the word snowday and nothing else') + resp = col.ask('what is the color of my hair?') + print("persona answer", resp.answer) + + resp = app_chat1.ask('what is the color of my hair?') + print("[app] persona answer", resp.answer) + + finally: + cb.delete_application(app.application_name) + cb.delete_collection(col.name) + + def assertRefsAreFromDoc(self, refs: List[AnswerReference], doc: str): + assert len(refs) > 0 + for ref in refs: + assert ref.doc_name == doc + + def get_file_md5(self, path): + with open(path, 'rb') as file_to_check: + # read contents of the file + data = file_to_check.read() + # pipe contents of the file through + return hashlib.md5(data).hexdigest() + def test_fs(self): + try: + cols = cb.list_collections() + assert 'another_collection' not in cols + + # Create another collection + col = cb.Collection(name='another_collection') + cb.create_collection(col) + + cols = cb.list_collections() + assert 'another_collection' in cols + + # List docs, should be empty + docs = col.list_documents() + assert docs == [] + + # Upload transformer pdf and ask question + doc_name = 'transformer.pdf' + + file_md5 = self.get_file_md5(doc_name) + + col.upload_document(doc_name) + downloaded = col.download_document(doc_name, './downloads') + assert file_md5 == self.get_file_md5(downloaded), f'MD5 mismatch. original={doc_name}, downloaded={downloaded}' + + q = 'what is a transformer?' + print(f"[global] q: {q}, a={col.ask('what is a transformer')}") + print(f"[doc] q: {q}, a={col.ask('what is a transformer', doc_name=doc_name)}") + + # List docs, should see doc_name + docs = col.list_documents() + assert doc_name in docs, f'Documents are {docs}' + + col.delete_document(doc_name) + + # Describe, delete Delete + desc = cb.describe_collection('another_collection') + print(desc) + finally: + cb.delete_collection('another_collection') + + def _test_doc(self, clname1: str, clname2: str, write: bool = True): + cols = cb.list_collections() + logging.info(f"cols={cols}") + + # upload_document + col = cb.Collection(name=clname1) + doc_name = 'transformer-paper.pdf' + if write: + col.upload_document(doc_name) + self.ask(clname1, 'what is transformer?') + + summary = col.summarize_document(doc_name) + logging.info(f"doc={doc_name} summary={summary}") + + # test outline_faqs + outline_faqs = col.get_document_outline_faq(doc_name) + logging.info(f"doc={doc_name} outlines={len(outline_faqs.outlines)} " + f"{outline_faqs.outlines}, faqs={len(outline_faqs.faqs)} " + f"{outline_faqs.faqs}") + + # docs.piratenation.game has 49 pages + col = cb.collection(clname2) + + docs = col.list_documents() + docs.sort() + logging.info(f"docs={len(docs)} {docs}") + + q = 'what is pirate nation?' + self.ask(clname2, q) diff --git a/chatbees/tests/regression_test.py b/chatbees/tests/regression_test.py index f75fdff..bd349a5 100644 --- a/chatbees/tests/regression_test.py +++ b/chatbees/tests/regression_test.py @@ -1,3 +1,4 @@ +import hashlib import logging import os import time diff --git a/chatbees/tests/transformer.pdf b/chatbees/tests/transformer.pdf new file mode 100644 index 0000000..97d7c51 Binary files /dev/null and b/chatbees/tests/transformer.pdf differ diff --git a/chatbees/utils/ask.py b/chatbees/utils/ask.py index 9086120..67a4360 100644 --- a/chatbees/utils/ask.py +++ b/chatbees/utils/ask.py @@ -1,6 +1,12 @@ +import logging from typing import List, Tuple -from chatbees.server_models.doc_api import AskRequest, AskResponse, AnswerReference +from chatbees.server_models.doc_api import ( + AskRequest, + AskResponse, + AnswerReference, + AskApplicationRequest, AskApplicationResponse, +) from chatbees.utils.config import Config @@ -31,3 +37,29 @@ def ask( enforce_api_key=False ) return AskResponse.model_validate(resp.json()) + +def ask_application( + application_name: str, + question: str, + top_k: int = 5, + doc_name: str = None, + history_messages: List[Tuple[str, str]] = None, + conversation_id: str = None, +) -> AskResponse: + url = f'{Config.get_base_url()}/applications/ask' + + req = AskRequest( + namespace_name=Config.namespace, + application_name=application_name, + question=question, + top_k=top_k, + doc_name=doc_name, + history_messages=history_messages, + conversation_id=conversation_id, + ) + resp = Config.post( + url=url, + data=req.model_dump_json(), + enforce_api_key=False + ) + return AskResponse.model_validate(resp.json()) diff --git a/chatbees/utils/config.py b/chatbees/utils/config.py index d0d509a..7ae2d26 100644 --- a/chatbees/utils/config.py +++ b/chatbees/utils/config.py @@ -6,8 +6,8 @@ ENV_TEST_BASE_URL = os.environ.get("ENV_TEST_BASE_URL", "") class Config: - api_key: str - account_id: str + api_key: str = '' + account_id: str = '' PUBLIC_NAMESPACE: str = "public" namespace: str = PUBLIC_NAMESPACE @@ -18,11 +18,12 @@ def validate_setup(cls): @classmethod def get_base_url(cls): + # Default - prod + if ENV_TEST_BASE_URL == '': + return f"https://{cls.account_id}.us-west-2.aws.chatbees.ai" if ENV_TEST_BASE_URL == 'preprod': return f"https://{cls.account_id}.preprod.aws.chatbees.ai" - if ENV_TEST_BASE_URL.find("localhost") >= 0: - return ENV_TEST_BASE_URL - return f"https://{cls.account_id}.us-west-2.aws.chatbees.ai" + return ENV_TEST_BASE_URL @classmethod def post(cls, url, data=None, files=None, enforce_api_key=True): @@ -31,6 +32,7 @@ def post(cls, url, data=None, files=None, enforce_api_key=True): # Encode data if it is a string if data is not None and isinstance(data, str): data = data.encode('utf-8') + print(f'Sending to URL: {url}, data={data}, header={cls._construct_header()}') resp = requests.post( url, data=data, files=files, headers=cls._construct_header()) raise_for_error(resp) @@ -47,4 +49,4 @@ def get(cls, url: str): @classmethod def _construct_header(cls): - return None if cls.api_key is None else {'api-key': cls.api_key} + return None if cls.api_key is None else {'api-key': cls.api_key, 'x-org-url': cls.account_id} diff --git a/poetry.lock b/poetry.lock index f6f989c..e6f189a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,36 +1,36 @@ -# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand. [[package]] name = "annotated-types" -version = "0.6.0" +version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" -category = "main" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, - {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] [[package]] name = "certifi" -version = "2023.7.22" +version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" +groups = ["main", "dev"] files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] [[package]] name = "charset-normalizer" version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" optional = false python-versions = ">=3.7.0" +groups = ["main", "dev"] files = [ {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, @@ -128,9 +128,10 @@ files = [ name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -138,14 +139,14 @@ files = [ [[package]] name = "croniter" -version = "2.0.5" +version = "2.0.7" description = "croniter provides iteration for datetime object with cron like format" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.6" +groups = ["main"] files = [ - {file = "croniter-2.0.5-py2.py3-none-any.whl", hash = "sha256:fdbb44920944045cc323db54599b321325141d82d14fa7453bc0699826bbe9ed"}, - {file = "croniter-2.0.5.tar.gz", hash = "sha256:f1f8ca0af64212fbe99b1bee125ee5a1b53a9c1b433968d8bca8817b79d237f3"}, + {file = "croniter-2.0.7-py2.py3-none-any.whl", hash = "sha256:f15e80828d23920c4bb7f4d9340b932c9dcabecafc7775703c8b36d1253ed526"}, + {file = "croniter-2.0.7.tar.gz", hash = "sha256:1041b912b4b1e03751a0993531becf77851ae6e8b334c9c76ffeffb8f055f53f"}, ] [package.dependencies] @@ -154,14 +155,15 @@ pytz = ">2021.1" [[package]] name = "exceptiongroup" -version = "1.1.3" +version = "1.2.2" description = "Backport of PEP 654 (exception groups)" -category = "dev" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ - {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, - {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] @@ -169,23 +171,26 @@ test = ["pytest (>=6)"] [[package]] name = "idna" -version = "3.4" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" +groups = ["main", "dev"] files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -193,26 +198,26 @@ files = [ [[package]] name = "packaging" -version = "23.2" +version = "24.1" description = "Core utilities for Python packages" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [[package]] name = "pluggy" -version = "1.3.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] @@ -221,133 +226,125 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pydantic" -version = "2.5.0" +version = "2.9.2" description = "Data validation using Python type hints" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "pydantic-2.5.0-py3-none-any.whl", hash = "sha256:7ce6e766c456ad026fe5712f7bcf036efc34bd5d107b3e669ef7ea01b3a9050c"}, - {file = "pydantic-2.5.0.tar.gz", hash = "sha256:69bd6fb62d2d04b7055f59a396993486a2ee586c43a0b89231ce0000de07627c"}, + {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, + {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, ] [package.dependencies] -annotated-types = ">=0.4.0" -pydantic-core = "2.14.1" -typing-extensions = ">=4.6.1" +annotated-types = ">=0.6.0" +pydantic-core = "2.23.4" +typing-extensions = [ + {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, + {version = ">=4.6.1", markers = "python_version < \"3.13\""}, +] [package.extras] email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.14.1" -description = "" -category = "main" +version = "2.23.4" +description = "Core functionality for Pydantic validation and serialization" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "pydantic_core-2.14.1-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:812beca1dcb2b722cccc7e9c620bd972cbc323321194ec2725eab3222e6ac573"}, - {file = "pydantic_core-2.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2ccdc53cb88e51c7d47d74c59630d7be844428f6b8d463055ffad6f0392d8da"}, - {file = "pydantic_core-2.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd937733bf2fe7d6a8bf208c12741f1f730b7bf5636033877767a75093c29b8a"}, - {file = "pydantic_core-2.14.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:581bb606a31749a00796f5257947a0968182d7fe91e1dada41f06aeb6bfbc91a"}, - {file = "pydantic_core-2.14.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aadf74a40a7ae49c3c1aa7d32334fe94f4f968e21dd948e301bb4ed431fb2412"}, - {file = "pydantic_core-2.14.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b89821a2c77cc1b8f2c1fc3aacd6a3ecc5df8f7e518dc3f18aef8c4dcf66003d"}, - {file = "pydantic_core-2.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49ee28d65f506b2858a60745cc974ed005298ebab12693646b97641dd7c99c35"}, - {file = "pydantic_core-2.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:97246f896b4df7fd84caa8a75a67abb95f94bc0b547665bf0889e3262b060399"}, - {file = "pydantic_core-2.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1185548665bc61bbab0dc78f10c8eafa0db0aa1e920fe9a451b77782b10a65cc"}, - {file = "pydantic_core-2.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2a7d08b39fac97540fba785fce3b21ee01a81f081a07a4d031efd791da6666f9"}, - {file = "pydantic_core-2.14.1-cp310-none-win32.whl", hash = "sha256:0a8c8daf4e3aa3aeb98e3638fc3d58a359738f3d12590b2474c6bb64031a0764"}, - {file = "pydantic_core-2.14.1-cp310-none-win_amd64.whl", hash = "sha256:4f0788699a92d604f348e9c1ac5e97e304e97127ba8325c7d0af88dcc7d35bd3"}, - {file = "pydantic_core-2.14.1-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:2be018a84995b6be1bbd40d6064395dbf71592a981169cf154c0885637f5f54a"}, - {file = "pydantic_core-2.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fc3227408808ba7df8e95eb1d8389f4ba2203bed8240b308de1d7ae66d828f24"}, - {file = "pydantic_core-2.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42d5d0e9bbb50481a049bd0203224b339d4db04006b78564df2b782e2fd16ebc"}, - {file = "pydantic_core-2.14.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bc6a4ea9f88a810cb65ccae14404da846e2a02dd5c0ad21dee712ff69d142638"}, - {file = "pydantic_core-2.14.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d312ad20e3c6d179cb97c42232b53111bcd8dcdd5c1136083db9d6bdd489bc73"}, - {file = "pydantic_core-2.14.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:679cc4e184f213c8227862e57340d12fd4d4d19dc0e3ddb0f653f86f01e90f94"}, - {file = "pydantic_core-2.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101df420e954966868b8bc992aefed5fa71dd1f2755104da62ee247abab28e2f"}, - {file = "pydantic_core-2.14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c964c0cc443d6c08a2347c0e5c1fc2d85a272dc66c1a6f3cde4fc4843882ada4"}, - {file = "pydantic_core-2.14.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8276bbab68a9dbe721da92d19cbc061f76655248fe24fb63969d0c3e0e5755e7"}, - {file = "pydantic_core-2.14.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:12163197fec7c95751a3c71b36dcc1909eed9959f011ffc79cc8170a6a74c826"}, - {file = "pydantic_core-2.14.1-cp311-none-win32.whl", hash = "sha256:b8ff0302518dcd001bd722bbe342919c29e5066c7eda86828fe08cdc112668b8"}, - {file = "pydantic_core-2.14.1-cp311-none-win_amd64.whl", hash = "sha256:59fa83873223f856d898452c6162a390af4297756f6ba38493a67533387d85d9"}, - {file = "pydantic_core-2.14.1-cp311-none-win_arm64.whl", hash = "sha256:798590d38c9381f07c48d13af1f1ef337cebf76ee452fcec5deb04aceced51c7"}, - {file = "pydantic_core-2.14.1-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:587d75aec9ae50d0d63788cec38bf13c5128b3fc1411aa4b9398ebac884ab179"}, - {file = "pydantic_core-2.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26242e3593d4929123615bd9365dd86ef79b7b0592d64a96cd11fd83c69c9f34"}, - {file = "pydantic_core-2.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5879ac4791508d8f0eb7dec71ff8521855180688dac0c55f8c99fc4d1a939845"}, - {file = "pydantic_core-2.14.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad9ea86f5fc50f1b62c31184767fe0cacaa13b54fe57d38898c3776d30602411"}, - {file = "pydantic_core-2.14.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:102ac85a775e77821943ae38da9634ddd774b37a8d407181b4f7b05cdfb36b55"}, - {file = "pydantic_core-2.14.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2459cc06572730e079ec1e694e8f68c99d977b40d98748ae72ff11ef21a56b0b"}, - {file = "pydantic_core-2.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:217dcbfaf429a9b8f1d54eb380908b9c778e78f31378283b30ba463c21e89d5d"}, - {file = "pydantic_core-2.14.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9d59e0d7cdfe8ed1d4fcd28aad09625c715dc18976c7067e37d8a11b06f4be3e"}, - {file = "pydantic_core-2.14.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e2be646a5155d408e68b560c0553e8a83dc7b9f90ec6e5a2fc3ff216719385db"}, - {file = "pydantic_core-2.14.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ffba979801e3931a19cd30ed2049450820effe8f152aaa317e2fd93795d318d7"}, - {file = "pydantic_core-2.14.1-cp312-none-win32.whl", hash = "sha256:132b40e479cb5cebbbb681f77aaceabbc8355df16c9124cff1d4060ada83cde2"}, - {file = "pydantic_core-2.14.1-cp312-none-win_amd64.whl", hash = "sha256:744b807fe2733b6da3b53e8ad93e8b3ea3ee3dfc3abece4dd2824cc1f39aa343"}, - {file = "pydantic_core-2.14.1-cp312-none-win_arm64.whl", hash = "sha256:24ba48f9d0b8d64fc5e42e1600366c3d7db701201294989aebdaca23110c02ab"}, - {file = "pydantic_core-2.14.1-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:ba55d73a2df4771b211d0bcdea8b79454980a81ed34a1d77a19ddcc81f98c895"}, - {file = "pydantic_core-2.14.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:e905014815687d88cbb14bbc0496420526cf20d49f20606537d87646b70f1046"}, - {file = "pydantic_core-2.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:443dc5eede7fa76b2370213e0abe881eb17c96f7d694501853c11d5d56916602"}, - {file = "pydantic_core-2.14.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:abae6fd5504e5e438e4f6f739f8364fd9ff5a5cdca897e68363e2318af90bc28"}, - {file = "pydantic_core-2.14.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9486e27bb3f137f33e2315be2baa0b0b983dae9e2f5f5395240178ad8e644728"}, - {file = "pydantic_core-2.14.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69df82892ff00491d673b1929538efb8c8d68f534fdc6cb7fd3ac8a5852b9034"}, - {file = "pydantic_core-2.14.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:184ff7b30c3f60e1b775378c060099285fd4b5249271046c9005f8b247b39377"}, - {file = "pydantic_core-2.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3d5b2a4b3c10cad0615670cab99059441ff42e92cf793a0336f4bc611e895204"}, - {file = "pydantic_core-2.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:871c641a83719caaa856a11dcc61c5e5b35b0db888e1a0d338fe67ce744575e2"}, - {file = "pydantic_core-2.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1e7208946ea9b27a8cef13822c339d4ae96e45952cc01fc4a91c7f1cb0ae2861"}, - {file = "pydantic_core-2.14.1-cp37-none-win32.whl", hash = "sha256:b4ff385a525017f5adf6066d7f9fb309f99ade725dcf17ed623dc7dce1f85d9f"}, - {file = "pydantic_core-2.14.1-cp37-none-win_amd64.whl", hash = "sha256:c7411cd06afeb263182e38c6ca5b4f5fe4f20d91466ad7db0cd6af453a02edec"}, - {file = "pydantic_core-2.14.1-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:2871daf5b2823bf77bf7d3d43825e5d904030c155affdf84b21a00a2e00821d2"}, - {file = "pydantic_core-2.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7977e261cac5f99873dc2c6f044315d09b19a71c4246560e1e67593889a90978"}, - {file = "pydantic_core-2.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5a111f9158555582deadd202a60bd7803b6c68f406391b7cf6905adf0af6811"}, - {file = "pydantic_core-2.14.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac417312bf6b7a0223ba73fb12e26b2854c93bf5b1911f7afef6d24c379b22aa"}, - {file = "pydantic_core-2.14.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c36987f5eb2a7856b5f5feacc3be206b4d1852a6ce799f6799dd9ffb0cba56ae"}, - {file = "pydantic_core-2.14.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6e98227eb02623d57e1fd061788837834b68bb995a869565211b9abf3de4bf4"}, - {file = "pydantic_core-2.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:023b6d7ec4e97890b28eb2ee24413e69a6d48de4e8b75123957edd5432f4eeb3"}, - {file = "pydantic_core-2.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6015beb28deb5306049ecf2519a59627e9e050892927850a884df6d5672f8c7d"}, - {file = "pydantic_core-2.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3f48d4afd973abbd65266ac24b24de1591116880efc7729caf6b6b94a9654c9e"}, - {file = "pydantic_core-2.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:28734bcfb8fc5b03293dec5eb5ea73b32ff767f6ef79a31f6e41dad2f5470270"}, - {file = "pydantic_core-2.14.1-cp38-none-win32.whl", hash = "sha256:3303113fdfaca927ef11e0c5f109e2ec196c404f9d7ba5f8ddb63cdf287ea159"}, - {file = "pydantic_core-2.14.1-cp38-none-win_amd64.whl", hash = "sha256:144f2c1d5579108b6ed1193fcc9926124bd4142b0f7020a7744980d1235c8a40"}, - {file = "pydantic_core-2.14.1-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:893bf4fb9bfb9c4639bc12f3de323325ada4c6d60e478d5cded65453e9364890"}, - {file = "pydantic_core-2.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:052d8731aaf844f91fe4cd3faf28983b109a5865b3a256ec550b80a5689ead87"}, - {file = "pydantic_core-2.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb1c6ecb53e4b907ee8486f453dd940b8cbb509946e2b671e3bf807d310a96fc"}, - {file = "pydantic_core-2.14.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:94cf6d0274eb899d39189144dcf52814c67f9b0fd196f211420d9aac793df2da"}, - {file = "pydantic_core-2.14.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36c3bf96f803e207a80dbcb633d82b98ff02a9faa76dd446e969424dec8e2b9f"}, - {file = "pydantic_core-2.14.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb290491f1f0786a7da4585250f1feee200fc17ff64855bdd7c42fb54526fa29"}, - {file = "pydantic_core-2.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6590ed9d13eb51b28ea17ddcc6c8dbd6050b4eb589d497105f0e13339f223b72"}, - {file = "pydantic_core-2.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:69cd74e55a5326d920e7b46daa2d81c2bdb8bcf588eafb2330d981297b742ddc"}, - {file = "pydantic_core-2.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d965bdb50725a805b083f5f58d05669a85705f50a6a864e31b545c589290ee31"}, - {file = "pydantic_core-2.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ca942a2dc066ca5e04c27feaa8dfb9d353ddad14c6641660c565149186095343"}, - {file = "pydantic_core-2.14.1-cp39-none-win32.whl", hash = "sha256:72c2ef3787c3b577e5d6225d73a77167b942d12cef3c1fbd5e74e55b7f881c36"}, - {file = "pydantic_core-2.14.1-cp39-none-win_amd64.whl", hash = "sha256:55713d155da1e508083c4b08d0b1ad2c3054f68b8ef7eb3d3864822e456f0bb5"}, - {file = "pydantic_core-2.14.1-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:53efe03cc383a83660cfdda6a3cb40ee31372cedea0fde0b2a2e55e838873ab6"}, - {file = "pydantic_core-2.14.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f523e116879bc6714e61d447ce934676473b068069dce6563ea040381dc7a257"}, - {file = "pydantic_core-2.14.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85bb66d661be51b2cba9ca06759264b3469d2dbb53c3e6effb3f05fec6322be6"}, - {file = "pydantic_core-2.14.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f53a3ccdc30234cb4342cec541e3e6ed87799c7ca552f0b5f44e3967a5fed526"}, - {file = "pydantic_core-2.14.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:1bfb63821ada76719ffcd703fc40dd57962e0d8c253e3c565252e6de6d3e0bc6"}, - {file = "pydantic_core-2.14.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e2c689439f262c29cf3fcd5364da1e64d8600facecf9eabea8643b8755d2f0de"}, - {file = "pydantic_core-2.14.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a15f6e5588f7afb7f6fc4b0f4ff064749e515d34f34c666ed6e37933873d8ad8"}, - {file = "pydantic_core-2.14.1-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:f1a30eef060e21af22c7d23349f1028de0611f522941c80efa51c05a63142c62"}, - {file = "pydantic_core-2.14.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16f4a7e1ec6b3ea98a1e108a2739710cd659d68b33fbbeaba066202cab69c7b6"}, - {file = "pydantic_core-2.14.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fd80a2d383940eec3db6a5b59d1820f947317acc5c75482ff8d79bf700f8ad6a"}, - {file = "pydantic_core-2.14.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a68a36d71c7f638dda6c9e6b67f6aabf3fa1471b198d246457bfdc7c777cdeb7"}, - {file = "pydantic_core-2.14.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ebc79120e105e4bcd7865f369e3b9dbabb0d492d221e1a7f62a3e8e292550278"}, - {file = "pydantic_core-2.14.1-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:c8c466facec2ccdf025b0b1455b18f2c3d574d5f64d24df905d3d7b8f05d5f4e"}, - {file = "pydantic_core-2.14.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b91b5ec423e88caa16777094c4b2b97f11453283e7a837e5e5e1b886abba1251"}, - {file = "pydantic_core-2.14.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130e49aa0cb316f743bc7792c36aefa39fc2221312f1d4b333b19edbdd71f2b1"}, - {file = "pydantic_core-2.14.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f483467c046f549572f8aca3b7128829e09ae3a9fe933ea421f7cb7c58120edb"}, - {file = "pydantic_core-2.14.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dee4682bd7947afc682d342a8d65ad1834583132383f8e801601a8698cb8d17a"}, - {file = "pydantic_core-2.14.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:8d927d042c0ef04607ee7822828b208ab045867d20477ec6593d612156798547"}, - {file = "pydantic_core-2.14.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5a1570875eb0d1479fb2270ed80c88c231aaaf68b0c3f114f35e7fb610435e4f"}, - {file = "pydantic_core-2.14.1-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:cb2fd3ab67558eb16aecfb4f2db4febb4d37dc74e6b8613dc2e7160fb58158a9"}, - {file = "pydantic_core-2.14.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a7991f25b98038252363a03e6a9fe92e60fe390fda2631d238dc3b0e396632f8"}, - {file = "pydantic_core-2.14.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b45b7be9f99991405ecd6f6172fb6798908a8097106ae78d5cc5cc15121bad9"}, - {file = "pydantic_core-2.14.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:51506e7652a2ef1d1cf763c4b51b972ff4568d1dddc96ca83931a6941f5e6389"}, - {file = "pydantic_core-2.14.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:66dc0e63349ec39c1ea66622aa5c2c1f84382112afd3ab2fa0cca4fb01f7db39"}, - {file = "pydantic_core-2.14.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:8e17f0c3ba4cb07faa0038a59ce162de584ed48ba645c8d05a5de1e40d4c21e7"}, - {file = "pydantic_core-2.14.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d983222223f63e323a5f497f5b85e211557a5d8fb670dc88f343784502b466ba"}, - {file = "pydantic_core-2.14.1.tar.gz", hash = "sha256:0d82a6ee815388a362885186e431fac84c7a06623bc136f508e9f88261d8cadb"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, + {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, + {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, + {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, + {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, + {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, + {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, + {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, + {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, + {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, + {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, + {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, + {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, + {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, ] [package.dependencies] @@ -355,14 +352,14 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pytest" -version = "7.4.3" +version = "7.4.4" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ - {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, - {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] @@ -380,9 +377,9 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no name = "python-dateutil" version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -393,26 +390,26 @@ six = ">=1.5" [[package]] name = "pytz" -version = "2024.1" +version = "2024.2" description = "World timezone definitions, modern and historical" -category = "main" optional = false python-versions = "*" +groups = ["main"] files = [ - {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, - {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, + {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, + {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, ] [[package]] name = "requests" -version = "2.31.0" +version = "2.32.3" description = "Python HTTP for Humans." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["main", "dev"] files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] @@ -427,31 +424,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-mock" -version = "1.11.0" +version = "1.12.1" description = "Mock out responses from the requests package" -category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.5" +groups = ["dev"] files = [ - {file = "requests-mock-1.11.0.tar.gz", hash = "sha256:ef10b572b489a5f28e09b708697208c4a3b2b89ef80a9f01584340ea357ec3c4"}, - {file = "requests_mock-1.11.0-py2.py3-none-any.whl", hash = "sha256:f7fae383f228633f6bececebdab236c478ace2284d6292c6e7e2867b9ab74d15"}, + {file = "requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401"}, + {file = "requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563"}, ] [package.dependencies] -requests = ">=2.3,<3" -six = "*" +requests = ">=2.22,<3" [package.extras] fixture = ["fixtures"] -test = ["fixtures", "mock", "purl", "pytest", "requests-futures", "sphinx", "testtools"] [[package]] name = "shortuuid" version = "1.0.13" description = "A generator library for concise, unambiguous and URL-safe UUIDs." -category = "main" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "shortuuid-1.0.13-py3-none-any.whl", hash = "sha256:a482a497300b49b4953e15108a7913244e1bb0d41f9d332f5e9925dba33a3c5a"}, {file = "shortuuid-1.0.13.tar.gz", hash = "sha256:3bb9cf07f606260584b1df46399c0b87dd84773e7b25912b7e391e30797c5e72"}, @@ -461,9 +456,9 @@ files = [ name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main"] files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -471,46 +466,48 @@ files = [ [[package]] name = "tomli" -version = "2.0.1" +version = "2.0.2" description = "A lil' TOML parser" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, + {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, ] [[package]] name = "typing-extensions" -version = "4.8.0" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" -category = "main" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, - {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] name = "urllib3" -version = "2.1.0" +version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ - {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, - {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.10" content-hash = "b5052939a621abf814b25c54be96f5541cb42826393113d65b4c13c14690413c"