diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index 0efaac54..dca636ed 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -40,5 +40,8 @@ jobs: - name: Install the project run: uv sync --locked --all-extras --dev + - name: Check Python formatting with Black + run: uv run --group dev black --check --diff . + - name: Run tests run: uv run pytest tests diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..3e33492a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,6 @@ +repos: + - repo: https://github.com/psf/black + rev: 24.10.0 + hooks: + - id: black + language_version: python3.9 \ No newline at end of file diff --git a/README.md b/README.md index 7a064da4..dd3e6d8d 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,20 @@ All contributions must abide by our code of conduct. Please see Here are a few handy tips and common workflows when developing the Tower CLI. +### Getting Started + +1. Install development dependencies: + ```bash + uv sync --group dev + ``` + +2. Set up pre-commit hooks for code formatting: + ```bash + uv run --group dev pre-commit install + ``` + +This will automatically run Black formatter on Python files before each commit. + ### Python SDK development We use `uv` for all development. You can spawn a REPL in context using `uv` very @@ -187,6 +201,18 @@ uv sync --locked --all-extras --dev uv run pytest tests ``` +### Code Formatting + +We use Black for Python code formatting. The pre-commit hooks will automatically format your code, but you can also run it manually: + +```bash +# Format all Python files in the project +uv run --group dev black . + +# Check formatting without making changes +uv run --group dev black --check . +``` + If you need to get the latest OpenAPI SDK, you can run `./scripts/generate-python-api-client.sh`. diff --git a/crates/tower-runtime/tests/example-apps/02-use-faker/main.py b/crates/tower-runtime/tests/example-apps/02-use-faker/main.py index 95b8a506..107fad27 100644 --- a/crates/tower-runtime/tests/example-apps/02-use-faker/main.py +++ b/crates/tower-runtime/tests/example-apps/02-use-faker/main.py @@ -1,5 +1,6 @@ from faker import Faker + def main(): fake = Faker() print(fake.name()) diff --git a/crates/tower-runtime/tests/example-apps/03-legacy-app/task.py b/crates/tower-runtime/tests/example-apps/03-legacy-app/task.py index 2005385d..1531deda 100644 --- a/crates/tower-runtime/tests/example-apps/03-legacy-app/task.py +++ b/crates/tower-runtime/tests/example-apps/03-legacy-app/task.py @@ -1,2 +1,3 @@ import dlt + print(dlt.version.__version__) diff --git a/pyproject.toml b/pyproject.toml index 9fbcae7c..72fc75bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,9 +70,31 @@ include = ["rust-toolchain.toml"] [tool.uv.sources] tower = { workspace = true } +[tool.black] +line-length = 88 +target-version = ['py39'] +include = '\.pyi?$' +extend-exclude = ''' +/( + # directories + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist +)/ +''' + [dependency-groups] dev = [ + "black==24.10.0", "openapi-python-client==0.24.3", + "pre-commit==4.0.1", "pytest==8.3.5", "pytest-httpx==0.35.0", "pytest-env>=1.1.3", diff --git a/scripts/semver.py b/scripts/semver.py index bc5b5340..ce5e9b4e 100755 --- a/scripts/semver.py +++ b/scripts/semver.py @@ -8,6 +8,7 @@ SEMVER_EXP = re.compile(r"\d+\.\d+(\.\d+)?(-rc\.(\d+))?") + class Version: def __init__(self, version_str): version_str = version_str.removeprefix("v") @@ -43,21 +44,40 @@ def is_valid(self): def __eq__(self, other): if isinstance(other, Version): - return self.major == other.major and self.minor == other.minor and self.patch == other.patch + return ( + self.major == other.major + and self.minor == other.minor + and self.patch == other.patch + ) else: return False def to_tag_string(self): if self.prerelease > 0: - return "{major}.{minor}.{patch}-rc.{prerelease}".format(major=self.major, minor=self.minor, patch=self.patch, prerelease=self.prerelease) + return "{major}.{minor}.{patch}-rc.{prerelease}".format( + major=self.major, + minor=self.minor, + patch=self.patch, + prerelease=self.prerelease, + ) else: - return "{major}.{minor}.{patch}".format(major=self.major, minor=self.minor, patch=self.patch) + return "{major}.{minor}.{patch}".format( + major=self.major, minor=self.minor, patch=self.patch + ) def to_python_string(self): if self.prerelease > 0: - return "{major}.{minor}.{patch}rc{prerelease}".format(major=self.major, minor=self.minor, patch=self.patch, prerelease=self.prerelease) + return "{major}.{minor}.{patch}rc{prerelease}".format( + major=self.major, + minor=self.minor, + patch=self.patch, + prerelease=self.prerelease, + ) else: - return "{major}.{minor}.{patch}".format(major=self.major, minor=self.minor, patch=self.patch) + return "{major}.{minor}.{patch}".format( + major=self.major, minor=self.minor, patch=self.patch + ) + def get_all_versions(): # Wait for this to complete. @@ -70,13 +90,18 @@ def get_all_versions(): tags = stream.read().split("\n") return [Version(tag) for tag in tags] + def get_version_set(version): all_versions = get_all_versions() - return [v for v in all_versions if v.major == version.major and v.minor == version.minor] + return [ + v for v in all_versions if v.major == version.major and v.minor == version.minor + ] + def get_version_patch(version): return version.patch + def get_current_version(base): v = Version(base) versions = get_version_set(v) @@ -101,6 +126,7 @@ def get_current_version(base): else: return same_versions[0] + def get_version_base(): path = os.path.join(BASE_PATH, "version.txt") @@ -108,53 +134,90 @@ def get_version_base(): line = file.readline().rstrip() return line + def str2bool(value): if isinstance(value, bool): return value - if value.lower() in {'true', 'yes', '1'}: + if value.lower() in {"true", "yes", "1"}: return True - elif value.lower() in {'false', 'no', '0'}: + elif value.lower() in {"false", "no", "0"}: return False else: - raise argparse.ArgumentTypeError('Boolean value expected (true/false).') + raise argparse.ArgumentTypeError("Boolean value expected (true/false).") + def replace_line_with_regex(file_path, pattern, replace_text): """ Replace lines matching a regex pattern with replace_text in the specified file. - + Args: file_path (str): Path to the file to modify pattern (re.Pattern): Regex pattern to match lines replace_text (str): Text to replace the entire line with """ - with open(file_path, 'r') as file: + with open(file_path, "r") as file: content = file.read() - + # Use regex to replace lines matching the pattern - new_content = pattern.sub(replace_text + '\n', content) - - with open(file_path, 'w') as file: + new_content = pattern.sub(replace_text + "\n", content) + + with open(file_path, "w") as file: file.write(new_content) + def update_cargo_file(version): pattern = re.compile(r'^\s*version\s*=\s*".*"$', re.MULTILINE) - replace_line_with_regex("Cargo.toml", pattern, f'version = "{version.to_tag_string()}"') + replace_line_with_regex( + "Cargo.toml", pattern, f'version = "{version.to_tag_string()}"' + ) + def update_pyproject_file(version): pattern = re.compile(r'^\s*version\s*=\s*".*"$', re.MULTILINE) - replace_line_with_regex("pyproject.toml", pattern, f'version = "{version.to_python_string()}"') + replace_line_with_regex( + "pyproject.toml", pattern, f'version = "{version.to_python_string()}"' + ) + if __name__ == "__main__": parser = argparse.ArgumentParser( - prog='semver', - description='Manages the semantic versioning of the projects', - epilog='This is the epilog' + prog="semver", + description="Manages the semantic versioning of the projects", + epilog="This is the epilog", ) - parser.add_argument("-i", "--patch", type=str2bool, required=False, default=False, help="Increment the patch version") - parser.add_argument("-p", "--prerelease", type=str2bool, required=False, default=False, help="Include the fact that this is a prerelease version") - parser.add_argument("-r", "--release", type=str2bool, required=False, default=False, help="Remove the perelease designation") - parser.add_argument("-w", "--write", type=str2bool, required=False, default=False, help="Update the various tools in this repository") + parser.add_argument( + "-i", + "--patch", + type=str2bool, + required=False, + default=False, + help="Increment the patch version", + ) + parser.add_argument( + "-p", + "--prerelease", + type=str2bool, + required=False, + default=False, + help="Include the fact that this is a prerelease version", + ) + parser.add_argument( + "-r", + "--release", + type=str2bool, + required=False, + default=False, + help="Remove the perelease designation", + ) + parser.add_argument( + "-w", + "--write", + type=str2bool, + required=False, + default=False, + help="Update the various tools in this repository", + ) args = parser.parse_args() version_base = get_version_base() @@ -183,4 +246,4 @@ def update_pyproject_file(version): os.system("cargo build") os.system("uv lock") else: - print(version.to_tag_string(), end='', flush=True) + print(version.to_tag_string(), end="", flush=True) diff --git a/src/tower/_client.py b/src/tower/_client.py index 2eefb905..b4598f74 100644 --- a/src/tower/_client.py +++ b/src/tower/_client.py @@ -103,11 +103,14 @@ def run_app( if e.status_code == 404: raise AppNotFoundError(name) else: - raise UnknownException(f"Unexpected status code {e.status_code} when running app {name}") + raise UnknownException( + f"Unexpected status code {e.status_code} when running app {name}" + ) + def wait_for_run( run: Run, - timeout: Optional[float] = 86_400.0, # one day + timeout: Optional[float] = 86_400.0, # one day raise_on_failure: bool = False, ) -> Run: """ @@ -173,12 +176,12 @@ def wait_for_run( retries += 1 if retries >= DEFAULT_NUM_TIMEOUT_RETRIES: - raise UnknownException("There was a problem with the Tower API.") + raise UnknownException("There was a problem with the Tower API.") def wait_for_runs( runs: List[Run], - timeout: Optional[float] = 86_400.0, # one day + timeout: Optional[float] = 86_400.0, # one day raise_on_failure: bool = False, ) -> tuple[List[Run], List[Run]]: """ @@ -255,7 +258,7 @@ def wait_for_runs( retries += 1 if retries >= DEFAULT_NUM_TIMEOUT_RETRIES: - raise UnknownException("There was a problem with the Tower API.") + raise UnknownException("There was a problem with the Tower API.") else: # Add the item back on the list for retry later on. awaiting_runs.append(run) @@ -273,7 +276,7 @@ def _is_failed_run(run: Run) -> bool: Returns: bool: True if the run has failed, False otherwise. """ - return run.status in ["crashed", "cancelled", "errored"] + return run.status in ["crashed", "cancelled", "errored"] def _is_successful_run(run: Run) -> bool: @@ -302,7 +305,9 @@ def _is_run_awaiting_completion(run: Run) -> bool: return run.status in ["pending", "scheduled", "running"] -def _env_client(ctx: TowerContext, timeout: Optional[float] = None) -> AuthenticatedClient: +def _env_client( + ctx: TowerContext, timeout: Optional[float] = None +) -> AuthenticatedClient: tower_url = ctx.tower_url if not tower_url.endswith("/v1"): @@ -338,15 +343,13 @@ def _time_since(start_time: float) -> float: def _check_run_status( ctx: TowerContext, run: Run, - timeout: Optional[float] = 2.0, # one day + timeout: Optional[float] = 2.0, # one day ) -> Run: client = _env_client(ctx, timeout=timeout) try: - output: Optional[Union[DescribeRunResponse, ErrorModel]] = describe_run_api.sync( - name=run.app_name, - seq=run.number, - client=client + output: Optional[Union[DescribeRunResponse, ErrorModel]] = ( + describe_run_api.sync(name=run.app_name, seq=run.number, client=client) ) if output is None: diff --git a/src/tower/_context.py b/src/tower/_context.py index 51c63697..f7c134c6 100644 --- a/src/tower/_context.py +++ b/src/tower/_context.py @@ -1,9 +1,17 @@ import os + class TowerContext: - def __init__(self, tower_url: str, environment: str, api_key: str = None, - inference_router: str = None, inference_router_api_key: str = None, - inference_provider: str = None, jwt: str = None): + def __init__( + self, + tower_url: str, + environment: str, + api_key: str = None, + inference_router: str = None, + inference_router_api_key: str = None, + inference_provider: str = None, + jwt: str = None, + ): self.tower_url = tower_url self.environment = environment self.api_key = api_key @@ -33,12 +41,11 @@ def build(cls): inference_provider = os.getenv("TOWER_INFERENCE_PROVIDER") return cls( - tower_url = tower_url, - environment = tower_environment, - api_key = tower_api_key, - inference_router = inference_router, - inference_router_api_key = inference_router_api_key, - inference_provider = inference_provider, - jwt = tower_jwt, + tower_url=tower_url, + environment=tower_environment, + api_key=tower_api_key, + inference_router=inference_router, + inference_router_api_key=inference_router_api_key, + inference_provider=inference_provider, + jwt=tower_jwt, ) - diff --git a/src/tower/_llms.py b/src/tower/_llms.py index da8c7bea..b34589f9 100644 --- a/src/tower/_llms.py +++ b/src/tower/_llms.py @@ -13,12 +13,10 @@ # TODO: add vllm back in when we have a way to use it LOCAL_INFERENCE_ROUTERS = [ - "ollama", + "ollama", ] -INFERENCE_ROUTERS = LOCAL_INFERENCE_ROUTERS + [ - "hugging_face_hub" -] +INFERENCE_ROUTERS = LOCAL_INFERENCE_ROUTERS + ["hugging_face_hub"] RAW_MODEL_FAMILIES = [ "all-minilm", @@ -186,9 +184,10 @@ "yarn-mistral", "yi", "yi-coder", - "zephyr" + "zephyr", ] + def normalize_model_family(name: str) -> str: """ Normalize a model family name by removing '-' and '.' characters. @@ -197,28 +196,27 @@ def normalize_model_family(name: str) -> str: Returns: str: The normalized model family name. """ - return name.replace('-', '').replace('.', '').lower() + return name.replace("-", "").replace(".", "").lower() -MODEL_FAMILIES = {normalize_model_family(name) : name for name in RAW_MODEL_FAMILIES} +MODEL_FAMILIES = {normalize_model_family(name): name for name in RAW_MODEL_FAMILIES} # the %-ge of memory that we can use for inference # TODO: add this back in when implementing memory checking for LLMs # MEMORY_THRESHOLD = 0.8 - def parse_parameter_size(size_str: str) -> float: """ Convert parameter size string (e.g., '8.0B', '7.2B') to number of parameters. """ if not size_str: return 0 - multiplier = {'B': 1e9, 'M': 1e6, 'K': 1e3} + multiplier = {"B": 1e9, "M": 1e6, "K": 1e3} size_str = size_str.upper() for suffix, mult in multiplier.items(): if suffix in size_str: - return float(size_str.replace(suffix, '')) * mult + return float(size_str.replace(suffix, "")) * mult return float(size_str) @@ -240,9 +238,10 @@ def resolve_model_name(ctx: TowerContext, requested_model: str) -> str: raise ValueError(f"Inference router {ctx.inference_router} not supported.") if ctx.inference_router == "ollama": - return resolve_ollama_model_name(ctx,requested_model) + return resolve_ollama_model_name(ctx, requested_model) elif ctx.inference_router == "hugging_face_hub": - return resolve_hugging_face_hub_model_name(ctx,requested_model) + return resolve_hugging_face_hub_model_name(ctx, requested_model) + def get_local_ollama_models() -> List[dict]: """ @@ -257,21 +256,23 @@ def get_local_ollama_models() -> List[dict]: try: models = ollama_list_models() model_list = [] - for model in models['models']: - model_name = model.get('model', '') - model_family = model_name.split(':')[0] - size = model.get('size', 0) - details = model.get('details', {}) - parameter_size=details.get('parameter_size', '') - quantization_level=details.get('quantization_level', '') - - model_list.append({ - 'model': model_name, - 'model_family': model_family, - 'size': size, - 'parameter_size': parameter_size, - 'quantization_level': quantization_level - }) + for model in models["models"]: + model_name = model.get("model", "") + model_family = model_name.split(":")[0] + size = model.get("size", 0) + details = model.get("details", {}) + parameter_size = details.get("parameter_size", "") + quantization_level = details.get("quantization_level", "") + + model_list.append( + { + "model": model_name, + "model_family": model_family, + "size": size, + "parameter_size": parameter_size, + "quantization_level": quantization_level, + } + ) return model_list except Exception as e: raise RuntimeError(f"Failed to list Ollama models: {str(e)}") @@ -282,15 +283,17 @@ def resolve_ollama_model_name(ctx: TowerContext, requested_model: str) -> str: Resolve the Ollama model name to use. """ local_models = get_local_ollama_models() - local_model_names = [model['model'] for model in local_models] - + local_model_names = [model["model"] for model in local_models] + # TODO: add this back in when implementing memory checking for LLMs - #memory = get_available_memory() - #memory_threshold = memory['available'] * MEMORY_THRESHOLD + # memory = get_available_memory() + # memory_threshold = memory['available'] * MEMORY_THRESHOLD if normalize_model_family(requested_model) in MODEL_FAMILIES: # Filter models by family - matching_models = [model for model in local_models if model['model_family'] == requested_model] + matching_models = [ + model for model in local_models if model["model_family"] == requested_model + ] # TODO: add this back in when implementing memory checking for LLMs # Filter models by memory @@ -299,16 +302,23 @@ def resolve_ollama_model_name(ctx: TowerContext, requested_model: str) -> str: # Return the model with the largest parameter size if matching_models: - best_model = max(matching_models, key=lambda x: parse_parameter_size(x['parameter_size']))['model'] + best_model = max( + matching_models, key=lambda x: parse_parameter_size(x["parameter_size"]) + )["model"] return best_model else: # TODO: add this back in when implementing memory checking for LLMs # raise ValueError(f"No models in family {requested_model} fit in available memory ({memory['available'] / (1024**3):.2f} GB) with max memory threshold {MEMORY_THRESHOLD} or are not available locally. Please pull a model first using 'ollama pull {requested_model}'") - raise ValueError(f"No models found with name {requested_model}. Please pull a model first using 'ollama pull {requested_model}'") + raise ValueError( + f"No models found with name {requested_model}. Please pull a model first using 'ollama pull {requested_model}'" + ) elif requested_model in local_model_names: return requested_model else: - raise ValueError(f"No models found with name {requested_model}. Please pull a model first using 'ollama pull {requested_model}'") + raise ValueError( + f"No models found with name {requested_model}. Please pull a model first using 'ollama pull {requested_model}'" + ) + def resolve_hugging_face_hub_model_name(ctx: TowerContext, requested_model: str) -> str: """ @@ -329,73 +339,82 @@ def resolve_hugging_face_hub_model_name(ctx: TowerContext, requested_model: str) pass except Exception as e: # for the rest of the errors, we will raise an error - raise RuntimeError(f"Error while getting model_info for {requested_model}: {str(e)}") - + raise RuntimeError( + f"Error while getting model_info for {requested_model}: {str(e)}" + ) # If inference_provider is specified, search by inference provider # We will use "search" instead of "filter" because only search allows searching inside the model name # TODO: Add more filtering options e.g. by number of parameters, so that we do not have to retrieve so many models # TODO: We need to retrieve >1 model because "search" returns a full text match in both model IDs and Descriptions - + if models is None: if ctx.inference_provider is not None: models = api.list_models( search=f"{requested_model}", - #filter=f"inference_provider:{ctx.inference_provider}", + # filter=f"inference_provider:{ctx.inference_provider}", # this is supposed to work in recent HF versions, but it doesn't work for me # we will do the filtering manually below expand="inferenceProviderMapping", - limit=20) + limit=20, + ) else: models = api.list_models( - search=f"{requested_model}", - expand="inferenceProviderMapping", - limit=20) + search=f"{requested_model}", expand="inferenceProviderMapping", limit=20 + ) # Create a list of models with their inference provider mappings model_list = [] try: for model in models: # Handle the case where inference_provider_mapping might be None or empty - inference_provider_mapping = getattr(model, 'inference_provider_mapping', []) or [] - + inference_provider_mapping = ( + getattr(model, "inference_provider_mapping", []) or [] + ) + model_info = { - 'model_name': model.id, - 'inference_provider_mapping': inference_provider_mapping + "model_name": model.id, + "inference_provider_mapping": inference_provider_mapping, } - + # If inference_provider is specified, only add models that support it if ctx.inference_provider is not None: - if ctx.inference_provider not in [mapping.provider for mapping in inference_provider_mapping]: + if ctx.inference_provider not in [ + mapping.provider for mapping in inference_provider_mapping + ]: continue - + # Check that requested_model is partially contained in model.id - if normalize_model_family(requested_model) not in normalize_model_family(model.id): + if normalize_model_family(requested_model) not in normalize_model_family( + model.id + ): continue - + model_list.append(model_info) except Exception as e: raise RuntimeError(f"Error while iterating: {str(e)}") if not model_list: - raise ValueError(f"No models found with name {requested_model} on Hugging Face Hub") - - return model_list[0]['model_name'] + raise ValueError( + f"No models found with name {requested_model} on Hugging Face Hub" + ) + + return model_list[0]["model_name"] class Llm: """ This class provides a unified interface for interacting with language models through different inference providers (e.g. Ollama for local inference, Hugging Face Hub for remote). - It abstracts away model name resolution, inference provider selection, and local/remote inference API differences + It abstracts away model name resolution, inference provider selection, and local/remote inference API differences to provide a consistent interface for text generation tasks. - + The class supports both chat-based interactions (similar to OpenAI Chat Completions API) and simple prompt-based interactions (similar to legacy OpenAI Completions API). This class is typically instantiated through the llms() factory function rather than directly. - + Attributes: context (TowerContext): The Tower context containing configuration and settings. requested_model_name (str): The original model name requested by the user. @@ -404,23 +423,23 @@ class Llm: inference_router (str): The inference router to use (e.g., "ollama", "hugging_face_hub"). inference_provider (str): The inference provider (same as router when in local mode). inference_router_api_key (str): API key for the inference router if required. - + Example: # Create an Llm instance (typically done via the llms() factory function) llm = tower.llms("llama3.2", max_tokens=1000) - + # Use for chat completions messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Hello!"} ] response = llm.complete_chat(messages) - + # Use for simple prompts response = llm.prompt("What is the capital of France?") - + """ - + def __init__(self, context: TowerContext, model_name: str, max_tokens: int = 1000): """ Wraps up interfacing with a language model in the Tower system. @@ -442,28 +461,28 @@ def __init__(self, context: TowerContext, model_name: str, max_tokens: int = 100 # Check that we know this router. This will also check that router was set when not in local mode. if context.inference_router not in INFERENCE_ROUTERS: - raise ValueError(f"Inference router {context.inference_router} not supported.") - - self.model_name = resolve_model_name( - self.context, self.requested_model_name) - + raise ValueError( + f"Inference router {context.inference_router} not supported." + ) + + self.model_name = resolve_model_name(self.context, self.requested_model_name) def complete_chat(self, messages: List) -> str: """ Mimics the OpenAI Chat Completions API by sending a list of messages to the language model and returning the generated response. - + This function provides a unified interface for chat-based interactions with different language model providers (Ollama, Hugging Face Hub, etc.) while maintaining compatibility with the OpenAI Chat Completions API format. - + Args: messages: A list of message dictionaries, each containing 'role' and 'content' keys. Follows the OpenAI Chat Completions API message format. - + Returns: str: The generated response from the language model. - + Example: messages = [ {"role": "system", "content": "You are a helpful assistant."}, @@ -475,92 +494,96 @@ def complete_chat(self, messages: List) -> str: if self.inference_router == "ollama": # Use Ollama for local inference response = complete_chat_with_ollama( - ctx = self.context, - model = self.model_name, - messages = messages + ctx=self.context, model=self.model_name, messages=messages ) elif self.inference_router == "hugging_face_hub": response = complete_chat_with_hugging_face_hub( - ctx = self.context, - model = self.model_name, - messages = messages, - max_tokens=self.max_tokens + ctx=self.context, + model=self.model_name, + messages=messages, + max_tokens=self.max_tokens, ) return response def prompt(self, prompt: str) -> str: """ - Mimics the old-style OpenAI Completions API (not Chat Completions!) by sending a single prompt string + Mimics the old-style OpenAI Completions API (not Chat Completions!) by sending a single prompt string to the language model and returning the generated response. - + This function provides a simple interface for single-prompt interactions, similar to the legacy OpenAI /v1/completions endpoint. It internally converts the prompt to a chat message format and uses the complete_chat method. - + Args: prompt: A single string containing the prompt to send to the language model. - + Returns: str: The generated response from the language model. - + Example: response = llm.prompt("What is the capital of France?") """ - return self.complete_chat([{ - "role": "user", - "content": prompt, - }]) + return self.complete_chat( + [ + { + "role": "user", + "content": prompt, + } + ] + ) + def llms(model_name: str, max_tokens: int = 1000) -> Llm: """ - This factory function creates an Llm instance configured with the specified model parameters. - It automatically resolves the model name based on the available inference providers + This factory function creates an Llm instance configured with the specified model parameters. + It automatically resolves the model name based on the available inference providers (Ollama for local inference, Hugging Face Hub for remote). The max_tokens parameter is used to set the maximum number of tokens to generate in responses. - + Args: - model_name: Can be a model family name (e.g., "llama3.2", "gemma3.2", "deepseek-r1") + model_name: Can be a model family name (e.g., "llama3.2", "gemma3.2", "deepseek-r1") or a specific model identifier (e.g., "deepseek-r1:14b", "deepseek-ai/DeepSeek-R1-0528"). The function will automatically resolve the exact model name based on available models in the configured inference provider. max_tokens: Maximum number of tokens to generate in responses. Defaults to 1000. - + Returns: Llm: A configured language model instance that can be used for text generation, chat completions, and other language model interactions. - + Raises: ValueError: If the configured inference router is not supported or if the model cannot be resolved. - + Example: # Create a language model instance llm = llms("llama3.2", max_tokens=500) - + # Use for chat completions messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Hello!"} ] response = llm.complete_chat(messages) - + """ ctx = TowerContext.build() - return Llm( - context = ctx, - model_name=model_name, - max_tokens=max_tokens - ) + return Llm(context=ctx, model_name=model_name, max_tokens=max_tokens) + def extract_ollama_message(resp: ChatResponse) -> str: return resp.message.content + def extract_hugging_face_hub_message(resp: ChatCompletionOutput) -> str: return resp.choices[0].message.content -def complete_chat_with_ollama(ctx: TowerContext, model: str, messages: list, is_retry: bool = False) -> str: - + +def complete_chat_with_ollama( + ctx: TowerContext, model: str, messages: list, is_retry: bool = False +) -> str: + # TODO: remove the try/except and don't pull the model if it doesn't exist. sso 7/20/25 # the except code is not reachable right now because we always call this function with a model that exists try: @@ -578,18 +601,21 @@ def complete_chat_with_ollama(ctx: TowerContext, model: str, messages: list, is_ # Couldn't figure out what the error was, so we'll just raise it accordingly. raise e -def complete_chat_with_hugging_face_hub(ctx: TowerContext, model: str, messages: List, **kwargs) -> str: + +def complete_chat_with_hugging_face_hub( + ctx: TowerContext, model: str, messages: List, **kwargs +) -> str: """ Uses the Hugging Face Hub API to perform inference. Will use configuration supplied by the environment to determine which client to connect to and all that. """ client = InferenceClient( - provider=ctx.inference_provider, - api_key=ctx.inference_router_api_key + provider=ctx.inference_provider, api_key=ctx.inference_router_api_key ) - completion = client.chat_completion(messages, + completion = client.chat_completion( + messages, model=model, max_tokens=kwargs.get("max_tokens", 1000), ) @@ -617,5 +643,3 @@ def complete_chat_with_hugging_face_hub(ctx: TowerContext, model: str, messages: # } # except Exception as e: # raise RuntimeError(f"Failed to get memory information: {str(e)}") - - diff --git a/src/tower/_tables.py b/src/tower/_tables.py index 30441526..451ebed6 100644 --- a/src/tower/_tables.py +++ b/src/tower/_tables.py @@ -25,6 +25,7 @@ namespace_or_default, ) + @dataclass class RowsAffectedInformation: inserts: int @@ -66,7 +67,6 @@ def __init__(self, context: TowerContext, table: IcebergTable): self._context = context self._table = table - def read(self) -> pl.DataFrame: """ Reads all data from the Iceberg table and returns it as a Polars DataFrame. @@ -91,7 +91,6 @@ def read(self) -> pl.DataFrame: # the result as a DataFrame. return pl.scan_iceberg(self._table).collect() - def to_polars(self) -> pl.LazyFrame: """ Converts the table to a Polars LazyFrame for efficient, lazy evaluation. @@ -117,10 +116,9 @@ def to_polars(self) -> pl.LazyFrame: ... .sort("department")) >>> # Execute the plan >>> final_df = result.collect() - """ + """ return pl.scan_iceberg(self._table) - def rows_affected(self) -> RowsAffectedInformation: """ Returns statistics about the number of rows affected by write operations on the table. @@ -147,7 +145,6 @@ def rows_affected(self) -> RowsAffectedInformation: """ return self._stats - def insert(self, data: pa.Table) -> TTable: """ Inserts new rows into the Iceberg table. @@ -181,7 +178,6 @@ def insert(self, data: pa.Table) -> TTable: self._stats.inserts += data.num_rows return self - def upsert(self, data: pa.Table, join_cols: Optional[list[str]] = None) -> TTable: """ Performs an upsert operation (update or insert) on the Iceberg table. @@ -224,11 +220,9 @@ def upsert(self, data: pa.Table, join_cols: Optional[list[str]] = None) -> TTabl res = self._table.upsert( data, join_cols=join_cols, - # All upserts will always be case sensitive. Perhaps we'll add this # as a parameter in the future? case_sensitive=True, - # These are the defaults, but we're including them to be complete. when_matched_update_all=True, when_not_matched_insert_all=True, @@ -240,7 +234,6 @@ def upsert(self, data: pa.Table, join_cols: Optional[list[str]] = None) -> TTabl return self - def delete(self, filters: Union[str, List[pc.Expression]]) -> TTable: """ Deletes rows from the Iceberg table that match the specified filter conditions. @@ -284,7 +277,6 @@ def delete(self, filters: Union[str, List[pc.Expression]]) -> TTable: self._table.delete( delete_filter=filters, - # We want this to always be the case. Not sure why you wouldn't? case_sensitive=True, ) @@ -294,7 +286,6 @@ def delete(self, filters: Union[str, List[pc.Expression]]) -> TTable: return self - def schema(self) -> pa.Schema: """ Returns the schema of the table as a PyArrow schema. @@ -311,7 +302,6 @@ def schema(self) -> pa.Schema: iceberg_schema = self._table.schema() return iceberg_schema.as_arrow() - def column(self, name: str) -> pa.compute.Expression: """ Returns a column from the table as a PyArrow compute expression. @@ -337,7 +327,7 @@ def column(self, name: str) -> pa.compute.Expression: >>> table.delete(age_expr) """ field = self.schema().field(name) - + if field is None: raise ValueError(f"Column {name} not found in table schema") @@ -346,13 +336,18 @@ def column(self, name: str) -> pa.compute.Expression: class TableReference: - def __init__(self, ctx: TowerContext, catalog: Catalog, name: str, namespace: Optional[str] = None): + def __init__( + self, + ctx: TowerContext, + catalog: Catalog, + name: str, + namespace: Optional[str] = None, + ): self._context = ctx self._catalog = catalog self._name = name self._namespace = namespace - def load(self) -> Table: """ Loads an existing Iceberg table from the catalog. @@ -379,9 +374,7 @@ def load(self) -> Table: table = self._catalog.load_table(table_name) return Table(self._context, table) - def create(self, schema: pa.Schema) -> Table: - """ Creates a new Iceberg table with the specified schema. @@ -432,7 +425,6 @@ def create(self, schema: pa.Schema) -> Table: return Table(self._context, table) - def create_if_not_exists(self, schema: pa.Schema) -> Table: """ Creates a new Iceberg table if it doesn't exist, or returns the existing table. @@ -488,7 +480,6 @@ def create_if_not_exists(self, schema: pa.Schema) -> Table: return Table(self._context, table) - def drop(self) -> bool: """ Drops (deletes) the Iceberg table from the catalog. @@ -527,9 +518,7 @@ def drop(self) -> bool: def tables( - name: str, - catalog: Union[str, Catalog] = "default", - namespace: Optional[str] = None + name: str, catalog: Union[str, Catalog] = "default", namespace: Optional[str] = None ) -> TableReference: """ Creates a reference to an Iceberg table that can be used to load or create tables. @@ -564,17 +553,17 @@ def tables( >>> # Load an existing table from the default catalog >>> table = tables("my_table").load() >>> df = table.read() - + >>> # Create a new table in a specific namespace >>> schema = pa.schema([ ... pa.field("id", pa.int64()), ... pa.field("name", pa.string()) ... ]) >>> table = tables("new_table", namespace="my_namespace").create(schema) - + >>> # Use a specific catalog >>> table = tables("my_table", catalog="my_catalog").load() - + >>> # Create a table if it doesn't exist >>> table = tables("my_table").create_if_not_exists(schema) diff --git a/src/tower/exceptions.py b/src/tower/exceptions.py index cdd845e5..b37c2a80 100644 --- a/src/tower/exceptions.py +++ b/src/tower/exceptions.py @@ -28,6 +28,7 @@ class RunFailedError(RuntimeError): def __init__(self, app_name: str, number: int, state: str): super().__init__(f"Run {app_name}#{number} failed with status '{state}'") + class AppNotFoundError(RuntimeError): def __init__(self, app_name: str): super().__init__(f"App '{app_name}' not found in the Tower.") diff --git a/src/tower/polars.py b/src/tower/polars.py index b770c21c..abab0570 100644 --- a/src/tower/polars.py +++ b/src/tower/polars.py @@ -1,8 +1,9 @@ try: import polars as _polars + # Re-export everything from polars from polars import * - + # Or if you prefer, you can be explicit about what you re-export # from polars import DataFrame, Series, etc. except ImportError: diff --git a/src/tower/pyarrow.py b/src/tower/pyarrow.py index 02125566..dba7be99 100644 --- a/src/tower/pyarrow.py +++ b/src/tower/pyarrow.py @@ -1,5 +1,6 @@ try: import pyarrow as _pyarrow + # Re-export everything from pyarrow import * except ImportError: diff --git a/src/tower/pyiceberg.py b/src/tower/pyiceberg.py index ba05f294..e3e4e36e 100644 --- a/src/tower/pyiceberg.py +++ b/src/tower/pyiceberg.py @@ -1,17 +1,19 @@ try: import pyiceberg as _pyiceberg + # Re-export everything from pyiceberg import * except ImportError: _pyiceberg = None - + # Dynamic dispatch for submodules, as relevant. def __getattr__(name): - """Forward attribute access to the original module.""" - return getattr(_pyiceberg, name) + """Forward attribute access to the original module.""" + return getattr(_pyiceberg, name) + # Optionally, also set up the module to handle subpackage imports # This requires Python 3.7+ def __dir__(): - return dir(_pyiceberg) + return dir(_pyiceberg) diff --git a/src/tower/utils/tables.py b/src/tower/utils/tables.py index 8a963026..49db08f5 100644 --- a/src/tower/utils/tables.py +++ b/src/tower/utils/tables.py @@ -1,5 +1,6 @@ from typing import Optional + def make_table_name(name: str, namespace: Optional[str]) -> str: namespace = namespace_or_default(namespace) @@ -8,6 +9,7 @@ def make_table_name(name: str, namespace: Optional[str]) -> str: else: return f"{namespace}.{name}" + def namespace_or_default(namespace: Optional[str]) -> str: if namespace is None: return "default" diff --git a/tests/tower/test_client.py b/tests/tower/test_client.py index 82365c70..9e54b9c4 100644 --- a/tests/tower/test_client.py +++ b/tests/tower/test_client.py @@ -12,30 +12,32 @@ def mock_api_config(): """Configure the Tower API client to use mock server.""" os.environ["TOWER_URL"] = "https://api.example.com" os.environ["TOWER_API_KEY"] = "abc123" - + # Only import after environment is configured import tower + # Set WAIT_TIMEOUT to 0 to avoid actual waiting in tests tower._client.WAIT_TIMEOUT = 0 - + return tower @pytest.fixture def mock_run_response_factory(): """Factory to create consistent run response objects.""" + def _create_run_response( app_version: str = "v6", number: int = 0, run_id: str = "50ac9bc1-c783-4359-9917-a706f20dc02c", status: str = "pending", status_group: str = "", - parameters: Optional[List[Dict[str, Any]]] = None + parameters: Optional[List[Dict[str, Any]]] = None, ) -> Dict[str, Any]: """Create a mock run response with the given parameters.""" if parameters is None: parameters = [] - + return { "run": { "$link": f"https://api.example.com/v1/apps/my-app/runs/{number}", @@ -52,28 +54,29 @@ def _create_run_response( "status": status, "exit_code": None, "status_group": status_group, - "parameters": parameters + "parameters": parameters, } } - + return _create_run_response @pytest.fixture def create_run_object(): """Factory to create Run objects for testing.""" + def _create_run( app_version: str = "v6", number: int = 0, run_id: str = "50ac9bc1-c783-4359-9917-a706f20dc02c", status: str = "running", status_group: str = "failed", - parameters: Optional[List[Dict[str, Any]]] = None + parameters: Optional[List[Dict[str, Any]]] = None, ) -> Run: """Create a Run object with the given parameters.""" if parameters is None: parameters = [] - + return Run( link="https://api.example.com/v1/apps/my-app/runs/0", app_name="my-app", @@ -89,9 +92,9 @@ def _create_run( started_at="2025-04-25T20:54:59.366937Z", status=status, status_group=status_group, - parameters=parameters + parameters=parameters, ) - + return _create_run @@ -113,9 +116,11 @@ def test_running_apps(httpx_mock, mock_api_config, mock_run_response_factory): assert run.status == "pending" -def test_waiting_for_a_run(httpx_mock, mock_api_config, mock_run_response_factory, create_run_object): +def test_waiting_for_a_run( + httpx_mock, mock_api_config, mock_run_response_factory, create_run_object +): run_number = 3 - + # First response: pending status httpx_mock.add_response( method="GET", @@ -128,7 +133,9 @@ def test_waiting_for_a_run(httpx_mock, mock_api_config, mock_run_response_factor httpx_mock.add_response( method="GET", url=f"https://api.example.com/v1/apps/my-app/runs/{run_number}", - json=mock_run_response_factory(number=run_number, status="exited", status_group="successful"), + json=mock_run_response_factory( + number=run_number, status="exited", status_group="successful" + ), status_code=200, ) @@ -137,7 +144,7 @@ def test_waiting_for_a_run(httpx_mock, mock_api_config, mock_run_response_factor # Now actually wait for the run final_run = tower.wait_for_run(run) - + # Verify the final state assert final_run.status == "exited" assert final_run.status_group == "successful" @@ -145,15 +152,15 @@ def test_waiting_for_a_run(httpx_mock, mock_api_config, mock_run_response_factor @pytest.mark.parametrize("run_numbers", [(3, 4)]) def test_waiting_for_multiple_runs( - httpx_mock, - mock_api_config, - mock_run_response_factory, - create_run_object, - run_numbers + httpx_mock, + mock_api_config, + mock_run_response_factory, + create_run_object, + run_numbers, ): tower = mock_api_config runs = [] - + # Setup mocks for each run for run_number in run_numbers: # First response: pending status @@ -168,18 +175,20 @@ def test_waiting_for_multiple_runs( httpx_mock.add_response( method="GET", url=f"https://api.example.com/v1/apps/my-app/runs/{run_number}", - json=mock_run_response_factory(number=run_number, status="exited", status_group="successful"), + json=mock_run_response_factory( + number=run_number, status="exited", status_group="successful" + ), status_code=200, ) - + # Create the Run object runs.append(create_run_object(number=run_number)) - + # Now actually wait for the runs successful_runs, failed_runs = tower.wait_for_runs(runs) assert len(failed_runs) == 0 - + # Verify all runs completed successfully for run in successful_runs: assert run.status == "exited" @@ -187,14 +196,11 @@ def test_waiting_for_multiple_runs( def test_failed_runs_in_the_list( - httpx_mock, - mock_api_config, - mock_run_response_factory, - create_run_object + httpx_mock, mock_api_config, mock_run_response_factory, create_run_object ): tower = mock_api_config runs = [] - + # For the first run, we're going to simulate a success. httpx_mock.add_response( method="GET", @@ -206,12 +212,14 @@ def test_failed_runs_in_the_list( httpx_mock.add_response( method="GET", url=f"https://api.example.com/v1/apps/my-app/runs/1", - json=mock_run_response_factory(number=1, status="exited", status_group="successful"), + json=mock_run_response_factory( + number=1, status="exited", status_group="successful" + ), status_code=200, ) - + runs.append(create_run_object(number=1)) - + # Second run will have been a failure. httpx_mock.add_response( method="GET", @@ -223,10 +231,12 @@ def test_failed_runs_in_the_list( httpx_mock.add_response( method="GET", url=f"https://api.example.com/v1/apps/my-app/runs/2", - json=mock_run_response_factory(number=2, status="crashed", status_group="failed"), + json=mock_run_response_factory( + number=2, status="crashed", status_group="failed" + ), status_code=200, ) - + runs.append(create_run_object(number=2)) # Third run was a success. @@ -240,23 +250,24 @@ def test_failed_runs_in_the_list( httpx_mock.add_response( method="GET", url=f"https://api.example.com/v1/apps/my-app/runs/3", - json=mock_run_response_factory(number=3, status="exited", status_group="successful"), + json=mock_run_response_factory( + number=3, status="exited", status_group="successful" + ), status_code=200, ) - + runs.append(create_run_object(number=3)) - # Now actually wait for the runs successful_runs, failed_runs = tower.wait_for_runs(runs) assert len(failed_runs) == 1 - - # Verify all successful runs + + # Verify all successful runs for run in successful_runs: assert run.status == "exited" assert run.status_group == "successful" - + # Verify all failed for run in failed_runs: assert run.status == "crashed" @@ -264,14 +275,11 @@ def test_failed_runs_in_the_list( def test_raising_an_error_during_partial_failure( - httpx_mock, - mock_api_config, - mock_run_response_factory, - create_run_object + httpx_mock, mock_api_config, mock_run_response_factory, create_run_object ): tower = mock_api_config runs = [] - + # For the first run, we're going to simulate a success. httpx_mock.add_response( method="GET", @@ -283,12 +291,14 @@ def test_raising_an_error_during_partial_failure( httpx_mock.add_response( method="GET", url=f"https://api.example.com/v1/apps/my-app/runs/1", - json=mock_run_response_factory(number=1, status="exited", status_group="successful"), + json=mock_run_response_factory( + number=1, status="exited", status_group="successful" + ), status_code=200, ) - + runs.append(create_run_object(number=1)) - + # Second run will have been a failure. httpx_mock.add_response( method="GET", @@ -300,10 +310,12 @@ def test_raising_an_error_during_partial_failure( httpx_mock.add_response( method="GET", url=f"https://api.example.com/v1/apps/my-app/runs/2", - json=mock_run_response_factory(number=2, status="crashed", status_group="failed"), + json=mock_run_response_factory( + number=2, status="crashed", status_group="failed" + ), status_code=200, ) - + runs.append(create_run_object(number=2)) # Third run was a success. @@ -313,12 +325,11 @@ def test_raising_an_error_during_partial_failure( json=mock_run_response_factory(number=3, status="pending"), status_code=200, ) - + # NOTE: We don't have a second response for this run because we'll never # get to it. runs.append(create_run_object(number=3)) - # Now actually wait for the runs with pytest.raises(RunFailedError) as excinfo: @@ -326,13 +337,10 @@ def test_raising_an_error_during_partial_failure( def test_raising_an_error_for_a_not_found_app( - httpx_mock, - mock_api_config, - mock_run_response_factory, - create_run_object + httpx_mock, mock_api_config, mock_run_response_factory, create_run_object ): tower = mock_api_config - + # Mock a 404 response with error model error_response = { "$schema": "https://api.tower.dev/v1/schemas/ErrorModel.json", @@ -340,9 +348,9 @@ def test_raising_an_error_for_a_not_found_app( "title": "Not Found", "detail": "The requested app 'non-existent-app' was not found", "instance": "https://api.example.com/v1/apps/non-existent-app", - "type": "about:blank" + "type": "about:blank", } - + httpx_mock.add_response( method="POST", url="https://api.example.com/v1/apps/non-existent-app/runs", @@ -353,18 +361,15 @@ def test_raising_an_error_for_a_not_found_app( # Attempt to run a non-existent app and verify it raises the correct error with pytest.raises(tower.exceptions.AppNotFoundError) as excinfo: tower.run_app("non-existent-app") - + assert "not found" in str(excinfo.value).lower() def test_raising_an_unexpected_error_based_on_status_code( - httpx_mock, - mock_api_config, - mock_run_response_factory, - create_run_object + httpx_mock, mock_api_config, mock_run_response_factory, create_run_object ): tower = mock_api_config - + # Mock a 404 response with error model error_response = { "$schema": "https://api.tower.dev/v1/schemas/ErrorModel.json", @@ -372,9 +377,9 @@ def test_raising_an_unexpected_error_based_on_status_code( "title": "Not Found", "detail": "The requested app 'non-existent-app' was not found", "instance": "https://api.example.com/v1/apps/non-existent-app", - "type": "about:blank" + "type": "about:blank", } - + httpx_mock.add_response( method="POST", url="https://api.example.com/v1/apps/non-existent-app/runs", @@ -385,5 +390,5 @@ def test_raising_an_unexpected_error_based_on_status_code( # Attempt to run a non-existent app and verify it raises the correct error with pytest.raises(tower.exceptions.UnknownException) as excinfo: tower.run_app("non-existent-app") - + assert "unexpected status code" in str(excinfo.value).lower() diff --git a/tests/tower/test_llms.py b/tests/tower/test_llms.py index 575a6c87..e23f032c 100644 --- a/tests/tower/test_llms.py +++ b/tests/tower/test_llms.py @@ -5,6 +5,7 @@ from tower._llms import llms, Llm from tower._context import TowerContext + @pytest.fixture def mock_ollama_context(): """Create a mock TowerContext for testing.""" @@ -15,6 +16,7 @@ def mock_ollama_context(): context.inference_router_api_key = None return context + @pytest.fixture def mock_hf_together_context(): """Create a mock TowerContext for Hugging Face Hub testing.""" @@ -25,6 +27,7 @@ def mock_hf_together_context(): context.inference_provider = "together" return context + @pytest.fixture def mock_hf_context(): """Create a mock TowerContext for Hugging Face Hub testing.""" @@ -43,6 +46,7 @@ def mock_ollama_response(): response.message.content = "This is a test response" return response + @pytest.mark.skip(reason="Not runnable right now in GH Actions") def test_llms_nameres_with_model_family_locally_1(mock_ollama_context): """ @@ -51,17 +55,18 @@ def test_llms_nameres_with_model_family_locally_1(mock_ollama_context): deepseek-r1 is a name that is used by both ollama and HF """ # Mock the TowerContext.build() to return our mock context - with patch('tower._llms.TowerContext.build', return_value=mock_ollama_context): - + with patch("tower._llms.TowerContext.build", return_value=mock_ollama_context): + # Create LLM instance based on model family name llm = llms("deepseek-r1") - + # Verify it's an Llm instance assert isinstance(llm, Llm) # Verify the resolved model was found locally assert llm.model_name.startswith("deepseek-r1:") - + + @pytest.mark.skip(reason="Not runnable right now in GH Actions") def test_llms_nameres_with_model_family_on_hugging_face_hub_1(mock_hf_together_context): """ @@ -70,19 +75,20 @@ def test_llms_nameres_with_model_family_on_hugging_face_hub_1(mock_hf_together_c deepseek-r1 is a name that is used by both ollama and HF """ # Mock the TowerContext.build() to return our mock context - with patch('tower._llms.TowerContext.build', return_value=mock_hf_together_context): - + with patch("tower._llms.TowerContext.build", return_value=mock_hf_together_context): + assert mock_hf_together_context.inference_router_api_key is not None - + # Create LLM instance llm = llms("deepseek-r1") - + # Verify it's an Llm instance assert isinstance(llm, Llm) # Verify the resolved model was found on the Hub assert llm.model_name.startswith("deepseek-ai") - + + @pytest.mark.skip(reason="Not runnable right now in GH Actions") def test_llms_nameres_with_model_family_locally_2(mock_ollama_context): """ @@ -92,17 +98,18 @@ def test_llms_nameres_with_model_family_locally_2(mock_ollama_context): Llama-3.2 is a name used on HF. """ # Mock the TowerContext.build() to return our mock context - with patch('tower._llms.TowerContext.build', return_value=mock_ollama_context): - + with patch("tower._llms.TowerContext.build", return_value=mock_ollama_context): + # Create LLM instance based on model family name llm = llms("llama3.2") - + # Verify it's an Llm instance assert isinstance(llm, Llm) # Verify the resolved model was found locally assert llm.model_name.startswith("llama3.2:") - + + @pytest.mark.skip(reason="Not runnable right now in GH Actions") def test_llms_nameres_with_model_family_on_hugging_face_hub_2(mock_hf_together_context): """ @@ -112,13 +119,13 @@ def test_llms_nameres_with_model_family_on_hugging_face_hub_2(mock_hf_together_c Llama-3.2 is a name used on HF. """ # Mock the TowerContext.build() to return our mock context - with patch('tower._llms.TowerContext.build', return_value=mock_hf_together_context): + with patch("tower._llms.TowerContext.build", return_value=mock_hf_together_context): assert mock_hf_together_context.inference_router_api_key is not None - + # Create LLM instance llm = llms("llama3.2") - + # Verify it's an Llm instance assert isinstance(llm, Llm) @@ -130,61 +137,66 @@ def test_llms_nameres_with_model_family_on_hugging_face_hub_2(mock_hf_together_c def test_llms_nameres_with_nonexistent_model_locally(mock_ollama_context): """Test llms function with a model that doesn't exist locally.""" # Mock the TowerContext.build() to return our mock context - with patch('tower._llms.TowerContext.build', return_value=mock_ollama_context): + with patch("tower._llms.TowerContext.build", return_value=mock_ollama_context): # Mock get_local_ollama_models to return empty list - with patch('tower._llms.get_local_ollama_models', return_value=[]): + with patch("tower._llms.get_local_ollama_models", return_value=[]): # Test with a non-existent model with pytest.raises(ValueError) as exc_info: llm = llms("nonexistent-model") - + # Verify the error message assert "No models found" in str(exc_info.value) @pytest.mark.skip(reason="Not runnable right now in GH Actions") -def test_llms_nameres_with_nonexistent_model_on_hugging_face_hub(mock_hf_together_context): +def test_llms_nameres_with_nonexistent_model_on_hugging_face_hub( + mock_hf_together_context, +): """Test llms function with a model that doesn't exist on huggingface hub.""" # Mock the TowerContext.build() to return our mock context - with patch('tower._llms.TowerContext.build', return_value=mock_hf_together_context): + with patch("tower._llms.TowerContext.build", return_value=mock_hf_together_context): with pytest.raises(ValueError) as exc_info: llm = llms("nonexistent-model") - + # Verify the error message assert "No models found" in str(exc_info.value) @pytest.mark.skip(reason="Not runnable right now in GH Actions") -def test_llms_nameres_with_exact_model_name_on_hugging_face_hub(mock_hf_together_context): +def test_llms_nameres_with_exact_model_name_on_hugging_face_hub( + mock_hf_together_context, +): """Test specifying the exact name of a model on Hugging Face Hub.""" # Mock the TowerContext.build() to return our mock context - with patch('tower._llms.TowerContext.build', return_value=mock_hf_together_context): - + with patch("tower._llms.TowerContext.build", return_value=mock_hf_together_context): + assert mock_hf_together_context.inference_router_api_key is not None - + # Create LLM instance llm = llms("deepseek-ai/DeepSeek-R1") - + # Verify it's an Llm instance assert isinstance(llm, Llm) # Verify the context was set assert llm.context == mock_hf_together_context - + # Verify the resolved model was found on the Hub assert llm.model_name.startswith("deepseek-ai/DeepSeek-R1") + @pytest.mark.skip(reason="Not runnable right now in GH Actions") def test_llms_nameres_with_partial_model_name_on_hugging_face_hub(mock_hf_context): """Test specifying a partial model name on Hugging Face Hub.""" # Mock the TowerContext.build() to return our mock context - with patch('tower._llms.TowerContext.build', return_value=mock_hf_context): - + with patch("tower._llms.TowerContext.build", return_value=mock_hf_context): + assert mock_hf_context.inference_router_api_key is not None - + # Create LLM instance llm = llms("google/gemma-3") - + # Verify it's an Llm instance assert isinstance(llm, Llm) @@ -194,38 +206,34 @@ def test_llms_nameres_with_partial_model_name_on_hugging_face_hub(mock_hf_contex # Verify the resolved model was found on the Hub assert llm.model_name.startswith("google/gemma-3") + @pytest.mark.skip(reason="Not runnable right now in GH Actions") def test_llms_inference_with_hugging_face_hub_1(mock_hf_together_context): """Test actual inference on a model served by together via Hugging Face Hub.""" # Mock the TowerContext.build() to return our mock context - with patch('tower._llms.TowerContext.build', return_value=mock_hf_together_context): - + with patch("tower._llms.TowerContext.build", return_value=mock_hf_together_context): + assert mock_hf_together_context.inference_router_api_key is not None - + # Create LLM instance llm = llms("deepseek-ai/DeepSeek-R1") - + # Test a simple prompt response = llm.prompt("What is your model name?") assert "DeepSeek-R1" in response - + + @pytest.mark.skip(reason="Not runnable right now in GH Actions") def test_llms_inference_locally_1(mock_ollama_context, mock_ollama_response): """Test local inference, but against a stubbed response.""" # Mock the TowerContext.build() to return our mock context - with patch('tower._llms.TowerContext.build', return_value=mock_ollama_context): + with patch("tower._llms.TowerContext.build", return_value=mock_ollama_context): # Mock the chat function to return our mock response - with patch('tower._llms.chat', return_value=mock_ollama_response): - + with patch("tower._llms.chat", return_value=mock_ollama_response): + # Create LLM instance based on model family name llm = llms("deepseek-r1") - + # Test a simple prompt response = llm.prompt("Hello, how are you?") assert response == "This is a test response" - - - - - - diff --git a/tests/tower/test_tables.py b/tests/tower/test_tables.py index db965049..6aa36f2c 100644 --- a/tests/tower/test_tables.py +++ b/tests/tower/test_tables.py @@ -564,10 +564,12 @@ def test_map_type_simple(in_memory_catalog): def test_drop_existing_table(in_memory_catalog): """Test dropping an existing table returns True.""" - schema = pa.schema([ - pa.field("id", pa.int64()), - pa.field("name", pa.string()), - ]) + schema = pa.schema( + [ + pa.field("id", pa.int64()), + pa.field("name", pa.string()), + ] + ) # Create a table first ref = tower.tables("users_to_drop", catalog=in_memory_catalog) @@ -598,13 +600,17 @@ def test_drop_nonexistent_table(in_memory_catalog): def test_drop_table_with_namespace(in_memory_catalog): """Test dropping a table with a specific namespace.""" - schema = pa.schema([ - pa.field("id", pa.int64()), - pa.field("data", pa.string()), - ]) + schema = pa.schema( + [ + pa.field("id", pa.int64()), + pa.field("data", pa.string()), + ] + ) # Create a table in a specific namespace - ref = tower.tables("test_table", catalog=in_memory_catalog, namespace="test_namespace") + ref = tower.tables( + "test_table", catalog=in_memory_catalog, namespace="test_namespace" + ) table = ref.create(schema) assert table is not None @@ -627,20 +633,21 @@ def test_drop_table_with_namespace(in_memory_catalog): def test_drop_and_recreate_table(in_memory_catalog): """Test that we can drop a table and then recreate it.""" - schema = pa.schema([ - pa.field("id", pa.int64()), - pa.field("value", pa.string()), - ]) + schema = pa.schema( + [ + pa.field("id", pa.int64()), + pa.field("value", pa.string()), + ] + ) table_name = "drop_recreate_test" ref = tower.tables(table_name, catalog=in_memory_catalog) # Create and populate the table table = ref.create(schema) - data = pa.Table.from_pylist([ - {"id": 1, "value": "first"}, - {"id": 2, "value": "second"} - ], schema=schema) + data = pa.Table.from_pylist( + [{"id": 1, "value": "first"}, {"id": 2, "value": "second"}], schema=schema + ) table.insert(data) # Verify original data @@ -653,11 +660,14 @@ def test_drop_and_recreate_table(in_memory_catalog): # Recreate the table with different data new_table = ref.create(schema) - new_data = pa.Table.from_pylist([ - {"id": 10, "value": "new_first"}, - {"id": 20, "value": "new_second"}, - {"id": 30, "value": "new_third"} - ], schema=schema) + new_data = pa.Table.from_pylist( + [ + {"id": 10, "value": "new_first"}, + {"id": 20, "value": "new_second"}, + {"id": 30, "value": "new_third"}, + ], + schema=schema, + ) new_table.insert(new_data) # Verify new data @@ -675,10 +685,12 @@ def test_drop_and_recreate_table(in_memory_catalog): def test_drop_multiple_tables(in_memory_catalog): """Test dropping multiple tables.""" - schema = pa.schema([ - pa.field("id", pa.int64()), - pa.field("name", pa.string()), - ]) + schema = pa.schema( + [ + pa.field("id", pa.int64()), + pa.field("name", pa.string()), + ] + ) # Create multiple tables table_names = ["table1", "table2", "table3"] @@ -716,14 +728,14 @@ def test_drop_with_catalog_errors(in_memory_catalog): ref = tower.tables("test_table", catalog=in_memory_catalog) # Test that NoSuchTableError returns False - with patch.object(in_memory_catalog, 'drop_table') as mock_drop: + with patch.object(in_memory_catalog, "drop_table") as mock_drop: mock_drop.side_effect = NoSuchTableError("Table not found") success = ref.drop() assert success is False mock_drop.assert_called_once() # Test that other exceptions are propagated - with patch.object(in_memory_catalog, 'drop_table') as mock_drop: + with patch.object(in_memory_catalog, "drop_table") as mock_drop: mock_drop.side_effect = RuntimeError("Catalog connection failed") with pytest.raises(RuntimeError, match="Catalog connection failed"): diff --git a/uv.lock b/uv.lock index 338f2f22..96aaa17e 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.9" resolution-markers = [ "python_full_version >= '3.10'", @@ -39,6 +39,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001, upload-time = "2024-08-06T14:37:36.958Z" }, ] +[[package]] +name = "black" +version = "24.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/0d/cc2fb42b8c50d80143221515dd7e4766995bd07c56c9a3ed30baf080b6dc/black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875", size = 645813, upload-time = "2024-10-07T19:20:50.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/f3/465c0eb5cddf7dbbfe1fecd9b875d1dcf51b88923cd2c1d7e9ab95c6336b/black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812", size = 1623211, upload-time = "2024-10-07T19:26:12.43Z" }, + { url = "https://files.pythonhosted.org/packages/df/57/b6d2da7d200773fdfcc224ffb87052cf283cec4d7102fab450b4a05996d8/black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea", size = 1457139, upload-time = "2024-10-07T19:25:06.453Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c5/9023b7673904a5188f9be81f5e129fff69f51f5515655fbd1d5a4e80a47b/black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f", size = 1753774, upload-time = "2024-10-07T19:23:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/e1/32/df7f18bd0e724e0d9748829765455d6643ec847b3f87e77456fc99d0edab/black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e", size = 1414209, upload-time = "2024-10-07T19:24:42.54Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cc/7496bb63a9b06a954d3d0ac9fe7a73f3bf1cd92d7a58877c27f4ad1e9d41/black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad", size = 1607468, upload-time = "2024-10-07T19:26:14.966Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e3/69a738fb5ba18b5422f50b4f143544c664d7da40f09c13969b2fd52900e0/black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50", size = 1437270, upload-time = "2024-10-07T19:25:24.291Z" }, + { url = "https://files.pythonhosted.org/packages/c9/9b/2db8045b45844665c720dcfe292fdaf2e49825810c0103e1191515fc101a/black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392", size = 1737061, upload-time = "2024-10-07T19:23:52.18Z" }, + { url = "https://files.pythonhosted.org/packages/a3/95/17d4a09a5be5f8c65aa4a361444d95edc45def0de887810f508d3f65db7a/black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175", size = 1423293, upload-time = "2024-10-07T19:24:41.7Z" }, + { url = "https://files.pythonhosted.org/packages/90/04/bf74c71f592bcd761610bbf67e23e6a3cff824780761f536512437f1e655/black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3", size = 1644256, upload-time = "2024-10-07T19:27:53.355Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ea/a77bab4cf1887f4b2e0bce5516ea0b3ff7d04ba96af21d65024629afedb6/black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65", size = 1448534, upload-time = "2024-10-07T19:26:44.953Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3e/443ef8bc1fbda78e61f79157f303893f3fddf19ca3c8989b163eb3469a12/black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f", size = 1761892, upload-time = "2024-10-07T19:24:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/52/93/eac95ff229049a6901bc84fec6908a5124b8a0b7c26ea766b3b8a5debd22/black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8", size = 1434796, upload-time = "2024-10-07T19:25:06.239Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a0/a993f58d4ecfba035e61fca4e9f64a2ecae838fc9f33ab798c62173ed75c/black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981", size = 1643986, upload-time = "2024-10-07T19:28:50.684Z" }, + { url = "https://files.pythonhosted.org/packages/37/d5/602d0ef5dfcace3fb4f79c436762f130abd9ee8d950fa2abdbf8bbc555e0/black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b", size = 1448085, upload-time = "2024-10-07T19:28:12.093Z" }, + { url = "https://files.pythonhosted.org/packages/47/6d/a3a239e938960df1a662b93d6230d4f3e9b4a22982d060fc38c42f45a56b/black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2", size = 1760928, upload-time = "2024-10-07T19:24:15.233Z" }, + { url = "https://files.pythonhosted.org/packages/dd/cf/af018e13b0eddfb434df4d9cd1b2b7892bab119f7a20123e93f6910982e8/black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b", size = 1436875, upload-time = "2024-10-07T19:24:42.762Z" }, + { url = "https://files.pythonhosted.org/packages/fe/02/f408c804e0ee78c367dcea0a01aedde4f1712af93b8b6e60df981e0228c7/black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd", size = 1622516, upload-time = "2024-10-07T19:29:40.629Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b9/9b706ed2f55bfb28b436225a9c57da35990c9005b90b8c91f03924454ad7/black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f", size = 1456181, upload-time = "2024-10-07T19:28:11.16Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1c/314d7f17434a5375682ad097f6f4cc0e3f414f3c95a9b1bb4df14a0f11f9/black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800", size = 1752801, upload-time = "2024-10-07T19:23:56.594Z" }, + { url = "https://files.pythonhosted.org/packages/39/a7/20e5cd9237d28ad0b31438de5d9f01c8b99814576f4c0cda1edd62caf4b0/black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7", size = 1413626, upload-time = "2024-10-07T19:24:46.133Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a7/4b27c50537ebca8bec139b872861f9d2bf501c5ec51fcf897cb924d9e264/black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d", size = 206898, upload-time = "2024-10-07T19:20:48.317Z" }, +] + [[package]] name = "cachetools" version = "5.5.2" @@ -57,6 +96,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, ] +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.2" @@ -170,6 +218,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.0" @@ -332,6 +389,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/59/a8/4677014e771ed1591a87b63a2392ce6923baf807193deef302dcfde17542/huggingface_hub-0.34.3-py3-none-any.whl", hash = "sha256:5444550099e2d86e68b2898b09e85878fbd788fc2957b506c6a79ce060e39492", size = 558847, upload-time = "2025-07-29T08:38:51.904Z" }, ] +[[package]] +name = "identify" +version = "2.6.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/c4/62963f25a678f6a050fb0505a65e9e726996171e6dbe1547f79619eefb15/identify-2.6.14.tar.gz", hash = "sha256:663494103b4f717cb26921c52f8751363dc89db64364cd836a9bf1535f53cd6a", size = 99283, upload-time = "2025-09-06T19:30:52.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/ae/2ad30f4652712c82f1c23423d79136fbce338932ad166d70c1efb86a5998/identify-2.6.14-py2.py3-none-any.whl", hash = "sha256:11a073da82212c6646b1f39bb20d4483bfb9543bd5566fec60053c4bb309bf2e", size = 99172, upload-time = "2025-09-06T19:30:51.759Z" }, +] + [[package]] name = "idna" version = "3.10" @@ -539,6 +605,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/bb/cb97418e487632eb1f6fb0f2fa86adbeec102cbf6bfa4ebfc10a8889da2c/mmh3-5.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:0daaeaedd78773b70378f2413c7d6b10239a75d955d30d54f460fb25d599942d", size = 38870, upload-time = "2025-01-25T08:39:41.986Z" }, ] +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + [[package]] name = "ollama" version = "0.5.3" @@ -583,6 +667,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -606,6 +708,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/5c/cc23daf0a228d6fadbbfc8a8c5165be33157abe5b9d72af3e127e0542857/polars-1.27.1-cp39-abi3-win_arm64.whl", hash = "sha256:4f238ee2e3c5660345cb62c0f731bbd6768362db96c058098359ecffa42c3c6c", size = 31891470, upload-time = "2025-04-11T10:25:38.74Z" }, ] +[[package]] +name = "pre-commit" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/e22c292035f1bac8b9f5237a2622305bc0304e776080b246f3df57c4ff9f/pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", size = 191678, upload-time = "2024-10-08T16:09:37.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/8f/496e10d51edd6671ebe0432e33ff800aa86775d2d147ce7d43389324a525/pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878", size = 218713, upload-time = "2024-10-08T16:09:35.726Z" }, +] + [[package]] name = "pyarrow" version = "19.0.1" @@ -1249,7 +1367,9 @@ iceberg = [ [package.dev-dependencies] dev = [ + { name = "black" }, { name = "openapi-python-client" }, + { name = "pre-commit" }, { name = "pyiceberg", extra = ["sql-sqlite"] }, { name = "pytest" }, { name = "pytest-env" }, @@ -1277,7 +1397,9 @@ provides-extras = ["ai", "iceberg", "all"] [package.metadata.requires-dev] dev = [ + { name = "black", specifier = "==24.10.0" }, { name = "openapi-python-client", specifier = "==0.24.3" }, + { name = "pre-commit", specifier = "==4.0.1" }, { name = "pyiceberg", extras = ["sql-sqlite"], specifier = "==0.9.1" }, { name = "pytest", specifier = "==8.3.5" }, { name = "pytest-env", specifier = ">=1.1.3" }, @@ -1330,3 +1452,18 @@ sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e wheels = [ { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, ] + +[[package]] +name = "virtualenv" +version = "20.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a", size = 6003808, upload-time = "2025-08-13T14:24:07.464Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload-time = "2025-08-13T14:24:05.111Z" }, +]