diff --git a/data_rentgen/consumer/extractors/batch_extraction_result.py b/data_rentgen/consumer/extractors/batch_extraction_result.py index 1b1d7ecf..6635ab9f 100644 --- a/data_rentgen/consumer/extractors/batch_extraction_result.py +++ b/data_rentgen/consumer/extractors/batch_extraction_result.py @@ -106,17 +106,18 @@ def __repr__(self): @staticmethod def _add(context: dict[tuple, T], new_item: T) -> T: key = new_item.unique_key - if key in context: - old_item = context[key] - if old_item is new_item: - return old_item - - merged_item = old_item.merge(new_item) - context[key] = merged_item - return merged_item - - context[key] = new_item - return new_item + old_item = context.get(key) + if not old_item: + context[key] = new_item + return new_item + + if old_item is new_item: + # optimization + return old_item + + merged_item = old_item.merge(new_item) + context[key] = merged_item + return merged_item def add_location(self, location: LocationDTO): return self._add(self._locations, location) diff --git a/data_rentgen/db/models/column_lineage.py b/data_rentgen/db/models/column_lineage.py index 74cdeab8..813b0d59 100644 --- a/data_rentgen/db/models/column_lineage.py +++ b/data_rentgen/db/models/column_lineage.py @@ -113,4 +113,5 @@ class ColumnLineage(Base): primaryjoin="ColumnLineage.fingerprint == DatasetColumnRelation.fingerprint", lazy="noload", foreign_keys=[fingerprint], + order_by=(DatasetColumnRelation.source_column, DatasetColumnRelation.target_column), ) diff --git a/data_rentgen/db/models/location.py b/data_rentgen/db/models/location.py index f6638314..3d0a7fdf 100644 --- a/data_rentgen/db/models/location.py +++ b/data_rentgen/db/models/location.py @@ -43,6 +43,7 @@ class Location(Base): "Address", lazy="noload", back_populates="location", + order_by="Address.url", ) search_vector: Mapped[str] = mapped_column( diff --git a/data_rentgen/db/repositories/column_lineage.py b/data_rentgen/db/repositories/column_lineage.py index 8bf475cd..e294488c 100644 --- a/data_rentgen/db/repositories/column_lineage.py +++ b/data_rentgen/db/repositories/column_lineage.py @@ -32,8 +32,15 @@ async def create_bulk(self, items: list[ColumnLineageDTO]): return insert_statement = insert(ColumnLineage) - statement = insert_statement.on_conflict_do_nothing( + inserted_row = insert_statement.excluded + statement = insert_statement.on_conflict_do_update( index_elements=[ColumnLineage.created_at, ColumnLineage.id], + set_={ + # in case if job or run was changed - workaround for + # https://github.com/OpenLineage/OpenLineage/issues/3846 + "job_id": inserted_row.job_id, + "run_id": inserted_row.run_id, + }, ) await self._session.execute( diff --git a/data_rentgen/db/repositories/dataset.py b/data_rentgen/db/repositories/dataset.py index 5e08fe85..c5613a4a 100644 --- a/data_rentgen/db/repositories/dataset.py +++ b/data_rentgen/db/repositories/dataset.py @@ -51,9 +51,13 @@ .options(selectinload(Dataset.tag_values).selectinload(TagValue.tag)) ) -get_one_query = select(Dataset).where( - Dataset.location_id == bindparam("location_id"), - func.lower(Dataset.name) == bindparam("name_lower"), +get_one_query = ( + select(Dataset) + .where( + Dataset.location_id == bindparam("location_id"), + func.lower(Dataset.name) == bindparam("name_lower"), + ) + .limit(1) ) get_stats_query = ( diff --git a/data_rentgen/db/repositories/dataset_symlink.py b/data_rentgen/db/repositories/dataset_symlink.py index 0ca48fc4..da9a3a69 100644 --- a/data_rentgen/db/repositories/dataset_symlink.py +++ b/data_rentgen/db/repositories/dataset_symlink.py @@ -29,9 +29,13 @@ ), ) -get_one_query = select(DatasetSymlink).where( - DatasetSymlink.from_dataset_id == bindparam("from_dataset_id"), - DatasetSymlink.to_dataset_id == bindparam("to_dataset_id"), +get_one_query = ( + select(DatasetSymlink) + .where( + DatasetSymlink.from_dataset_id == bindparam("from_dataset_id"), + DatasetSymlink.to_dataset_id == bindparam("to_dataset_id"), + ) + .limit(1) ) diff --git a/data_rentgen/db/repositories/input.py b/data_rentgen/db/repositories/input.py index b8873def..b60140cd 100644 --- a/data_rentgen/db/repositories/input.py +++ b/data_rentgen/db/repositories/input.py @@ -21,6 +21,10 @@ insert_statement = insert_statement.on_conflict_do_update( index_elements=[Input.created_at, Input.id], set_={ + # in case if job or run was changed - workaround for + # https://github.com/OpenLineage/OpenLineage/issues/3846 + "job_id": inserted_row.job_id, + "run_id": inserted_row.run_id, "num_bytes": func.greatest(inserted_row.num_bytes, Input.num_bytes), "num_rows": func.greatest(inserted_row.num_rows, Input.num_rows), "num_files": func.greatest(inserted_row.num_files, Input.num_files), diff --git a/data_rentgen/db/repositories/job.py b/data_rentgen/db/repositories/job.py index 44a69a57..57aaaeda 100644 --- a/data_rentgen/db/repositories/job.py +++ b/data_rentgen/db/repositories/job.py @@ -47,9 +47,13 @@ ), ) -get_one_query = select(Job).where( - Job.location_id == bindparam("location_id"), - func.lower(Job.name) == bindparam("name_lower"), +get_one_query = ( + select(Job) + .where( + Job.location_id == bindparam("location_id"), + func.lower(Job.name) == bindparam("name_lower"), + ) + .limit(1) ) get_list_query = ( diff --git a/data_rentgen/db/repositories/job_dependency.py b/data_rentgen/db/repositories/job_dependency.py index ef510a62..b30bd79a 100644 --- a/data_rentgen/db/repositories/job_dependency.py +++ b/data_rentgen/db/repositories/job_dependency.py @@ -40,9 +40,13 @@ ), ) -get_one_query = select(JobDependency).where( - JobDependency.from_job_id == bindparam("from_job_id"), - JobDependency.to_job_id == bindparam("to_job_id"), +get_one_query = ( + select(JobDependency) + .where( + JobDependency.from_job_id == bindparam("from_job_id"), + JobDependency.to_job_id == bindparam("to_job_id"), + ) + .limit(1) ) diff --git a/data_rentgen/db/repositories/job_type.py b/data_rentgen/db/repositories/job_type.py index 09ebe572..9f014ff5 100644 --- a/data_rentgen/db/repositories/job_type.py +++ b/data_rentgen/db/repositories/job_type.py @@ -16,8 +16,12 @@ JobType.type == any_(bindparam("types")), ) -get_one_query = select(JobType).where( - JobType.type == bindparam("type"), +get_one_query = ( + select(JobType) + .where( + JobType.type == bindparam("type"), + ) + .limit(1) ) get_distinct_query = select(JobType.type).distinct(JobType.type).order_by(JobType.type) diff --git a/data_rentgen/db/repositories/location.py b/data_rentgen/db/repositories/location.py index 9ab9554e..27900d32 100644 --- a/data_rentgen/db/repositories/location.py +++ b/data_rentgen/db/repositories/location.py @@ -39,7 +39,7 @@ get_one_query = ( select(Location) .from_statement( - get_one_by_name_query.union(get_one_by_addresses_query), + get_one_by_name_query.union(get_one_by_addresses_query).limit(1), ) .options(selectinload(Location.addresses)) ) @@ -51,7 +51,7 @@ { "location_id": bindparam("location_id"), "url": bindparam("url"), - } + }, ) .on_conflict_do_nothing(index_elements=["location_id", "url"]) ) diff --git a/data_rentgen/db/repositories/operation.py b/data_rentgen/db/repositories/operation.py index 779b42ab..33c8c434 100644 --- a/data_rentgen/db/repositories/operation.py +++ b/data_rentgen/db/repositories/operation.py @@ -77,6 +77,9 @@ async def create_or_update_bulk(self, operations: list[OperationDTO]) -> None: await self._session.execute( update_statement.values( { + # in case if run was changed - workaround for + # https://github.com/OpenLineage/OpenLineage/issues/3846 + "run_id": bindparam("run_id"), "name": func.coalesce(bindparam("name"), Operation.name), "type": func.coalesce(bindparam("type"), Operation.type), "status": func.greatest(bindparam("status"), Operation.status), diff --git a/data_rentgen/db/repositories/output.py b/data_rentgen/db/repositories/output.py index 2aee2f46..515b476d 100644 --- a/data_rentgen/db/repositories/output.py +++ b/data_rentgen/db/repositories/output.py @@ -21,6 +21,10 @@ insert_statement = insert_statement.on_conflict_do_update( index_elements=[Output.created_at, Output.id], set_={ + # in case if job or run was changed - workaround for + # https://github.com/OpenLineage/OpenLineage/issues/3846 + "job_id": inserted_row.job_id, + "run_id": inserted_row.run_id, "type": inserted_row.type.op("|")(Output.type), "num_bytes": func.greatest(inserted_row.num_bytes, Output.num_bytes), "num_rows": func.greatest(inserted_row.num_rows, Output.num_rows), diff --git a/data_rentgen/db/repositories/tag_value.py b/data_rentgen/db/repositories/tag_value.py index 94f0249d..34fa3dfa 100644 --- a/data_rentgen/db/repositories/tag_value.py +++ b/data_rentgen/db/repositories/tag_value.py @@ -29,7 +29,9 @@ ), ), ) -get_one_query = select(TagValue).where(TagValue.tag_id == bindparam("tag_id"), TagValue.value == bindparam("value")) +get_one_query = ( + select(TagValue).where(TagValue.tag_id == bindparam("tag_id"), TagValue.value == bindparam("value")).limit(1) +) class TagValueRepository(Repository[TagValue]): diff --git a/data_rentgen/dto/job.py b/data_rentgen/dto/job.py index 8941f693..17533a5f 100644 --- a/data_rentgen/dto/job.py +++ b/data_rentgen/dto/job.py @@ -3,7 +3,6 @@ from __future__ import annotations -from copy import copy from dataclasses import dataclass, field from data_rentgen.dto.job_type import JobTypeDTO @@ -26,23 +25,18 @@ def unique_key(self) -> tuple: def merge(self, new: JobDTO) -> JobDTO: self.id = new.id or self.id - self.location = self.location.merge(new.location) - if new.parent_job and self.parent_job: - self.parent_job = self.parent_job.merge(new.parent_job) + self.location.merge(new.location) + + # Workaround for https://github.com/OpenLineage/OpenLineage/issues/3846 + if new.parent_job and self.parent_job and new.parent_job.unique_key == self.parent_job.unique_key: + self.parent_job.merge(new.parent_job) else: self.parent_job = new.parent_job or self.parent_job if new.type and self.type: - self.type = self.type.merge(new.type) + self.type.merge(new.type) else: self.type = new.type or self.type self.tag_values.update(new.tag_values) - - if self.name == "unknown" and new.name != "unknown": - # Workaround for https://github.com/OpenLineage/OpenLineage/issues/3846 - result = copy(self) - result.name = new.name - return result - return self diff --git a/data_rentgen/dto/job_dependency.py b/data_rentgen/dto/job_dependency.py index aed6fa4c..12cb2ac1 100644 --- a/data_rentgen/dto/job_dependency.py +++ b/data_rentgen/dto/job_dependency.py @@ -20,7 +20,7 @@ def unique_key(self) -> tuple: return (self.from_job.unique_key, self.to_job.unique_key, self.type) def merge(self, new: JobDependencyDTO) -> JobDependencyDTO: - self.from_job = self.from_job.merge(new.from_job) - self.to_job = self.to_job.merge(new.to_job) + self.from_job.merge(new.from_job) + self.to_job.merge(new.to_job) self.id = new.id or self.id return self diff --git a/data_rentgen/dto/operation.py b/data_rentgen/dto/operation.py index 6414a4e4..9e39d2dd 100644 --- a/data_rentgen/dto/operation.py +++ b/data_rentgen/dto/operation.py @@ -58,9 +58,9 @@ def unique_key(self) -> tuple: return (self.id,) def merge(self, new: OperationDTO) -> OperationDTO: - self.run = self.run.merge(new.run) + self.run.merge(new.run) if self.sql_query and new.sql_query: - self.sql_query = self.sql_query.merge(new.sql_query) + self.sql_query.merge(new.sql_query) else: self.sql_query = new.sql_query or self.sql_query diff --git a/data_rentgen/dto/output.py b/data_rentgen/dto/output.py index 75672f94..482f8893 100644 --- a/data_rentgen/dto/output.py +++ b/data_rentgen/dto/output.py @@ -63,11 +63,11 @@ def generate_id(self) -> UUID: return generate_incremental_uuid(self.created_at, ".".join(id_components)) def merge(self, new: OutputDTO) -> OutputDTO: - self.operation = self.operation.merge(new.operation) - self.dataset = self.dataset.merge(new.dataset) + self.operation.merge(new.operation) + self.dataset.merge(new.dataset) if self.schema and new.schema: - self.schema = self.schema.merge(new.schema) + self.schema.merge(new.schema) else: self.schema = new.schema or self.schema diff --git a/data_rentgen/dto/run.py b/data_rentgen/dto/run.py index 45a73d25..6bdf420f 100644 --- a/data_rentgen/dto/run.py +++ b/data_rentgen/dto/run.py @@ -57,22 +57,28 @@ def unique_key(self) -> tuple: return (self.id,) def merge(self, new: RunDTO) -> RunDTO: - self.job = self.job.merge(new.job) + if new.job.unique_key == self.job.unique_key: + self.job.merge(new.job) + else: + # Workaround for https://github.com/OpenLineage/OpenLineage/issues/3846 + self.job = new.job + if new.parent_run and self.parent_run: - self.parent_run = self.parent_run.merge(new.parent_run) + self.parent_run.merge(new.parent_run) else: self.parent_run = new.parent_run or self.parent_run if new.user and self.user: - self.user = self.user.merge(new.user) + self.user.merge(new.user) else: self.user = new.user or self.user existing_dependencies = {item.unique_key: item for item in self.job_dependencies} merged_dependencies = [] for job_dependency in new.job_dependencies: - if job_dependency.unique_key in existing_dependencies: - merged_dependencies.append(existing_dependencies[job_dependency.unique_key].merge(job_dependency)) + old_job_dependency = existing_dependencies.get(job_dependency.unique_key) + if old_job_dependency: + merged_dependencies.append(old_job_dependency.merge(job_dependency)) else: merged_dependencies.append(job_dependency) self.job_dependencies = merged_dependencies diff --git a/pyproject.toml b/pyproject.toml index aa26d492..3ec06521 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ exclude = ["docs", "tests"] [project.optional-dependencies] server = [ - "fastapi~=0.136.3", + "fastapi>=0.136.3,<0.139.0", # starlette 1.0.0 break cookies "starlette<1.0", "uvicorn~=0.49", @@ -81,7 +81,7 @@ consumer = [ "cramjam~=2.11.0", ] http2kafka = [ - "fastapi~=0.136.3", + "fastapi>=0.136.3,<0.139.0", # starlette 1.0.0 break cookies "starlette<1.0", "uvicorn~=0.49", diff --git a/uv.lock b/uv.lock index acc1b348..c7e0d35c 100644 --- a/uv.lock +++ b/uv.lock @@ -69,16 +69,16 @@ wheels = [ [[package]] name = "alembic" -version = "1.18.4" +version = "1.18.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mako" }, { name = "sqlalchemy" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725, upload-time = "2026-02-10T16:00:47.195Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/cc/ac0bed8e562e7407fe55c3ba85a4dce86e6dbd8730887bd1e406a6c5c18a/alembic-1.18.5.tar.gz", hash = "sha256:1554982221dd17e9a749b53902407578eb305e453f71999e8c7f0a48389fff8e", size = 2060480, upload-time = "2026-06-25T15:20:54.888Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/96/78/5fe6dc3a3a5b2f5a2a4faef8bfe336d5fa049a38884ab3172e0098160c01/alembic-1.18.5-py3-none-any.whl", hash = "sha256:06d8ba9d04558022f5395e9317de03d270f3dced49cee01f89fe7a13c26f14bc", size = 264664, upload-time = "2026-06-25T15:20:56.673Z" }, ] [[package]] @@ -801,8 +801,8 @@ requires-dist = [ { name = "cramjam", marker = "extra == 'consumer'", specifier = "~=2.11.0" }, { name = "cramjam", marker = "extra == 'http2kafka'", specifier = "~=2.11.0" }, { name = "faker", marker = "extra == 'seed'", specifier = ">=40.21,<40.24" }, - { name = "fastapi", marker = "extra == 'http2kafka'", specifier = "~=0.136.3" }, - { name = "fastapi", marker = "extra == 'server'", specifier = "~=0.136.3" }, + { name = "fastapi", marker = "extra == 'http2kafka'", specifier = ">=0.136.3,<0.139.0" }, + { name = "fastapi", marker = "extra == 'server'", specifier = ">=0.136.3,<0.139.0" }, { name = "faststream", extras = ["cli", "kafka"], marker = "extra == 'consumer'", specifier = "~=0.7.1" }, { name = "faststream", extras = ["cli", "kafka"], marker = "extra == 'http2kafka'", specifier = "~=0.7.1" }, { name = "greenlet", specifier = "~=3.5.2" }, @@ -955,7 +955,7 @@ pydantic = [ [[package]] name = "fastapi" -version = "0.136.3" +version = "0.138.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -964,9 +964,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/2d/ff8d91d7b564d464629a0fd50a4489c97fcb836ac230bf3a7269232a9b1f/fastapi-0.136.3.tar.gz", hash = "sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab", size = 396410, upload-time = "2026-05-23T18:53:15.192Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/c9/5e8defe249899c0dc900643695fc07829a67fc88b4ff2cdb03fcbdbf5a4b/fastapi-0.138.1.tar.gz", hash = "sha256:96e3702dce09ee0dce48856135620d3d865ca684a79fe7513fd7b13a12f82862", size = 419646, upload-time = "2026-06-25T15:40:42.115Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/82/45359b62a067409bd929ae8a56b8ed13e5a8c8a61194b3c236920999ab83/fastapi-0.136.3-py3-none-any.whl", hash = "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620", size = 117481, upload-time = "2026-05-23T18:53:16.924Z" }, + { url = "https://files.pythonhosted.org/packages/38/a9/69a6924f645eb4dd8cd625bf255b3625990eb3e14e073438a53c405dcd3e/fastapi-0.138.1-py3-none-any.whl", hash = "sha256:b994cae7ba8b82c976a728b544244de31333fa5f7d261f9a1dffe526444cae23", size = 129182, upload-time = "2026-06-25T15:40:40.771Z" }, ] [[package]] @@ -1968,27 +1968,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.19" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d5/e6/15800dfde183a1a106594016c912b4c12d050a301989d1aca6cb63759fe8/ruff-0.15.19.tar.gz", hash = "sha256:edc27f7172a93b32b102687009d6a588508815072141543ae603a8b9b0823063", size = 4772071, upload-time = "2026-06-24T01:10:46.942Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/4c/9ded7626c39a0440c575bf69e2bf500d443388272c842662c59852ee7fcd/ruff-0.15.19-py3-none-linux_armv6l.whl", hash = "sha256:922d1eb283161564759bd49f507e91dc6112c15da8bd5b84ed714e086243cf86", size = 10950859, upload-time = "2026-06-24T01:10:38.491Z" }, - { url = "https://files.pythonhosted.org/packages/fb/ef/c211505ece1d00ef493d58e54e3b6383c946a21e9874774eb531f2512cf3/ruff-0.15.19-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4d190d8f62a0b94aba8f721116538a9ee29b1e74d26650846ba9b99f0ae21c40", size = 11294529, upload-time = "2026-06-24T01:10:36.481Z" }, - { url = "https://files.pythonhosted.org/packages/fe/93/78d462e7d39968e58094dc57be7d09ffb14ce37da5b68ed70338a35a1f21/ruff-0.15.19-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5a2c86ba6870dd415a9d9eb8be94d7924ebec6a26ffc7958ec7ca29d4bff967d", size = 10641416, upload-time = "2026-06-24T01:10:48.923Z" }, - { url = "https://files.pythonhosted.org/packages/76/c4/5cb66cfd1f865d5cca908b86c93ac785e7f572193d3c7426079ca6643e24/ruff-0.15.19-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b432bc087264aea70fd25ac198918b70bd9e2aa0db4297b0bb91bbfbbc63ce", size = 11015582, upload-time = "2026-06-24T01:10:30.089Z" }, - { url = "https://files.pythonhosted.org/packages/51/9f/8ecfaec10cf5eecd28fbc00ff4fb867db90a1be54bf3d39ebf93f893cd52/ruff-0.15.19-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8530a09d03b3a8c994f8b559a7dcdabc690bcd3f78ef276c38c83166798ebf56", size = 10744059, upload-time = "2026-06-24T01:10:32.48Z" }, - { url = "https://files.pythonhosted.org/packages/35/6b/983249d04562bc2d590edd75f32455cdb473affb3ba4bc8d883e939c697d/ruff-0.15.19-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87bf21fb3875fe69f0eacc825411657e2e85589cce633c35c0adf1113649c62b", size = 11568461, upload-time = "2026-06-24T01:10:17.435Z" }, - { url = "https://files.pythonhosted.org/packages/eb/39/bc7794f127b18f492a3b4ee82bba5a900c985ff13b72b46f46e3c171ba34/ruff-0.15.19-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b229cb3ef56ecc2c1c8ebeca64b7a7740ccaef40a9eb097e78dde5a8560b83", size = 12429690, upload-time = "2026-06-24T01:10:40.638Z" }, - { url = "https://files.pythonhosted.org/packages/0a/3b/0de6859e698ed11c8a49e765196c8d333599b6a546c0715df39b6ba1aa2e/ruff-0.15.19-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c754515be7b76afe6e7e62df7776709571bcfc1631183828afcf3bafa869e3", size = 11693067, upload-time = "2026-06-24T01:10:25.681Z" }, - { url = "https://files.pythonhosted.org/packages/89/3d/0b1f30f84bee9ae6ae8d349c2ba8b6f4b040966744efdd3acc804ae7c024/ruff-0.15.19-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a498f82e0f4d8904c4e0aea5139cdfac1f39d19a3c51d491292f63a36e83b2e", size = 11616911, upload-time = "2026-06-24T01:10:44.809Z" }, - { url = "https://files.pythonhosted.org/packages/4d/eb/c90bd3dfc12eed9032c2c1bfe05105b93a1b2c8bce555db6308315b853ce/ruff-0.15.19-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d48caa34488fb521fd0ef4aea2b0e8fe758298df044138f0d67b687a6a0d07ed", size = 11649343, upload-time = "2026-06-24T01:10:23.472Z" }, - { url = "https://files.pythonhosted.org/packages/82/91/01caa13602a2f12fae5edbe8caf78b3c1e6db1293132aee6959eecce095c/ruff-0.15.19-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4171b6613effa9363cd46dd4f75bd1827b6d1b946b5e278ed0c600d305379445", size = 10977610, upload-time = "2026-06-24T01:10:50.892Z" }, - { url = "https://files.pythonhosted.org/packages/3c/51/acb817922feab9ecbb3201377d4dbe7a25f1395e46545820061973f03468/ruff-0.15.19-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:27c15b2a241dd4d995557949a094fe78b8ad99122a38ccae1595849bcc947b3f", size = 10744900, upload-time = "2026-06-24T01:10:42.726Z" }, - { url = "https://files.pythonhosted.org/packages/84/bc/5c8ca46b8a7a3f2b16cfbec88721d772b1c93912904e8f8c2e49470fea63/ruff-0.15.19-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ed03b7862d68f0a8771d50ee129980cbf1b113f96e250b73954bc292f689e0bb", size = 11293560, upload-time = "2026-06-24T01:10:21.262Z" }, - { url = "https://files.pythonhosted.org/packages/81/e0/4a888cbe4d5523b3f77a2b1fa043f46cfeba1b32eac35dcfadee0578fa8a/ruff-0.15.19-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:08143f0685ae278b30727ea72e90c61e5bd9c31b91aac4f5bb989538f73d24b8", size = 11696533, upload-time = "2026-06-24T01:10:53.046Z" }, - { url = "https://files.pythonhosted.org/packages/98/43/c34b2fcd79262a85161764a97aaca89c3e4f574340ab61430cefa2bdd2c1/ruff-0.15.19-py3-none-win32.whl", hash = "sha256:8f47f0f92952af2557212bb10cf3e695cd4cf28b2c6e42cdb18ec6c9ebfa19da", size = 10986299, upload-time = "2026-06-24T01:10:55.185Z" }, - { url = "https://files.pythonhosted.org/packages/22/e8/15fd23e02b2442b56b2026b455977bc3057aa34b26e6323d1e99e8531a9f/ruff-0.15.19-py3-none-win_amd64.whl", hash = "sha256:efeca47ee3f9d4a7162655a3b8e6ee4a878646044233978d4d2c1ff8cdd914f0", size = 12123473, upload-time = "2026-06-24T01:10:27.74Z" }, - { url = "https://files.pythonhosted.org/packages/30/66/9a73695e31eaee04f35d8475998bf8ab354465f9c638936d76111603dcc5/ruff-0.15.19-py3-none-win_arm64.whl", hash = "sha256:6c6b607466e47349332eb1d9be52fb1467423fc07c217341af41cd0f3f0573be", size = 11376779, upload-time = "2026-06-24T01:10:34.465Z" }, +version = "0.15.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" }, + { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" }, + { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" }, + { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" }, + { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" }, + { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" }, + { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, ] [[package]]