diff --git a/dagster_sqlmesh/asset.py b/dagster_sqlmesh/asset.py index aada67e..8f614bc 100644 --- a/dagster_sqlmesh/asset.py +++ b/dagster_sqlmesh/asset.py @@ -1,11 +1,7 @@ import logging import typing as t -from dagster import ( - AssetsDefinition, - RetryPolicy, - multi_asset, -) +from dagster import AssetsDefinition, RetryPolicy, multi_asset from dagster_sqlmesh.controller import DagsterSQLMeshController from dagster_sqlmesh.translator import SQLMeshDagsterTranslator diff --git a/dagster_sqlmesh/conftest.py b/dagster_sqlmesh/conftest.py index 3a371a2..9556b09 100644 --- a/dagster_sqlmesh/conftest.py +++ b/dagster_sqlmesh/conftest.py @@ -4,10 +4,7 @@ import sys import tempfile import typing as t -from dataclasses import dataclass -import duckdb -import polars import pytest from sqlmesh.core.config import ( Config as SQLMeshConfig, @@ -15,14 +12,9 @@ GatewayConfig, ModelDefaultsConfig, ) -from sqlmesh.core.console import get_console -from sqlmesh.utils.date import TimeLike from dagster_sqlmesh.config import SQLMeshContextConfig -from dagster_sqlmesh.console import ConsoleEvent -from dagster_sqlmesh.controller.base import PlanOptions, RunOptions -from dagster_sqlmesh.controller.dagster import DagsterSQLMeshController -from dagster_sqlmesh.events import ConsoleRecorder +from dagster_sqlmesh.testing import SQLMeshTestContext logger = logging.getLogger(__name__) @@ -50,110 +42,6 @@ def sample_sqlmesh_project() -> t.Iterator[str]: yield str(project_dir) -@dataclass -class SQLMeshTestContext: - """A test context for running SQLMesh""" - - db_path: str - context_config: SQLMeshContextConfig - - def create_controller( - self, enable_debug_console: bool = False - ) -> DagsterSQLMeshController: - console = None - if enable_debug_console: - console = get_console() - return DagsterSQLMeshController.setup_with_config( - self.context_config, debug_console=console - ) - - def query(self, *args: t.Any, **kwargs: t.Any) -> t.Any: - conn = duckdb.connect(self.db_path) - return conn.sql(*args, **kwargs).fetchall() - - def initialize_test_source(self) -> None: - conn = duckdb.connect(self.db_path) - conn.sql( - """ - CREATE SCHEMA sources; - """ - ) - conn.sql( - """ - CREATE TABLE sources.test_source (id INTEGER, name VARCHAR); - """ - ) - conn.sql( - """ - INSERT INTO sources.test_source (id, name) - VALUES (1, 'abc'), (2, 'def'); - """ - ) - conn.close() - - def append_to_test_source(self, df: polars.DataFrame): - logger.debug("appending data to the test source") - conn = duckdb.connect(self.db_path) - conn.sql( - """ - INSERT INTO sources.test_source - SELECT * FROM df - """ - ) - - def plan_and_run( - self, - *, - environment: str, - execution_time: TimeLike | None = None, - enable_debug_console: bool = False, - start: TimeLike | None = None, - end: TimeLike | None = None, - select_models: list[str] | None = None, - restate_selected: bool = False, - skip_run: bool = False, - ) -> t.Iterator[ConsoleEvent] | None: - """Runs plan and run on SQLMesh with the given configuration and record all of the generated events. - - Args: - environment (str): The environment to run SQLMesh in. - execution_time (TimeLike, optional): The execution timestamp for the run. Defaults to None. - enable_debug_console (bool, optional): Flag to enable debug console. Defaults to False. - start (TimeLike, optional): Start time for the run interval. Defaults to None. - end (TimeLike, optional): End time for the run interval. Defaults to None. - restate_models (List[str], optional): List of models to restate. Defaults to None. - - Returns: - None: The function records events to a debug console but doesn't return anything. - - Note: - TimeLike can be any time-like object that SQLMesh accepts (datetime, str, etc.). - The function creates a controller and recorder to capture all SQLMesh events during execution. - """ - controller = self.create_controller(enable_debug_console=enable_debug_console) - recorder = ConsoleRecorder() - # controller.add_event_handler(ConsoleRecorder()) - plan_options = PlanOptions( - enable_preview=True, - ) - run_options = RunOptions() - if execution_time: - plan_options["execution_time"] = execution_time - run_options["execution_time"] = execution_time - - for event in controller.plan_and_run( - environment, - start=start, - end=end, - select_models=select_models, - restate_selected=restate_selected, - plan_options=plan_options, - run_options=run_options, - skip_run=skip_run, - ): - recorder(event) - - @pytest.fixture def sample_sqlmesh_test_context( sample_sqlmesh_project: str, diff --git a/dagster_sqlmesh/console.py b/dagster_sqlmesh/console.py index d8cdce5..e884573 100644 --- a/dagster_sqlmesh/console.py +++ b/dagster_sqlmesh/console.py @@ -12,11 +12,7 @@ from sqlmesh.core.linter.rule import RuleViolation from sqlmesh.core.model import Model from sqlmesh.core.plan import EvaluatablePlan, PlanBuilder -from sqlmesh.core.snapshot import ( - Snapshot, - SnapshotChangeCategory, - SnapshotInfoLike, -) +from sqlmesh.core.snapshot import Snapshot, SnapshotChangeCategory, SnapshotInfoLike from sqlmesh.core.table_diff import RowDiff, SchemaDiff, TableDiff from sqlmesh.utils.concurrency import NodeExecutionFailedError diff --git a/dagster_sqlmesh/controller/base.py b/dagster_sqlmesh/controller/base.py index 00137b4..6f4cbf1 100644 --- a/dagster_sqlmesh/controller/base.py +++ b/dagster_sqlmesh/controller/base.py @@ -128,9 +128,7 @@ def __init__( self.logger = logger @contextmanager - def console_context( - self, handler: ConsoleEventHandler - ) -> t.Iterator[None]: + def console_context(self, handler: ConsoleEventHandler) -> t.Iterator[None]: id = self.console.add_handler(handler) yield self.console.remove_handler(id) @@ -224,9 +222,7 @@ def run_sqlmesh_thread( thread.join() - def run( - self, **run_options: t.Unpack[RunOptions] - ) -> t.Iterator[ConsoleEvent]: + def run(self, **run_options: t.Unpack[RunOptions]) -> t.Iterator[ConsoleEvent]: """Executes sqlmesh run in a separate thread with console output. This method executes SQLMesh operations in a dedicated thread while @@ -295,7 +291,7 @@ def plan_and_run( end: TimeLike | None = None, categorizer: SnapshotCategorizer | None = None, default_catalog: str | None = None, - plan_options: PlanOptions | None= None, + plan_options: PlanOptions | None = None, run_options: RunOptions | None = None, skip_run: bool = False, ) -> t.Iterator[ConsoleEvent]: @@ -309,11 +305,11 @@ def plan_and_run( if plan_options.get("select_models") or run_options.get("select_models"): raise ValueError( - "select_models should not be set in plan_options or run_options use the `select_models` or `select_models_func` arguments instead" + "select_models should not be set in plan_options or run_options use the `select_models` option instead" ) if plan_options.get("restate_models"): raise ValueError( - "restate_models should not be set in plan_options use the `restate_selected` argument with `select_models` or `select_models_func` instead" + "restate_models should not be set in plan_options use the `restate_selected` argument with `select_models` instead" ) select_models = select_models or [] @@ -352,6 +348,16 @@ def models(self) -> MappingProxyType[str, Model]: def models_dag(self) -> DAG[str]: return self.context.dag + def non_external_models_dag(self) -> t.Iterable[tuple[Model, set[str]]]: + dag = self.context.dag + + for model_fqn, deps in dag.graph.items(): + logger.debug(f"model found: {model_fqn}") + model = self.context.get_model(model_fqn) + if not model: + continue + yield (model, deps) + class SQLMeshController: """Allows control of sqlmesh via a python interface. It is not suggested to diff --git a/dagster_sqlmesh/controller/dagster.py b/dagster_sqlmesh/controller/dagster.py index bf1adb4..bb66dd6 100644 --- a/dagster_sqlmesh/controller/dagster.py +++ b/dagster_sqlmesh/controller/dagster.py @@ -1,10 +1,6 @@ import logging -from dagster import ( - AssetDep, - AssetKey, - AssetOut, -) +from dagster import AssetDep, AssetKey, AssetOut from dagster._core.definitions.asset_dep import CoercibleToAssetDep from ..translator import SQLMeshDagsterTranslator @@ -23,16 +19,10 @@ def to_asset_outs( ) -> SQLMeshMultiAssetOptions: with self.instance(environment, "to_asset_outs") as instance: context = instance.context - dag = context.dag output = SQLMeshMultiAssetOptions() depsMap: dict[str, CoercibleToAssetDep] = {} - for model_fqn, deps in dag.graph.items(): - logger.debug(f"model found: {model_fqn}") - model = context.get_model(model_fqn) - if not model: - # If no model is returned this seems to be an asset dependency - continue + for model, deps in instance.non_external_models_dag(): asset_key = translator.get_asset_key_from_model( context, model, diff --git a/dagster_sqlmesh/resource.py b/dagster_sqlmesh/resource.py index 8e351c0..f80d079 100644 --- a/dagster_sqlmesh/resource.py +++ b/dagster_sqlmesh/resource.py @@ -1,11 +1,7 @@ import logging import typing as t -from dagster import ( - AssetExecutionContext, - ConfigurableResource, - MaterializeResult, -) +from dagster import AssetExecutionContext, ConfigurableResource, MaterializeResult from sqlmesh import Model from sqlmesh.core.context import Context as SQLMeshContext from sqlmesh.core.snapshot import Snapshot @@ -14,7 +10,8 @@ from . import console from .config import SQLMeshContextConfig -from .controller import PlanOptions, RunOptions, SQLMeshController +from .controller import PlanOptions, RunOptions +from .controller.dagster import DagsterSQLMeshController from .utils import sqlmesh_model_name_to_key @@ -250,6 +247,7 @@ def run( logger = context.log controller = self.get_controller(logger) + with controller.instance(environment) as mesh: dag = mesh.models_dag() @@ -257,6 +255,9 @@ def run( models = mesh.models() models_map = models.copy() + all_available_models = set( + [model.name for model, _ in mesh.non_external_models_dag()] + ) if context.selected_output_names: models_map = {} for key, model in models.items(): @@ -268,6 +269,14 @@ def run( models_map[key] = model select_models.append(model.name) + selected_models_set = set(models_map.keys()) + + if all_available_models == selected_models_set: + logger.info("all models selected") + + # Setting this to none to allow sqlmesh to select all models and + # also remove any models + select_models = None event_handler = DagsterSQLMeshEventHandler( context, models_map, dag, "sqlmesh: " @@ -285,7 +294,7 @@ def run( def get_controller( self, log_override: logging.Logger | None = None - ) -> SQLMeshController: - return SQLMeshController.setup_with_config( + ) -> DagsterSQLMeshController: + return DagsterSQLMeshController.setup_with_config( self.config, log_override=log_override ) diff --git a/dagster_sqlmesh/test_asset.py b/dagster_sqlmesh/test_asset.py index 65132d9..8a578a7 100644 --- a/dagster_sqlmesh/test_asset.py +++ b/dagster_sqlmesh/test_asset.py @@ -1,6 +1,4 @@ -from dagster_sqlmesh.asset import ( - SQLMeshDagsterTranslator, -) +from dagster_sqlmesh.asset import SQLMeshDagsterTranslator from dagster_sqlmesh.conftest import SQLMeshTestContext diff --git a/dagster_sqlmesh/test_sqlmesh_context.py b/dagster_sqlmesh/test_sqlmesh_context.py index efc02ad..075a544 100644 --- a/dagster_sqlmesh/test_sqlmesh_context.py +++ b/dagster_sqlmesh/test_sqlmesh_context.py @@ -2,7 +2,7 @@ import polars -from .conftest import SQLMeshTestContext +from .testing import SQLMeshTestContext logger = logging.getLogger(__name__) @@ -184,12 +184,12 @@ def test_restating_models(sample_sqlmesh_test_context: SQLMeshTestContext): """ ) - assert feb_sum_query_restate[0][0] == feb_sum_query[0][0], ( - "February sum should not change" - ) - assert march_sum_query_restate[0][0] != march_sum_query[0][0], ( - "March sum should change" - ) - assert intermediate_2_query_restate[0][0] == intermediate_2_query[0][0], ( - "Intermediate model should not change during restate" - ) + assert ( + feb_sum_query_restate[0][0] == feb_sum_query[0][0] + ), "February sum should not change" + assert ( + march_sum_query_restate[0][0] != march_sum_query[0][0] + ), "March sum should change" + assert ( + intermediate_2_query_restate[0][0] == intermediate_2_query[0][0] + ), "Intermediate model should not change during restate" diff --git a/dagster_sqlmesh/testing/__init__.py b/dagster_sqlmesh/testing/__init__.py new file mode 100644 index 0000000..194a7f5 --- /dev/null +++ b/dagster_sqlmesh/testing/__init__.py @@ -0,0 +1,2 @@ +# ruff: noqa: F403 F401 +from .context import * diff --git a/dagster_sqlmesh/testing/context.py b/dagster_sqlmesh/testing/context.py new file mode 100644 index 0000000..2532c12 --- /dev/null +++ b/dagster_sqlmesh/testing/context.py @@ -0,0 +1,117 @@ +import logging +import typing as t +from dataclasses import dataclass + +import duckdb +import polars +from sqlmesh.core.console import get_console +from sqlmesh.utils.date import TimeLike + +from dagster_sqlmesh.config import SQLMeshContextConfig +from dagster_sqlmesh.controller.base import PlanOptions, RunOptions +from dagster_sqlmesh.controller.dagster import DagsterSQLMeshController +from dagster_sqlmesh.events import ConsoleRecorder + +logger = logging.getLogger(__name__) + + +@dataclass +class SQLMeshTestContext: + """A test context for running SQLMesh""" + + db_path: str + context_config: SQLMeshContextConfig + + def create_controller(self, enable_debug_console: bool = False): + console = None + if enable_debug_console: + console = get_console() + return DagsterSQLMeshController.setup_with_config( + self.context_config, debug_console=console + ) + + def query(self, *args: t.Any, **kwargs: t.Any) -> list[t.Any]: + conn = duckdb.connect(self.db_path) + return conn.sql(*args, **kwargs).fetchall() + + def initialize_test_source(self) -> None: + conn = duckdb.connect(self.db_path) + conn.sql( + """ + CREATE SCHEMA sources; + """ + ) + conn.sql( + """ + CREATE TABLE sources.test_source (id INTEGER, name VARCHAR); + """ + ) + conn.sql( + """ + INSERT INTO sources.test_source (id, name) + VALUES (1, 'abc'), (2, 'def'); + """ + ) + conn.close() + + def append_to_test_source(self, df: polars.DataFrame): + logger.debug("appending data to the test source") + conn = duckdb.connect(self.db_path) + conn.sql( + """ + INSERT INTO sources.test_source + SELECT * FROM df + """ + ) + + def plan_and_run( + self, + *, + environment: str, + execution_time: TimeLike | None = None, + enable_debug_console: bool = False, + start: TimeLike | None = None, + end: TimeLike | None = None, + select_models: list[str] | None = None, + restate_selected: bool = False, + skip_run: bool = False, + ): + """Runs plan and run on SQLMesh with the given configuration and record all of the generated events. + + Args: + environment (str): The environment to run SQLMesh in. + execution_time (TimeLike, optional): The execution timestamp for the run. Defaults to None. + enable_debug_console (bool, optional): Flag to enable debug console. Defaults to False. + start (TimeLike, optional): Start time for the run interval. Defaults to None. + end (TimeLike, optional): End time for the run interval. Defaults to None. + restate_models (List[str], optional): List of models to restate. Defaults to None. + + Returns: + None: The function records events to a debug console but doesn't return anything. + + Note: + TimeLike can be any time-like object that SQLMesh accepts (datetime, str, etc.). + The function creates a controller and recorder to capture all SQLMesh events during execution. + """ + controller = self.create_controller(enable_debug_console=enable_debug_console) + recorder = ConsoleRecorder() + # controller.add_event_handler(ConsoleRecorder()) + plan_options = PlanOptions( + enable_preview=True, + ) + run_options = RunOptions() + if execution_time: + plan_options["execution_time"] = execution_time + run_options["execution_time"] = execution_time + + for event in controller.plan_and_run( + environment, + start=start, + end=end, + select_models=select_models, + restate_selected=restate_selected, + plan_options=plan_options, + run_options=run_options, + skip_run=skip_run, + ): + recorder(event) diff --git a/dagster_sqlmesh/types.py b/dagster_sqlmesh/types.py index 9a5a965..b567df8 100644 --- a/dagster_sqlmesh/types.py +++ b/dagster_sqlmesh/types.py @@ -1,12 +1,7 @@ import typing as t from dataclasses import dataclass, field -from dagster import ( - AssetCheckResult, - AssetKey, - AssetMaterialization, - AssetOut, -) +from dagster import AssetCheckResult, AssetKey, AssetMaterialization, AssetOut from dagster._core.definitions.asset_dep import CoercibleToAssetDep from sqlmesh.core.model import Model diff --git a/package.json b/package.json index 4f22347..cda3ff5 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "scripts": { "build": "turbo run build --concurrency=100%", "format:staged": "lint-staged", - "pyright": "uv run -q -c \"import sys; print(sys.executable)\" | xargs -r pyright --pythonpath", + "pyright": "pyright --pythonpath $(echo 'import sys; print(sys.prefix)' | uv run -)/bin/python", "prepare": "husky install" }, "devDependencies": { @@ -27,4 +27,4 @@ "node": ">=18.x", "pnpm": ">=8" } -} \ No newline at end of file +} diff --git a/pyproject.toml b/pyproject.toml index ef02669..9f5e7ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "dagster-sqlmesh" -version = "0.8.0" +version = "0.9.0a1" description = "" authors = [ {name = "Reuven Gonzales", email = "reuven@karibalabs.co"} @@ -20,11 +20,11 @@ dev = [ "ipython>=8.26.0", "pdbpp>=0.10.3", "dagster-webserver>=1.8.1", - "ruff>=0.6.2", + "ruff==0.10.0", "polars>=1.5.0", "dagster-duckdb-polars>=0.24.2", "fastapi", # this is for sqlmesh ui - "sse-starlette", # this is for sqlmesh ui + "sse-starlette", ] [build-system] @@ -78,4 +78,4 @@ line-ending = "auto" known-first-party = ["dagster_sqlmesh"] section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"] combine-as-imports = true -split-on-trailing-comma = true \ No newline at end of file +split-on-trailing-comma = true diff --git a/uv.lock b/uv.lock index e0a8ff8..28c4afc 100644 --- a/uv.lock +++ b/uv.lock @@ -324,7 +324,7 @@ dev = [ { name = "ipython", specifier = ">=8.26.0" }, { name = "pdbpp", specifier = ">=0.10.3" }, { name = "polars", specifier = ">=1.5.0" }, - { name = "ruff", specifier = ">=0.6.2" }, + { name = "ruff", specifier = "==0.10.0" }, { name = "sse-starlette" }, ] @@ -1391,27 +1391,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.9.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/8e/fafaa6f15c332e73425d9c44ada85360501045d5ab0b81400076aff27cf6/ruff-0.9.10.tar.gz", hash = "sha256:9bacb735d7bada9cfb0f2c227d3658fc443d90a727b47f206fb33f52f3c0eac7", size = 3759776 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/b2/af7c2cc9e438cbc19fafeec4f20bfcd72165460fe75b2b6e9a0958c8c62b/ruff-0.9.10-py3-none-linux_armv6l.whl", hash = "sha256:eb4d25532cfd9fe461acc83498361ec2e2252795b4f40b17e80692814329e42d", size = 10049494 }, - { url = "https://files.pythonhosted.org/packages/6d/12/03f6dfa1b95ddd47e6969f0225d60d9d7437c91938a310835feb27927ca0/ruff-0.9.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:188a6638dab1aa9bb6228a7302387b2c9954e455fb25d6b4470cb0641d16759d", size = 10853584 }, - { url = "https://files.pythonhosted.org/packages/02/49/1c79e0906b6ff551fb0894168763f705bf980864739572b2815ecd3c9df0/ruff-0.9.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5284dcac6b9dbc2fcb71fdfc26a217b2ca4ede6ccd57476f52a587451ebe450d", size = 10155692 }, - { url = "https://files.pythonhosted.org/packages/5b/01/85e8082e41585e0e1ceb11e41c054e9e36fed45f4b210991052d8a75089f/ruff-0.9.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47678f39fa2a3da62724851107f438c8229a3470f533894b5568a39b40029c0c", size = 10369760 }, - { url = "https://files.pythonhosted.org/packages/a1/90/0bc60bd4e5db051f12445046d0c85cc2c617095c0904f1aa81067dc64aea/ruff-0.9.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99713a6e2766b7a17147b309e8c915b32b07a25c9efd12ada79f217c9c778b3e", size = 9912196 }, - { url = "https://files.pythonhosted.org/packages/66/ea/0b7e8c42b1ec608033c4d5a02939c82097ddcb0b3e393e4238584b7054ab/ruff-0.9.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524ee184d92f7c7304aa568e2db20f50c32d1d0caa235d8ddf10497566ea1a12", size = 11434985 }, - { url = "https://files.pythonhosted.org/packages/d5/86/3171d1eff893db4f91755175a6e1163c5887be1f1e2f4f6c0c59527c2bfd/ruff-0.9.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df92aeac30af821f9acf819fc01b4afc3dfb829d2782884f8739fb52a8119a16", size = 12155842 }, - { url = "https://files.pythonhosted.org/packages/89/9e/700ca289f172a38eb0bca752056d0a42637fa17b81649b9331786cb791d7/ruff-0.9.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de42e4edc296f520bb84954eb992a07a0ec5a02fecb834498415908469854a52", size = 11613804 }, - { url = "https://files.pythonhosted.org/packages/f2/92/648020b3b5db180f41a931a68b1c8575cca3e63cec86fd26807422a0dbad/ruff-0.9.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d257f95b65806104b6b1ffca0ea53f4ef98454036df65b1eda3693534813ecd1", size = 13823776 }, - { url = "https://files.pythonhosted.org/packages/5e/a6/cc472161cd04d30a09d5c90698696b70c169eeba2c41030344194242db45/ruff-0.9.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60dec7201c0b10d6d11be00e8f2dbb6f40ef1828ee75ed739923799513db24c", size = 11302673 }, - { url = "https://files.pythonhosted.org/packages/6c/db/d31c361c4025b1b9102b4d032c70a69adb9ee6fde093f6c3bf29f831c85c/ruff-0.9.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d838b60007da7a39c046fcdd317293d10b845001f38bcb55ba766c3875b01e43", size = 10235358 }, - { url = "https://files.pythonhosted.org/packages/d1/86/d6374e24a14d4d93ebe120f45edd82ad7dcf3ef999ffc92b197d81cdc2a5/ruff-0.9.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ccaf903108b899beb8e09a63ffae5869057ab649c1e9231c05ae354ebc62066c", size = 9886177 }, - { url = "https://files.pythonhosted.org/packages/00/62/a61691f6eaaac1e945a1f3f59f1eea9a218513139d5b6c2b8f88b43b5b8f/ruff-0.9.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f9567d135265d46e59d62dc60c0bfad10e9a6822e231f5b24032dba5a55be6b5", size = 10864747 }, - { url = "https://files.pythonhosted.org/packages/ee/94/2c7065e1d92a8a8a46d46d9c3cf07b0aa7e0a1e0153d74baa5e6620b4102/ruff-0.9.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5f202f0d93738c28a89f8ed9eaba01b7be339e5d8d642c994347eaa81c6d75b8", size = 11360441 }, - { url = "https://files.pythonhosted.org/packages/a7/8f/1f545ea6f9fcd7bf4368551fb91d2064d8f0577b3079bb3f0ae5779fb773/ruff-0.9.10-py3-none-win32.whl", hash = "sha256:bfb834e87c916521ce46b1788fbb8484966e5113c02df216680102e9eb960029", size = 10247401 }, - { url = "https://files.pythonhosted.org/packages/4f/18/fb703603ab108e5c165f52f5b86ee2aa9be43bb781703ec87c66a5f5d604/ruff-0.9.10-py3-none-win_amd64.whl", hash = "sha256:f2160eeef3031bf4b17df74e307d4c5fb689a6f3a26a2de3f7ef4044e3c484f1", size = 11366360 }, - { url = "https://files.pythonhosted.org/packages/35/85/338e603dc68e7d9994d5d84f24adbf69bae760ba5efd3e20f5ff2cec18da/ruff-0.9.10-py3-none-win_arm64.whl", hash = "sha256:5fd804c0327a5e5ea26615550e706942f348b197d5475ff34c19733aee4b2e69", size = 10436892 }, +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/ec/9c59d2956566517c98ac8267554f4eaceafb2a19710a429368518b7fab43/ruff-0.10.0.tar.gz", hash = "sha256:fa1554e18deaf8aa097dbcfeafaf38b17a2a1e98fdc18f50e62e8a836abee392", size = 3789921 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/3f/742afe91b43def2a75990b293c676355576c0ff9cdbcf4249f78fa592544/ruff-0.10.0-py3-none-linux_armv6l.whl", hash = "sha256:46a2aa0eaae5048e5f804f0be9489d8a661633e23277b7293089e70d5c1a35c4", size = 10078369 }, + { url = "https://files.pythonhosted.org/packages/8d/a0/8696fb4862e82f7b40bbbc2917137594b22826cc62d77278a91391507514/ruff-0.10.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:775a6bc61af9dd0a2e1763406522a137e62aabb743d8b43ed95f019cdd1526c7", size = 10876912 }, + { url = "https://files.pythonhosted.org/packages/40/aa/0d48b7b7d7a1f168bb8fd893ed559d633c7d68c4a8ef9b996f0c2bd07aca/ruff-0.10.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8b03e6fcd39d20f0004f9956f0ed5eadc404d3a299f9d9286323884e3b663730", size = 10229962 }, + { url = "https://files.pythonhosted.org/packages/21/de/861ced2f75b045d8cfc038d68961d8ac117344df1f43a11abdd05bf7991b/ruff-0.10.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:621101d1af80248827f2409a78c8177c8319986a57b4663613b9c72f8617bfcd", size = 10404627 }, + { url = "https://files.pythonhosted.org/packages/21/69/666e0b840191c3ce433962c0d05fc0f6800afe259ea5d230cc731655d8e2/ruff-0.10.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2dfe85cb6bfbd4259801e7d4982f2a72bdbd5749dc73a09d68a6dbf77f2209a", size = 9939383 }, + { url = "https://files.pythonhosted.org/packages/76/bf/34a2adc58092c99cdfa9f1303acd82d840d56412022e477e2ab20c261d2d/ruff-0.10.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43ac3879a20c22fdc57e559f0bb27f0c71828656841d0b42d3505b1e5b3a83c8", size = 11492269 }, + { url = "https://files.pythonhosted.org/packages/31/3d/f7ccfcf69f15948623b190feea9d411d5029ae39725fcc078f8d43bd07a6/ruff-0.10.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ef5e3aac421bbc62f8a7aab21edd49a359ed42205f7a5091a74386bca1efa293", size = 12186939 }, + { url = "https://files.pythonhosted.org/packages/6e/3e/c557c0abfdea85c7d238a3cb238c73e7b6d17c30a584234c4fd8fe2cafb6/ruff-0.10.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9f4f62d7fac8b748fce67ad308116b4d4cc1a9f964b4804fc5408fbd06e13ba9", size = 11655896 }, + { url = "https://files.pythonhosted.org/packages/3b/8e/3bfa110f37e5192eb3943f14943d05fbb9a76fea380aa87655e6f6276a54/ruff-0.10.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:02f9f6205c5b0d626f98da01a0e75b724a64c21c554bba24b12522c9e9ba6a04", size = 13885502 }, + { url = "https://files.pythonhosted.org/packages/51/4a/22cdab59b5563dd7f4c504d0f1e6bb25fc800a5a057395bc24f8ff3a85b2/ruff-0.10.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46a97f3d55f68464c48d1e929a8582c7e5bb80ac73336bbc7b0da894d8e6cd9e", size = 11344767 }, + { url = "https://files.pythonhosted.org/packages/3d/0f/8f85de2ac565f82f47c6d8fb7ae04383e6300560f2d1b91c1268ff91e507/ruff-0.10.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a0b811197d0dc96c13d610f8cfdc56030b405bcff5c2f10eab187b329da0ca4a", size = 10300331 }, + { url = "https://files.pythonhosted.org/packages/90/4a/b337df327832cb30bd8607e8d1fdf1b6b5ca228307d5008dd49028fb66ae/ruff-0.10.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a13a3fda0870c1c964b47ff5d73805ae80d2a9de93ee2d185d453b8fddf85a84", size = 9926551 }, + { url = "https://files.pythonhosted.org/packages/d7/e9/141233730b85675ac806c4b62f70516bd9c0aae8a55823f3a6589ed411be/ruff-0.10.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6ceb8d9f062e90ddcbad929f6136edf764bbf6411420a07e8357602ea28cd99f", size = 10925061 }, + { url = "https://files.pythonhosted.org/packages/24/09/02987935b55c2d353a226ac1b4f9718830e2e195834929f46c07eeede746/ruff-0.10.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c41d07d573617ed2f287ea892af2446fd8a8d877481e8e1ba6928e020665d240", size = 11394949 }, + { url = "https://files.pythonhosted.org/packages/d6/ec/054f9879fb6f4122d43ffe5c9f88c8c323a9cd14220d5c813aea5805e02c/ruff-0.10.0-py3-none-win32.whl", hash = "sha256:76e2de0cbdd587e373cd3b4050d2c45babdd7014c1888a6f121c29525c748a15", size = 10272077 }, + { url = "https://files.pythonhosted.org/packages/6e/49/915d8682f24645b904fe6a1aac36101464fc814923fdf293c1388dc5533c/ruff-0.10.0-py3-none-win_amd64.whl", hash = "sha256:f943acdecdcc6786a8d1dad455dd9f94e6d57ccc115be4993f9b52ef8316027a", size = 11393300 }, + { url = "https://files.pythonhosted.org/packages/82/ed/5c59941634c9026ceeccc7c119f23f4356f09aafd28c15c1bc734ac66b01/ruff-0.10.0-py3-none-win_arm64.whl", hash = "sha256:935a943bdbd9ff0685acd80d484ea91088e27617537b5f7ef8907187d19d28d0", size = 10510133 }, ] [[package]]