From 53ccdee37cdf137f14465ac5e2b03e20511ca1cd Mon Sep 17 00:00:00 2001 From: Yidi Sprei Date: Mon, 24 Feb 2025 16:33:23 -0500 Subject: [PATCH 1/9] feat: add version retrieval utility and refactor main execution - Introduced `get_version` function in `utils.py` to read and validate the application's version from a `.package-version` file. - Updated `__init__.py` to include `get_version` in the module exports, allowing for easy access to version information. - Encapsulated the logic in `main.py` under a `__main__` check to ensure the environment is only loaded when the script is executed directly, promoting better modularity and testability. --- main.py | 27 ++++++++++++++------------- src/infuzu/__init__.py | 3 +++ src/infuzu/utils.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 13 deletions(-) create mode 100644 src/infuzu/utils.py diff --git a/main.py b/main.py index cb94d9e..ff74894 100644 --- a/main.py +++ b/main.py @@ -1,18 +1,19 @@ -from src.infuzu import (create_chat_completion, ChatCompletionsHandlerRequestMessage, ChatCompletionsObject) +from src.infuzu import ( + create_chat_completion, ChatCompletionsHandlerRequestMessage, ChatCompletionsObject +) from dotenv import load_dotenv -load_dotenv() +if __name__ == "__main__": + load_dotenv() + messages: list[ChatCompletionsHandlerRequestMessage] = [ + ChatCompletionsHandlerRequestMessage(role="system", content="You are a helpful assistant."), + ChatCompletionsHandlerRequestMessage(role="user", content="What is the capital of France?"), + ] -messages: list[ChatCompletionsHandlerRequestMessage] = [ - ChatCompletionsHandlerRequestMessage(role="system", content="You are a helpful assistant."), - ChatCompletionsHandlerRequestMessage(role="user", content="What is the capital of France?"), -] - - -try: - response: ChatCompletionsObject = create_chat_completion(messages=messages) - print(response) -except Exception as e: - print(f"Error: {e}") + try: + response: ChatCompletionsObject = create_chat_completion(messages=messages) + print(response) + except Exception as e: + print(f"Error: {e}") diff --git a/src/infuzu/__init__.py b/src/infuzu/__init__.py index 4d59bc5..50f1294 100644 --- a/src/infuzu/__init__.py +++ b/src/infuzu/__init__.py @@ -17,6 +17,7 @@ ChatCompletionsObject, ) from .errors import (InfuzuAPIError, APIWarning, APIError) +from .utils import get_version __all__: list[str] = [ @@ -40,4 +41,6 @@ "InfuzuAPIError", "APIWarning", "APIError", + + "get_version", ] diff --git a/src/infuzu/utils.py b/src/infuzu/utils.py new file mode 100644 index 0000000..5b4311c --- /dev/null +++ b/src/infuzu/utils.py @@ -0,0 +1,33 @@ +def get_version() -> str: + """ + Returns the current version of the application. + + This function reads the version information from a file named 'version.txt' + located in the same directory as the script. The file should contain a single + line with the version number in the format 'x.y.z'. + + Returns: + str: The version number as a string in the format 'x.y.z'. + + Raises: + FileNotFoundError: If the 'version.txt' file is not found. + IOError: If there's an error reading the file. + ValueError: If the version number in the file is not in the correct format. + + Example: + >>> get_version() + '1.2.3' + """ + try: + with open('.package-version', 'r') as f: + version = f.read().strip() + + parts = version.split('.') + if len(parts) != 3 or not all(part.isdigit() for part in parts): + raise ValueError("Invalid version format") + + return version + except FileNotFoundError: + raise FileNotFoundError("Version file not found") + except IOError: + raise IOError("Error reading version file") \ No newline at end of file From 33b2b76ac6ccf041a5e1c55c5d4a46ffbb83d58d Mon Sep 17 00:00:00 2001 From: Yidi Sprei Date: Mon, 24 Feb 2025 16:34:54 -0500 Subject: [PATCH 2/9] fix(utils.py): ensure newline consistency at EOF Added a newline at the end of the utils.py file to maintain consistency and adhere to POSIX standards. This prevents potential issues with file concatenation and improves code readability. --- src/infuzu/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/infuzu/utils.py b/src/infuzu/utils.py index 5b4311c..0bd5031 100644 --- a/src/infuzu/utils.py +++ b/src/infuzu/utils.py @@ -30,4 +30,4 @@ def get_version() -> str: except FileNotFoundError: raise FileNotFoundError("Version file not found") except IOError: - raise IOError("Error reading version file") \ No newline at end of file + raise IOError("Error reading version file") From f2fae7bc952a4504c74223acaf0e783fa0515c90 Mon Sep 17 00:00:00 2001 From: Yidi Sprei Date: Mon, 24 Feb 2025 16:42:54 -0500 Subject: [PATCH 3/9] feat(api_client): add dynamic User-Agent header for API requests Added a dynamic User-Agent header to enhance request tracking and compatibility. The header now includes the infuzu-python version, Python version, httpx version, and the operating system info. This improvement aids in better server analytics and debugging. --- src/infuzu/api_client.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/infuzu/api_client.py b/src/infuzu/api_client.py index b8c7b7c..c63501c 100644 --- a/src/infuzu/api_client.py +++ b/src/infuzu/api_client.py @@ -1,9 +1,11 @@ +import platform import time import uuid import httpx import os from typing import (Optional, Dict, Union, List) from pydantic import (BaseModel, validator, Field) +from .utils import get_version from .errors import InfuzuAPIError @@ -226,6 +228,12 @@ def create_chat_completion( headers = { "Content-Type": "application/json", "Infuzu-API-Key": api_key, + "User-Agent": ( + f"infuzu-python/{get_version()} " + f"(Python {platform.python_version()}; " + f"httpx/{httpx.__version__}; " + f"{platform.system()} {platform.release()})" + ) } payload = { From 4fd4dde9c99849d07ba835292dc7f6a9771fd8f8 Mon Sep 17 00:00:00 2001 From: Yidi Sprei Date: Mon, 24 Feb 2025 16:43:31 -0500 Subject: [PATCH 4/9] refactor(api-client): replace HTTPStatusError with InfuzuAPIError Updated exception handling to use a custom InfuzuAPIError instead of httpx.HTTPStatusError for clearer error reporting when API requests fail. This enhances code readability and maintains consistency in error management across the codebase. --- src/infuzu/api_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/infuzu/api_client.py b/src/infuzu/api_client.py index c63501c..772f0a4 100644 --- a/src/infuzu/api_client.py +++ b/src/infuzu/api_client.py @@ -215,7 +215,7 @@ def create_chat_completion( Raises: ValueError: If the API key is not provided and the INFUZU_API_KEY environment variable is not set. - httpx.HTTPStatusError: If the API request returns an error status code. + InfuzuAPIError: If the API request returns an error status code. """ if api_key is None: From 321f83bbe93757cd572ae41643daf522a3fa9d43 Mon Sep 17 00:00:00 2001 From: Yidi Sprei Date: Mon, 24 Feb 2025 16:44:24 -0500 Subject: [PATCH 5/9] refactor(api_client): update return type for chat completion function Updated the return type in the docstring of the chat completion function from a dictionary to a ChatCompletionsObject. This clarifies the expected output and improves code readability and understanding for developers using this function. --- src/infuzu/api_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/infuzu/api_client.py b/src/infuzu/api_client.py index 772f0a4..f6a8b3c 100644 --- a/src/infuzu/api_client.py +++ b/src/infuzu/api_client.py @@ -206,11 +206,11 @@ def create_chat_completion( messages: A list of message objects. api_key: Your Infuzu API key. If not provided, it will be read from the INFUZU_API_KEY environment variable. - model: The model to use for the chat completion. Can be a string (model name) + model: The model to use for the chat completion. Can be a string (model name) or a InfuzuModelParams object for more advanced configuration. Returns: - A dictionary containing the JSON response from the API. + The ChatCompletionsObject Object Raises: ValueError: If the API key is not provided and the INFUZU_API_KEY From 56e334d4c33592cc6300581aad4bacf9b4cc5458 Mon Sep 17 00:00:00 2001 From: Yidi Sprei Date: Mon, 24 Feb 2025 16:45:58 -0500 Subject: [PATCH 6/9] refactor(api_client): add type annotations for clarity and safety Added type annotations to improve code readability and catch potential type-related errors early. This helps in understanding the expected types of variables and enhances the maintainability of the codebase. --- src/infuzu/api_client.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/infuzu/api_client.py b/src/infuzu/api_client.py index f6a8b3c..8375c71 100644 --- a/src/infuzu/api_client.py +++ b/src/infuzu/api_client.py @@ -191,7 +191,7 @@ class Config: extra: str = "allow" -API_BASE_URL = "https://chat.infuzu.com/api" +API_BASE_URL: str = "https://chat.infuzu.com/api" def create_chat_completion( @@ -219,13 +219,13 @@ def create_chat_completion( """ if api_key is None: - api_key = os.environ.get("INFUZU_API_KEY") + api_key: str | None = os.environ.get("INFUZU_API_KEY") if api_key is None: raise ValueError( "API key not provided and INFUZU_API_KEY environment variable not set." ) - headers = { + headers: dict[str, str] = { "Content-Type": "application/json", "Infuzu-API-Key": api_key, "User-Agent": ( @@ -236,7 +236,7 @@ def create_chat_completion( ) } - payload = { + payload: dict[str, any] = { "messages": [message.dict(by_alias=True) for message in messages], } @@ -248,7 +248,7 @@ def create_chat_completion( try: with httpx.Client() as client: - response = client.post( + response: httpx.Response = client.post( f"{API_BASE_URL}/v1/chat/completions", headers=headers, json=payload, From 84f1b541dbc2a1ed26e9c7230288a871e6940fe3 Mon Sep 17 00:00:00 2001 From: Yidi Sprei Date: Mon, 24 Feb 2025 17:27:07 -0500 Subject: [PATCH 7/9] refactor: replace Config class with ConfigDict for model config Refactor Pydantic models to use ConfigDict instead of the Config inner class. This change streamlines the code by directly setting the 'extra' attribute to 'allow' within the model_config attribute, enhancing readability and maintainability. This approach aligns with the latest Pydantic practices and ensures consistent behavior across all models in handling extra fields. --- src/infuzu/api_client.py | 87 ++++++++++++++++------------------------ src/infuzu/errors.py | 12 +++--- 2 files changed, 40 insertions(+), 59 deletions(-) diff --git a/src/infuzu/api_client.py b/src/infuzu/api_client.py index 8375c71..80f1921 100644 --- a/src/infuzu/api_client.py +++ b/src/infuzu/api_client.py @@ -4,22 +4,23 @@ import httpx import os from typing import (Optional, Dict, Union, List) -from pydantic import (BaseModel, validator, Field) +from pydantic import (BaseModel, validator, Field, ConfigDict) from .utils import get_version from .errors import InfuzuAPIError class ModelWeights(BaseModel): + model_config = ConfigDict(extra="allow") + price: Optional[float] = None error: Optional[float] = None start_latency: Optional[float] = None end_latency: Optional[float] = None - class Config: - extra: str = "allow" - class InfuzuModelParams(BaseModel): + model_config = ConfigDict(extra="allow") + llms: Optional[List[str]] = None exclude_llms: Optional[List[str]] = None weights: Optional[ModelWeights] = None @@ -27,19 +28,15 @@ class InfuzuModelParams(BaseModel): max_input_cost: Optional[float] = None max_output_cost: Optional[float] = None - class Config: - extra: str = "allow" - class ChatCompletionsRequestContentPart(BaseModel): + model_config = ConfigDict(extra="allow") + type: str text: Optional[str] = None image_url: Optional[str] = None input_audio: Optional[str] = None - class Config: - extra: str = "allow" - @validator("text", always=True) def check_content_fields(cls, value, values): if "type" in values: @@ -52,13 +49,12 @@ def check_content_fields(cls, value, values): class ChatCompletionsHandlerRequestMessage(BaseModel): + model_config = ConfigDict(extra="allow") + content: Union[str, List[ChatCompletionsRequestContentPart]] role: str name: Optional[str] = None - class Config: - extra: str = "allow" - @validator('role') def role_must_be_valid(cls, v): if v not in ('system', 'user', 'assistant'): @@ -67,41 +63,39 @@ def role_must_be_valid(cls, v): class ChatCompletionsChoiceMessageAudioObject(BaseModel): + model_config = ConfigDict(extra="allow") + id: Optional[str] = None expired_at: Optional[int] = None data: Optional[str] = None transcript: Optional[str] = None - class Config: - extra: str = "allow" - class ChatCompletionsChoiceMessageFunctionCallObject(BaseModel): + model_config = ConfigDict(extra="allow") + name: Optional[str] = None arguments: Optional[str] = None - class Config: - extra: str = "allow" - class ChatCompletionsChoiceMessageToolCallFunctionObject(BaseModel): + model_config = ConfigDict(extra="allow") + name: Optional[str] = None arguments: Optional[str] = None - class Config: - extra: str = "allow" - class chatCompletionsChoiceMessageToolCallObject(BaseModel): + model_config = ConfigDict(extra="allow") + id: Optional[str] = None type: Optional[str] = None function: Optional[ChatCompletionsChoiceMessageToolCallFunctionObject] = None - class Config: - extra: str = "allow" - class ChatCompletionsChoiceMessageObject(BaseModel): + model_config = ConfigDict(extra="allow") + content: Optional[str] = None refusal: Optional[str] = None tool_calls: Optional[List[chatCompletionsChoiceMessageToolCallObject]] = None @@ -109,62 +103,55 @@ class ChatCompletionsChoiceMessageObject(BaseModel): function_call: Optional[ChatCompletionsChoiceMessageFunctionCallObject] = None audio: Optional[ChatCompletionsChoiceMessageAudioObject] = None - class Config: - extra: str = "allow" - class ChatCompletionsChoiceLogprobsItemTopLogprobObject(BaseModel): + model_config = ConfigDict(extra="allow") + token: Optional[str] = None logprob: Optional[int] = None bytes: Optional[List[int]] = None - class Config: - extra: str = "allow" - class ChatCompletionsLogprobsItemObject(BaseModel): + model_config = ConfigDict(extra="allow") + token: Optional[str] = None logprob: Optional[int] = None bytes: Optional[List[int]] = None content: Optional[List[ChatCompletionsChoiceLogprobsItemTopLogprobObject]] = None - class Config: - extra: str = "allow" - class ChatCompletionsChoiceLogprobsObject(BaseModel): + model_config = ConfigDict(extra="allow") + content: Optional[List[ChatCompletionsLogprobsItemObject]] = None refusal: Optional[List[ChatCompletionsLogprobsItemObject]] = None - class Config: - extra: str = "allow" - class ChatCompletionsChoiceModelObject(BaseModel): + model_config = ConfigDict(extra="allow") + ref: Optional[str] = None rank: Optional[int] = None - class Config: - extra: str = "allow" - class ChatCompletionsChoiceErrorObject(BaseModel): + model_config = ConfigDict(extra="allow") + message: Optional[str] = None code: Optional[str] = None - class Config: - extra: str = "allow" - class ChatCompletionsChoiceLatencyObject(BaseModel): + model_config = ConfigDict(extra="allow") + start: Optional[int] = Field(None, alias='start_latency') end: Optional[int] = Field(None, alias='end_latency') - class Config: - extra: str = "allow" - class ChatCompletionsChoiceObject(BaseModel): + model_config = ConfigDict(extra="allow") + finish_reason: Optional[str] = None index: Optional[int] = None message: Optional[ChatCompletionsChoiceMessageObject] = None @@ -173,11 +160,10 @@ class ChatCompletionsChoiceObject(BaseModel): error: Optional[ChatCompletionsChoiceErrorObject] = None latency: Optional[ChatCompletionsChoiceLatencyObject] = None - class Config: - extra: str = "allow" - class ChatCompletionsObject(BaseModel): + model_config = ConfigDict(extra="allow") + id: Optional[str] = None choices: Optional[List[ChatCompletionsChoiceObject]] = None created: Optional[int] = None @@ -187,9 +173,6 @@ class ChatCompletionsObject(BaseModel): object: Optional[str] = None usage: Optional[Dict[str, int]] = None - class Config: - extra: str = "allow" - API_BASE_URL: str = "https://chat.infuzu.com/api" diff --git a/src/infuzu/errors.py b/src/infuzu/errors.py index deaabb9..8f6bb0f 100644 --- a/src/infuzu/errors.py +++ b/src/infuzu/errors.py @@ -2,27 +2,25 @@ from json import JSONDecodeError from typing import Optional import httpx -from pydantic import BaseModel +from pydantic import (BaseModel, ConfigDict) logger: logging.Logger = logging.getLogger(__name__) class APIError(BaseModel): + model_config = ConfigDict(extra="allow") + code: Optional[str] = None message: Optional[str] = None - class Config: - extra: str = "allow" - class APIWarning(BaseModel): + model_config = ConfigDict(extra="allow") + code: Optional[str] = None message: Optional[str] = None - class Config: - extra: str = "allow" - class InfuzuAPIError(httpx.HTTPStatusError): def __init__(self, base_error: httpx.HTTPStatusError) -> None: From e7ff42579317ed007a00a0edfb7906dc1ee3b3ab Mon Sep 17 00:00:00 2001 From: Yidi Sprei Date: Mon, 24 Feb 2025 17:31:54 -0500 Subject: [PATCH 8/9] refactor(api_client): switch to model_validator and model_dump Refactored validation logic to use `model_validator` for improved clarity and maintainability. Replaced `validator` with `model_validator` in `ChatCompletionsRequestContentPart` and `ChatCompletionsHandlerRequestMessage`. Changed `dict` to `model_dump` for payload serialization, enhancing data handling consistency. --- src/infuzu/api_client.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/infuzu/api_client.py b/src/infuzu/api_client.py index 80f1921..591b394 100644 --- a/src/infuzu/api_client.py +++ b/src/infuzu/api_client.py @@ -4,7 +4,7 @@ import httpx import os from typing import (Optional, Dict, Union, List) -from pydantic import (BaseModel, validator, Field, ConfigDict) +from pydantic import (BaseModel, validator, Field, ConfigDict, model_validator) from .utils import get_version from .errors import InfuzuAPIError @@ -37,15 +37,13 @@ class ChatCompletionsRequestContentPart(BaseModel): image_url: Optional[str] = None input_audio: Optional[str] = None - @validator("text", always=True) - def check_content_fields(cls, value, values): - if "type" in values: - content_type = values["type"] - if content_type == "text" and value is None: - raise ValueError("Text must be provided when type is 'text'") - if content_type != "text" and value is not None: - raise ValueError("Text cannot be provided when type is not 'text'") - return value + @model_validator(mode='after') + def check_content_fields(self) -> 'ChatCompletionsRequestContentPart': + if self.type == "text" and self.text is None: + raise ValueError("Text must be provided when type is 'text'") + if self.type != "text" and self.text is not None: + raise ValueError("Text cannot be provided when type is not 'text'") + return self class ChatCompletionsHandlerRequestMessage(BaseModel): @@ -55,11 +53,11 @@ class ChatCompletionsHandlerRequestMessage(BaseModel): role: str name: Optional[str] = None - @validator('role') - def role_must_be_valid(cls, v): - if v not in ('system', 'user', 'assistant'): + @model_validator(mode='after') + def role_must_be_valid(self) -> 'ChatCompletionsHandlerRequestMessage': + if self.role not in ('system', 'user', 'assistant'): raise ValueError('Role must be one of: system, user, assistant') - return v + return self class ChatCompletionsChoiceMessageAudioObject(BaseModel): @@ -220,14 +218,14 @@ def create_chat_completion( } payload: dict[str, any] = { - "messages": [message.dict(by_alias=True) for message in messages], + "messages": [message.model_dump(by_alias=True) for message in messages], } if model: if isinstance(model, str): payload["model"] = model else: - payload["model"] = model.dict(by_alias=True) + payload["model"] = model.model_dump(by_alias=True) try: with httpx.Client() as client: From 287c80f0e81ae9eef0b4fe40665376cb3fe5140c Mon Sep 17 00:00:00 2001 From: Yidi Sprei Date: Mon, 24 Feb 2025 17:32:23 -0500 Subject: [PATCH 9/9] refactor(api_client): remove unused pydantic validator import This change removes an unused 'validator' import from the pydantic module in the api_client.py file. By cleaning up unused imports, it improves code readability and reduces potential confusion for future developers working on the codebase. --- src/infuzu/api_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/infuzu/api_client.py b/src/infuzu/api_client.py index 591b394..4427c09 100644 --- a/src/infuzu/api_client.py +++ b/src/infuzu/api_client.py @@ -4,7 +4,7 @@ import httpx import os from typing import (Optional, Dict, Union, List) -from pydantic import (BaseModel, validator, Field, ConfigDict, model_validator) +from pydantic import (BaseModel, Field, ConfigDict, model_validator) from .utils import get_version from .errors import InfuzuAPIError