diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml
index db71dd183..d18229a63 100644
--- a/.github/workflows/build_and_test.yml
+++ b/.github/workflows/build_and_test.yml
@@ -37,7 +37,7 @@ jobs:
run: uv run python3 -m pytest .
- name: Check Python Types
- run: uv run pyright .
+ run: uvx ty check
- name: Build Core
run: uv build
diff --git a/.github/workflows/format_and_lint.yml b/.github/workflows/format_and_lint.yml
index ab77e6a74..e01eb7ee3 100644
--- a/.github/workflows/format_and_lint.yml
+++ b/.github/workflows/format_and_lint.yml
@@ -34,11 +34,15 @@ jobs:
run: uv python install
- name: Install the project
- run: uv tool install ruff
+ run: uv sync
- name: Lint with ruff
run: |
uvx ruff check --select I .
+
- name: Format with ruff
run: |
uvx ruff format --check .
+
+ - name: Type Check with Ty
+ run: uvx ty check
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 000000000..f7e1cd7a7
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,22 @@
+{
+ // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
+ // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
+
+ // List of extensions which should be recommended for users of this workspace.
+ "recommendations": [
+ "astral-sh.ty",
+ "charliermarsh.ruff",
+ "esbenp.prettier-vscode",
+ "ms-python.debugpy",
+ "svelte.svelte-vscode",
+ "vitest.explorer",
+ "dbaeumer.vscode-eslint",
+ "ms-python.python"
+ ],
+ // List of extensions recommended by VS Code that should not be recommended for users of this workspace.
+ "unwantedRecommendations": [
+ // We've moved to ty and ruff! They are much faster than pylance/basedpyright
+ "anysphere.cursorpyright",
+ "ms-python.vscode-pylance"
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index b3ffd1937..2bc917f6c 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -15,10 +15,7 @@
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
- "python.analysis.typeCheckingMode": "basic",
- "python.analysis.autoImportCompletions": true,
- "python.analysis.extraPaths": ["./"],
- "python.analysis.ignore": ["**/test_*.py"],
+ "python.languageServer": "None",
"files.exclude": {
"**/__pycache__": true,
"**/.pytest_cache": true,
diff --git a/AGENTS.md b/AGENTS.md
index b30d3236d..41ad205c1 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -12,7 +12,7 @@ uvx ruff check --select I
# Formatting 2:
uvx ruff format --check .
# type checking: warnings in output are acceptable, but error codes are not
-uv run pyright .
+uvx ty check
# tests:
uv run python3 -m pytest --benchmark-quiet -q .
```
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b512a0b11..1d95b8473 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -61,13 +61,12 @@ uv run ./checks.sh
### IDE Extensions
-We suggest the following extensions for VSCode/Cursor. With them, you'll get compliant formatting and linting in your IDE.
+We suggest the following extensions for VSCode/Cursor. With them, you'll get compliant formatting and linting in your IDE. They are also listed in `.vscode/extensions.json` so they will be suggested in the extension store.
- Prettier
-- Python
- Python Debugger
-- Type checking by pyright via one of: Cursor Python if using Cursor, Pylance if VSCode
-- Ruff
+- Ty - language server and type checker for Python
+- Ruff - formatter/linter for Python
- Svelte for VS Code
- Vitest
- ESLint
diff --git a/README.md b/README.md
index b2bc980fc..e8527f881 100644
--- a/README.md
+++ b/README.md
@@ -17,14 +17,14 @@
Docs
-| | |
-| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| | |
+| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| CI | [](https://github.com/Kiln-AI/kiln/actions/workflows/build_and_test.yml) [](https://github.com/Kiln-AI/kiln/actions/workflows/format_and_lint.yml) [](https://github.com/Kiln-AI/kiln/actions/workflows/build_desktop.yml) [](https://github.com/Kiln-AI/kiln/actions/workflows/web_format_lint_build.yml) [](https://github.com/Kiln-AI/Kiln/actions/workflows/build_docs.yml) |
-| Tests | [](https://github.com/Kiln-AI/kiln/actions/workflows/test_count.yml) [](https://github.com/Kiln-AI/kiln/actions/workflows/test_count.yml) |
-| Package | [](https://pypi.org/project/kiln-ai/) [](https://pypi.org/project/kiln-ai/) |
-| Meta | [](https://github.com/astral-sh/uv) [](https://github.com/astral-sh/ruff) [](https://github.com/microsoft/pyright) [](https://kiln-ai.github.io/Kiln/kiln_core_docs/index.html) |
-| Apps | [](https://getkiln.ai/download) [](https://getkiln.ai/download) [](https://getkiln.ai/download)  |
-| Connect | [](https://getkiln.ai/discord) [](https://getkiln.ai/blog) |
+| Tests | [](https://github.com/Kiln-AI/kiln/actions/workflows/test_count.yml) [](https://github.com/Kiln-AI/kiln/actions/workflows/test_count.yml) |
+| Package | [](https://pypi.org/project/kiln-ai/) [](https://pypi.org/project/kiln-ai/) |
+| Meta | [](https://github.com/astral-sh/uv) [](https://github.com/astral-sh/ruff) [](https://github.com/astral-sh/ty) [](https://kiln-ai.github.io/Kiln/kiln_core_docs/index.html) |
+| Apps | [](https://getkiln.ai/download) [](https://getkiln.ai/download) [](https://getkiln.ai/download)  |
+| Connect | [](https://getkiln.ai/discord) [](https://getkiln.ai/blog) |
[
](https://getkiln.ai/download) [
](https://docs.getkiln.ai/getting-started/quickstart)
diff --git a/app/desktop/studio_server/finetune_api.py b/app/desktop/studio_server/finetune_api.py
index 2821c0445..2cbc021c3 100644
--- a/app/desktop/studio_server/finetune_api.py
+++ b/app/desktop/studio_server/finetune_api.py
@@ -5,7 +5,11 @@
import httpx
from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
-from kiln_ai.adapters.fine_tune.base_finetune import FineTuneParameter, FineTuneStatus
+from kiln_ai.adapters.fine_tune.base_finetune import (
+ BaseFinetuneAdapter,
+ FineTuneParameter,
+ FineTuneStatus,
+)
from kiln_ai.adapters.fine_tune.dataset_formatter import (
DatasetFormat,
DatasetFormatter,
@@ -50,6 +54,19 @@
logger = logging.getLogger(__name__)
+def base_provider_from_str_id(provider_str: str) -> type[BaseFinetuneAdapter]:
+ """
+ Validates that a provider string is a valid model provider, throwing an Http error if not.
+ """
+ if provider_str not in finetune_registry: # type: ignore
+ valid_providers = list(finetune_registry.keys())
+ raise HTTPException(
+ status_code=400,
+ detail=f"Invalid provider '{provider_str}'. Valid providers are: {valid_providers}",
+ )
+ return finetune_registry[provider_str] # type: ignore
+
+
class FinetuneProviderModel(BaseModel):
"""Finetune provider model: a model a provider supports for fine-tuning"""
@@ -208,7 +225,7 @@ async def finetune(
status_code=400,
detail=f"Fine tune provider '{finetune.provider}' not found",
)
- finetune_adapter = finetune_registry[finetune.provider]
+ finetune_adapter = base_provider_from_str_id(finetune.provider)
status = await finetune_adapter(finetune).status()
return FinetuneWithStatus(finetune=finetune, status=status)
@@ -276,11 +293,7 @@ async def finetune_providers() -> list[FinetuneProvider]:
async def finetune_hyperparameters(
provider_id: str,
) -> list[FineTuneParameter]:
- if provider_id not in finetune_registry:
- raise HTTPException(
- status_code=400, detail=f"Fine tune provider '{provider_id}' not found"
- )
- finetune_adapter_class = finetune_registry[provider_id]
+ finetune_adapter_class = base_provider_from_str_id(provider_id)
return finetune_adapter_class.available_parameters()
@app.get("/api/projects/{project_id}/tasks/{task_id}/finetune_dataset_info")
@@ -358,7 +371,7 @@ async def create_finetune(
status_code=400,
detail=f"Fine tune provider '{request.provider}' not found",
)
- finetune_adapter_class = finetune_registry[request.provider]
+ finetune_adapter_class = base_provider_from_str_id(request.provider)
dataset = DatasetSplit.from_id_and_parent_path(request.dataset_id, task.path)
if dataset is None:
diff --git a/app/desktop/studio_server/provider_api.py b/app/desktop/studio_server/provider_api.py
index e931cd99f..a45f9f41c 100644
--- a/app/desktop/studio_server/provider_api.py
+++ b/app/desktop/studio_server/provider_api.py
@@ -24,8 +24,8 @@
from kiln_ai.adapters.provider_tools import provider_name_from_id, provider_warnings
from kiln_ai.datamodel.registry import all_projects
from kiln_ai.utils.config import Config
-from kiln_ai.utils.exhaustive_error import raise_exhaustive_enum_error
from pydantic import BaseModel, Field
+from typing_extensions import assert_never
logger = logging.getLogger(__name__)
@@ -45,7 +45,7 @@ async def connect_ollama(custom_ollama_url: str | None = None) -> OllamaConnecti
try:
base_url = custom_ollama_url or ollama_base_url()
tags = requests.get(base_url + "/api/tags", timeout=5).json()
- except requests.exceptions.ConnectionError:
+ except requests.ConnectionError:
raise HTTPException(
status_code=417,
detail="Failed to connect. Ensure Ollama app is running.",
@@ -308,7 +308,7 @@ async def connect_api_key(payload: dict):
content={"message": "Provider not supported for API keys"},
)
case _:
- raise_exhaustive_enum_error(typed_provider)
+ assert_never(typed_provider)
@app.post("/api/provider/disconnect_api_key")
async def disconnect_api_key(provider_id: str) -> JSONResponse:
@@ -363,8 +363,8 @@ async def disconnect_api_key(provider_id: str) -> JSONResponse:
content={"message": "Provider not supported"},
)
case _:
- # Raises a pyright error if I miss a case
- raise_exhaustive_enum_error(typed_provider_id)
+ # Raises a type error if I miss a case
+ assert_never(typed_provider_id)
return JSONResponse(
status_code=200,
@@ -812,15 +812,15 @@ async def connect_bedrock(key_data: dict):
)
except Exception as e:
# Improve error message if it's a confirmed authentication error
- if isinstance(e, litellm.exceptions.AuthenticationError):
+ if isinstance(e, litellm.AuthenticationError):
return JSONResponse(
status_code=401,
content={
"message": "Failed to connect to Bedrock. Invalid credentials."
},
)
- # If it's a bad request, it's a valid key (but the model is fake)
- if isinstance(e, litellm.exceptions.BadRequestError):
+ # It passed the authentication test, but as expected it's a bad request (expected since the model is fake). This means it worked.
+ if isinstance(e, litellm.BadRequestError):
Config.shared().bedrock_access_key = access_key
Config.shared().bedrock_secret_key = secret_key
return JSONResponse(
diff --git a/app/desktop/studio_server/test_finetune_api.py b/app/desktop/studio_server/test_finetune_api.py
index 81e0b3dc0..4ffcff98b 100644
--- a/app/desktop/studio_server/test_finetune_api.py
+++ b/app/desktop/studio_server/test_finetune_api.py
@@ -366,9 +366,8 @@ def test_get_finetune_hyperparameters_invalid_provider(client, mock_finetune_reg
response = client.get("/api/finetune/hyperparameters/invalid_provider")
assert response.status_code == 400
- assert (
- response.json()["detail"] == "Fine tune provider 'invalid_provider' not found"
- )
+ expected = "Invalid provider 'invalid_provider'. Valid providers are: "
+ assert expected in response.json()["detail"]
def test_dataset_split_type_enum():
diff --git a/checks.sh b/checks.sh
index 73f4d1ef7..bc8362a3a 100755
--- a/checks.sh
+++ b/checks.sh
@@ -13,11 +13,14 @@ cd "$(dirname "$0")"
headerStart="\n\033[4;34m=== "
headerEnd=" ===\033[0m\n"
-echo "${headerStart}Checking Python: Ruff, format, check${headerEnd}"
+echo "${headerStart}Checking Python Linting and Formatting (ruff)${headerEnd}"
# I is import sorting, F401 is unused imports
uvx ruff check --select I,F401
uvx ruff format --check .
+echo "${headerStart}Checking Python Types (ty)${headerEnd}"
+uvx ty check
+
echo "${headerStart}Checking for Misspellings${headerEnd}"
if command -v misspell >/dev/null 2>&1; then
find . -type f | grep -v "/node_modules/" | grep -v "/\." | grep -v "/dist/" | grep -v "/desktop/build/" | xargs misspell -error
@@ -43,8 +46,5 @@ else
echo "Skipping Web UI: no files changed"
fi
-echo "${headerStart}Checking Types${headerEnd}"
-pyright .
-
echo "${headerStart}Running Python Tests${headerEnd}"
python3 -m pytest --benchmark-quiet -q .
diff --git a/libs/core/kiln_ai/adapters/adapter_registry.py b/libs/core/kiln_ai/adapters/adapter_registry.py
index 90d433b96..31a1d1cb9 100644
--- a/libs/core/kiln_ai/adapters/adapter_registry.py
+++ b/libs/core/kiln_ai/adapters/adapter_registry.py
@@ -1,5 +1,7 @@
from os import getenv
+from typing_extensions import assert_never
+
from kiln_ai import datamodel
from kiln_ai.adapters.ml_model_list import ModelProviderName
from kiln_ai.adapters.model_adapters.base_adapter import AdapterConfig, BaseAdapter
@@ -13,7 +15,6 @@
)
from kiln_ai.datamodel.task import RunConfigProperties
from kiln_ai.utils.config import Config
-from kiln_ai.utils.exhaustive_error import raise_exhaustive_enum_error
def adapter_for_task(
@@ -196,4 +197,4 @@ def adapter_for_task(
"Custom openai compatible provider is not a supported core provider. It should map to an actual provider."
)
case _:
- raise_exhaustive_enum_error(core_provider_name)
+ assert_never(core_provider_name)
diff --git a/libs/core/kiln_ai/adapters/chat/chat_formatter.py b/libs/core/kiln_ai/adapters/chat/chat_formatter.py
index b95386fbb..86b2ee1c2 100644
--- a/libs/core/kiln_ai/adapters/chat/chat_formatter.py
+++ b/libs/core/kiln_ai/adapters/chat/chat_formatter.py
@@ -5,8 +5,9 @@
from dataclasses import dataclass
from typing import Dict, List, Literal, Optional
+from typing_extensions import assert_never
+
from kiln_ai.datamodel.datamodel_enums import ChatStrategy
-from kiln_ai.utils.exhaustive_error import raise_exhaustive_enum_error
COT_FINAL_ANSWER_PROMPT = "Considering the above, return a final result."
@@ -215,7 +216,7 @@ def get_chat_formatter(
case ChatStrategy.single_turn_r1_thinking:
return SingleTurnR1ThinkingFormatter(system_message, user_input)
case _:
- raise_exhaustive_enum_error(strategy)
+ assert_never(strategy)
def format_user_message(input: Dict | str) -> str:
@@ -227,7 +228,7 @@ def format_user_message(input: Dict | str) -> str:
Returns:
str: The formatted user message.
"""
- if isinstance(input, dict):
- return json.dumps(input, ensure_ascii=False)
+ if isinstance(input, str):
+ return input
- return input
+ return json.dumps(input, ensure_ascii=False)
diff --git a/libs/core/kiln_ai/adapters/eval/base_eval.py b/libs/core/kiln_ai/adapters/eval/base_eval.py
index 2e27fb0be..6d4029e35 100644
--- a/libs/core/kiln_ai/adapters/eval/base_eval.py
+++ b/libs/core/kiln_ai/adapters/eval/base_eval.py
@@ -2,13 +2,14 @@
from abc import abstractmethod
from typing import Dict
+from typing_extensions import assert_never
+
from kiln_ai.adapters.adapter_registry import adapter_for_task
from kiln_ai.adapters.ml_model_list import ModelProviderName
from kiln_ai.adapters.model_adapters.base_adapter import AdapterConfig
from kiln_ai.datamodel.eval import Eval, EvalConfig, EvalScores
from kiln_ai.datamodel.json_schema import validate_schema_with_value_error
from kiln_ai.datamodel.task import RunConfig, TaskOutputRatingType, TaskRun
-from kiln_ai.utils.exhaustive_error import raise_exhaustive_enum_error
class BaseEval:
@@ -158,7 +159,7 @@ def build_score_schema(cls, eval: Eval, allow_float_scores: bool = False) -> str
# Skip custom rating types in evals
continue
case _:
- raise_exhaustive_enum_error(output_score.type)
+ assert_never(output_score.type)
properties[output_score_json_key] = property
diff --git a/libs/core/kiln_ai/adapters/eval/registry.py b/libs/core/kiln_ai/adapters/eval/registry.py
index b4b6722e5..be8ae9b9d 100644
--- a/libs/core/kiln_ai/adapters/eval/registry.py
+++ b/libs/core/kiln_ai/adapters/eval/registry.py
@@ -1,7 +1,8 @@
+from typing_extensions import assert_never
+
from kiln_ai.adapters.eval.base_eval import BaseEval
from kiln_ai.adapters.eval.g_eval import GEval
from kiln_ai.datamodel.eval import EvalConfigType
-from kiln_ai.utils.exhaustive_error import raise_exhaustive_enum_error
def eval_adapter_from_type(eval_config_type: EvalConfigType) -> type[BaseEval]:
@@ -13,4 +14,4 @@ def eval_adapter_from_type(eval_config_type: EvalConfigType) -> type[BaseEval]:
return GEval
case _:
# type checking will catch missing cases
- raise_exhaustive_enum_error(eval_config_type)
+ assert_never(eval_config_type)
diff --git a/libs/core/kiln_ai/adapters/fine_tune/dataset_formatter.py b/libs/core/kiln_ai/adapters/fine_tune/dataset_formatter.py
index 3c5ee27b6..05317dc74 100644
--- a/libs/core/kiln_ai/adapters/fine_tune/dataset_formatter.py
+++ b/libs/core/kiln_ai/adapters/fine_tune/dataset_formatter.py
@@ -5,10 +5,11 @@
from typing import Any, Dict, Protocol
from uuid import uuid4
+from typing_extensions import assert_never
+
from kiln_ai.adapters.chat.chat_formatter import ChatMessage, get_chat_formatter
from kiln_ai.datamodel import DatasetSplit, TaskRun
from kiln_ai.datamodel.datamodel_enums import THINKING_DATA_STRATEGIES, ChatStrategy
-from kiln_ai.utils.exhaustive_error import raise_exhaustive_enum_error
class DatasetFormat(str, Enum):
@@ -93,7 +94,7 @@ def build_training_chat(
response_msg = serialize_r1_style_message(thinking, final_output)
chat_formatter.next_turn(response_msg)
case _:
- raise_exhaustive_enum_error(data_strategy)
+ assert_never(data_strategy)
return chat_formatter.messages
diff --git a/libs/core/kiln_ai/adapters/model_adapters/base_adapter.py b/libs/core/kiln_ai/adapters/model_adapters/base_adapter.py
index f43347b79..2da1dc317 100644
--- a/libs/core/kiln_ai/adapters/model_adapters/base_adapter.py
+++ b/libs/core/kiln_ai/adapters/model_adapters/base_adapter.py
@@ -256,13 +256,13 @@ def generate_run(
usage: Usage | None = None,
) -> TaskRun:
# Convert input and output to JSON strings if they are dictionaries
- input_str = (
- json.dumps(input, ensure_ascii=False) if isinstance(input, dict) else input
+ input_str: str = (
+ input if isinstance(input, str) else json.dumps(input, ensure_ascii=False)
)
output_str = (
- json.dumps(run_output.output, ensure_ascii=False)
- if isinstance(run_output.output, dict)
- else run_output.output
+ run_output.output
+ if isinstance(run_output.output, str)
+ else json.dumps(run_output.output, ensure_ascii=False)
)
# If no input source is provided, use the human data source
diff --git a/libs/core/kiln_ai/adapters/model_adapters/litellm_adapter.py b/libs/core/kiln_ai/adapters/model_adapters/litellm_adapter.py
index 6a45d5ae6..762b8d60f 100644
--- a/libs/core/kiln_ai/adapters/model_adapters/litellm_adapter.py
+++ b/libs/core/kiln_ai/adapters/model_adapters/litellm_adapter.py
@@ -1,9 +1,10 @@
import logging
-from typing import Any, Dict
+from typing import Any, Dict, cast
import litellm
from litellm.types.utils import ChoiceLogprobs, Choices, ModelResponse
from litellm.types.utils import Usage as LiteLlmUsage
+from typing_extensions import assert_never
import kiln_ai.datamodel as datamodel
from kiln_ai.adapters.ml_model_list import (
@@ -19,7 +20,6 @@
)
from kiln_ai.adapters.model_adapters.litellm_config import LiteLlmConfig
from kiln_ai.datamodel.task import run_config_from_run_config_properties
-from kiln_ai.utils.exhaustive_error import raise_exhaustive_enum_error
logger = logging.getLogger(__name__)
@@ -152,12 +152,14 @@ def adapter_name(self) -> str:
return "kiln_openai_compatible_adapter"
async def response_format_options(self) -> dict[str, Any]:
+ # shouldn't be needed - https://github.com/astral-sh/ty/issues/893
+ typed_self = cast(LiteLlmAdapter, self)
+
# Unstructured if task isn't structured
- if not self.has_structured_output():
+ if not typed_self.has_structured_output():
return {}
- structured_output_mode = self.run_config.structured_output_mode
-
+ structured_output_mode = typed_self.run_config.structured_output_mode
match structured_output_mode:
case StructuredOutputMode.json_mode:
return {"response_format": {"type": "json_object"}}
@@ -190,7 +192,7 @@ async def response_format_options(self) -> dict[str, Any]:
# See above, but this case should never happen.
raise ValueError("Structured output mode is unknown.")
case _:
- raise_exhaustive_enum_error(structured_output_mode)
+ assert_never(structured_output_mode)
def json_schema_response_format(self) -> dict[str, Any]:
output_schema = self.task().output_schema()
@@ -303,7 +305,9 @@ def litellm_model_id(self) -> str:
litellm_provider_name: str | None = None
is_custom = False
- match provider.name:
+ # cast shouldn't be needed, but type inference is not working
+ provider_name = cast(ModelProviderName, provider.name)
+ match provider_name:
case ModelProviderName.openrouter:
litellm_provider_name = "openrouter"
case ModelProviderName.openai:
@@ -337,7 +341,7 @@ def litellm_model_id(self) -> str:
case ModelProviderName.kiln_fine_tune:
is_custom = True
case _:
- raise_exhaustive_enum_error(provider.name)
+ assert_never(provider.name)
if is_custom:
if self._api_base is None:
diff --git a/libs/core/kiln_ai/adapters/model_adapters/test_litellm_adapter.py b/libs/core/kiln_ai/adapters/model_adapters/test_litellm_adapter.py
index 4e016de9a..a401dfb2b 100644
--- a/libs/core/kiln_ai/adapters/model_adapters/test_litellm_adapter.py
+++ b/libs/core/kiln_ai/adapters/model_adapters/test_litellm_adapter.py
@@ -343,13 +343,8 @@ def test_litellm_model_id_unknown_provider(config, mock_task):
mock_provider.model_id = "test-model"
with patch.object(adapter, "model_provider", return_value=mock_provider):
- with patch(
- "kiln_ai.adapters.model_adapters.litellm_adapter.raise_exhaustive_enum_error"
- ) as mock_raise_error:
- mock_raise_error.side_effect = Exception("Test error")
-
- with pytest.raises(Exception, match="Test error"):
- adapter.litellm_model_id()
+ with pytest.raises(AssertionError, match="Expected code to be unreachable"):
+ adapter.litellm_model_id()
@pytest.mark.parametrize(
diff --git a/libs/core/kiln_ai/adapters/parsers/parser_registry.py b/libs/core/kiln_ai/adapters/parsers/parser_registry.py
index 30d539ce4..314ad4167 100644
--- a/libs/core/kiln_ai/adapters/parsers/parser_registry.py
+++ b/libs/core/kiln_ai/adapters/parsers/parser_registry.py
@@ -1,7 +1,8 @@
+from typing_extensions import assert_never
+
from kiln_ai.adapters.ml_model_list import ModelParserID
from kiln_ai.adapters.parsers.base_parser import BaseParser
from kiln_ai.adapters.parsers.r1_parser import R1ThinkingParser
-from kiln_ai.utils.exhaustive_error import raise_exhaustive_enum_error
def model_parser_from_id(parser_id: ModelParserID | None) -> BaseParser:
@@ -16,4 +17,4 @@ def model_parser_from_id(parser_id: ModelParserID | None) -> BaseParser:
case ModelParserID.optional_r1_thinking:
return R1ThinkingParser(allow_missing_thinking=True)
case _:
- raise_exhaustive_enum_error(parser_id)
+ assert_never(parser_id)
diff --git a/libs/core/kiln_ai/adapters/parsers/request_formatters.py b/libs/core/kiln_ai/adapters/parsers/request_formatters.py
index c6a129249..a9d79cce2 100644
--- a/libs/core/kiln_ai/adapters/parsers/request_formatters.py
+++ b/libs/core/kiln_ai/adapters/parsers/request_formatters.py
@@ -1,8 +1,9 @@
import json
from typing import Dict, Protocol
+from typing_extensions import assert_never
+
from kiln_ai.adapters.ml_model_list import ModelFormatterID
-from kiln_ai.utils.exhaustive_error import raise_exhaustive_enum_error
class RequestFormatter(Protocol):
@@ -37,4 +38,4 @@ def request_formatter_from_id(
case ModelFormatterID.qwen3_style_no_think:
return Qwen3StyleNoThinkFormatter()
case _:
- raise_exhaustive_enum_error(formatter_id)
+ assert_never(formatter_id)
diff --git a/libs/core/kiln_ai/adapters/parsers/test_parser_registry.py b/libs/core/kiln_ai/adapters/parsers/test_parser_registry.py
index 9ec9c38f7..7f00f8a9b 100644
--- a/libs/core/kiln_ai/adapters/parsers/test_parser_registry.py
+++ b/libs/core/kiln_ai/adapters/parsers/test_parser_registry.py
@@ -13,10 +13,10 @@ def test_model_parser_from_id_invalid():
class MockModelParserID:
mock_value = "mock_value"
- with pytest.raises(ValueError) as exc_info:
+ with pytest.raises(AssertionError) as exc_info:
model_parser_from_id(MockModelParserID.mock_value) # type: ignore
- assert "Unhandled enum value" in str(exc_info.value)
+ assert "Expected code to be unreachable" in str(exc_info.value)
@pytest.mark.parametrize(
diff --git a/libs/core/kiln_ai/adapters/parsers/test_request_formatters.py b/libs/core/kiln_ai/adapters/parsers/test_request_formatters.py
index acc494dc3..c59487289 100644
--- a/libs/core/kiln_ai/adapters/parsers/test_request_formatters.py
+++ b/libs/core/kiln_ai/adapters/parsers/test_request_formatters.py
@@ -72,5 +72,5 @@ def test_request_formatter_factory():
def test_request_formatter_factory_invalid_id():
# Test with an invalid enum value by using a string that doesn't exist in the enum
- with pytest.raises(ValueError, match="Unhandled enum value"):
+ with pytest.raises(AssertionError, match="Expected code to be unreachable"):
request_formatter_from_id("invalid_formatter_id") # type: ignore
diff --git a/libs/core/kiln_ai/adapters/prompt_builders.py b/libs/core/kiln_ai/adapters/prompt_builders.py
index 398c3a4e0..e0546abeb 100644
--- a/libs/core/kiln_ai/adapters/prompt_builders.py
+++ b/libs/core/kiln_ai/adapters/prompt_builders.py
@@ -1,7 +1,8 @@
from abc import ABCMeta, abstractmethod
+from typing_extensions import assert_never
+
from kiln_ai.datamodel import PromptGenerators, PromptId, Task, TaskRun
-from kiln_ai.utils.exhaustive_error import raise_exhaustive_enum_error
class BasePromptBuilder(metaclass=ABCMeta):
@@ -425,4 +426,4 @@ def prompt_builder_from_id(prompt_id: PromptId, task: Task) -> BasePromptBuilder
return MultiShotChainOfThoughtPromptBuilder(task)
case _:
# Type checking will find missing cases
- raise_exhaustive_enum_error(typed_prompt_generator)
+ assert_never(typed_prompt_generator)
diff --git a/libs/core/kiln_ai/adapters/provider_tools.py b/libs/core/kiln_ai/adapters/provider_tools.py
index 3a0460ac1..2dc71331e 100644
--- a/libs/core/kiln_ai/adapters/provider_tools.py
+++ b/libs/core/kiln_ai/adapters/provider_tools.py
@@ -22,7 +22,6 @@
from kiln_ai.datamodel.registry import project_from_id
from kiln_ai.datamodel.task import RunConfigProperties
from kiln_ai.utils.config import Config
-from kiln_ai.utils.exhaustive_error import raise_exhaustive_enum_error
logger = logging.getLogger(__name__)
@@ -385,8 +384,8 @@ def provider_name_from_id(id: str) -> str:
case ModelProviderName.together_ai:
return "Together AI"
case _:
- # triggers pyright warning if I miss a case
- raise_exhaustive_enum_error(enum_id)
+ # triggers type error if I miss a case
+ assert_never(enum_id)
return "Unknown provider: " + id
diff --git a/libs/core/kiln_ai/datamodel/basemodel.py b/libs/core/kiln_ai/datamodel/basemodel.py
index a8750b2ea..8db3ad528 100644
--- a/libs/core/kiln_ai/datamodel/basemodel.py
+++ b/libs/core/kiln_ai/datamodel/basemodel.py
@@ -416,7 +416,7 @@ class KilnParentModel(KilnBaseModel, metaclass=ABCMeta):
def _create_child_method(
cls, relationship_name: str, child_class: Type[KilnParentedModel]
):
- def child_method(self, readonly: bool = False) -> list[child_class]:
+ def child_method(self, readonly: bool = False) -> list[child_class]: # type: ignore
return child_class.all_children_of_parent_path(self.path, readonly=readonly)
child_method.__name__ = relationship_name
diff --git a/libs/core/kiln_ai/datamodel/eval.py b/libs/core/kiln_ai/datamodel/eval.py
index 9f04213b6..1558d4fa3 100644
--- a/libs/core/kiln_ai/datamodel/eval.py
+++ b/libs/core/kiln_ai/datamodel/eval.py
@@ -1,9 +1,9 @@
import json
from enum import Enum
-from typing import TYPE_CHECKING, Any, Dict, List, Union
+from typing import TYPE_CHECKING, Any, Dict, List, Union, cast
from pydantic import BaseModel, Field, model_validator
-from typing_extensions import Self
+from typing_extensions import Self, assert_never
from kiln_ai.datamodel.basemodel import (
ID_TYPE,
@@ -15,7 +15,6 @@
from kiln_ai.datamodel.dataset_filters import DatasetFilterId
from kiln_ai.datamodel.json_schema import string_to_json_key
from kiln_ai.datamodel.task_run import Usage
-from kiln_ai.utils.exhaustive_error import raise_exhaustive_enum_error
if TYPE_CHECKING:
from kiln_ai.datamodel.task import Task
@@ -136,12 +135,16 @@ def validate_eval_run_types(self) -> Self:
@model_validator(mode="after")
def validate_scores(self) -> Self:
+ # shouldn't be needed - https://github.com/astral-sh/ty/issues/893
+ typed_self = cast(EvalRun, self)
# We're checking the scores have the expected keys from the grand-parent eval
- if self.scores is None or len(self.scores) == 0:
+ if typed_self.scores is None or len(typed_self.scores) == 0:
raise ValueError("scores are required, and must have at least one score.")
- parent_eval_config = self.parent_eval_config()
- eval = parent_eval_config.parent_eval() if parent_eval_config else None
+ parent_eval_config = typed_self.parent_eval_config()
+ if not parent_eval_config:
+ return self
+ eval = parent_eval_config.parent_eval()
if not eval:
# Can't validate without the grand-parent eval, allow it to be validated later
return self
@@ -190,8 +193,8 @@ def validate_scores(self) -> Self:
f"Custom scores are not supported in evaluators. '{output_score.name}' was set to a custom score."
)
case _:
- # Catch missing cases
- raise_exhaustive_enum_error(output_score.type)
+ # This should never happen since all enum cases are handled above
+ assert_never(output_score.type)
return self
diff --git a/libs/core/kiln_ai/datamodel/task_output.py b/libs/core/kiln_ai/datamodel/task_output.py
index cee7f459c..6d6687d34 100644
--- a/libs/core/kiln_ai/datamodel/task_output.py
+++ b/libs/core/kiln_ai/datamodel/task_output.py
@@ -3,13 +3,12 @@
from typing import TYPE_CHECKING, Dict, List, Type, Union
from pydantic import BaseModel, Field, ValidationInfo, model_validator
-from typing_extensions import Self
+from typing_extensions import Self, assert_never
from kiln_ai.datamodel.basemodel import ID_TYPE, KilnBaseModel
from kiln_ai.datamodel.datamodel_enums import TaskOutputRatingType
from kiln_ai.datamodel.json_schema import validate_schema_with_value_error
from kiln_ai.datamodel.strict_mode import strict_mode
-from kiln_ai.utils.exhaustive_error import raise_exhaustive_enum_error
if TYPE_CHECKING:
from kiln_ai.datamodel.task import Task
@@ -42,7 +41,7 @@ def normalize_rating(rating: float, rating_type: TaskOutputRatingType) -> float:
case TaskOutputRatingType.custom:
raise ValueError("Custom rating type can not be normalized")
case _:
- raise_exhaustive_enum_error(rating_type)
+ assert_never(rating_type)
class TaskOutputRating(KilnBaseModel):
diff --git a/libs/core/kiln_ai/utils/exhaustive_error.py b/libs/core/kiln_ai/utils/exhaustive_error.py
deleted file mode 100644
index 6bfdf648b..000000000
--- a/libs/core/kiln_ai/utils/exhaustive_error.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from typing import NoReturn
-
-
-# Weird trick, but passing a enum to NoReturn triggers the type checker to complain unless all values are handled.
-def raise_exhaustive_enum_error(value: NoReturn) -> NoReturn:
- raise ValueError(f"Unhandled enum value: {value}")
diff --git a/libs/core/pyproject.toml b/libs/core/pyproject.toml
index 0d62a307b..7788c1e44 100644
--- a/libs/core/pyproject.toml
+++ b/libs/core/pyproject.toml
@@ -37,11 +37,11 @@ dependencies = [
[dependency-groups]
dev = [
"isort>=5.13.2",
- "pyright==1.1.376",
"pytest-asyncio>=0.24.0",
"pytest>=8.3.3",
"python-dotenv>=1.0.1",
"ruff>=0.9.0",
+ "ty>=0.0.1a11",
]
[build-system]
diff --git a/libs/core/uv.lock b/libs/core/uv.lock
index 616c70852..c4964b7a0 100644
--- a/libs/core/uv.lock
+++ b/libs/core/uv.lock
@@ -771,7 +771,6 @@ dependencies = [
[package.dev-dependencies]
dev = [
{ name = "isort" },
- { name = "pyright" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "python-dotenv" },
@@ -796,7 +795,6 @@ requires-dist = [
[package.metadata.requires-dev]
dev = [
{ name = "isort", specifier = ">=5.13.2" },
- { name = "pyright", specifier = ">=1.1.387" },
{ name = "pytest", specifier = ">=8.3.3" },
{ name = "pytest-asyncio", specifier = ">=0.24.0" },
{ name = "python-dotenv", specifier = ">=1.0.1" },
@@ -1450,18 +1448,7 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 },
]
-[[package]]
-name = "pyright"
-version = "1.1.387"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "nodeenv" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/c2/32/e7187478d3105d6d7edc9b754d56472ee06557c25cc404911288fee1796a/pyright-1.1.387.tar.gz", hash = "sha256:577de60224f7fe36505d5b181231e3a395d427b7873be0bbcaa962a29ea93a60", size = 21939 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a0/18/c497df36641b0572f5bd59ae147b08ccaa6b8086397d50e1af97cc2ddcf6/pyright-1.1.387-py3-none-any.whl", hash = "sha256:6a1f495a261a72e12ad17e20d1ae3df4511223c773b19407cfa006229b1b08a5", size = 18577 },
-]
+
[[package]]
name = "pytest"
diff --git a/libs/server/pyproject.toml b/libs/server/pyproject.toml
index bc4c76687..242b04dd9 100644
--- a/libs/server/pyproject.toml
+++ b/libs/server/pyproject.toml
@@ -30,11 +30,11 @@ dependencies = [
[dependency-groups]
dev = [
"isort>=5.13.2",
- "pyright==1.1.376",
"pytest-asyncio>=0.24.0",
"pytest>=8.3.3",
"python-dotenv>=1.0.1",
"ruff>=0.9.0",
+ "ty>=0.0.1a11",
]
diff --git a/libs/server/uv.lock b/libs/server/uv.lock
index 41d82c930..a8e84f681 100644
--- a/libs/server/uv.lock
+++ b/libs/server/uv.lock
@@ -799,7 +799,6 @@ requires-dist = [
[package.metadata.requires-dev]
dev = [
{ name = "isort", specifier = ">=5.13.2" },
- { name = "pyright", specifier = ">=1.1.387" },
{ name = "pytest", specifier = ">=8.3.3" },
{ name = "pytest-asyncio", specifier = ">=0.24.0" },
{ name = "python-dotenv", specifier = ">=1.0.1" },
@@ -821,7 +820,6 @@ dependencies = [
[package.dev-dependencies]
dev = [
{ name = "isort" },
- { name = "pyright" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "python-dotenv" },
@@ -840,7 +838,6 @@ requires-dist = [
[package.metadata.requires-dev]
dev = [
{ name = "isort", specifier = ">=5.13.2" },
- { name = "pyright", specifier = ">=1.1.387" },
{ name = "pytest", specifier = ">=8.3.3" },
{ name = "pytest-asyncio", specifier = ">=0.24.0" },
{ name = "python-dotenv", specifier = ">=1.0.1" },
@@ -1494,19 +1491,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 },
]
-[[package]]
-name = "pyright"
-version = "1.1.387"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "nodeenv" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/c2/32/e7187478d3105d6d7edc9b754d56472ee06557c25cc404911288fee1796a/pyright-1.1.387.tar.gz", hash = "sha256:577de60224f7fe36505d5b181231e3a395d427b7873be0bbcaa962a29ea93a60", size = 21939 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a0/18/c497df36641b0572f5bd59ae147b08ccaa6b8086397d50e1af97cc2ddcf6/pyright-1.1.387-py3-none-any.whl", hash = "sha256:6a1f495a261a72e12ad17e20d1ae3df4511223c773b19407cfa006229b1b08a5", size = 18577 },
-]
-
[[package]]
name = "pytest"
version = "8.3.3"
diff --git a/pyproject.toml b/pyproject.toml
index dc02656c7..ae18b5d3f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,12 +15,12 @@ dependencies = [
[dependency-groups]
dev = [
"isort>=5.13.2",
- "pyright==1.1.376",
"pytest-asyncio>=0.24.0",
"pytest>=8.3.3",
"pytest-xdist>=3.5",
"python-dotenv>=1.0.1",
"ruff>=0.9.0",
+ "ty>=0.0.1a16",
]
@@ -39,14 +39,12 @@ kiln-server = { workspace = true }
kiln-studio-desktop = { workspace = true }
kiln-ai = { workspace = true }
-[tool.pyright]
-strictListInference = true
-reportMissingTypeArgument = true
-
-
[tool.ruff]
exclude = [
]
+[tool.ty.src]
+exclude = ["**/test_*.py", "app/desktop/build/**", "app/web_ui/**", "**/.venv"]
+
[tool.pytest.ini_options]
addopts="-n auto"
diff --git a/pyrightconfig.json b/pyrightconfig.json
deleted file mode 100644
index ece7acd31..000000000
--- a/pyrightconfig.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "exclude": ["**/test_*.py", "app/desktop/build/**", "app/web_ui/**", "**/.venv"],
- "typeCheckingMode": "basic",
- "autoImportCompletions": true,
- "extraPaths": ["./"]
-}
diff --git a/uv.lock b/uv.lock
index eab4b1c6c..531972076 100644
--- a/uv.lock
+++ b/uv.lock
@@ -989,11 +989,11 @@ dependencies = [
[package.dev-dependencies]
dev = [
{ name = "isort" },
- { name = "pyright" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "python-dotenv" },
{ name = "ruff" },
+ { name = "ty" },
]
[package.metadata]
@@ -1017,11 +1017,11 @@ requires-dist = [
[package.metadata.requires-dev]
dev = [
{ name = "isort", specifier = ">=5.13.2" },
- { name = "pyright", specifier = "==1.1.376" },
{ name = "pytest", specifier = ">=8.3.3" },
{ name = "pytest-asyncio", specifier = ">=0.24.0" },
{ name = "python-dotenv", specifier = ">=1.0.1" },
{ name = "ruff", specifier = ">=0.9.0" },
+ { name = "ty", specifier = ">=0.0.1a11" },
]
[[package]]
@@ -1038,12 +1038,12 @@ dependencies = [
[package.dev-dependencies]
dev = [
{ name = "isort" },
- { name = "pyright" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-xdist" },
{ name = "python-dotenv" },
{ name = "ruff" },
+ { name = "ty" },
]
[package.metadata]
@@ -1057,12 +1057,12 @@ requires-dist = [
[package.metadata.requires-dev]
dev = [
{ name = "isort", specifier = ">=5.13.2" },
- { name = "pyright", specifier = "==1.1.376" },
{ name = "pytest", specifier = ">=8.3.3" },
{ name = "pytest-asyncio", specifier = ">=0.24.0" },
{ name = "pytest-xdist", specifier = ">=3.5" },
{ name = "python-dotenv", specifier = ">=1.0.1" },
{ name = "ruff", specifier = ">=0.9.0" },
+ { name = "ty", specifier = ">=0.0.1a16" },
]
[[package]]
@@ -1082,11 +1082,11 @@ dependencies = [
[package.dev-dependencies]
dev = [
{ name = "isort" },
- { name = "pyright" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "python-dotenv" },
{ name = "ruff" },
+ { name = "ty" },
]
[package.metadata]
@@ -1103,11 +1103,11 @@ requires-dist = [
[package.metadata.requires-dev]
dev = [
{ name = "isort", specifier = ">=5.13.2" },
- { name = "pyright", specifier = "==1.1.376" },
{ name = "pytest", specifier = ">=8.3.3" },
{ name = "pytest-asyncio", specifier = ">=0.24.0" },
{ name = "python-dotenv", specifier = ">=1.0.1" },
{ name = "ruff", specifier = ">=0.9.0" },
+ { name = "ty", specifier = ">=0.0.1a11" },
]
[[package]]
@@ -1316,15 +1316,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 },
]
-[[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 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 },
-]
-
[[package]]
name = "numpy"
version = "2.2.3"
@@ -1831,18 +1822,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/7a/78b512061af37a4466607143a9876192f04c5810b16e4cb097fbbfa02dc5/pyobjc_framework_Quartz-10.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:00a0933267e3a46ea4afcc35d117b2efb920f06de797fa66279c52e7057e3590", size = 226586 },
]
-[[package]]
-name = "pyright"
-version = "1.1.376"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "nodeenv" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/dc/d0/c17d5b6ccdebe9f8becd75562d7c960727ec34a463ff363cb03f2624bc38/pyright-1.1.376.tar.gz", hash = "sha256:bffd63b197cd0810395bb3245c06b01f95a85ddf6bfa0e5644ed69c841e954dd", size = 17492 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/94/fd/35f6b518ff93bc09974ba88db5cacaf6ae3f231c10e25f7c069bb5c0af4e/pyright-1.1.376-py3-none-any.whl", hash = "sha256:0f2473b12c15c46b3207f0eec224c3cea2bdc07cd45dd4a037687cbbca0fbeff", size = 18222 },
-]
-
[[package]]
name = "pystray"
version = "0.19.5"
@@ -2502,6 +2481,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/41/73/02342de9c2d20922115f787e101527b831c0cffd2105c946c4a4826bcfd4/tqdm-4.66.6-py3-none-any.whl", hash = "sha256:223e8b5359c2efc4b30555531f09e9f2f3589bcd7fdd389271191031b49b7a63", size = 78326 },
]
+[[package]]
+name = "ty"
+version = "0.0.1a16"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ca/62/f021cdbdda9dbd553be4b841c2e9329ecd3ddc630a17c1ab5179832fbca8/ty-0.0.1a16.tar.gz", hash = "sha256:9ade26904870dc9bd988e58bad4382857f75ae05edb682ee0ba2f26fcc2d4c0f", size = 3961822 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c8/8d/fe6a4161ee493005d2d59bb02c37a746723eb65e45740cc8aa2367da5ddb/ty-0.0.1a16-py3-none-linux_armv6l.whl", hash = "sha256:dfb55d28df78ca40f8aff91ec3ae01f4b7bc23aa04c72ace7ec00fbc5e0468c0", size = 7840414 },
+ { url = "https://files.pythonhosted.org/packages/88/85/70bef8b680216276e941480a0bac3d00b89d1d64d4e281bd3daaa85fc5ed/ty-0.0.1a16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5a0e9917efadf2ec173ee755db3653243b64fa8b26fa4d740dea68e969a99898", size = 7979261 },
+ { url = "https://files.pythonhosted.org/packages/5a/07/400b56734c7b8a29ea1d6927f36dd75bf263c8a223ea4bd05e25bdbbc8a2/ty-0.0.1a16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9253cb8b5c4052337b1600f581ecd8e6929e635a07ec9e8dc5cc2fa4008e6b3b", size = 7567959 },
+ { url = "https://files.pythonhosted.org/packages/02/c9/095cb09e33a4d547a71f4f698d09f3f9edc92746e029945fe8412f59d421/ty-0.0.1a16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:374c059e184f8abc969e07965355ddbbf7205a713721d3867ee42f976249c9ac", size = 7697398 },
+ { url = "https://files.pythonhosted.org/packages/48/39/e2ce5b1151dfc80659486f74113972bc994c39b8f7f39084b271d03c4b04/ty-0.0.1a16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c5364c6d1a1a3d5b8e765a730303f8e07094ab9e63682aa82f73755d92749852", size = 7681504 },
+ { url = "https://files.pythonhosted.org/packages/a3/44/5c1158bd3e2e939e5b0ddb6b15c8e158870fa44915b5535909f83d4bd4ed/ty-0.0.1a16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f201ff0ab3267123b9e42cc8584a193aa76e6e0865003d1b0a41bd025f08229e", size = 8551057 },
+ { url = "https://files.pythonhosted.org/packages/0d/20/2564cd89f1c06ce329ab25d91ce457d2dc00d2559c519111874811250442/ty-0.0.1a16-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:57f14207d88043ba27f4b84d84dfdaa1bfbcc5170d5f50814d2997cbc3d75366", size = 8999239 },
+ { url = "https://files.pythonhosted.org/packages/41/5f/64b74a8aaa080267c71a9d591b980a9c915b2439034b9075520c56ef1e4b/ty-0.0.1a16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:950c45e1d6c58e61ad77ed5d2d04f091e44b0d13e6d5d79143bb81078ab526b1", size = 8638649 },
+ { url = "https://files.pythonhosted.org/packages/67/c7/80ad1c11d896cd1a52f24f0b3660ed368187ba152337b8f18b2d0591bd02/ty-0.0.1a16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad133d0eac5291d738e40052df98ca9f194e0f0433d6086a4890fd6733217969", size = 8443175 },
+ { url = "https://files.pythonhosted.org/packages/94/6c/eb3c214a44bd0f6ad359c1ce28de62cbaecfd5823553a35b0163e9f3e738/ty-0.0.1a16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e59f877ef8b967c06173a7a663271a6e66edb049f0db00f7873be5e41d61d5b", size = 8278214 },
+ { url = "https://files.pythonhosted.org/packages/18/ab/f44474a526f3a1ac770c8839a23fac51f93a4ad5e6ec2770d74e20bd5684/ty-0.0.1a16-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4e973b8cb2c382263aaf77a40889ad236bd06ddca671cc973f9e33e8e02f0af1", size = 7591502 },
+ { url = "https://files.pythonhosted.org/packages/b9/d3/825975f1277b097883ed3428c23e0e8f67ed4fffd25d00b8b60650b663cb/ty-0.0.1a16-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4a82d9c4b76a73aff60cab93b71f2dd83952c2eb68a86578e1db56aee8f7e338", size = 7715602 },
+ { url = "https://files.pythonhosted.org/packages/f8/f0/2805b4172c46b832c2efa368731d4aa4af0aa35ce120a4726ccdb3b102a0/ty-0.0.1a16-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7993f48def35f1707a2dc675bf7d08906cc5f26204b0b479746664301eda15b9", size = 8156780 },
+ { url = "https://files.pythonhosted.org/packages/25/a5/f47c11a3dc52b3e148aaaa2bf7c37ea75998cfd50ad5f4b56fd2cc79c708/ty-0.0.1a16-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d9887ec65984e7dbf3b5e906ef44e8f47ff5351c7ac04d49e793b324d744050f", size = 8350253 },
+ { url = "https://files.pythonhosted.org/packages/1e/e4/498c0bed38385d0c8babfe5707fe157700ae698d77dd9a1a8ffaaa97baea/ty-0.0.1a16-py3-none-win32.whl", hash = "sha256:4113a176a8343196d73145668460873d26ccef8766ff4e5287eec2622ce8754d", size = 7460041 },
+ { url = "https://files.pythonhosted.org/packages/af/9e/5a8a690a5542405fd20cab6b0aa97a5af76de1e39582de545fac48e53f3a/ty-0.0.1a16-py3-none-win_amd64.whl", hash = "sha256:508ba4c50bc88f1a7c730d40f28d6c679696ee824bc09630c7c6763911de862a", size = 8074666 },
+ { url = "https://files.pythonhosted.org/packages/dc/53/2a2eb8cc22b3e12d2040ed78d98842d0dddfa593d824b7ff60e30afe6f41/ty-0.0.1a16-py3-none-win_arm64.whl", hash = "sha256:36f53e430b5e0231d6b6672160c981eaf7f9390162380bcd1096941b2c746b5d", size = 7612948 },
+]
+
[[package]]
name = "typer"
version = "0.15.2"