diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 07aa1970..a974cabe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: # biome-format Format the committed files # biome-lint Lint and apply safe fixes to the committed files - repo: https://github.com/biomejs/pre-commit - rev: v2.3.9 + rev: v2.3.10 hooks: - id: biome-check additional_dependencies: ["@biomejs/biome@^1.0.0"] @@ -45,7 +45,7 @@ repos: # runs the ruff linter and formatter - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.9 + rev: v0.14.10 hooks: # linter - id: ruff-check # runs ruff check --force-exclude diff --git a/sdk/docs/mkdocs.yaml b/sdk/docs/mkdocs.yaml index aa9f75d4..72c98792 100644 --- a/sdk/docs/mkdocs.yaml +++ b/sdk/docs/mkdocs.yaml @@ -19,7 +19,8 @@ nav: - "Basics": - "Getting Started": "getting-started.md" - "Terminology": "terminology.md" - - "Common Workflows": "common-workflows.md" + - "Common Workflows": + - "Dataset downloads": "common-workflows/dataset-downloads.md" - "Changelog": "changelog.md" - "Advanced": - "Concurrent Access": "advanced/concurrent-access.md" diff --git a/sdk/docs/mkdocs/changelog.md b/sdk/docs/mkdocs/changelog.md index e05bfac8..046bd448 100644 --- a/sdk/docs/mkdocs/changelog.md +++ b/sdk/docs/mkdocs/changelog.md @@ -2,10 +2,10 @@ ## `0.1.18` - YYYY-MM-DD -## `0.1.17` - YYYY-MM-DD +## `0.1.17` - 2025-12-20 + Fixes: - + [**Improved file downloads**](https://github.com/spectrumx/sds-code/pull/236): + + [**Improved file downloads**](https://github.com/spectrumx/sds-code/pull/236): more reliability for file downloads when they need to be resumed. + File downloads now use a temporary file during download to avoid partial files being left behind if the download is interrupted. + When overwrite is `False` and a local file would be overwritten, we skip re-downloading it. + When overwrite is `True` and the checksums don't match with server, we re-download and replace the local file to match the server's. diff --git a/sdk/docs/mkdocs/common-workflows/common-workflows.md b/sdk/docs/mkdocs/common-workflows/common-workflows.md deleted file mode 100644 index da131fdc..00000000 --- a/sdk/docs/mkdocs/common-workflows/common-workflows.md +++ /dev/null @@ -1,5 +0,0 @@ -# Common Workflow Scripts - -/// note | To be Completed -This section is under construction. More workflow scripts will be added soon. -/// diff --git a/sdk/docs/mkdocs/faq.md b/sdk/docs/mkdocs/faq.md index 8d100547..b6060c12 100644 --- a/sdk/docs/mkdocs/faq.md +++ b/sdk/docs/mkdocs/faq.md @@ -32,6 +32,7 @@ And others not as much. + [Can multiple clients write to the same location simultaneously?](#can-multiple-clients-write-to-the-same-location-simultaneously) + [Is it safe to have multiple clients reading from the same location?](#is-it-safe-to-have-multiple-clients-reading-from-the-same-location) + [Why is the SDK stateless?](#why-is-the-sdk-stateless) + + [What protections help prevent accidental deletions?](#what-protections-help-prevent-accidental-deletions) + [Troubleshooting](#troubleshooting) + [I'm getting an `AuthError` when trying to authenticate. What should I check?](#im-getting-an-autherror-when-trying-to-authenticate-what-should-i-check) + [I'm getting a `NetworkError`. What does this mean?](#im-getting-a-networkerror-what-does-this-mean) @@ -392,6 +393,16 @@ simultaneously without the complexity of session management. However, this means request must contain all information needed to complete it, and the SDK cannot detect or prevent concurrent writes to the same location. +### What protections help prevent accidental deletions? + +SDS layers several safeguards to keep assets from being removed unintentionally: + ++ Sharing defaults to **Viewer** access. Granting write permissions always requires an explicit choice. ++ Files that belong to captures or datasets cannot be deleted until they are unlinked from that grouping. Captures linked into datasets are equally protected while that relationship exists. ++ **Final** datasets are read-only, even for their owners. This is the recommended state for broader distribution once contents are stable. See [What are Draft and Final Datasets?](#what-are-draft-and-final-datasets) for details. ++ All SDS assets use soft deletion. Administrators can restore items for a short window after removal if contacted promptly. Reach out to support if needed. ++ When working in the SDK, keep shared-asset listings distinct from your own to reduce the chance of edits in the wrong context. + ## Troubleshooting ### I'm getting an `AuthError` when trying to authenticate. What should I check? diff --git a/sdk/pyproject.toml b/sdk/pyproject.toml index f9ec25d3..349b382b 100644 --- a/sdk/pyproject.toml +++ b/sdk/pyproject.toml @@ -17,7 +17,7 @@ name = "spectrumx" readme = "./docs/README.md" requires-python = ">=3.11" - version = "0.1.16" + version = "0.1.17" # https://pypi.org/classifiers/ classifiers = [ diff --git a/sdk/tests/e2e_examples/check_build_acceptance.py b/sdk/tests/e2e_examples/check_build_acceptance.py index c06313b6..3f0edfe8 100644 --- a/sdk/tests/e2e_examples/check_build_acceptance.py +++ b/sdk/tests/e2e_examples/check_build_acceptance.py @@ -90,7 +90,9 @@ def check_basic_usage() -> None: sds.upload( local_path=local_dir, # may be a single file or a directory sds_path=reference_name, # files will be created under this virtual directory + persist_state=False, # do not persist state in tests verbose=True, # shows a progress bar (default) + warn_skipped=True, # warn if some files were skipped (default) ) # download the files from an SDS directory @@ -153,7 +155,9 @@ def check_error_handling() -> None: upload_results: list[Result[File]] = sds.upload( local_path=local_dir, sds_path=reference_name, + persist_state=False, # do not persist state in tests verbose=True, + warn_skipped=True, ) # Since `upload()` is a batch operation, some files may succeed and some @@ -268,21 +272,23 @@ def check_capture_usage() -> None: # upload a single-channel capture local_dir = Path("my_spectrum_files") sds.upload_capture( - local_path=local_dir, - sds_path=capture_sds_dir, capture_type=CaptureType.RadioHound, - index_name="", # automatically inferred from capture type channel=None, - scan_group=None, + index_name="", # automatically inferred from capture type + local_path=local_dir, name="Test Single Channel Capture", + persist_state=False, # do not persist state in tests + scan_group=None, + sds_path=capture_sds_dir, verbose=True, ) # upload a multi-channel capture sds.upload_multichannel_drf_capture( + channels=[], local_path=local_dir, + persist_state=False, # do not persist state in tests sds_path=capture_sds_dir, - channels=[], verbose=True, ) @@ -300,6 +306,7 @@ def check_download_modes() -> None: files_to_download=file_paginator, to_local_path=Path("sds-downloads") / "files" / reference_name, overwrite=False, # do not overwrite local existing files (default) + skip_contents=False, verbose=True, ) diff --git a/sdk/tests/integration/regressions/test_paths.py b/sdk/tests/integration/regressions/test_paths.py index 45e46243..9d8e496e 100644 --- a/sdk/tests/integration/regressions/test_paths.py +++ b/sdk/tests/integration/regressions/test_paths.py @@ -168,13 +168,13 @@ def test_paths_sds_capture_ops( f"{type(sds_path_random)!s:>40} '{sds_path_random}'" ) capture = integration_client.upload_capture( - local_path=drf_sample_top_level_dir, - sds_path=sds_path_random, capture_type=CaptureType.DigitalRF, channel=drf_channel, + local_path=drf_sample_top_level_dir, + persist_state=False, + sds_path=sds_path_random, verbose=False, warn_skipped=False, - persist_state=False, ) assert capture is not None, ( f"Failed to upload capture to '{sds_path_random}'" @@ -206,13 +206,13 @@ def test_paths_sds_capture_ops( f"{type(sds_path_random)!s:>40} '{sds_path_random}'" ) capture = integration_client.upload_capture( - local_path=rh_sample_top_level_dir, - sds_path=sds_path_random, capture_type=CaptureType.RadioHound, + local_path=rh_sample_top_level_dir, + persist_state=False, scan_group=rh_data.get("scan_group"), + sds_path=sds_path_random, verbose=False, warn_skipped=False, - persist_state=False, ) assert capture is not None, f"Failed to upload capture to '{sds_path_random}'" assert capture.uuid is not None, f"Capture UUID is None for '{sds_path_random}'" diff --git a/sdk/tests/integration/test_captures.py b/sdk/tests/integration/test_captures.py index f92fefbd..aa5d2128 100644 --- a/sdk/tests/integration/test_captures.py +++ b/sdk/tests/integration/test_captures.py @@ -448,13 +448,13 @@ def test_capture_upload_drf( # ACT by uploading the capture capture = integration_client.upload_capture( - local_path=test_dir, - sds_path=sds_path, capture_type=capture_type, channel=drf_channel, - warn_skipped=True, - raise_on_error=True, + local_path=test_dir, persist_state=False, + raise_on_error=True, + sds_path=sds_path, + warn_skipped=True, ) # ASSERT capture was correctly created @@ -508,13 +508,13 @@ def test_capture_upload_rh(integration_client: Client) -> None: # ACT capture = integration_client.upload_capture( - local_path=dir_top_level, - sds_path=sds_path, capture_type=CaptureType.RadioHound, + local_path=dir_top_level, + persist_state=False, + raise_on_error=True, scan_group=scan_group, + sds_path=sds_path, warn_skipped=True, - raise_on_error=True, - persist_state=False, ) # ASSERT @@ -569,11 +569,11 @@ def test_capture_upload_missing_required_fields_drf( # ACT & ASSERT - Missing channel for DigitalRF with pytest.raises(CaptureError): integration_client.upload_capture( - local_path=test_dir, - sds_path=sds_path, capture_type=capture_type, - raise_on_error=True, + local_path=test_dir, persist_state=False, + raise_on_error=True, + sds_path=sds_path, # Missing required channel parameter ) @@ -869,9 +869,9 @@ def _upload_assets( log.debug(f"Uploading assets as '/{sds_path}'") upload_results = integration_client.upload( local_path=local_path, + persist_state=False, sds_path=sds_path, verbose=False, - persist_state=False, ) success_results = [success for success in upload_results if success] failed_results = [success for success in upload_results if not success] diff --git a/sdk/tests/integration/test_file_ops.py b/sdk/tests/integration/test_file_ops.py index f828f7f4..56213f6d 100644 --- a/sdk/tests/integration/test_file_ops.py +++ b/sdk/tests/integration/test_file_ops.py @@ -234,6 +234,7 @@ def test_upload_files_in_bulk(integration_client: Client, temp_file_tree: Path) with disable_ssl_warnings(): results = integration_client.upload( local_path=temp_file_tree, + persist_state=False, # do not persist state in tests sds_path=PurePosixPath("/test-tree") / random_subdir_name, verbose=True, warn_skipped=False, diff --git a/sdk/tests/ops/test_captures.py b/sdk/tests/ops/test_captures.py index 13045b3e..a1f6a114 100644 --- a/sdk/tests/ops/test_captures.py +++ b/sdk/tests/ops/test_captures.py @@ -38,7 +38,8 @@ # globally toggles dry run mode in case we want to run these under an integration mode. DRY_RUN: bool = False -MULTICHANNEL_EXPECTED_COUNT = 2 +MULTICHANNEL_EXPECTED_COUNT: int = 2 # expected num of captures in multi-channel tests +TEST_STATE_PERSISTENCE: bool = False # don't persist upload state in tests @pytest.fixture @@ -384,9 +385,10 @@ def test_upload_capture_dry_run(client: Client, tmp_path: Path) -> None: # ACT capture = client.upload_capture( + capture_type=capture_type, local_path=test_dir, + persist_state=TEST_STATE_PERSISTENCE, sds_path="/test/capture/dry-run", - capture_type=capture_type, ) # ASSERT @@ -419,10 +421,11 @@ def test_upload_capture_upload_fails( # ACT & ASSERT with pytest.raises(SDSError): client.upload_capture( - local_path=test_dir, - sds_path="/test/capture/fail", capture_type=CaptureType.DigitalRF, + local_path=test_dir, + persist_state=TEST_STATE_PERSISTENCE, raise_on_error=True, + sds_path="/test/capture/fail", ) # Test with raise_on_error=False @@ -430,6 +433,7 @@ def test_upload_capture_upload_fails( local_path=test_dir, sds_path="/test/capture/fail", capture_type=CaptureType.DigitalRF, + persist_state=TEST_STATE_PERSISTENCE, raise_on_error=False, verbose=False, ) @@ -451,6 +455,7 @@ def test_upload_capture_no_files(client: Client, tmp_path: Path) -> None: local_path=empty_dir, sds_path="/test/capture/empty", capture_type=CaptureType.DigitalRF, + persist_state=TEST_STATE_PERSISTENCE, verbose=False, ) @@ -476,6 +481,7 @@ def test_upload_multichannel_drf_capture_dry_run( local_path=test_dir, sds_path="/test/multichannel/dry-run", channels=channels, + persist_state=TEST_STATE_PERSISTENCE, ) # ASSERT @@ -540,6 +546,7 @@ def test_upload_multichannel_drf_capture_success( local_path=test_dir, sds_path="/test/multichannel", channels=channels, + persist_state=TEST_STATE_PERSISTENCE, ) # ASSERT @@ -633,6 +640,7 @@ def test_upload_multichannel_drf_capture_existing_capture( local_path=test_dir, sds_path="/test/multichannel", channels=channels, + persist_state=TEST_STATE_PERSISTENCE, ) # ASSERT @@ -707,6 +715,7 @@ def test_upload_multichannel_drf_capture_creation_fails( local_path=test_dir, sds_path="/test/multichannel", channels=channels, + persist_state=TEST_STATE_PERSISTENCE, raise_on_error=False, ) @@ -916,11 +925,12 @@ def test_upload_capture_with_name_dry_run(client: Client, tmp_path: Path) -> Non # ACT capture = client.upload_capture( - local_path=test_dir, - sds_path="/test/capture/with/name", capture_type=CaptureType.DigitalRF, channel="test_channel", + local_path=test_dir, name=capture_name, + persist_state=TEST_STATE_PERSISTENCE, + sds_path="/test/capture/with/name", verbose=False, ) @@ -941,10 +951,11 @@ def test_upload_capture_without_name_dry_run(client: Client, tmp_path: Path) -> # ACT capture = client.upload_capture( - local_path=test_dir, - sds_path="/test/capture/no/name", capture_type=CaptureType.DigitalRF, channel="test_channel", + local_path=test_dir, + persist_state=TEST_STATE_PERSISTENCE, + sds_path="/test/capture/no/name", verbose=False, ) @@ -1116,11 +1127,12 @@ def test_upload_capture_with_name_success( # ACT capture = client.upload_capture( - local_path=test_dir, - sds_path="/test/upload/success", capture_type=CaptureType.DigitalRF, channel="test_channel", + local_path=test_dir, name=capture_name, + persist_state=TEST_STATE_PERSISTENCE, + sds_path="/test/upload/success", verbose=False, ) diff --git a/sdk/uv.lock b/sdk/uv.lock index 89eaaec3..86e81565 100644 --- a/sdk/uv.lock +++ b/sdk/uv.lock @@ -645,7 +645,7 @@ wheels = [ [[package]] name = "mkdocs-material" -version = "9.7.0" +version = "9.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, @@ -660,9 +660,9 @@ dependencies = [ { name = "pymdown-extensions" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/3b/111b84cd6ff28d9e955b5f799ef217a17bc1684ac346af333e6100e413cb/mkdocs_material-9.7.0.tar.gz", hash = "sha256:602b359844e906ee402b7ed9640340cf8a474420d02d8891451733b6b02314ec", size = 4094546, upload-time = "2025-11-11T08:49:09.73Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/e2/2ffc356cd72f1473d07c7719d82a8f2cbd261666828614ecb95b12169f41/mkdocs_material-9.7.1.tar.gz", hash = "sha256:89601b8f2c3e6c6ee0a918cc3566cb201d40bf37c3cd3c2067e26fadb8cce2b8", size = 4094392, upload-time = "2025-12-18T09:49:00.308Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/87/eefe8d5e764f4cf50ed91b943f8e8f96b5efd65489d8303b7a36e2e79834/mkdocs_material-9.7.0-py3-none-any.whl", hash = "sha256:da2866ea53601125ff5baa8aa06404c6e07af3c5ce3d5de95e3b52b80b442887", size = 9283770, upload-time = "2025-11-11T08:49:06.26Z" }, + { url = "https://files.pythonhosted.org/packages/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-py3-none-any.whl", hash = "sha256:3f6100937d7d731f87f1e3e3b021c97f7239666b9ba1151ab476cabb96c60d5c", size = 9297166, upload-time = "2025-12-18T09:48:56.664Z" }, ] [[package]] @@ -1121,28 +1121,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/1b/ab712a9d5044435be8e9a2beb17cbfa4c241aa9b5e4413febac2a8b79ef2/ruff-0.14.9.tar.gz", hash = "sha256:35f85b25dd586381c0cc053f48826109384c81c00ad7ef1bd977bfcc28119d5b", size = 5809165, upload-time = "2025-12-11T21:39:47.381Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/1c/d1b1bba22cffec02351c78ab9ed4f7d7391876e12720298448b29b7229c1/ruff-0.14.9-py3-none-linux_armv6l.whl", hash = "sha256:f1ec5de1ce150ca6e43691f4a9ef5c04574ad9ca35c8b3b0e18877314aba7e75", size = 13576541, upload-time = "2025-12-11T21:39:14.806Z" }, - { url = "https://files.pythonhosted.org/packages/94/ab/ffe580e6ea1fca67f6337b0af59fc7e683344a43642d2d55d251ff83ceae/ruff-0.14.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ed9d7417a299fc6030b4f26333bf1117ed82a61ea91238558c0268c14e00d0c2", size = 13779363, upload-time = "2025-12-11T21:39:20.29Z" }, - { url = "https://files.pythonhosted.org/packages/7d/f8/2be49047f929d6965401855461e697ab185e1a6a683d914c5c19c7962d9e/ruff-0.14.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d5dc3473c3f0e4a1008d0ef1d75cee24a48e254c8bed3a7afdd2b4392657ed2c", size = 12925292, upload-time = "2025-12-11T21:39:38.757Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e9/08840ff5127916bb989c86f18924fd568938b06f58b60e206176f327c0fe/ruff-0.14.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84bf7c698fc8f3cb8278830fb6b5a47f9bcc1ed8cb4f689b9dd02698fa840697", size = 13362894, upload-time = "2025-12-11T21:39:02.524Z" }, - { url = "https://files.pythonhosted.org/packages/31/1c/5b4e8e7750613ef43390bb58658eaf1d862c0cc3352d139cd718a2cea164/ruff-0.14.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa733093d1f9d88a5d98988d8834ef5d6f9828d03743bf5e338bf980a19fce27", size = 13311482, upload-time = "2025-12-11T21:39:17.51Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3a/459dce7a8cb35ba1ea3e9c88f19077667a7977234f3b5ab197fad240b404/ruff-0.14.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a1cfb04eda979b20c8c19550c8b5f498df64ff8da151283311ce3199e8b3648", size = 14016100, upload-time = "2025-12-11T21:39:41.948Z" }, - { url = "https://files.pythonhosted.org/packages/a6/31/f064f4ec32524f9956a0890fc6a944e5cf06c63c554e39957d208c0ffc45/ruff-0.14.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1e5cb521e5ccf0008bd74d5595a4580313844a42b9103b7388eca5a12c970743", size = 15477729, upload-time = "2025-12-11T21:39:23.279Z" }, - { url = "https://files.pythonhosted.org/packages/7a/6d/f364252aad36ccd443494bc5f02e41bf677f964b58902a17c0b16c53d890/ruff-0.14.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd429a8926be6bba4befa8cdcf3f4dd2591c413ea5066b1e99155ed245ae42bb", size = 15122386, upload-time = "2025-12-11T21:39:33.125Z" }, - { url = "https://files.pythonhosted.org/packages/20/02/e848787912d16209aba2799a4d5a1775660b6a3d0ab3944a4ccc13e64a02/ruff-0.14.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab208c1b7a492e37caeaf290b1378148f75e13c2225af5d44628b95fd7834273", size = 14497124, upload-time = "2025-12-11T21:38:59.33Z" }, - { url = "https://files.pythonhosted.org/packages/f3/51/0489a6a5595b7760b5dbac0dd82852b510326e7d88d51dbffcd2e07e3ff3/ruff-0.14.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72034534e5b11e8a593f517b2f2f2b273eb68a30978c6a2d40473ad0aaa4cb4a", size = 14195343, upload-time = "2025-12-11T21:39:44.866Z" }, - { url = "https://files.pythonhosted.org/packages/f6/53/3bb8d2fa73e4c2f80acc65213ee0830fa0c49c6479313f7a68a00f39e208/ruff-0.14.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:712ff04f44663f1b90a1195f51525836e3413c8a773574a7b7775554269c30ed", size = 14346425, upload-time = "2025-12-11T21:39:05.927Z" }, - { url = "https://files.pythonhosted.org/packages/ad/04/bdb1d0ab876372da3e983896481760867fc84f969c5c09d428e8f01b557f/ruff-0.14.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a111fee1db6f1d5d5810245295527cda1d367c5aa8f42e0fca9a78ede9b4498b", size = 13258768, upload-time = "2025-12-11T21:39:08.691Z" }, - { url = "https://files.pythonhosted.org/packages/40/d9/8bf8e1e41a311afd2abc8ad12be1b6c6c8b925506d9069b67bb5e9a04af3/ruff-0.14.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8769efc71558fecc25eb295ddec7d1030d41a51e9dcf127cbd63ec517f22d567", size = 13326939, upload-time = "2025-12-11T21:39:53.842Z" }, - { url = "https://files.pythonhosted.org/packages/f4/56/a213fa9edb6dd849f1cfbc236206ead10913693c72a67fb7ddc1833bf95d/ruff-0.14.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:347e3bf16197e8a2de17940cd75fd6491e25c0aa7edf7d61aa03f146a1aa885a", size = 13578888, upload-time = "2025-12-11T21:39:35.988Z" }, - { url = "https://files.pythonhosted.org/packages/33/09/6a4a67ffa4abae6bf44c972a4521337ffce9cbc7808faadede754ef7a79c/ruff-0.14.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7715d14e5bccf5b660f54516558aa94781d3eb0838f8e706fb60e3ff6eff03a8", size = 14314473, upload-time = "2025-12-11T21:39:50.78Z" }, - { url = "https://files.pythonhosted.org/packages/12/0d/15cc82da5d83f27a3c6b04f3a232d61bc8c50d38a6cd8da79228e5f8b8d6/ruff-0.14.9-py3-none-win32.whl", hash = "sha256:df0937f30aaabe83da172adaf8937003ff28172f59ca9f17883b4213783df197", size = 13202651, upload-time = "2025-12-11T21:39:26.628Z" }, - { url = "https://files.pythonhosted.org/packages/32/f7/c78b060388eefe0304d9d42e68fab8cffd049128ec466456cef9b8d4f06f/ruff-0.14.9-py3-none-win_amd64.whl", hash = "sha256:c0b53a10e61df15a42ed711ec0bda0c582039cf6c754c49c020084c55b5b0bc2", size = 14702079, upload-time = "2025-12-11T21:39:11.954Z" }, - { url = "https://files.pythonhosted.org/packages/26/09/7a9520315decd2334afa65ed258fed438f070e31f05a2e43dd480a5e5911/ruff-0.14.9-py3-none-win_arm64.whl", hash = "sha256:8e821c366517a074046d92f0e9213ed1c13dbc5b37a7fc20b07f79b64d62cc84", size = 13744730, upload-time = "2025-12-11T21:39:29.659Z" }, +version = "0.14.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" }, + { url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" }, + { url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" }, + { url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" }, + { url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" }, + { url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" }, + { url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" }, + { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" }, ] [[package]] @@ -1174,16 +1174,16 @@ wheels = [ [[package]] name = "soupsieve" -version = "2.8" +version = "2.8.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" } +sdist = { url = "https://files.pythonhosted.org/packages/89/23/adf3796d740536d63a6fbda113d07e60c734b6ed5d3058d1e47fc0495e47/soupsieve-2.8.1.tar.gz", hash = "sha256:4cf733bc50fa805f5df4b8ef4740fc0e0fa6218cf3006269afd3f9d6d80fd350", size = 117856, upload-time = "2025-12-18T13:50:34.655Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, + { url = "https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl", hash = "sha256:a11fe2a6f3d76ab3cf2de04eb339c1be5b506a8a47f2ceb6d139803177f85434", size = 36710, upload-time = "2025-12-18T13:50:33.267Z" }, ] [[package]] name = "spectrumx" -version = "0.1.16" +version = "0.1.17" source = { editable = "." } dependencies = [ { name = "blake3" },