From 57a50faebb93365a56f337e53120ca215c03774b Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Wed, 25 Feb 2026 16:34:55 -0500 Subject: [PATCH 01/56] Allow running "verify release candidate" github workflow on Windows (#1392) * run for windows * readme --- .../workflows/verify-release-candidate.yml | 8 ++--- dev/release/README.md | 2 +- dev/release/verify-release-candidate.sh | 31 +++++++++++++++---- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/.github/workflows/verify-release-candidate.yml b/.github/workflows/verify-release-candidate.yml index a7293b369..a10a4faa9 100644 --- a/.github/workflows/verify-release-candidate.yml +++ b/.github/workflows/verify-release-candidate.yml @@ -58,10 +58,10 @@ jobs: arch: x64 runner: macos-15-intel - # Windows (disabled for now) - # - os: windows - # arch: x64 - # runner: windows-latest + # Windows + - os: windows + arch: x64 + runner: windows-latest runs-on: ${{ matrix.runner }} steps: - name: Checkout repository diff --git a/dev/release/README.md b/dev/release/README.md index 5b70b80f4..ed28f4aa6 100644 --- a/dev/release/README.md +++ b/dev/release/README.md @@ -158,7 +158,7 @@ python3 -m twine upload --repository testpypi dist/datafusion-0.7.0.tar.gz Before sending the vote email, run the manually triggered GitHub Actions workflow "Verify Release Candidate" and confirm all matrix jobs pass across the OS/architecture matrix -(for example, Linux and macOS runners): +(for example, Linux, macOS, and Windows runners): 1. Go to https://github.com/apache/datafusion-python/actions/workflows/verify-release-candidate.yml 2. Click "Run workflow" diff --git a/dev/release/verify-release-candidate.sh b/dev/release/verify-release-candidate.sh index 2bfce0e2d..9591e0335 100755 --- a/dev/release/verify-release-candidate.sh +++ b/dev/release/verify-release-candidate.sh @@ -112,8 +112,17 @@ test_source_distribution() { curl https://sh.rustup.rs -sSf | sh -s -- -y --no-modify-path - export PATH=$RUSTUP_HOME/bin:$PATH - source $RUSTUP_HOME/env + # On Unix, rustup creates an env file. On Windows GitHub runners (MSYS bash), + # that file may not exist, so fall back to adding Cargo bin directly. + if [ -f "$CARGO_HOME/env" ]; then + # shellcheck disable=SC1090 + source "$CARGO_HOME/env" + elif [ -f "$RUSTUP_HOME/env" ]; then + # shellcheck disable=SC1090 + source "$RUSTUP_HOME/env" + else + export PATH="$CARGO_HOME/bin:$PATH" + fi # build and test rust @@ -126,10 +135,20 @@ test_source_distribution() { git clone https://github.com/apache/parquet-testing.git parquet-testing python3 -m venv .venv - source .venv/bin/activate - python3 -m pip install -U pip - python3 -m pip install -U maturin - maturin develop + if [ -x ".venv/bin/python" ]; then + VENV_PYTHON=".venv/bin/python" + elif [ -x ".venv/Scripts/python.exe" ]; then + VENV_PYTHON=".venv/Scripts/python.exe" + elif [ -x ".venv/Scripts/python" ]; then + VENV_PYTHON=".venv/Scripts/python" + else + echo "Unable to find python executable in virtual environment" + exit 1 + fi + + "$VENV_PYTHON" -m pip install -U pip + "$VENV_PYTHON" -m pip install -U maturin + "$VENV_PYTHON" -m maturin develop #TODO: we should really run tests here as well #python3 -m pytest From e42775c2fcfe8929df0874414ba2bcd6bbea174c Mon Sep 17 00:00:00 2001 From: dario curreri <48800335+dariocurr@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:13:38 +0100 Subject: [PATCH 02/56] ci: update pre-commit hooks, fix linting, and refresh dependencies (#1385) * ci: update pre-commit hooks and fix linting issues * Update Ruff version in pre-commit configuration to v0.15.1. * Add noqa comments to suppress specific linting warnings in various files. * Update regex patterns in test cases for better matching. * style: correct indentation in GitHub Actions workflow file * Adjusted indentation for the enable-cache option in the test.yml workflow file to ensure proper YAML formatting. * refactor: reorder imports in indexed_field.rs for clarity * Adjusted the order of imports in indexed_field.rs to improve readability and maintain consistency with project conventions. * build: update dependencies in Cargo.toml and Cargo.lock * Bump versions of several dependencies including tokio, pyo3-log, prost, uuid, and log to their latest releases. * Update Cargo.lock to reflect the changes in dependency versions. * style: format pyproject.toml for consistency * Adjusted formatting in pyproject.toml for improved readability by aligning lists and ensuring consistent indentation. * Updated dependencies and configuration settings for better organization. * style: remove noqa comments for import statements * Cleaned up import statements in multiple files by removing unnecessary noqa comments, enhancing code readability and maintaining consistency across the codebase. * style: simplify formatting in pyproject.toml * Streamlined list formatting in pyproject.toml for improved readability by removing unnecessary line breaks and ensuring consistent structure across sections. * No functional changes were made; the focus was solely on code style and organization. --- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 2 +- Cargo.lock | 4 +- Cargo.toml | 12 ++--- pyproject.toml | 81 ++++++++++++++++--------------- python/datafusion/expr.py | 2 + python/datafusion/user_defined.py | 4 +- python/tests/test_catalog.py | 2 +- python/tests/test_dataframe.py | 2 +- python/tests/test_functions.py | 24 ++++----- python/tests/test_sql.py | 2 +- python/tests/test_udf.py | 7 ++- python/tests/test_udwf.py | 4 +- src/expr/indexed_field.rs | 5 +- 14 files changed, 78 insertions(+), 75 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 55248b6bf..4cad8db24 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -67,7 +67,7 @@ jobs: - name: Install dependencies uses: astral-sh/setup-uv@v7 with: - enable-cache: true + enable-cache: true # Download the Linux wheel built in the build workflow - name: Download pre-built Linux wheel diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bcefa405d..8ae6a4e32 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,7 @@ repos: - id: actionlint-docker - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.9.10 + rev: v0.15.1 hooks: # Run the linter. - id: ruff diff --git a/Cargo.lock b/Cargo.lock index e830a46b3..f35f10bcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -593,9 +593,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.0" +version = "3.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c81d250916401487680ed13b8b675660281dcfc3ab0121fe44c94bcab9eae2fb" +checksum = "5c6f81257d10a0f602a294ae4182251151ff97dbb504ef9afcdda4a64b24d9b4" [[package]] name = "byteorder" diff --git a/Cargo.toml b/Cargo.toml index afa167bb1..313640ec2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ protoc = ["datafusion-substrait/protoc"] substrait = ["dep:datafusion-substrait"] [dependencies] -tokio = { version = "1.47", features = [ +tokio = { version = "1.49", features = [ "macros", "rt", "rt-multi-thread", @@ -54,16 +54,16 @@ pyo3 = { version = "0.26", features = [ "abi3-py310", ] } pyo3-async-runtimes = { version = "0.26", features = ["tokio-runtime"] } -pyo3-log = "0.13.2" +pyo3-log = "0.13.3" arrow = { version = "57", features = ["pyarrow"] } arrow-select = { version = "57" } datafusion = { version = "52", features = ["avro", "unicode_expressions"] } datafusion-substrait = { version = "52", optional = true } datafusion-proto = { version = "52" } datafusion-ffi = { version = "52" } -prost = "0.14.1" # keep in line with `datafusion-substrait` +prost = "0.14.3" # keep in line with `datafusion-substrait` serde_json = "1" -uuid = { version = "1.18", features = ["v4"] } +uuid = { version = "1.21", features = ["v4"] } mimalloc = { version = "0.1", optional = true, default-features = false, features = [ "local_dynamic_tls", ] } @@ -77,11 +77,11 @@ object_store = { version = "0.12.4", features = [ "http", ] } url = "2" -log = "0.4.27" +log = "0.4.29" parking_lot = "0.12" [build-dependencies] -prost-types = "0.14.1" # keep in line with `datafusion-substrait` +prost-types = "0.14.3" # keep in line with `datafusion-substrait` pyo3-build-config = "0.26" [lib] diff --git a/pyproject.toml b/pyproject.toml index 5a5128a2f..08d64eca0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ description = "Build and run queries against data" readme = "README.md" license = { file = "LICENSE.txt" } requires-python = ">=3.10" -keywords = ["datafusion", "dataframe", "rust", "query-engine"] +keywords = ["dataframe", "datafusion", "query-engine", "rust"] classifiers = [ "Development Status :: 2 - Pre-Alpha", "Intended Audience :: Developers", @@ -62,7 +62,7 @@ profile = "black" python-source = "python" module-name = "datafusion._internal" include = [{ path = "Cargo.lock", format = "sdist" }] -exclude = [".github/**", "ci/**", ".asf.yaml"] +exclude = [".asf.yaml", ".github/**", "ci/**"] # Require Cargo.lock is up to date locked = true features = ["substrait"] @@ -77,19 +77,19 @@ select = ["ALL"] ignore = [ "A001", # Allow using words like min as variable names "A002", # Allow using words like filter as variable names + "A005", # Allow module named io "ANN401", # Allow Any for wrapper classes "COM812", # Recommended to ignore these rules when using with ruff-format - "FIX002", # Allow TODO lines - consider removing at some point "FBT001", # Allow boolean positional args "FBT002", # Allow boolean positional args + "FIX002", # Allow TODO lines - consider removing at some point "ISC001", # Recommended to ignore these rules when using with ruff-format + "N812", # Allow importing functions as `F` + "PD901", # Allow variable name df + "PLR0913", # Allow many arguments in function definition "SLF001", # Allow accessing private members "TD002", # Do not require author names in TODO statements "TD003", # Allow TODO lines - "PLR0913", # Allow many arguments in function definition - "PD901", # Allow variable name df - "N812", # Allow importing functions as `F` - "A005", # Allow module named io ] [tool.ruff.lint.pydocstyle] @@ -99,7 +99,7 @@ convention = "google" max-doc-length = 88 [tool.ruff.lint.flake8-boolean-trap] -extend-allowed-calls = ["lit", "datafusion.lit"] +extend-allowed-calls = ["datafusion.lit", "lit"] # Disable docstring checking for these directories [tool.ruff.lint.per-file-ignores] @@ -108,68 +108,69 @@ extend-allowed-calls = ["lit", "datafusion.lit"] "ARG", "BLE001", "D", - "S101", - "SLF", "PD", + "PLC0415", + "PLR0913", "PLR2004", + "PT004", "PT011", "RUF015", + "S101", "S608", - "PLR0913", - "PT004", + "SLF", ] "examples/*" = [ - "D", - "W505", - "E501", - "T201", - "S101", - "PLR2004", "ANN001", "ANN202", - "INP001", + "D", "DTZ007", + "E501", + "INP001", + "PLR2004", "RUF015", + "S101", + "T201", + "W505", ] "dev/*" = [ + "ANN001", + "C", "D", "E", - "T", - "S", + "ERA001", + "EXE", + "N817", "PLR", - "C", + "S", "SIM", + "T", "UP", - "EXE", - "N817", - "ERA001", - "ANN001", ] "benchmarks/*" = [ + "ANN001", + "BLE", "D", + "E", + "ERA001", + "EXE", "F", - "T", - "BLE", "FURB", + "INP001", "PLR", - "E", - "TD", - "TRY", "S", "SIM", - "EXE", + "T", + "TD", + "TRY", "UP", - "ERA001", - "ANN001", - "INP001", ] "docs/*" = ["D"] -"docs/source/conf.py" = ["ERA001", "ANN001", "INP001"] +"docs/source/conf.py" = ["ANN001", "ERA001", "INP001"] [tool.codespell] -skip = ["./target", "uv.lock", "./python/tests/test_functions.py"] +skip = ["./python/tests/test_functions.py", "./target", "uv.lock"] count = true -ignore-words-list = ["ans", "IST"] +ignore-words-list = ["IST", "ans"] [dependency-groups] dev = [ @@ -182,8 +183,8 @@ dev = [ "pre-commit>=4.3.0", "pyarrow>=19.0.0", "pygithub==2.5.0", - "pytest>=7.4.4", "pytest-asyncio>=0.23.3", + "pytest>=7.4.4", "pyyaml>=6.0.3", "ruff>=0.9.1", "toml>=0.10.2", @@ -196,6 +197,6 @@ docs = [ "pickleshare>=0.7.5", "pydata-sphinx-theme==0.8.0", "setuptools>=75.3.0", - "sphinx>=7.1.2", "sphinx-autoapi>=3.4.0", + "sphinx>=7.1.2", ] diff --git a/python/datafusion/expr.py b/python/datafusion/expr.py index 9df58f52a..5760b8948 100644 --- a/python/datafusion/expr.py +++ b/python/datafusion/expr.py @@ -20,6 +20,8 @@ See :ref:`Expressions` in the online documentation for more details. """ +# ruff: noqa: PLC0415 + from __future__ import annotations from collections.abc import Iterable, Sequence diff --git a/python/datafusion/user_defined.py b/python/datafusion/user_defined.py index d4e5302b5..eef23e741 100644 --- a/python/datafusion/user_defined.py +++ b/python/datafusion/user_defined.py @@ -583,11 +583,11 @@ def from_pycapsule(func: AggregateUDFExportable | _PyCapsule) -> AggregateUDF: AggregateUDF that is exported via the FFI bindings. """ if _is_pycapsule(func): - aggregate = cast(AggregateUDF, object.__new__(AggregateUDF)) + aggregate = cast("AggregateUDF", object.__new__(AggregateUDF)) aggregate._udaf = df_internal.AggregateUDF.from_pycapsule(func) return aggregate - capsule = cast(AggregateUDFExportable, func) + capsule = cast("AggregateUDFExportable", func) name = str(capsule.__class__) return AggregateUDF( name=name, diff --git a/python/tests/test_catalog.py b/python/tests/test_catalog.py index 71c08da26..9310da506 100644 --- a/python/tests/test_catalog.py +++ b/python/tests/test_catalog.py @@ -248,7 +248,7 @@ def test_exception_not_mangled(ctx: SessionContext): schema.register_table("test_table", create_dataset()) - with pytest.raises(ValueError, match="^test_table is not an acceptable name$"): + with pytest.raises(ValueError, match=r"^test_table is not an acceptable name$"): ctx.sql(f"select * from {catalog_name}.{schema_name}.test_table") diff --git a/python/tests/test_dataframe.py b/python/tests/test_dataframe.py index 71abe2925..de6b00acf 100644 --- a/python/tests/test_dataframe.py +++ b/python/tests/test_dataframe.py @@ -2790,7 +2790,7 @@ def test_write_parquet_with_options_encoding(tmp_path, encoding, data_types, res def test_write_parquet_with_options_unsupported_encoding(df, tmp_path, encoding): """Test that unsupported Parquet encodings do not work.""" # BaseException is used since this throws a Rust panic: https://github.com/PyO3/pyo3/issues/3519 - with pytest.raises(BaseException, match="Encoding .*? is not supported"): + with pytest.raises(BaseException, match=r"Encoding .*? is not supported"): df.write_parquet_with_options(tmp_path, ParquetWriterOptions(encoding=encoding)) diff --git a/python/tests/test_functions.py b/python/tests/test_functions.py index 7b3332ed7..5a61a2dd1 100644 --- a/python/tests/test_functions.py +++ b/python/tests/test_functions.py @@ -303,19 +303,19 @@ def py_flatten(arr): lambda data: [np.concatenate([arr, arr]) for arr in data], ), ( - lambda col: f.array_dims(col), + f.array_dims, lambda data: [[len(r)] for r in data], ), ( - lambda col: f.array_distinct(col), + f.array_distinct, lambda data: [list(set(r)) for r in data], ), ( - lambda col: f.list_distinct(col), + f.list_distinct, lambda data: [list(set(r)) for r in data], ), ( - lambda col: f.list_dims(col), + f.list_dims, lambda data: [[len(r)] for r in data], ), ( @@ -323,11 +323,11 @@ def py_flatten(arr): lambda data: [r[0] for r in data], ), ( - lambda col: f.array_empty(col), + f.array_empty, lambda data: [len(r) == 0 for r in data], ), ( - lambda col: f.empty(col), + f.empty, lambda data: [len(r) == 0 for r in data], ), ( @@ -343,11 +343,11 @@ def py_flatten(arr): lambda data: [r[0] for r in data], ), ( - lambda col: f.array_length(col), + f.array_length, lambda data: [len(r) for r in data], ), ( - lambda col: f.list_length(col), + f.list_length, lambda data: [len(r) for r in data], ), ( @@ -391,11 +391,11 @@ def py_flatten(arr): lambda data: [[i + 1 for i, _v in enumerate(r) if _v == 1.0] for r in data], ), ( - lambda col: f.array_ndims(col), + f.array_ndims, lambda data: [np.array(r).ndim for r in data], ), ( - lambda col: f.list_ndims(col), + f.list_ndims, lambda data: [np.array(r).ndim for r in data], ), ( @@ -415,11 +415,11 @@ def py_flatten(arr): lambda data: [np.insert(arr, 0, 99.0) for arr in data], ), ( - lambda col: f.array_pop_back(col), + f.array_pop_back, lambda data: [arr[:-1] for arr in data], ), ( - lambda col: f.array_pop_front(col), + f.array_pop_front, lambda data: [arr[1:] for arr in data], ), ( diff --git a/python/tests/test_sql.py b/python/tests/test_sql.py index 12710cf08..92c311930 100644 --- a/python/tests/test_sql.py +++ b/python/tests/test_sql.py @@ -31,7 +31,7 @@ def test_no_table(ctx): with pytest.raises( ValueError, - match="^Error during planning: table 'datafusion.public.b' not found$", + match=r"^Error during planning: table 'datafusion.public.b' not found$", ): ctx.sql("SELECT a FROM b").collect() diff --git a/python/tests/test_udf.py b/python/tests/test_udf.py index c0ba1d831..b2540fb57 100644 --- a/python/tests/test_udf.py +++ b/python/tests/test_udf.py @@ -15,7 +15,10 @@ # specific language governing permissions and limitations # under the License. +from uuid import UUID + import pyarrow as pa +import pyarrow.compute as pc import pytest from datafusion import SessionContext, column, udf from datafusion import functions as f @@ -128,8 +131,6 @@ def udf_with_param(values: pa.Array) -> pa.Array: def test_udf_with_metadata(ctx) -> None: - from uuid import UUID - @udf([pa.string()], pa.uuid(), "stable") def uuid_from_string(uuid_string): return pa.array((UUID(s).bytes for s in uuid_string.to_pylist()), pa.uuid()) @@ -151,8 +152,6 @@ def uuid_version(uuid): def test_udf_with_nullability(ctx: SessionContext) -> None: - import pyarrow.compute as pc - field_nullable_i64 = pa.field("with_nulls", type=pa.int64(), nullable=True) field_non_nullable_i64 = pa.field("no_nulls", type=pa.int64(), nullable=False) diff --git a/python/tests/test_udwf.py b/python/tests/test_udwf.py index 5aaf00664..38b935b7e 100644 --- a/python/tests/test_udwf.py +++ b/python/tests/test_udwf.py @@ -433,8 +433,8 @@ def test_udwf_functions(complex_window_df, name, expr, expected): [ udwf(SimpleWindowCount, pa.int64(), pa.int64(), "immutable"), udwf(SimpleWindowCount, [pa.int64()], pa.int64(), "immutable"), - udwf([pa.int64()], pa.int64(), "immutable")(lambda: SimpleWindowCount()), - udwf(pa.int64(), pa.int64(), "immutable")(lambda: SimpleWindowCount()), + udwf([pa.int64()], pa.int64(), "immutable")(SimpleWindowCount), + udwf(pa.int64(), pa.int64(), "immutable")(SimpleWindowCount), ], ) def test_udwf_overloads(udwf_func, count_window_df): diff --git a/src/expr/indexed_field.rs b/src/expr/indexed_field.rs index 1dfa0ed2f..79f528179 100644 --- a/src/expr/indexed_field.rs +++ b/src/expr/indexed_field.rs @@ -15,12 +15,13 @@ // specific language governing permissions and limitations // under the License. -use crate::expr::PyExpr; +use std::fmt::{Display, Formatter}; + use datafusion::logical_expr::expr::{GetFieldAccess, GetIndexedField}; use pyo3::prelude::*; -use std::fmt::{Display, Formatter}; use super::literal::PyLiteral; +use crate::expr::PyExpr; #[pyclass(frozen, name = "GetIndexedField", module = "datafusion.expr", subclass)] #[derive(Clone)] From 0c1499cddea5fa20c13728b0c2726aea4fbd1b08 Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Mon, 2 Mar 2026 08:21:37 -0500 Subject: [PATCH 03/56] fix: satisfy rustfmt check in lib.rs re-exports (#1406) --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 081366b20..468243a3d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,9 +16,9 @@ // under the License. // Re-export Apache Arrow DataFusion dependencies -pub use datafusion; pub use datafusion::{ - common as datafusion_common, logical_expr as datafusion_expr, optimizer, sql as datafusion_sql, + self, common as datafusion_common, logical_expr as datafusion_expr, optimizer, + sql as datafusion_sql, }; #[cfg(feature = "substrait")] pub use datafusion_substrait; From 7b630ee893d6b81ae7a0f2c35b77dab723567b13 Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Wed, 4 Mar 2026 10:24:32 -0500 Subject: [PATCH 04/56] Add check for crates.io patches to CI (#1407) --- .github/workflows/build.yml | 9 ++++++ Cargo.toml | 5 +++ dev/check_crates_patch.py | 61 +++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 dev/check_crates_patch.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b86b37c6e..455a0dc1a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -98,6 +98,15 @@ jobs: - name: Check Cargo.toml formatting run: taplo format --check + check-crates-patch: + if: inputs.build_mode == 'release' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Ensure [patch.crates-io] is empty + run: python3 dev/check_crates_patch.py + generate-license: runs-on: ubuntu-latest steps: diff --git a/Cargo.toml b/Cargo.toml index 313640ec2..c19ae21f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,3 +91,8 @@ crate-type = ["cdylib", "rlib"] [profile.release] lto = true codegen-units = 1 + +# We cannot publish to crates.io with any patches in the below section. Developers +# must remove any entries in this section before creating a release candidate. +[patch.crates-io] +# datafusion = { git = "https://github.com/apache/datafusion.git", rev = "6713439497561fa74a94177e5b8632322fb7cea5" } diff --git a/dev/check_crates_patch.py b/dev/check_crates_patch.py new file mode 100644 index 000000000..74e489e1f --- /dev/null +++ b/dev/check_crates_patch.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""Check that no Cargo.toml files contain [patch.crates-io] entries. + +Release builds must not depend on patched crates. During development it is +common to temporarily patch crates-io dependencies, but those patches must +be removed before creating a release. + +An empty [patch.crates-io] section is allowed. +""" + +import sys +from pathlib import Path + +import tomllib + + +def main() -> int: + errors: list[str] = [] + for cargo_toml in sorted(Path().rglob("Cargo.toml")): + if "target" in cargo_toml.parts: + continue + with Path.open(cargo_toml, "rb") as f: + data = tomllib.load(f) + patch = data.get("patch", {}).get("crates-io", {}) + if patch: + errors.append(str(cargo_toml)) + for name, spec in patch.items(): + errors.append(f" {name} = {spec}") + + if errors: + print("ERROR: Release builds must not contain [patch.crates-io] entries.") + print() + for line in errors: + print(line) + print() + print("Remove all [patch.crates-io] entries before creating a release.") + return 1 + + print("OK: No [patch.crates-io] entries found.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 231ed2b1d375fefe9aa01cdc8ae41c620c772f76 Mon Sep 17 00:00:00 2001 From: Nick <24689722+ntjohnson1@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:42:20 -0500 Subject: [PATCH 05/56] Enable doc tests in local and CI testing (#1409) * Turn on doctests * Fix existing doc examples * Remove stale referenece to rust-toolchain removed in #1383, surpised pre-commit didn't flag for anyone else --- .github/workflows/test.yml | 4 ++-- pyproject.toml | 3 +++ python/datafusion/dataframe.py | 14 +++++++++----- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4cad8db24..692563019 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -62,7 +62,7 @@ jobs: uses: actions/cache@v5 with: path: ~/.cargo - key: cargo-cache-${{ steps.rust-toolchain.outputs.cachekey }}-${{ hashFiles('Cargo.lock') }} + key: cargo-cache-${{ matrix.toolchain }}-${{ hashFiles('Cargo.lock') }} - name: Install dependencies uses: astral-sh/setup-uv@v7 @@ -106,7 +106,7 @@ jobs: RUST_BACKTRACE: 1 run: | git submodule update --init - uv run --no-project pytest -v . --import-mode=importlib + uv run --no-project pytest -v --import-mode=importlib - name: FFI unit tests run: | diff --git a/pyproject.toml b/pyproject.toml index 08d64eca0..b238e049e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,9 @@ features = ["substrait"] [tool.pytest.ini_options] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" +addopts = "--doctest-modules" +doctest_optionflags = ["NORMALIZE_WHITESPACE", "ELLIPSIS"] +testpaths = ["python/tests", "python/datafusion"] # Enable docstring linting using the google style guide [tool.ruff.lint] diff --git a/python/datafusion/dataframe.py b/python/datafusion/dataframe.py index d302c12a5..5bd0eec2d 100644 --- a/python/datafusion/dataframe.py +++ b/python/datafusion/dataframe.py @@ -327,8 +327,9 @@ def into_view(self, temporary: bool = False) -> Table: >>> df = ctx.sql("SELECT 1 AS value") >>> view = df.into_view() >>> ctx.register_table("values_view", view) - >>> df.collect() # The DataFrame is still usable - >>> ctx.sql("SELECT value FROM values_view").collect() + >>> result = ctx.sql("SELECT value FROM values_view").collect() + >>> result[0].column("value").to_pylist() + [1] """ from datafusion.catalog import Table as _Table @@ -1389,9 +1390,12 @@ def fill_null(self, value: Any, subset: list[str] | None = None) -> DataFrame: DataFrame with null values replaced where type casting is possible Examples: - >>> df = df.fill_null(0) # Fill all nulls with 0 where possible - >>> # Fill nulls in specific string columns - >>> df = df.fill_null("missing", subset=["name", "category"]) + >>> from datafusion import SessionContext, col + >>> ctx = SessionContext() + >>> df = ctx.from_pydict({"a": [1, None, 3], "b": [None, 5, 6]}) + >>> filled = df.fill_null(0) + >>> filled.sort(col("a")).collect()[0].column("a").to_pylist() + [0, 1, 3] Notes: - Only fills nulls in columns where the value can be cast to the column type From 8ef2cd75d984758b3ae2db43629666da1a7bee19 Mon Sep 17 00:00:00 2001 From: Nuno Faria Date: Fri, 6 Mar 2026 16:11:42 +0000 Subject: [PATCH 06/56] Upgrade to DataFusion 53 (#1402) * Upgrade to DataFusion 53 * Fix fmt * Fix fmt * Fix docs * Bump datafusion rev to 53.0.0 * Bump ffi example datafusion commit to the same as main repo --------- Co-authored-by: Tim Saucer --- Cargo.lock | 427 ++--- Cargo.toml | 25 +- docs/source/contributor-guide/ffi.rst | 13 +- docs/source/user-guide/upgrade-guides.rst | 21 + examples/datafusion-ffi-example/Cargo.lock | 353 ++-- examples/datafusion-ffi-example/Cargo.toml | 31 +- .../src/aggregate_udf.rs | 7 +- .../src/catalog_provider.rs | 3 + .../datafusion-ffi-example/src/scalar_udf.rs | 7 +- .../src/table_function.rs | 7 +- .../src/table_provider.rs | 7 +- examples/datafusion-ffi-example/src/utils.rs | 11 +- .../datafusion-ffi-example/src/window_udf.rs | 7 +- python/datafusion/dataframe.py | 22 +- python/tests/test_aggregation.py | 4 +- python/tests/test_context.py | 2 +- python/tests/test_dataframe.py | 13 +- python/tests/test_expr.py | 6 +- python/tests/test_functions.py | 30 +- src/array.rs | 14 +- src/catalog.rs | 34 +- src/common/data_type.rs | 48 +- src/common/df_schema.rs | 8 +- src/common/function.rs | 8 +- src/common/schema.rs | 55 +- src/config.rs | 8 +- src/context.rs | 110 +- src/dataframe.rs | 53 +- src/dataset.rs | 2 +- src/dataset_exec.rs | 14 +- src/expr.rs | 30 +- src/expr/aggregate.rs | 8 +- src/expr/aggregate_expr.rs | 1 + src/expr/alias.rs | 8 +- src/expr/analyze.rs | 8 +- src/expr/between.rs | 8 +- src/expr/binary_expr.rs | 8 +- src/expr/bool_expr.rs | 80 +- src/expr/case.rs | 8 +- src/expr/cast.rs | 10 +- src/expr/column.rs | 8 +- src/expr/conditional_expr.rs | 8 +- src/expr/copy_to.rs | 16 +- src/expr/create_catalog.rs | 8 +- src/expr/create_catalog_schema.rs | 1 + src/expr/create_external_table.rs | 1 + src/expr/create_function.rs | 19 +- src/expr/create_index.rs | 8 +- src/expr/create_memory_table.rs | 1 + src/expr/create_view.rs | 8 +- src/expr/describe_table.rs | 8 +- src/expr/distinct.rs | 8 +- src/expr/dml.rs | 22 +- src/expr/drop_catalog_schema.rs | 1 + src/expr/drop_function.rs | 8 +- src/expr/drop_table.rs | 8 +- src/expr/drop_view.rs | 8 +- src/expr/empty_relation.rs | 8 +- src/expr/exists.rs | 8 +- src/expr/explain.rs | 8 +- src/expr/extension.rs | 8 +- src/expr/filter.rs | 8 +- src/expr/grouping_set.rs | 8 +- src/expr/in_list.rs | 8 +- src/expr/in_subquery.rs | 8 +- src/expr/indexed_field.rs | 8 +- src/expr/join.rs | 17 +- src/expr/like.rs | 24 +- src/expr/limit.rs | 8 +- src/expr/literal.rs | 8 +- src/expr/placeholder.rs | 8 +- src/expr/projection.rs | 8 +- src/expr/recursive_query.rs | 8 +- src/expr/repartition.rs | 16 +- src/expr/scalar_subquery.rs | 8 +- src/expr/scalar_variable.rs | 8 +- src/expr/set_comparison.rs | 59 + src/expr/signature.rs | 8 +- src/expr/sort.rs | 8 +- src/expr/sort_expr.rs | 8 +- src/expr/statement.rs | 52 +- src/expr/subquery.rs | 8 +- src/expr/subquery_alias.rs | 8 +- src/expr/table_scan.rs | 8 +- src/expr/union.rs | 8 +- src/expr/unnest.rs | 8 +- src/expr/unnest_expr.rs | 8 +- src/expr/values.rs | 8 +- src/expr/window.rs | 17 +- src/physical_plan.rs | 10 +- src/pyarrow_util.rs | 10 +- src/sql/logical.rs | 11 +- src/store.rs | 33 +- src/substrait.rs | 34 +- src/table.rs | 12 +- src/udaf.rs | 19 +- src/udf.rs | 17 +- src/udtf.rs | 11 +- src/udwf.rs | 17 +- src/unparser/dialect.rs | 8 +- src/unparser/mod.rs | 8 +- src/utils.rs | 18 +- uv.lock | 1595 +++++++++-------- 103 files changed, 2425 insertions(+), 1431 deletions(-) create mode 100644 src/expr/set_comparison.rs diff --git a/Cargo.lock b/Cargo.lock index f35f10bcc..82d5c66d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -176,9 +176,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "arrow" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4754a624e5ae42081f464514be454b39711daae0458906dacde5f4c632f33a8" +checksum = "602268ce9f569f282cedb9a9f6bac569b680af47b9b077d515900c03c5d190da" dependencies = [ "arrow-arith", "arrow-array", @@ -198,9 +198,9 @@ dependencies = [ [[package]] name = "arrow-arith" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7b3141e0ec5145a22d8694ea8b6d6f69305971c4fa1c1a13ef0195aef2d678b" +checksum = "cd53c6bf277dea91f136ae8e3a5d7041b44b5e489e244e637d00ae302051f56f" dependencies = [ "arrow-array", "arrow-buffer", @@ -212,9 +212,9 @@ dependencies = [ [[package]] name = "arrow-array" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8955af33b25f3b175ee10af580577280b4bd01f7e823d94c7cdef7cf8c9aef" +checksum = "e53796e07a6525edaf7dc28b540d477a934aff14af97967ad1d5550878969b9e" dependencies = [ "ahash", "arrow-buffer", @@ -231,9 +231,9 @@ dependencies = [ [[package]] name = "arrow-buffer" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c697ddca96183182f35b3a18e50b9110b11e916d7b7799cbfd4d34662f2c56c2" +checksum = "f2c1a85bb2e94ee10b76531d8bc3ce9b7b4c0d508cabfb17d477f63f2617bd20" dependencies = [ "bytes", "half", @@ -243,9 +243,9 @@ dependencies = [ [[package]] name = "arrow-cast" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "646bbb821e86fd57189c10b4fcdaa941deaf4181924917b0daa92735baa6ada5" +checksum = "89fb245db6b0e234ed8e15b644edb8664673fefe630575e94e62cd9d489a8a26" dependencies = [ "arrow-array", "arrow-buffer", @@ -265,9 +265,9 @@ dependencies = [ [[package]] name = "arrow-csv" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da746f4180004e3ce7b83c977daf6394d768332349d3d913998b10a120b790a" +checksum = "d374882fb465a194462527c0c15a93aa19a554cf690a6b77a26b2a02539937a7" dependencies = [ "arrow-array", "arrow-cast", @@ -280,9 +280,9 @@ dependencies = [ [[package]] name = "arrow-data" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fdd994a9d28e6365aa78e15da3f3950c0fdcea6b963a12fa1c391afb637b304" +checksum = "189d210bc4244c715fa3ed9e6e22864673cccb73d5da28c2723fb2e527329b33" dependencies = [ "arrow-buffer", "arrow-schema", @@ -293,9 +293,9 @@ dependencies = [ [[package]] name = "arrow-ipc" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abf7df950701ab528bf7c0cf7eeadc0445d03ef5d6ffc151eaae6b38a58feff1" +checksum = "7968c2e5210c41f4909b2ef76f6e05e172b99021c2def5edf3cc48fdd39d1d6c" dependencies = [ "arrow-array", "arrow-buffer", @@ -309,9 +309,9 @@ dependencies = [ [[package]] name = "arrow-json" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ff8357658bedc49792b13e2e862b80df908171275f8e6e075c460da5ee4bf86" +checksum = "92111dba5bf900f443488e01f00d8c4ddc2f47f5c50039d18120287b580baa22" dependencies = [ "arrow-array", "arrow-buffer", @@ -333,9 +333,9 @@ dependencies = [ [[package]] name = "arrow-ord" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d8f1870e03d4cbed632959498bcc84083b5a24bded52905ae1695bd29da45b" +checksum = "211136cb253577ee1a6665f741a13136d4e563f64f5093ffd6fb837af90b9495" dependencies = [ "arrow-array", "arrow-buffer", @@ -346,9 +346,9 @@ dependencies = [ [[package]] name = "arrow-pyarrow" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d18c442b4c266aaf3d7f7dd40fd7ae058cef7f113b00ff0cd8256e1e218ec544" +checksum = "205437da4c0877c756c81bfe847a621d0a740cd00a155109d65510a1a62ebcd9" dependencies = [ "arrow-array", "arrow-data", @@ -358,9 +358,9 @@ dependencies = [ [[package]] name = "arrow-row" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18228633bad92bff92a95746bbeb16e5fc318e8382b75619dec26db79e4de4c0" +checksum = "8e0f20145f9f5ea3fe383e2ba7a7487bf19be36aa9dbf5dd6a1f92f657179663" dependencies = [ "arrow-array", "arrow-buffer", @@ -371,9 +371,9 @@ dependencies = [ [[package]] name = "arrow-schema" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c872d36b7bf2a6a6a2b40de9156265f0242910791db366a2c17476ba8330d68" +checksum = "1b47e0ca91cc438d2c7879fe95e0bca5329fff28649e30a88c6f760b1faeddcb" dependencies = [ "bitflags", "serde_core", @@ -382,9 +382,9 @@ dependencies = [ [[package]] name = "arrow-select" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68bf3e3efbd1278f770d67e5dc410257300b161b93baedb3aae836144edcaf4b" +checksum = "750a7d1dda177735f5e82a314485b6915c7cccdbb278262ac44090f4aba4a325" dependencies = [ "ahash", "arrow-array", @@ -396,9 +396,9 @@ dependencies = [ [[package]] name = "arrow-string" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e968097061b3c0e9fe3079cf2e703e487890700546b5b0647f60fca1b5a8d8" +checksum = "e1eab1208bc4fe55d768cdc9b9f3d9df5a794cdb3ee2586bf89f9b30dc31ad8c" dependencies = [ "arrow-array", "arrow-buffer", @@ -452,7 +452,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -463,7 +463,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -567,7 +567,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -644,9 +644,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "num-traits", @@ -870,7 +870,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -881,7 +881,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -900,9 +900,8 @@ dependencies = [ [[package]] name = "datafusion" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d12ee9fdc6cdb5898c7691bb994f0ba606c4acc93a2258d78bb9f26ff8158bb3" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "arrow-schema", @@ -956,9 +955,8 @@ dependencies = [ [[package]] name = "datafusion-catalog" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "462dc9ef45e5d688aeaae49a7e310587e81b6016b9d03bace5626ad0043e5a9e" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "async-trait", @@ -981,9 +979,8 @@ dependencies = [ [[package]] name = "datafusion-catalog-listing" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b96dbf1d728fc321817b744eb5080cdd75312faa6980b338817f68f3caa4208" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "async-trait", @@ -1004,9 +1001,8 @@ dependencies = [ [[package]] name = "datafusion-common" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3237a6ff0d2149af4631290074289cae548c9863c885d821315d54c6673a074a" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "ahash", "apache-avro", @@ -1016,6 +1012,7 @@ dependencies = [ "half", "hashbrown 0.16.1", "indexmap", + "itertools", "libc", "log", "object_store", @@ -1029,9 +1026,8 @@ dependencies = [ [[package]] name = "datafusion-common-runtime" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70b5e34026af55a1bfccb1ef0a763cf1f64e77c696ffcf5a128a278c31236528" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "futures", "log", @@ -1040,9 +1036,8 @@ dependencies = [ [[package]] name = "datafusion-datasource" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b2a6be734cc3785e18bbf2a7f2b22537f6b9fb960d79617775a51568c281842" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "async-compression", @@ -1075,9 +1070,8 @@ dependencies = [ [[package]] name = "datafusion-datasource-arrow" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1739b9b07c9236389e09c74f770e88aff7055250774e9def7d3f4f56b3dcc7be" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "arrow-ipc", @@ -1099,9 +1093,8 @@ dependencies = [ [[package]] name = "datafusion-datasource-avro" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "828088c2fb681cc0e06fb42f541f76c82a0c10278f9fd6334e22c8d1e3574ee7" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "apache-avro", "arrow", @@ -1119,9 +1112,8 @@ dependencies = [ [[package]] name = "datafusion-datasource-csv" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c73bc54b518bbba7c7650299d07d58730293cfba4356f6f428cc94c20b7600" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "async-trait", @@ -1142,9 +1134,8 @@ dependencies = [ [[package]] name = "datafusion-datasource-json" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37812c8494c698c4d889374ecfabbff780f1f26d9ec095dd1bddfc2a8ca12559" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "async-trait", @@ -1159,14 +1150,15 @@ dependencies = [ "datafusion-session", "futures", "object_store", + "serde_json", "tokio", + "tokio-stream", ] [[package]] name = "datafusion-datasource-parquet" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2210937ecd9f0e824c397e73f4b5385c97cd1aff43ab2b5836fcfd2d321523fb" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "async-trait", @@ -1194,22 +1186,22 @@ dependencies = [ [[package]] name = "datafusion-doc" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c825f969126bc2ef6a6a02d94b3c07abff871acf4d6dd759ce1255edb7923ce" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" [[package]] name = "datafusion-execution" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa03ef05a2c2f90dd6c743e3e111078e322f4b395d20d4b4d431a245d79521ae" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", + "arrow-buffer", "async-trait", "chrono", "dashmap", "datafusion-common", "datafusion-expr", + "datafusion-physical-expr-common", "futures", "log", "object_store", @@ -1221,9 +1213,8 @@ dependencies = [ [[package]] name = "datafusion-expr" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef33934c1f98ee695cc51192cc5f9ed3a8febee84fdbcd9131bf9d3a9a78276f" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "async-trait", @@ -1244,9 +1235,8 @@ dependencies = [ [[package]] name = "datafusion-expr-common" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "000c98206e3dd47d2939a94b6c67af4bfa6732dd668ac4fafdbde408fd9134ea" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "datafusion-common", @@ -1257,9 +1247,8 @@ dependencies = [ [[package]] name = "datafusion-ffi" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30f57f7f63a25a0b78b3f2a5e18c0ecbd54851b64064ac0d5a9eb05efd5586d2" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "abi_stable", "arrow", @@ -1287,9 +1276,8 @@ dependencies = [ [[package]] name = "datafusion-functions" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "379b01418ab95ca947014066248c22139fe9af9289354de10b445bd000d5d276" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "arrow-buffer", @@ -1308,6 +1296,7 @@ dependencies = [ "itertools", "log", "md-5", + "memchr", "num-traits", "rand", "regex", @@ -1318,9 +1307,8 @@ dependencies = [ [[package]] name = "datafusion-functions-aggregate" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd00d5454ba4c3f8ebbd04bd6a6a9dc7ced7c56d883f70f2076c188be8459e4c" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "ahash", "arrow", @@ -1334,14 +1322,14 @@ dependencies = [ "datafusion-physical-expr-common", "half", "log", + "num-traits", "paste", ] [[package]] name = "datafusion-functions-aggregate-common" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aec06b380729a87210a4e11f555ec2d729a328142253f8d557b87593622ecc9f" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "ahash", "arrow", @@ -1352,9 +1340,8 @@ dependencies = [ [[package]] name = "datafusion-functions-nested" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904f48d45e0f1eb7d0eb5c0f80f2b5c6046a85454364a6b16a2e0b46f62e7dff" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "arrow-ord", @@ -1368,16 +1355,17 @@ dependencies = [ "datafusion-functions-aggregate-common", "datafusion-macros", "datafusion-physical-expr-common", + "hashbrown 0.16.1", "itertools", + "itoa", "log", "paste", ] [[package]] name = "datafusion-functions-table" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9a0d20e2b887e11bee24f7734d780a2588b925796ac741c3118dd06d5aa77f0" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "async-trait", @@ -1391,9 +1379,8 @@ dependencies = [ [[package]] name = "datafusion-functions-window" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3414b0a07e39b6979fe3a69c7aa79a9f1369f1d5c8e52146e66058be1b285ee" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "datafusion-common", @@ -1409,9 +1396,8 @@ dependencies = [ [[package]] name = "datafusion-functions-window-common" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf2feae63cd4754e31add64ce75cae07d015bce4bb41cd09872f93add32523a" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "datafusion-common", "datafusion-physical-expr-common", @@ -1419,20 +1405,18 @@ dependencies = [ [[package]] name = "datafusion-macros" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4fe888aeb6a095c4bcbe8ac1874c4b9a4c7ffa2ba849db7922683ba20875aaf" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "datafusion-doc", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] name = "datafusion-optimizer" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a6527c063ae305c11be397a86d8193936f4b84d137fe40bd706dfc178cf733c" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "chrono", @@ -1450,9 +1434,8 @@ dependencies = [ [[package]] name = "datafusion-physical-expr" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb028323dd4efd049dd8a78d78fe81b2b969447b39c51424167f973ac5811d9" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "ahash", "arrow", @@ -1474,9 +1457,8 @@ dependencies = [ [[package]] name = "datafusion-physical-expr-adapter" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78fe0826aef7eab6b4b61533d811234a7a9e5e458331ebbf94152a51fc8ab433" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "datafusion-common", @@ -1489,9 +1471,8 @@ dependencies = [ [[package]] name = "datafusion-physical-expr-common" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfccd388620734c661bd8b7ca93c44cdd59fecc9b550eea416a78ffcbb29475f" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "ahash", "arrow", @@ -1506,9 +1487,8 @@ dependencies = [ [[package]] name = "datafusion-physical-optimizer" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde5fa10e73259a03b705d5fddc136516814ab5f441b939525618a4070f5a059" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "datafusion-common", @@ -1525,9 +1505,8 @@ dependencies = [ [[package]] name = "datafusion-physical-plan" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1098760fb29127c24cc9ade3277051dc73c9ed0ac0131bd7bcd742e0ad7470" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "ahash", "arrow", @@ -1549,6 +1528,7 @@ dependencies = [ "indexmap", "itertools", "log", + "num-traits", "parking_lot", "pin-project-lite", "tokio", @@ -1556,9 +1536,8 @@ dependencies = [ [[package]] name = "datafusion-proto" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cf75daf56aa6b1c6867cc33ff0fb035d517d6d06737fd355a3e1ef67cba6e7a" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "chrono", @@ -1579,13 +1558,13 @@ dependencies = [ "datafusion-proto-common", "object_store", "prost", + "rand", ] [[package]] name = "datafusion-proto-common" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12a0cb3cce232a3de0d14ef44b58a6537aeb1362cfb6cf4d808691ddbb918956" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "datafusion-common", @@ -1594,9 +1573,8 @@ dependencies = [ [[package]] name = "datafusion-pruning" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d0fef4201777b52951edec086c21a5b246f3c82621569ddb4a26f488bc38a9" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "datafusion-common", @@ -1640,9 +1618,8 @@ dependencies = [ [[package]] name = "datafusion-session" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f71f1e39e8f2acbf1c63b0e93756c2e970a64729dab70ac789587d6237c4fde0" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "async-trait", "datafusion-common", @@ -1654,15 +1631,15 @@ dependencies = [ [[package]] name = "datafusion-sql" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f44693cfcaeb7a9f12d71d1c576c3a6dc025a12cef209375fa2d16fb3b5670ee" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "bigdecimal", "chrono", "datafusion-common", "datafusion-expr", + "datafusion-functions-nested", "indexmap", "log", "recursive", @@ -1672,9 +1649,8 @@ dependencies = [ [[package]] name = "datafusion-substrait" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6042adacd0bd64e56c22f6a7f9ce0ce1793dd367c899d868179d029f110d9215" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "async-recursion", "async-trait", @@ -1688,7 +1664,6 @@ dependencies = [ "substrait", "tokio", "url", - "uuid", ] [[package]] @@ -1710,7 +1685,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1863,7 +1838,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2286,15 +2261,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - [[package]] name = "integer-encoding" version = "3.0.4" @@ -2531,15 +2497,6 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "mimalloc" version = "0.1.48" @@ -2626,9 +2583,9 @@ dependencies = [ [[package]] name = "object_store" -version = "0.12.5" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbfbfff40aeccab00ec8a910b57ca8ecf4319b335c542f2edcd19dd25a1e2a00" +checksum = "c2858065e55c148d294a9f3aae3b0fa9458edadb41a108397094566f4e3c0dfb" dependencies = [ "async-trait", "base64", @@ -2649,7 +2606,7 @@ dependencies = [ "rand", "reqwest", "ring", - "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", @@ -2708,14 +2665,13 @@ dependencies = [ [[package]] name = "parquet" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ee96b29972a257b855ff2341b37e61af5f12d6af1158b6dcdb5b31ea07bb3cb" +checksum = "3f491d0ef1b510194426ee67ddc18a9b747ef3c42050c19322a2cd2e1666c29b" dependencies = [ "ahash", "arrow-array", "arrow-buffer", - "arrow-cast", "arrow-data", "arrow-ipc", "arrow-schema", @@ -2871,7 +2827,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2908,7 +2864,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.116", + "syn 2.0.117", "tempfile", ] @@ -2922,7 +2878,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2955,28 +2911,26 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.26.0" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba0117f4212101ee6544044dae45abe1083d30ce7b29c4b5cbdfa2354e07383" +checksum = "cf85e27e86080aafd5a22eae58a162e133a589551542b3e5cee4beb27e54f8e1" dependencies = [ - "indoc", "libc", - "memoffset", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", - "unindent", ] [[package]] name = "pyo3-async-runtimes" -version = "0.26.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ee6d4cb3e8d5b925f5cdb38da183e0ff18122eb2048d4041c9e7034d026e23" +checksum = "9e7364a95bf00e8377bbf9b0f09d7ff9715a29d8fcf93b47d1a967363b973178" dependencies = [ - "futures", + "futures-channel", + "futures-util", "once_cell", "pin-project-lite", "pyo3", @@ -2985,18 +2939,18 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.26.0" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc6ddaf24947d12a9aa31ac65431fb1b851b8f4365426e182901eabfb87df5f" +checksum = "8bf94ee265674bf76c09fa430b0e99c26e319c945d96ca0d5a8215f31bf81cf7" dependencies = [ "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.26.0" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "025474d3928738efb38ac36d4744a74a400c901c7596199e20e45d98eb194105" +checksum = "491aa5fc66d8059dd44a75f4580a2962c1862a1c2945359db36f6c2818b748dc" dependencies = [ "libc", "pyo3-build-config", @@ -3015,27 +2969,27 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.26.0" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e64eb489f22fe1c95911b77c44cc41e7c19f3082fc81cce90f657cdc42ffded" +checksum = "f5d671734e9d7a43449f8480f8b38115df67bef8d21f76837fa75ee7aaa5e52e" dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] name = "pyo3-macros-backend" -version = "0.26.0" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "100246c0ecf400b475341b8455a9213344569af29a3c841d29270e53102e0fcf" +checksum = "22faaa1ce6c430a1f71658760497291065e6450d7b5dc2bcf254d49f66ee700a" dependencies = [ "heck", "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -3170,7 +3124,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76009fbe0614077fc1a2ce255e3a1881a2e3a3527097d5dc6d8212c585e7e38b" dependencies = [ "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -3346,15 +3300,6 @@ dependencies = [ "security-framework", ] -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" version = "1.14.0" @@ -3427,7 +3372,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -3512,7 +3457,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -3523,7 +3468,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -3548,7 +3493,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -3641,9 +3586,9 @@ dependencies = [ [[package]] name = "sqlparser" -version = "0.59.0" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4591acadbcf52f0af60eafbb2c003232b2b4cd8de5f0e9437cb8b1b59046cc0f" +checksum = "dbf5ea8d4d7c808e1af1cbabebca9a2abe603bcefc22294c5b95018d53200cb7" dependencies = [ "log", "recursive", @@ -3652,13 +3597,13 @@ dependencies = [ [[package]] name = "sqlparser_derive" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da5fc6819faabb412da764b99d3b713bb55083c11e7e0c00144d386cd6a1939c" +checksum = "a6dd45d8fc1c79299bfbb7190e42ccbbdf6a5f52e4a6ad98d92357ea965bd289" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -3701,7 +3646,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -3725,7 +3670,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "syn 2.0.116", + "syn 2.0.117", "typify", "walkdir", ] @@ -3749,9 +3694,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.116" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -3775,7 +3720,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -3814,7 +3759,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -3885,7 +3830,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -3898,6 +3843,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -3975,7 +3932,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -4057,7 +4014,7 @@ dependencies = [ "semver", "serde", "serde_json", - "syn 2.0.116", + "syn 2.0.117", "thiserror", "unicode-ident", ] @@ -4075,7 +4032,7 @@ dependencies = [ "serde", "serde_json", "serde_tokenstream", - "syn 2.0.116", + "syn 2.0.117", "typify-impl", ] @@ -4103,12 +4060,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" - [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -4246,7 +4197,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -4378,7 +4329,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -4389,7 +4340,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -4611,7 +4562,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn 2.0.116", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -4627,7 +4578,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -4694,7 +4645,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "synstructure", ] @@ -4715,7 +4666,7 @@ checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -4735,7 +4686,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "synstructure", ] @@ -4775,7 +4726,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c19ae21f7..dfc7e4b60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,19 +48,19 @@ tokio = { version = "1.49", features = [ "rt-multi-thread", "sync", ] } -pyo3 = { version = "0.26", features = [ +pyo3 = { version = "0.28", features = [ "extension-module", "abi3", "abi3-py310", ] } -pyo3-async-runtimes = { version = "0.26", features = ["tokio-runtime"] } +pyo3-async-runtimes = { version = "0.28", features = ["tokio-runtime"] } pyo3-log = "0.13.3" -arrow = { version = "57", features = ["pyarrow"] } -arrow-select = { version = "57" } -datafusion = { version = "52", features = ["avro", "unicode_expressions"] } -datafusion-substrait = { version = "52", optional = true } -datafusion-proto = { version = "52" } -datafusion-ffi = { version = "52" } +arrow = { version = "58", features = ["pyarrow"] } +arrow-select = { version = "58" } +datafusion = { version = "53", features = ["avro", "unicode_expressions"] } +datafusion-substrait = { version = "53", optional = true } +datafusion-proto = { version = "53" } +datafusion-ffi = { version = "53" } prost = "0.14.3" # keep in line with `datafusion-substrait` serde_json = "1" uuid = { version = "1.21", features = ["v4"] } @@ -70,7 +70,7 @@ mimalloc = { version = "0.1", optional = true, default-features = false, feature async-trait = "0.1.89" futures = "0.3" cstr = "0.2" -object_store = { version = "0.12.4", features = [ +object_store = { version = "0.13.1", features = [ "aws", "gcp", "azure", @@ -82,7 +82,7 @@ parking_lot = "0.12" [build-dependencies] prost-types = "0.14.3" # keep in line with `datafusion-substrait` -pyo3-build-config = "0.26" +pyo3-build-config = "0.28" [lib] name = "datafusion_python" @@ -95,4 +95,7 @@ codegen-units = 1 # We cannot publish to crates.io with any patches in the below section. Developers # must remove any entries in this section before creating a release candidate. [patch.crates-io] -# datafusion = { git = "https://github.com/apache/datafusion.git", rev = "6713439497561fa74a94177e5b8632322fb7cea5" } +datafusion = { git = "https://github.com/apache/datafusion.git", rev = "518560246e87d489eba6d511fa167aa429b06728" } +datafusion-substrait = { git = "https://github.com/apache/datafusion.git", rev = "518560246e87d489eba6d511fa167aa429b06728" } +datafusion-proto = { git = "https://github.com/apache/datafusion.git", rev = "518560246e87d489eba6d511fa167aa429b06728" } +datafusion-ffi = { git = "https://github.com/apache/datafusion.git", rev = "518560246e87d489eba6d511fa167aa429b06728" } diff --git a/docs/source/contributor-guide/ffi.rst b/docs/source/contributor-guide/ffi.rst index 5006b0ca4..e0158e0a2 100644 --- a/docs/source/contributor-guide/ffi.rst +++ b/docs/source/contributor-guide/ffi.rst @@ -156,7 +156,7 @@ instead of mutating the container directly: .. code-block:: rust - #[pyclass(name = "Config", module = "datafusion", subclass, frozen)] + #[pyclass(from_py_object, name = "Config", module = "datafusion", subclass, frozen)] #[derive(Clone)] pub(crate) struct PyConfig { config: Arc>, @@ -170,7 +170,7 @@ existing instance in place: .. code-block:: rust - #[pyclass(frozen, name = "SessionContext", module = "datafusion", subclass)] + #[pyclass(from_py_object, frozen, name = "SessionContext", module = "datafusion", subclass)] #[derive(Clone)] pub struct PySessionContext { pub ctx: SessionContext, @@ -186,7 +186,7 @@ field updates: // TODO: This looks like this needs pyo3 tracking so leaving unfrozen for now #[derive(Debug, Clone)] - #[pyclass(name = "DataTypeMap", module = "datafusion.common", subclass)] + #[pyclass(from_py_object, name = "DataTypeMap", module = "datafusion.common", subclass)] pub struct DataTypeMap { #[pyo3(get, set)] pub arrow_type: PyDataType, @@ -232,8 +232,11 @@ can then be turned into a ``ForeignTableProvider`` the associated code is: .. code-block:: rust - let capsule = capsule.downcast::()?; - let provider = unsafe { capsule.reference::() }; + let capsule = capsule.cast::()?; + let data: NonNull = capsule + .pointer_checked(Some(name))? + .cast(); + let codec = unsafe { data.as_ref() }; By convention the ``datafusion-python`` library expects a Python object that has a ``TableProvider`` PyCapsule to have this capsule accessible by calling a function named diff --git a/docs/source/user-guide/upgrade-guides.rst b/docs/source/user-guide/upgrade-guides.rst index a77f60776..e3d7c2d87 100644 --- a/docs/source/user-guide/upgrade-guides.rst +++ b/docs/source/user-guide/upgrade-guides.rst @@ -18,6 +18,27 @@ Upgrade Guides ============== +DataFusion 53.0.0 +----------------- + +This version includes an upgraded version of ``pyo3``, which changed the way to extract an FFI +object. Example: + +Before: + +.. code-block:: rust + + let codec = unsafe { capsule.reference::() }; + +Now: + +.. code-block:: rust + + let data: NonNull = capsule + .pointer_checked(Some(c_str!("datafusion_logical_extension_codec")))? + .cast(); + let codec = unsafe { data.as_ref() }; + DataFusion 52.0.0 ----------------- diff --git a/examples/datafusion-ffi-example/Cargo.lock b/examples/datafusion-ffi-example/Cargo.lock index 02aa7d9d9..a5dda6575 100644 --- a/examples/datafusion-ffi-example/Cargo.lock +++ b/examples/datafusion-ffi-example/Cargo.lock @@ -117,9 +117,9 @@ checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "arrow" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4754a624e5ae42081f464514be454b39711daae0458906dacde5f4c632f33a8" +checksum = "602268ce9f569f282cedb9a9f6bac569b680af47b9b077d515900c03c5d190da" dependencies = [ "arrow-arith", "arrow-array", @@ -138,9 +138,9 @@ dependencies = [ [[package]] name = "arrow-arith" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7b3141e0ec5145a22d8694ea8b6d6f69305971c4fa1c1a13ef0195aef2d678b" +checksum = "cd53c6bf277dea91f136ae8e3a5d7041b44b5e489e244e637d00ae302051f56f" dependencies = [ "arrow-array", "arrow-buffer", @@ -152,9 +152,9 @@ dependencies = [ [[package]] name = "arrow-array" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8955af33b25f3b175ee10af580577280b4bd01f7e823d94c7cdef7cf8c9aef" +checksum = "e53796e07a6525edaf7dc28b540d477a934aff14af97967ad1d5550878969b9e" dependencies = [ "ahash", "arrow-buffer", @@ -171,9 +171,9 @@ dependencies = [ [[package]] name = "arrow-buffer" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c697ddca96183182f35b3a18e50b9110b11e916d7b7799cbfd4d34662f2c56c2" +checksum = "f2c1a85bb2e94ee10b76531d8bc3ce9b7b4c0d508cabfb17d477f63f2617bd20" dependencies = [ "bytes", "half", @@ -183,9 +183,9 @@ dependencies = [ [[package]] name = "arrow-cast" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "646bbb821e86fd57189c10b4fcdaa941deaf4181924917b0daa92735baa6ada5" +checksum = "89fb245db6b0e234ed8e15b644edb8664673fefe630575e94e62cd9d489a8a26" dependencies = [ "arrow-array", "arrow-buffer", @@ -205,9 +205,9 @@ dependencies = [ [[package]] name = "arrow-csv" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da746f4180004e3ce7b83c977daf6394d768332349d3d913998b10a120b790a" +checksum = "d374882fb465a194462527c0c15a93aa19a554cf690a6b77a26b2a02539937a7" dependencies = [ "arrow-array", "arrow-cast", @@ -220,9 +220,9 @@ dependencies = [ [[package]] name = "arrow-data" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fdd994a9d28e6365aa78e15da3f3950c0fdcea6b963a12fa1c391afb637b304" +checksum = "189d210bc4244c715fa3ed9e6e22864673cccb73d5da28c2723fb2e527329b33" dependencies = [ "arrow-buffer", "arrow-schema", @@ -233,9 +233,9 @@ dependencies = [ [[package]] name = "arrow-ipc" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abf7df950701ab528bf7c0cf7eeadc0445d03ef5d6ffc151eaae6b38a58feff1" +checksum = "7968c2e5210c41f4909b2ef76f6e05e172b99021c2def5edf3cc48fdd39d1d6c" dependencies = [ "arrow-array", "arrow-buffer", @@ -248,9 +248,9 @@ dependencies = [ [[package]] name = "arrow-json" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ff8357658bedc49792b13e2e862b80df908171275f8e6e075c460da5ee4bf86" +checksum = "92111dba5bf900f443488e01f00d8c4ddc2f47f5c50039d18120287b580baa22" dependencies = [ "arrow-array", "arrow-buffer", @@ -272,9 +272,9 @@ dependencies = [ [[package]] name = "arrow-ord" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d8f1870e03d4cbed632959498bcc84083b5a24bded52905ae1695bd29da45b" +checksum = "211136cb253577ee1a6665f741a13136d4e563f64f5093ffd6fb837af90b9495" dependencies = [ "arrow-array", "arrow-buffer", @@ -285,9 +285,9 @@ dependencies = [ [[package]] name = "arrow-row" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18228633bad92bff92a95746bbeb16e5fc318e8382b75619dec26db79e4de4c0" +checksum = "8e0f20145f9f5ea3fe383e2ba7a7487bf19be36aa9dbf5dd6a1f92f657179663" dependencies = [ "arrow-array", "arrow-buffer", @@ -298,18 +298,18 @@ dependencies = [ [[package]] name = "arrow-schema" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c872d36b7bf2a6a6a2b40de9156265f0242910791db366a2c17476ba8330d68" +checksum = "1b47e0ca91cc438d2c7879fe95e0bca5329fff28649e30a88c6f760b1faeddcb" dependencies = [ "bitflags", ] [[package]] name = "arrow-select" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68bf3e3efbd1278f770d67e5dc410257300b161b93baedb3aae836144edcaf4b" +checksum = "750a7d1dda177735f5e82a314485b6915c7cccdbb278262ac44090f4aba4a325" dependencies = [ "ahash", "arrow-array", @@ -321,9 +321,9 @@ dependencies = [ [[package]] name = "arrow-string" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e968097061b3c0e9fe3079cf2e703e487890700546b5b0647f60fca1b5a8d8" +checksum = "e1eab1208bc4fe55d768cdc9b9f3d9df5a794cdb3ee2586bf89f9b30dc31ad8c" dependencies = [ "arrow-array", "arrow-buffer", @@ -365,7 +365,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -454,9 +454,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "num-traits", @@ -591,9 +591,8 @@ dependencies = [ [[package]] name = "datafusion-catalog" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "462dc9ef45e5d688aeaae49a7e310587e81b6016b9d03bace5626ad0043e5a9e" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "async-trait", @@ -616,9 +615,8 @@ dependencies = [ [[package]] name = "datafusion-catalog-listing" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b96dbf1d728fc321817b744eb5080cdd75312faa6980b338817f68f3caa4208" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "async-trait", @@ -639,9 +637,8 @@ dependencies = [ [[package]] name = "datafusion-common" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3237a6ff0d2149af4631290074289cae548c9863c885d821315d54c6673a074a" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "ahash", "arrow", @@ -650,6 +647,7 @@ dependencies = [ "half", "hashbrown 0.16.1", "indexmap", + "itertools", "libc", "log", "object_store", @@ -661,9 +659,8 @@ dependencies = [ [[package]] name = "datafusion-common-runtime" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70b5e34026af55a1bfccb1ef0a763cf1f64e77c696ffcf5a128a278c31236528" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "futures", "log", @@ -672,9 +669,8 @@ dependencies = [ [[package]] name = "datafusion-datasource" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b2a6be734cc3785e18bbf2a7f2b22537f6b9fb960d79617775a51568c281842" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "async-trait", @@ -701,9 +697,8 @@ dependencies = [ [[package]] name = "datafusion-datasource-arrow" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1739b9b07c9236389e09c74f770e88aff7055250774e9def7d3f4f56b3dcc7be" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "arrow-ipc", @@ -725,9 +720,8 @@ dependencies = [ [[package]] name = "datafusion-datasource-csv" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c73bc54b518bbba7c7650299d07d58730293cfba4356f6f428cc94c20b7600" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "async-trait", @@ -748,9 +742,8 @@ dependencies = [ [[package]] name = "datafusion-datasource-json" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37812c8494c698c4d889374ecfabbff780f1f26d9ec095dd1bddfc2a8ca12559" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "async-trait", @@ -765,14 +758,15 @@ dependencies = [ "datafusion-session", "futures", "object_store", + "serde_json", "tokio", + "tokio-stream", ] [[package]] name = "datafusion-datasource-parquet" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2210937ecd9f0e824c397e73f4b5385c97cd1aff43ab2b5836fcfd2d321523fb" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "async-trait", @@ -800,22 +794,22 @@ dependencies = [ [[package]] name = "datafusion-doc" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c825f969126bc2ef6a6a02d94b3c07abff871acf4d6dd759ce1255edb7923ce" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" [[package]] name = "datafusion-execution" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa03ef05a2c2f90dd6c743e3e111078e322f4b395d20d4b4d431a245d79521ae" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", + "arrow-buffer", "async-trait", "chrono", "dashmap", "datafusion-common", "datafusion-expr", + "datafusion-physical-expr-common", "futures", "log", "object_store", @@ -827,9 +821,8 @@ dependencies = [ [[package]] name = "datafusion-expr" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef33934c1f98ee695cc51192cc5f9ed3a8febee84fdbcd9131bf9d3a9a78276f" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "async-trait", @@ -849,9 +842,8 @@ dependencies = [ [[package]] name = "datafusion-expr-common" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "000c98206e3dd47d2939a94b6c67af4bfa6732dd668ac4fafdbde408fd9134ea" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "datafusion-common", @@ -862,9 +854,8 @@ dependencies = [ [[package]] name = "datafusion-ffi" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30f57f7f63a25a0b78b3f2a5e18c0ecbd54851b64064ac0d5a9eb05efd5586d2" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "abi_stable", "arrow", @@ -910,9 +901,8 @@ dependencies = [ [[package]] name = "datafusion-functions" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "379b01418ab95ca947014066248c22139fe9af9289354de10b445bd000d5d276" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "arrow-buffer", @@ -928,6 +918,7 @@ dependencies = [ "hex", "itertools", "log", + "memchr", "num-traits", "rand", "regex", @@ -937,9 +928,8 @@ dependencies = [ [[package]] name = "datafusion-functions-aggregate" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd00d5454ba4c3f8ebbd04bd6a6a9dc7ced7c56d883f70f2076c188be8459e4c" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "ahash", "arrow", @@ -953,14 +943,14 @@ dependencies = [ "datafusion-physical-expr-common", "half", "log", + "num-traits", "paste", ] [[package]] name = "datafusion-functions-aggregate-common" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aec06b380729a87210a4e11f555ec2d729a328142253f8d557b87593622ecc9f" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "ahash", "arrow", @@ -971,9 +961,8 @@ dependencies = [ [[package]] name = "datafusion-functions-table" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9a0d20e2b887e11bee24f7734d780a2588b925796ac741c3118dd06d5aa77f0" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "async-trait", @@ -987,9 +976,8 @@ dependencies = [ [[package]] name = "datafusion-functions-window" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3414b0a07e39b6979fe3a69c7aa79a9f1369f1d5c8e52146e66058be1b285ee" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "datafusion-common", @@ -1005,9 +993,8 @@ dependencies = [ [[package]] name = "datafusion-functions-window-common" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf2feae63cd4754e31add64ce75cae07d015bce4bb41cd09872f93add32523a" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "datafusion-common", "datafusion-physical-expr-common", @@ -1015,20 +1002,18 @@ dependencies = [ [[package]] name = "datafusion-macros" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4fe888aeb6a095c4bcbe8ac1874c4b9a4c7ffa2ba849db7922683ba20875aaf" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "datafusion-doc", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] name = "datafusion-physical-expr" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb028323dd4efd049dd8a78d78fe81b2b969447b39c51424167f973ac5811d9" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "ahash", "arrow", @@ -1049,9 +1034,8 @@ dependencies = [ [[package]] name = "datafusion-physical-expr-adapter" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78fe0826aef7eab6b4b61533d811234a7a9e5e458331ebbf94152a51fc8ab433" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "datafusion-common", @@ -1064,9 +1048,8 @@ dependencies = [ [[package]] name = "datafusion-physical-expr-common" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfccd388620734c661bd8b7ca93c44cdd59fecc9b550eea416a78ffcbb29475f" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "ahash", "arrow", @@ -1081,9 +1064,8 @@ dependencies = [ [[package]] name = "datafusion-physical-plan" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1098760fb29127c24cc9ade3277051dc73c9ed0ac0131bd7bcd742e0ad7470" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "ahash", "arrow", @@ -1105,6 +1087,7 @@ dependencies = [ "indexmap", "itertools", "log", + "num-traits", "parking_lot", "pin-project-lite", "tokio", @@ -1112,9 +1095,8 @@ dependencies = [ [[package]] name = "datafusion-proto" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cf75daf56aa6b1c6867cc33ff0fb035d517d6d06737fd355a3e1ef67cba6e7a" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "chrono", @@ -1135,13 +1117,13 @@ dependencies = [ "datafusion-proto-common", "object_store", "prost", + "rand", ] [[package]] name = "datafusion-proto-common" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12a0cb3cce232a3de0d14ef44b58a6537aeb1362cfb6cf4d808691ddbb918956" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "datafusion-common", @@ -1150,9 +1132,8 @@ dependencies = [ [[package]] name = "datafusion-pruning" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d0fef4201777b52951edec086c21a5b246f3c82621569ddb4a26f488bc38a9" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "arrow", "datafusion-common", @@ -1167,9 +1148,8 @@ dependencies = [ [[package]] name = "datafusion-session" -version = "52.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f71f1e39e8f2acbf1c63b0e93756c2e970a64729dab70ac789587d6237c4fde0" +version = "53.0.0" +source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" dependencies = [ "async-trait", "datafusion-common", @@ -1187,7 +1167,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1327,7 +1307,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1620,15 +1600,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - [[package]] name = "integer-encoding" version = "3.0.4" @@ -1797,15 +1768,6 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1856,9 +1818,9 @@ dependencies = [ [[package]] name = "object_store" -version = "0.12.5" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbfbfff40aeccab00ec8a910b57ca8ecf4319b335c542f2edcd19dd25a1e2a00" +checksum = "c2858065e55c148d294a9f3aae3b0fa9458edadb41a108397094566f4e3c0dfb" dependencies = [ "async-trait", "bytes", @@ -1918,14 +1880,13 @@ dependencies = [ [[package]] name = "parquet" -version = "57.3.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ee96b29972a257b855ff2341b37e61af5f12d6af1158b6dcdb5b31ea07bb3cb" +checksum = "3f491d0ef1b510194426ee67ddc18a9b747ef3c42050c19322a2cd2e1666c29b" dependencies = [ "ahash", "arrow-array", "arrow-buffer", - "arrow-cast", "arrow-data", "arrow-ipc", "arrow-schema", @@ -2038,7 +1999,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2070,40 +2031,37 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] name = "pyo3" -version = "0.26.0" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba0117f4212101ee6544044dae45abe1083d30ce7b29c4b5cbdfa2354e07383" +checksum = "cf85e27e86080aafd5a22eae58a162e133a589551542b3e5cee4beb27e54f8e1" dependencies = [ - "indoc", "libc", - "memoffset", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", - "unindent", ] [[package]] name = "pyo3-build-config" -version = "0.26.0" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc6ddaf24947d12a9aa31ac65431fb1b851b8f4365426e182901eabfb87df5f" +checksum = "8bf94ee265674bf76c09fa430b0e99c26e319c945d96ca0d5a8215f31bf81cf7" dependencies = [ "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.26.0" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "025474d3928738efb38ac36d4744a74a400c901c7596199e20e45d98eb194105" +checksum = "491aa5fc66d8059dd44a75f4580a2962c1862a1c2945359db36f6c2818b748dc" dependencies = [ "libc", "pyo3-build-config", @@ -2111,27 +2069,27 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.26.0" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e64eb489f22fe1c95911b77c44cc41e7c19f3082fc81cce90f657cdc42ffded" +checksum = "f5d671734e9d7a43449f8480f8b38115df67bef8d21f76837fa75ee7aaa5e52e" dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] name = "pyo3-macros-backend" -version = "0.26.0" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "100246c0ecf400b475341b8455a9213344569af29a3c841d29270e53102e0fcf" +checksum = "22faaa1ce6c430a1f71658760497291065e6450d7b5dc2bcf254d49f66ee700a" dependencies = [ "heck", "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2313,7 +2271,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2373,9 +2331,9 @@ checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" [[package]] name = "sqlparser" -version = "0.59.0" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4591acadbcf52f0af60eafbb2c003232b2b4cd8de5f0e9437cb8b1b59046cc0f" +checksum = "dbf5ea8d4d7c808e1af1cbabebca9a2abe603bcefc22294c5b95018d53200cb7" dependencies = [ "log", "sqlparser_derive", @@ -2383,13 +2341,13 @@ dependencies = [ [[package]] name = "sqlparser_derive" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da5fc6819faabb412da764b99d3b713bb55083c11e7e0c00144d386cd6a1939c" +checksum = "a6dd45d8fc1c79299bfbb7190e42ccbbdf6a5f52e4a6ad98d92357ea965bd289" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2411,9 +2369,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.116" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -2428,7 +2386,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2467,7 +2425,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2519,7 +2477,32 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", ] [[package]] @@ -2541,7 +2524,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2610,12 +2593,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" - [[package]] name = "url" version = "2.5.8" @@ -2731,7 +2708,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -2850,7 +2827,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2861,7 +2838,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2927,7 +2904,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn 2.0.116", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -2943,7 +2920,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -3010,7 +2987,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "synstructure", ] @@ -3031,7 +3008,7 @@ checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -3051,7 +3028,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "synstructure", ] @@ -3085,7 +3062,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] diff --git a/examples/datafusion-ffi-example/Cargo.toml b/examples/datafusion-ffi-example/Cargo.toml index 15c11ea49..483838e31 100644 --- a/examples/datafusion-ffi-example/Cargo.toml +++ b/examples/datafusion-ffi-example/Cargo.toml @@ -21,26 +21,35 @@ version = "0.2.0" edition = "2024" [dependencies] -datafusion-catalog = { version = "52", default-features = false } -datafusion-common = { version = "52", default-features = false } -datafusion-functions-aggregate = { version = "52" } -datafusion-functions-window = { version = "52" } -datafusion-expr = { version = "52" } -datafusion-ffi = { version = "52" } +datafusion-catalog = { version = "53", default-features = false } +datafusion-common = { version = "53", default-features = false } +datafusion-functions-aggregate = { version = "53" } +datafusion-functions-window = { version = "53" } +datafusion-expr = { version = "53" } +datafusion-ffi = { version = "53" } -pyo3 = { version = "0.26", features = [ +pyo3 = { version = "0.28", features = [ "extension-module", "abi3", "abi3-py39", ] } -arrow = { version = "57" } -arrow-array = { version = "57" } -arrow-schema = { version = "57" } +arrow = { version = "58" } +arrow-array = { version = "58" } +arrow-schema = { version = "58" } async-trait = "0.1.89" [build-dependencies] -pyo3-build-config = "0.26" +pyo3-build-config = "0.28" [lib] name = "datafusion_ffi_example" crate-type = ["cdylib", "rlib"] + +# TODO: remove when datafusion-53 is released +[patch.crates-io] +datafusion-catalog = { git = "https://github.com/apache/datafusion.git", rev = "518560246e87d489eba6d511fa167aa429b06728" } +datafusion-common = { git = "https://github.com/apache/datafusion.git", rev = "518560246e87d489eba6d511fa167aa429b06728" } +datafusion-functions-aggregate = { git = "https://github.com/apache/datafusion.git", rev = "518560246e87d489eba6d511fa167aa429b06728" } +datafusion-functions-window = { git = "https://github.com/apache/datafusion.git", rev = "518560246e87d489eba6d511fa167aa429b06728" } +datafusion-expr = { git = "https://github.com/apache/datafusion.git", rev = "518560246e87d489eba6d511fa167aa429b06728" } +datafusion-ffi = { git = "https://github.com/apache/datafusion.git", rev = "518560246e87d489eba6d511fa167aa429b06728" } diff --git a/examples/datafusion-ffi-example/src/aggregate_udf.rs b/examples/datafusion-ffi-example/src/aggregate_udf.rs index 276ad0275..d5343ff91 100644 --- a/examples/datafusion-ffi-example/src/aggregate_udf.rs +++ b/examples/datafusion-ffi-example/src/aggregate_udf.rs @@ -27,7 +27,12 @@ use datafusion_functions_aggregate::sum::Sum; use pyo3::types::PyCapsule; use pyo3::{Bound, PyResult, Python, pyclass, pymethods}; -#[pyclass(name = "MySumUDF", module = "datafusion_ffi_example", subclass)] +#[pyclass( + from_py_object, + name = "MySumUDF", + module = "datafusion_ffi_example", + subclass +)] #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub(crate) struct MySumUDF { inner: Arc, diff --git a/examples/datafusion-ffi-example/src/catalog_provider.rs b/examples/datafusion-ffi-example/src/catalog_provider.rs index 3dc111cbc..d0e07c787 100644 --- a/examples/datafusion-ffi-example/src/catalog_provider.rs +++ b/examples/datafusion-ffi-example/src/catalog_provider.rs @@ -60,6 +60,7 @@ pub fn my_table() -> Arc { } #[pyclass( + skip_from_py_object, name = "FixedSchemaProvider", module = "datafusion_ffi_example", subclass @@ -138,6 +139,7 @@ impl SchemaProvider for FixedSchemaProvider { /// This catalog provider is intended only for unit tests. It prepopulates with one /// schema and only allows for schemas named after four types of fruit. #[pyclass( + skip_from_py_object, name = "MyCatalogProvider", module = "datafusion_ffi_example", subclass @@ -208,6 +210,7 @@ impl MyCatalogProvider { /// This catalog provider list is intended only for unit tests. /// It pre-populates with a single catalog. #[pyclass( + skip_from_py_object, name = "MyCatalogProviderList", module = "datafusion_ffi_example", subclass diff --git a/examples/datafusion-ffi-example/src/scalar_udf.rs b/examples/datafusion-ffi-example/src/scalar_udf.rs index 089d32d93..374924781 100644 --- a/examples/datafusion-ffi-example/src/scalar_udf.rs +++ b/examples/datafusion-ffi-example/src/scalar_udf.rs @@ -30,7 +30,12 @@ use datafusion_ffi::udf::FFI_ScalarUDF; use pyo3::types::PyCapsule; use pyo3::{Bound, PyResult, Python, pyclass, pymethods}; -#[pyclass(name = "IsNullUDF", module = "datafusion_ffi_example", subclass)] +#[pyclass( + from_py_object, + name = "IsNullUDF", + module = "datafusion_ffi_example", + subclass +)] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub(crate) struct IsNullUDF { signature: Signature, diff --git a/examples/datafusion-ffi-example/src/table_function.rs b/examples/datafusion-ffi-example/src/table_function.rs index 1cddb9e35..0914e161c 100644 --- a/examples/datafusion-ffi-example/src/table_function.rs +++ b/examples/datafusion-ffi-example/src/table_function.rs @@ -27,7 +27,12 @@ use pyo3::{Bound, PyAny, PyResult, Python, pyclass, pymethods}; use crate::table_provider::MyTableProvider; use crate::utils::ffi_logical_codec_from_pycapsule; -#[pyclass(name = "MyTableFunction", module = "datafusion_ffi_example", subclass)] +#[pyclass( + from_py_object, + name = "MyTableFunction", + module = "datafusion_ffi_example", + subclass +)] #[derive(Debug, Clone)] pub(crate) struct MyTableFunction {} diff --git a/examples/datafusion-ffi-example/src/table_provider.rs b/examples/datafusion-ffi-example/src/table_provider.rs index 887b2c671..2c79e6ef9 100644 --- a/examples/datafusion-ffi-example/src/table_provider.rs +++ b/examples/datafusion-ffi-example/src/table_provider.rs @@ -30,7 +30,12 @@ use crate::utils::ffi_logical_codec_from_pycapsule; /// In order to provide a test that demonstrates different sized record batches, /// the first batch will have num_rows, the second batch num_rows+1, and so on. -#[pyclass(name = "MyTableProvider", module = "datafusion_ffi_example", subclass)] +#[pyclass( + from_py_object, + name = "MyTableProvider", + module = "datafusion_ffi_example", + subclass +)] #[derive(Clone)] pub(crate) struct MyTableProvider { num_cols: usize, diff --git a/examples/datafusion-ffi-example/src/utils.rs b/examples/datafusion-ffi-example/src/utils.rs index a01d3fe27..5b0666837 100644 --- a/examples/datafusion-ffi-example/src/utils.rs +++ b/examples/datafusion-ffi-example/src/utils.rs @@ -17,9 +17,11 @@ use datafusion_ffi::proto::logical_extension_codec::FFI_LogicalExtensionCodec; use pyo3::exceptions::PyValueError; +use pyo3::ffi::c_str; use pyo3::prelude::{PyAnyMethods, PyCapsuleMethods}; use pyo3::types::PyCapsule; use pyo3::{Bound, PyAny, PyResult}; +use std::ptr::NonNull; pub(crate) fn ffi_logical_codec_from_pycapsule( obj: Bound, @@ -31,10 +33,13 @@ pub(crate) fn ffi_logical_codec_from_pycapsule( obj }; - let capsule = capsule.downcast::()?; + let capsule = capsule.cast::()?; validate_pycapsule(capsule, "datafusion_logical_extension_codec")?; - let codec = unsafe { capsule.reference::() }; + let data: NonNull = capsule + .pointer_checked(Some(c_str!("datafusion_logical_extension_codec")))? + .cast(); + let codec = unsafe { data.as_ref() }; Ok(codec.clone()) } @@ -47,7 +52,7 @@ pub(crate) fn validate_pycapsule(capsule: &Bound, name: &str) -> PyRe ))); } - let capsule_name = capsule_name.unwrap().to_str()?; + let capsule_name = unsafe { capsule_name.unwrap().as_cstr().to_str()? }; if capsule_name != name { return Err(PyValueError::new_err(format!( "Expected name '{name}' in PyCapsule, instead got '{capsule_name}'" diff --git a/examples/datafusion-ffi-example/src/window_udf.rs b/examples/datafusion-ffi-example/src/window_udf.rs index f3f565234..cbf179a86 100644 --- a/examples/datafusion-ffi-example/src/window_udf.rs +++ b/examples/datafusion-ffi-example/src/window_udf.rs @@ -27,7 +27,12 @@ use datafusion_functions_window::rank::rank_udwf; use pyo3::types::PyCapsule; use pyo3::{Bound, PyResult, Python, pyclass, pymethods}; -#[pyclass(name = "MyRankUDF", module = "datafusion_ffi_example", subclass)] +#[pyclass( + from_py_object, + name = "MyRankUDF", + module = "datafusion_ffi_example", + subclass +)] #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub(crate) struct MyRankUDF { inner: Arc, diff --git a/python/datafusion/dataframe.py b/python/datafusion/dataframe.py index 5bd0eec2d..214d44a42 100644 --- a/python/datafusion/dataframe.py +++ b/python/datafusion/dataframe.py @@ -442,30 +442,20 @@ def select(self, *exprs: Expr | str) -> DataFrame: def drop(self, *columns: str) -> DataFrame: """Drop arbitrary amount of columns. - Column names are case-sensitive and do not require double quotes like - other operations such as `select`. Leading and trailing double quotes - are allowed and will be automatically stripped if present. + Column names are case-sensitive and require double quotes to be dropped + if the original name is not strictly lower case. Args: - columns: Column names to drop from the dataframe. Both ``column_name`` - and ``"column_name"`` are accepted. + columns: Column names to drop from the dataframe. Returns: DataFrame with those columns removed in the projection. Example Usage:: - - df.drop('ID_For_Students') # Works - df.drop('"ID_For_Students"') # Also works (quotes stripped) + df.drop('a') # To drop a lower-cased column 'a' + df.drop('"a"') # To drop an upper-cased column 'A' """ - normalized_columns = [] - for col in columns: - if col.startswith('"') and col.endswith('"'): - normalized_columns.append(col.strip('"')) # Strip double quotes - else: - normalized_columns.append(col) - - return DataFrame(self.df.drop(*normalized_columns)) + return DataFrame(self.df.drop(*columns)) def filter(self, *predicates: Expr | str) -> DataFrame: """Return a DataFrame for which ``predicate`` evaluates to ``True``. diff --git a/python/tests/test_aggregation.py b/python/tests/test_aggregation.py index f595127fa..240332848 100644 --- a/python/tests/test_aggregation.py +++ b/python/tests/test_aggregation.py @@ -141,14 +141,14 @@ def test_aggregation_stats(df, agg_expr, calc_expected): ), ( f.approx_percentile_cont_with_weight(column("b"), lit(0.6), 0.5), - pa.array([6], type=pa.float64()), + pa.array([4], type=pa.float64()), False, ), ( f.approx_percentile_cont_with_weight( column("b").sort(ascending=False, nulls_first=False), lit(0.6), 0.5 ), - pa.array([6], type=pa.float64()), + pa.array([4], type=pa.float64()), False, ), ( diff --git a/python/tests/test_context.py b/python/tests/test_context.py index 5853f9feb..5df6ed20f 100644 --- a/python/tests/test_context.py +++ b/python/tests/test_context.py @@ -759,7 +759,7 @@ def read_csv_with_options_inner( group_dir.mkdir(exist_ok=True) csv_path = group_dir / "test.csv" - csv_path.write_text(csv_content) + csv_path.write_text(csv_content, newline="\n") ctx = SessionContext() diff --git a/python/tests/test_dataframe.py b/python/tests/test_dataframe.py index de6b00acf..759d6278c 100644 --- a/python/tests/test_dataframe.py +++ b/python/tests/test_dataframe.py @@ -295,10 +295,17 @@ def test_drop_quoted_columns(): ctx = SessionContext() batch = pa.RecordBatch.from_arrays([pa.array([1, 2, 3])], names=["ID_For_Students"]) df = ctx.create_dataframe([[batch]]) - - # Both should work + # here we must quote to match the original column name assert df.drop('"ID_For_Students"').schema().names == [] - assert df.drop("ID_For_Students").schema().names == [] + + batch = pa.RecordBatch.from_arrays( + [pa.array([1, 2, 3]), pa.array([4, 5, 6])], names=["a", "b"] + ) + df = ctx.create_dataframe([[batch]]) + # with a lower case column, both 'a' and '"a"' work + assert df.drop("a").schema().names == ["b"] + df = ctx.create_dataframe([[batch]]) + assert df.drop('"a"').schema().names == ["b"] def test_select_mixed_expr_string(df): diff --git a/python/tests/test_expr.py b/python/tests/test_expr.py index 92251827b..9a287c1f7 100644 --- a/python/tests/test_expr.py +++ b/python/tests/test_expr.py @@ -857,20 +857,20 @@ def test_alias_with_metadata(df): pytest.param( col("d").list_distinct(), pa.array( - [[-1, 0, 1], [5, 10, 15, 20], [], None], type=pa.list_(pa.int64()) + [[-1, 1, 0], [5, 10, 15, 20], [], None], type=pa.list_(pa.int64()) ), id="list_distinct", ), pytest.param( col("d").array_distinct(), pa.array( - [[-1, 0, 1], [5, 10, 15, 20], [], None], type=pa.list_(pa.int64()) + [[-1, 1, 0], [5, 10, 15, 20], [], None], type=pa.list_(pa.int64()) ), id="array_distinct", ), pytest.param( col("d").cardinality(), - pa.array([3, 4, None, None], type=pa.uint64()), + pa.array([3, 4, 0, None], type=pa.uint64()), id="cardinality", ), pytest.param( diff --git a/python/tests/test_functions.py b/python/tests/test_functions.py index 5a61a2dd1..7d642b722 100644 --- a/python/tests/test_functions.py +++ b/python/tests/test_functions.py @@ -307,12 +307,12 @@ def py_flatten(arr): lambda data: [[len(r)] for r in data], ), ( - f.array_distinct, - lambda data: [list(set(r)) for r in data], + lambda col: f.array_sort(f.array_distinct(col)), + lambda data: [sorted(set(r)) for r in data], ), ( - f.list_distinct, - lambda data: [list(set(r)) for r in data], + lambda col: f.list_sort(f.list_distinct(col)), + lambda data: [sorted(set(r)) for r in data], ), ( f.list_dims, @@ -519,19 +519,19 @@ def py_flatten(arr): lambda data: [arr[1:4:2] for arr in data], ), ( - lambda col: f.array_intersect(col, literal([3.0, 4.0])), + lambda col: f.array_sort(f.array_intersect(col, literal([3.0, 4.0]))), lambda data: [np.intersect1d(arr, [3.0, 4.0]) for arr in data], ), ( - lambda col: f.list_intersect(col, literal([3.0, 4.0])), + lambda col: f.list_sort(f.list_intersect(col, literal([3.0, 4.0]))), lambda data: [np.intersect1d(arr, [3.0, 4.0]) for arr in data], ), ( - lambda col: f.array_union(col, literal([12.0, 999.0])), + lambda col: f.array_sort(f.array_union(col, literal([12.0, 999.0]))), lambda data: [np.union1d(arr, [12.0, 999.0]) for arr in data], ), ( - lambda col: f.list_union(col, literal([12.0, 999.0])), + lambda col: f.list_sort(f.list_union(col, literal([12.0, 999.0]))), lambda data: [np.union1d(arr, [12.0, 999.0]) for arr in data], ), ( @@ -697,7 +697,10 @@ def test_array_function_obj_tests(stmt, py_expr): f.initcap(column("c")), pa.array(["Hello ", " World ", " !"], type=pa.string_view()), ), - (f.left(column("a"), literal(3)), pa.array(["Hel", "Wor", "!"])), + ( + f.left(column("a"), literal(3)), + pa.array(["Hel", "Wor", "!"], type=pa.string_view()), + ), (f.length(column("c")), pa.array([6, 7, 2], type=pa.int32())), (f.lower(column("a")), pa.array(["hello", "world", "!"])), (f.lpad(column("a"), literal(7)), pa.array([" Hello", " World", " !"])), @@ -726,7 +729,10 @@ def test_array_function_obj_tests(stmt, py_expr): pa.array(["He??o", "Wor?d", "!"]), ), (f.reverse(column("a")), pa.array(["olleH", "dlroW", "!"])), - (f.right(column("a"), literal(4)), pa.array(["ello", "orld", "!"])), + ( + f.right(column("a"), literal(4)), + pa.array(["ello", "orld", "!"], type=pa.string_view()), + ), ( f.rpad(column("a"), literal(8)), pa.array(["Hello ", "World ", "! "]), @@ -964,7 +970,7 @@ def test_temporal_functions(df): datetime(2027, 6, 1, tzinfo=DEFAULT_TZ), datetime(2020, 7, 1, tzinfo=DEFAULT_TZ), ], - type=pa.timestamp("ns", tz=DEFAULT_TZ), + type=pa.timestamp("us", tz=DEFAULT_TZ), ) assert result.column(3) == pa.array( [ @@ -972,7 +978,7 @@ def test_temporal_functions(df): datetime(2027, 6, 26, tzinfo=DEFAULT_TZ), datetime(2020, 7, 2, tzinfo=DEFAULT_TZ), ], - type=pa.timestamp("ns", tz=DEFAULT_TZ), + type=pa.timestamp("us", tz=DEFAULT_TZ), ) assert result.column(4) == pa.array( [ diff --git a/src/array.rs b/src/array.rs index 8be9a01e3..1ff08dfb2 100644 --- a/src/array.rs +++ b/src/array.rs @@ -15,12 +15,14 @@ // specific language governing permissions and limitations // under the License. +use std::ptr::NonNull; use std::sync::Arc; use arrow::array::{Array, ArrayRef}; use arrow::datatypes::{Field, FieldRef}; use arrow::ffi::{FFI_ArrowArray, FFI_ArrowSchema}; use arrow::pyarrow::ToPyArrow; +use pyo3::ffi::c_str; use pyo3::prelude::{PyAnyMethods, PyCapsuleMethods}; use pyo3::types::PyCapsule; use pyo3::{Bound, PyAny, PyResult, Python, pyclass, pymethods}; @@ -30,7 +32,12 @@ use crate::utils::validate_pycapsule; /// A Python object which implements the Arrow PyCapsule for importing /// into other libraries. -#[pyclass(name = "ArrowArrayExportable", module = "datafusion", frozen)] +#[pyclass( + from_py_object, + name = "ArrowArrayExportable", + module = "datafusion", + frozen +)] #[derive(Clone)] pub struct PyArrowArrayExportable { array: ArrayRef, @@ -48,7 +55,10 @@ impl PyArrowArrayExportable { let field = if let Some(schema_capsule) = requested_schema { validate_pycapsule(&schema_capsule, "arrow_schema")?; - let schema_ptr = unsafe { schema_capsule.reference::() }; + let data: NonNull = schema_capsule + .pointer_checked(Some(c_str!("arrow_schema")))? + .cast(); + let schema_ptr = unsafe { data.as_ref() }; let desired_field = Field::try_from(schema_ptr)?; Arc::new(desired_field) diff --git a/src/catalog.rs b/src/catalog.rs index 29d95ea5d..43325c30d 100644 --- a/src/catalog.rs +++ b/src/catalog.rs @@ -17,6 +17,7 @@ use std::any::Any; use std::collections::HashSet; +use std::ptr::NonNull; use std::sync::Arc; use async_trait::async_trait; @@ -31,6 +32,7 @@ use datafusion_ffi::proto::logical_extension_codec::FFI_LogicalExtensionCodec; use datafusion_ffi::schema_provider::FFI_SchemaProvider; use pyo3::IntoPyObjectExt; use pyo3::exceptions::PyKeyError; +use pyo3::ffi::c_str; use pyo3::prelude::*; use pyo3::types::PyCapsule; @@ -43,6 +45,7 @@ use crate::utils::{ }; #[pyclass( + from_py_object, frozen, name = "RawCatalogList", module = "datafusion.catalog", @@ -54,14 +57,26 @@ pub struct PyCatalogList { codec: Arc, } -#[pyclass(frozen, name = "RawCatalog", module = "datafusion.catalog", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "RawCatalog", + module = "datafusion.catalog", + subclass +)] #[derive(Clone)] pub struct PyCatalog { pub catalog: Arc, codec: Arc, } -#[pyclass(frozen, name = "RawSchema", module = "datafusion.catalog", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "RawSchema", + module = "datafusion.catalog", + subclass +)] #[derive(Clone)] pub struct PySchema { pub schema: Arc, @@ -642,10 +657,12 @@ fn extract_catalog_provider_from_pyobj( .call1((codec_capsule,))?; } - let provider = if let Ok(capsule) = catalog_provider.downcast::() { + let provider = if let Ok(capsule) = catalog_provider.cast::() { validate_pycapsule(capsule, "datafusion_catalog_provider")?; - - let provider = unsafe { capsule.reference::() }; + let data: NonNull = capsule + .pointer_checked(Some(c_str!("datafusion_catalog_provider")))? + .cast(); + let provider = unsafe { data.as_ref() }; let provider: Arc = provider.into(); provider as Arc } else { @@ -673,10 +690,13 @@ fn extract_schema_provider_from_pyobj( .call1((codec_capsule,))?; } - let provider = if let Ok(capsule) = schema_provider.downcast::() { + let provider = if let Ok(capsule) = schema_provider.cast::() { validate_pycapsule(capsule, "datafusion_schema_provider")?; - let provider = unsafe { capsule.reference::() }; + let data: NonNull = capsule + .pointer_checked(Some(c_str!("datafusion_schema_provider")))? + .cast(); + let provider = unsafe { data.as_ref() }; let provider: Arc = provider.into(); provider as Arc } else { diff --git a/src/common/data_type.rs b/src/common/data_type.rs index 1ff332ebb..af4179806 100644 --- a/src/common/data_type.rs +++ b/src/common/data_type.rs @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +use std::sync::Arc; + use datafusion::arrow::array::Array; use datafusion::arrow::datatypes::{DataType, IntervalUnit, TimeUnit}; use datafusion::common::ScalarValue; @@ -40,7 +42,14 @@ impl From for ScalarValue { } #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -#[pyclass(frozen, eq, eq_int, name = "RexType", module = "datafusion.common")] +#[pyclass( + from_py_object, + frozen, + eq, + eq_int, + name = "RexType", + module = "datafusion.common" +)] pub enum RexType { Alias, Literal, @@ -61,7 +70,12 @@ pub enum RexType { /// to map types from one system to another. // TODO: This looks like this needs pyo3 tracking so leaving unfrozen for now #[derive(Debug, Clone)] -#[pyclass(name = "DataTypeMap", module = "datafusion.common", subclass)] +#[pyclass( + from_py_object, + name = "DataTypeMap", + module = "datafusion.common", + subclass +)] pub struct DataTypeMap { #[pyo3(get, set)] pub arrow_type: PyDataType, @@ -347,6 +361,10 @@ impl DataTypeMap { ScalarValue::Map(_) => Err(PyNotImplementedError::new_err( "ScalarValue::Map".to_string(), )), + ScalarValue::RunEndEncoded(field1, field2, _) => Ok(DataType::RunEndEncoded( + Arc::clone(field1), + Arc::clone(field2), + )), } } } @@ -587,7 +605,12 @@ impl DataTypeMap { /// Since `DataType` exists in another package we cannot make that happen here so we wrap /// `DataType` as `PyDataType` This exists solely to satisfy those constraints. #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -#[pyclass(frozen, name = "DataType", module = "datafusion.common")] +#[pyclass( + from_py_object, + frozen, + name = "DataType", + module = "datafusion.common" +)] pub struct PyDataType { pub data_type: DataType, } @@ -645,7 +668,14 @@ impl From for PyDataType { /// Represents the possible Python types that can be mapped to the SQL types #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -#[pyclass(frozen, eq, eq_int, name = "PythonType", module = "datafusion.common")] +#[pyclass( + from_py_object, + frozen, + eq, + eq_int, + name = "PythonType", + module = "datafusion.common" +)] pub enum PythonType { Array, Bool, @@ -665,7 +695,14 @@ pub enum PythonType { #[allow(non_camel_case_types)] #[allow(clippy::upper_case_acronyms)] #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -#[pyclass(frozen, eq, eq_int, name = "SqlType", module = "datafusion.common")] +#[pyclass( + from_py_object, + frozen, + eq, + eq_int, + name = "SqlType", + module = "datafusion.common" +)] pub enum SqlType { ANY, ARRAY, @@ -724,6 +761,7 @@ pub enum SqlType { #[allow(clippy::upper_case_acronyms)] #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[pyclass( + from_py_object, frozen, eq, eq_int, diff --git a/src/common/df_schema.rs b/src/common/df_schema.rs index eb62469cf..9167e772e 100644 --- a/src/common/df_schema.rs +++ b/src/common/df_schema.rs @@ -21,7 +21,13 @@ use datafusion::common::DFSchema; use pyo3::prelude::*; #[derive(Debug, Clone)] -#[pyclass(frozen, name = "DFSchema", module = "datafusion.common", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "DFSchema", + module = "datafusion.common", + subclass +)] pub struct PyDFSchema { schema: Arc, } diff --git a/src/common/function.rs b/src/common/function.rs index bc6f23160..41cab515f 100644 --- a/src/common/function.rs +++ b/src/common/function.rs @@ -22,7 +22,13 @@ use pyo3::prelude::*; use super::data_type::PyDataType; -#[pyclass(frozen, name = "SqlFunction", module = "datafusion.common", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "SqlFunction", + module = "datafusion.common", + subclass +)] #[derive(Debug, Clone)] pub struct SqlFunction { pub name: String, diff --git a/src/common/schema.rs b/src/common/schema.rs index 4e46592aa..29a27b204 100644 --- a/src/common/schema.rs +++ b/src/common/schema.rs @@ -34,7 +34,13 @@ use super::data_type::DataTypeMap; use super::function::SqlFunction; use crate::sql::logical::PyLogicalPlan; -#[pyclass(name = "SqlSchema", module = "datafusion.common", subclass, frozen)] +#[pyclass( + from_py_object, + name = "SqlSchema", + module = "datafusion.common", + subclass, + frozen +)] #[derive(Debug, Clone)] pub struct SqlSchema { name: Arc>, @@ -43,7 +49,12 @@ pub struct SqlSchema { functions: Arc>>, } -#[pyclass(name = "SqlTable", module = "datafusion.common", subclass)] +#[pyclass( + from_py_object, + name = "SqlTable", + module = "datafusion.common", + subclass +)] #[derive(Debug, Clone)] pub struct SqlTable { #[pyo3(get, set)] @@ -87,7 +98,12 @@ impl SqlTable { } } -#[pyclass(name = "SqlView", module = "datafusion.common", subclass)] +#[pyclass( + from_py_object, + name = "SqlView", + module = "datafusion.common", + subclass +)] #[derive(Debug, Clone)] pub struct SqlView { #[pyo3(get, set)] @@ -247,7 +263,13 @@ fn is_supported_push_down_expr(_expr: &Expr) -> bool { true } -#[pyclass(frozen, name = "SqlStatistics", module = "datafusion.common", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "SqlStatistics", + module = "datafusion.common", + subclass +)] #[derive(Debug, Clone)] pub struct SqlStatistics { row_count: f64, @@ -266,7 +288,13 @@ impl SqlStatistics { } } -#[pyclass(frozen, name = "Constraints", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Constraints", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyConstraints { pub constraints: Constraints, @@ -291,7 +319,14 @@ impl Display for PyConstraints { } #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -#[pyclass(frozen, eq, eq_int, name = "TableType", module = "datafusion.common")] +#[pyclass( + from_py_object, + frozen, + eq, + eq_int, + name = "TableType", + module = "datafusion.common" +)] pub enum PyTableType { Base, View, @@ -318,7 +353,13 @@ impl From for PyTableType { } } -#[pyclass(frozen, name = "TableSource", module = "datafusion.common", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "TableSource", + module = "datafusion.common", + subclass +)] #[derive(Clone)] pub struct PyTableSource { pub table_source: Arc, diff --git a/src/config.rs b/src/config.rs index 38936e6c5..fdb693a12 100644 --- a/src/config.rs +++ b/src/config.rs @@ -24,7 +24,13 @@ use pyo3::types::*; use crate::common::data_type::PyScalarValue; use crate::errors::PyDataFusionResult; -#[pyclass(name = "Config", module = "datafusion", subclass, frozen)] +#[pyclass( + from_py_object, + name = "Config", + module = "datafusion", + subclass, + frozen +)] #[derive(Clone)] pub(crate) struct PyConfig { config: Arc>, diff --git a/src/context.rs b/src/context.rs index 336ce0258..2eaf5a737 100644 --- a/src/context.rs +++ b/src/context.rs @@ -17,6 +17,7 @@ use std::collections::{HashMap, HashSet}; use std::path::PathBuf; +use std::ptr::NonNull; use std::str::FromStr; use std::sync::Arc; @@ -44,7 +45,7 @@ use datafusion::execution::options::ReadOptions; use datafusion::execution::runtime_env::RuntimeEnvBuilder; use datafusion::execution::session_state::SessionStateBuilder; use datafusion::prelude::{ - AvroReadOptions, CsvReadOptions, DataFrame, NdJsonReadOptions, ParquetReadOptions, + AvroReadOptions, CsvReadOptions, DataFrame, JsonReadOptions, ParquetReadOptions, }; use datafusion_ffi::catalog_provider::FFI_CatalogProvider; use datafusion_ffi::catalog_provider_list::FFI_CatalogProviderList; @@ -54,6 +55,7 @@ use datafusion_proto::logical_plan::DefaultLogicalExtensionCodec; use object_store::ObjectStore; use pyo3::IntoPyObjectExt; use pyo3::exceptions::{PyKeyError, PyValueError}; +use pyo3::ffi::c_str; use pyo3::prelude::*; use pyo3::types::{PyCapsule, PyDict, PyList, PyTuple}; use url::Url; @@ -86,7 +88,13 @@ use crate::utils::{ }; /// Configuration options for a SessionContext -#[pyclass(frozen, name = "SessionConfig", module = "datafusion", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "SessionConfig", + module = "datafusion", + subclass +)] #[derive(Clone, Default)] pub struct PySessionConfig { pub config: SessionConfig, @@ -179,7 +187,13 @@ impl PySessionConfig { } /// Runtime options for a SessionContext -#[pyclass(frozen, name = "RuntimeEnvBuilder", module = "datafusion", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "RuntimeEnvBuilder", + module = "datafusion", + subclass +)] #[derive(Clone)] pub struct PyRuntimeEnvBuilder { pub builder: RuntimeEnvBuilder, @@ -266,7 +280,13 @@ impl PyRuntimeEnvBuilder { } /// `PySQLOptions` allows you to specify options to the sql execution. -#[pyclass(frozen, name = "SQLOptions", module = "datafusion", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "SQLOptions", + module = "datafusion", + subclass +)] #[derive(Clone)] pub struct PySQLOptions { pub options: SQLOptions, @@ -305,7 +325,13 @@ impl PySQLOptions { /// `PySessionContext` is able to plan and execute DataFusion plans. /// It has a powerful optimizer, a physical planner for local execution, and a /// multi-threaded execution engine to perform the execution. -#[pyclass(frozen, name = "SessionContext", module = "datafusion", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "SessionContext", + module = "datafusion", + subclass +)] #[derive(Clone)] pub struct PySessionContext { pub ctx: Arc, @@ -645,22 +671,25 @@ impl PySessionContext { .call1((codec_capsule,))?; } - let provider = - if let Ok(capsule) = provider.downcast::().map_err(py_datafusion_err) { - validate_pycapsule(capsule, "datafusion_catalog_provider_list")?; - - let provider = unsafe { capsule.reference::() }; - let provider: Arc = provider.into(); - provider as Arc - } else { - match provider.extract::() { - Ok(py_catalog_list) => py_catalog_list.catalog_list, - Err(_) => Arc::new(RustWrappedPyCatalogProviderList::new( - provider.into(), - Arc::clone(&self.logical_codec), - )) as Arc, - } - }; + let provider = if let Ok(capsule) = provider.cast::().map_err(py_datafusion_err) + { + validate_pycapsule(capsule, "datafusion_catalog_provider_list")?; + + let data: NonNull = capsule + .pointer_checked(Some(c_str!("datafusion_catalog_provider_list")))? + .cast(); + let provider = unsafe { data.as_ref() }; + let provider: Arc = provider.into(); + provider as Arc + } else { + match provider.extract::() { + Ok(py_catalog_list) => py_catalog_list.catalog_list, + Err(_) => Arc::new(RustWrappedPyCatalogProviderList::new( + provider.into(), + Arc::clone(&self.logical_codec), + )) as Arc, + } + }; self.ctx.register_catalog_list(provider); @@ -680,22 +709,25 @@ impl PySessionContext { .call1((codec_capsule,))?; } - let provider = - if let Ok(capsule) = provider.downcast::().map_err(py_datafusion_err) { - validate_pycapsule(capsule, "datafusion_catalog_provider")?; - - let provider = unsafe { capsule.reference::() }; - let provider: Arc = provider.into(); - provider as Arc - } else { - match provider.extract::() { - Ok(py_catalog) => py_catalog.catalog, - Err(_) => Arc::new(RustWrappedPyCatalogProvider::new( - provider.into(), - Arc::clone(&self.logical_codec), - )) as Arc, - } - }; + let provider = if let Ok(capsule) = provider.cast::().map_err(py_datafusion_err) + { + validate_pycapsule(capsule, "datafusion_catalog_provider")?; + + let data: NonNull = capsule + .pointer_checked(Some(c_str!("datafusion_catalog_provider")))? + .cast(); + let provider = unsafe { data.as_ref() }; + let provider: Arc = provider.into(); + provider as Arc + } else { + match provider.extract::() { + Ok(py_catalog) => py_catalog.catalog, + Err(_) => Arc::new(RustWrappedPyCatalogProvider::new( + provider.into(), + Arc::clone(&self.logical_codec), + )) as Arc, + } + }; let _ = self.ctx.register_catalog(name, provider); @@ -815,7 +847,7 @@ impl PySessionContext { .to_str() .ok_or_else(|| PyValueError::new_err("Unable to convert path to a string"))?; - let mut options = NdJsonReadOptions::default() + let mut options = JsonReadOptions::default() .file_compression_type(parse_file_compression_type(file_compression_type)?) .table_partition_cols( table_partition_cols @@ -976,7 +1008,7 @@ impl PySessionContext { let path = path .to_str() .ok_or_else(|| PyValueError::new_err("Unable to convert path to a string"))?; - let mut options = NdJsonReadOptions::default() + let mut options = JsonReadOptions::default() .table_partition_cols( table_partition_cols .into_iter() diff --git a/src/dataframe.rs b/src/dataframe.rs index 53fab58c6..eb1fa4a81 100644 --- a/src/dataframe.rs +++ b/src/dataframe.rs @@ -17,6 +17,7 @@ use std::collections::HashMap; use std::ffi::{CStr, CString}; +use std::ptr::NonNull; use std::str::FromStr; use std::sync::Arc; @@ -44,6 +45,7 @@ use futures::{StreamExt, TryStreamExt}; use parking_lot::Mutex; use pyo3::PyErr; use pyo3::exceptions::PyValueError; +use pyo3::ffi::c_str; use pyo3::prelude::*; use pyo3::pybacked::PyBackedStr; use pyo3::types::{PyCapsule, PyList, PyTuple, PyTupleMethods}; @@ -139,11 +141,11 @@ fn import_python_formatter(py: Python<'_>) -> PyResult> { // Helper function to extract attributes with fallback to default fn get_attr<'a, T>(py_object: &'a Bound<'a, PyAny>, attr_name: &str, default_value: T) -> T where - T: for<'py> pyo3::FromPyObject<'py> + Clone, + T: for<'py> pyo3::FromPyObject<'py, 'py> + Clone, { py_object .getattr(attr_name) - .and_then(|v| v.extract::()) + .and_then(|v| v.extract::().map_err(Into::::into)) .unwrap_or_else(|_| default_value.clone()) } @@ -183,7 +185,13 @@ fn build_formatter_config_from_python(formatter: &Bound<'_, PyAny>) -> PyResult< } /// Python mapping of `ParquetOptions` (includes just the writer-related options). -#[pyclass(frozen, name = "ParquetWriterOptions", module = "datafusion", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "ParquetWriterOptions", + module = "datafusion", + subclass +)] #[derive(Clone, Default)] pub struct PyParquetWriterOptions { options: ParquetOptions, @@ -247,7 +255,13 @@ impl PyParquetWriterOptions { } /// Python mapping of `ParquetColumnOptions`. -#[pyclass(frozen, name = "ParquetColumnOptions", module = "datafusion", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "ParquetColumnOptions", + module = "datafusion", + subclass +)] #[derive(Clone, Default)] pub struct PyParquetColumnOptions { options: ParquetColumnOptions, @@ -282,7 +296,13 @@ impl PyParquetColumnOptions { /// A PyDataFrame is a representation of a logical plan and an API to compose statements. /// Use it to build a plan and `.collect()` to execute the plan and collect the result. /// The actual execution of a plan runs natively on Rust and Arrow on a multi-threaded environment. -#[pyclass(name = "DataFrame", module = "datafusion", subclass, frozen)] +#[pyclass( + from_py_object, + name = "DataFrame", + module = "datafusion", + subclass, + frozen +)] #[derive(Clone)] pub struct PyDataFrame { df: Arc, @@ -450,7 +470,7 @@ impl PyDataFrame { if let Ok(key) = key.extract::() { // df[col] self.select_columns(vec![key]) - } else if let Ok(tuple) = key.downcast::() { + } else if let Ok(tuple) = key.cast::() { // df[col1, col2, col3] let keys = tuple .iter() @@ -1099,7 +1119,10 @@ impl PyDataFrame { if let Some(schema_capsule) = requested_schema { validate_pycapsule(&schema_capsule, "arrow_schema")?; - let schema_ptr = unsafe { schema_capsule.reference::() }; + let data: NonNull = schema_capsule + .pointer_checked(Some(c_str!("arrow_schema")))? + .cast(); + let schema_ptr = unsafe { data.as_ref() }; let desired_schema = Schema::try_from(schema_ptr)?; schema = project_schema(schema, desired_schema)?; @@ -1203,7 +1226,14 @@ impl PyDataFrame { } #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -#[pyclass(frozen, eq, eq_int, name = "InsertOp", module = "datafusion")] +#[pyclass( + from_py_object, + frozen, + eq, + eq_int, + name = "InsertOp", + module = "datafusion" +)] pub enum PyInsertOp { APPEND, REPLACE, @@ -1221,7 +1251,12 @@ impl From for InsertOp { } #[derive(Debug, Clone)] -#[pyclass(frozen, name = "DataFrameWriteOptions", module = "datafusion")] +#[pyclass( + from_py_object, + frozen, + name = "DataFrameWriteOptions", + module = "datafusion" +)] pub struct PyDataFrameWriteOptions { insert_operation: InsertOp, single_file_output: bool, diff --git a/src/dataset.rs b/src/dataset.rs index 6a4fdb1fa..dbeafcd9f 100644 --- a/src/dataset.rs +++ b/src/dataset.rs @@ -47,7 +47,7 @@ impl Dataset { // Ensure that we were passed an instance of pyarrow.dataset.Dataset let ds = PyModule::import(py, "pyarrow.dataset")?; let ds_attr = ds.getattr("Dataset")?; - let ds_type = ds_attr.downcast::()?; + let ds_type = ds_attr.cast::()?; if dataset.is_instance(ds_type)? { Ok(Dataset { dataset: dataset.clone().unbind(), diff --git a/src/dataset_exec.rs b/src/dataset_exec.rs index 61e1544cd..e3c058c07 100644 --- a/src/dataset_exec.rs +++ b/src/dataset_exec.rs @@ -31,7 +31,7 @@ use datafusion::physical_plan::execution_plan::{Boundedness, EmissionType}; use datafusion::physical_plan::stream::RecordBatchStreamAdapter; use datafusion::physical_plan::{ DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties, Partitioning, - SendableRecordBatchStream, Statistics, + PlanProperties, SendableRecordBatchStream, Statistics, }; use futures::{TryStreamExt, stream}; /// Implements a Datafusion physical ExecutionPlan that delegates to a PyArrow Dataset @@ -71,7 +71,7 @@ pub(crate) struct DatasetExec { columns: Option>, filter_expr: Option>, projected_statistics: Statistics, - plan_properties: datafusion::physical_plan::PlanProperties, + plan_properties: Arc, } impl DatasetExec { @@ -128,15 +128,15 @@ impl DatasetExec { )?; let fragments_iter = pylist.call1((fragments_iterator,))?; - let fragments = fragments_iter.downcast::().map_err(PyErr::from)?; + let fragments = fragments_iter.cast::().map_err(PyErr::from)?; let projected_statistics = Statistics::new_unknown(&schema); - let plan_properties = datafusion::physical_plan::PlanProperties::new( + let plan_properties = Arc::new(PlanProperties::new( EquivalenceProperties::new(schema.clone()), Partitioning::UnknownPartitioning(fragments.len()), EmissionType::Final, Boundedness::Bounded, - ); + )); Ok(DatasetExec { dataset: dataset.clone().unbind(), @@ -235,11 +235,11 @@ impl ExecutionPlan for DatasetExec { }) } - fn statistics(&self) -> DFResult { + fn partition_statistics(&self, _partition: Option) -> DFResult { Ok(self.projected_statistics.clone()) } - fn properties(&self) -> &datafusion::physical_plan::PlanProperties { + fn properties(&self) -> &Arc { &self.plan_properties } } diff --git a/src/expr.rs b/src/expr.rs index 919174029..4d4a73ff3 100644 --- a/src/expr.rs +++ b/src/expr.rs @@ -24,7 +24,7 @@ use datafusion::arrow::pyarrow::PyArrowType; use datafusion::functions::core::expr_ext::FieldAccessor; use datafusion::logical_expr::expr::{ AggregateFunction, AggregateFunctionParams, FieldMetadata, InList, InSubquery, ScalarFunction, - WindowFunction, + SetComparison, WindowFunction, }; use datafusion::logical_expr::utils::exprlist_to_fields; use datafusion::logical_expr::{ @@ -98,6 +98,7 @@ pub mod recursive_query; pub mod repartition; pub mod scalar_subquery; pub mod scalar_variable; +pub mod set_comparison; pub mod signature; pub mod sort; pub mod sort_expr; @@ -114,7 +115,13 @@ pub mod window; use sort_expr::{PySortExpr, to_sort_expressions}; /// A PyExpr that can be used on a DataFrame -#[pyclass(frozen, name = "RawExpr", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "RawExpr", + module = "datafusion.expr", + subclass +)] #[derive(Debug, Clone)] pub struct PyExpr { pub expr: Expr, @@ -210,6 +217,9 @@ impl PyExpr { Expr::Unnest(value) => { Ok(unnest_expr::PyUnnestExpr::from(value.clone()).into_bound_py_any(py)?) } + Expr::SetComparison(value) => { + Ok(set_comparison::PySetComparison::from(value.clone()).into_bound_py_any(py)?) + } }) } @@ -376,7 +386,8 @@ impl PyExpr { | Expr::Placeholder { .. } | Expr::OuterReferenceColumn(_, _) | Expr::Unnest(_) - | Expr::IsNotUnknown(_) => RexType::Call, + | Expr::IsNotUnknown(_) + | Expr::SetComparison(_) => RexType::Call, Expr::ScalarSubquery(..) => RexType::ScalarSubquery, #[allow(deprecated)] Expr::Wildcard { .. } => { @@ -427,7 +438,10 @@ impl PyExpr { | Expr::Negative(expr) | Expr::Cast(Cast { expr, .. }) | Expr::TryCast(TryCast { expr, .. }) - | Expr::InSubquery(InSubquery { expr, .. }) => Ok(vec![PyExpr::from(*expr.clone())]), + | Expr::InSubquery(InSubquery { expr, .. }) + | Expr::SetComparison(SetComparison { expr, .. }) => { + Ok(vec![PyExpr::from(*expr.clone())]) + } // Expr variants containing a collection of Expr(s) for operands Expr::AggregateFunction(AggregateFunction { @@ -648,7 +662,13 @@ impl PyExpr { } } -#[pyclass(frozen, name = "ExprFuncBuilder", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "ExprFuncBuilder", + module = "datafusion.expr", + subclass +)] #[derive(Debug, Clone)] pub struct PyExprFuncBuilder { pub builder: ExprFuncBuilder, diff --git a/src/expr/aggregate.rs b/src/expr/aggregate.rs index 9ff280fa9..5a6a771a7 100644 --- a/src/expr/aggregate.rs +++ b/src/expr/aggregate.rs @@ -30,7 +30,13 @@ use crate::errors::py_type_err; use crate::expr::PyExpr; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "Aggregate", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Aggregate", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyAggregate { aggregate: Aggregate, diff --git a/src/expr/aggregate_expr.rs b/src/expr/aggregate_expr.rs index d3b695a27..88e47999f 100644 --- a/src/expr/aggregate_expr.rs +++ b/src/expr/aggregate_expr.rs @@ -23,6 +23,7 @@ use pyo3::prelude::*; use crate::expr::PyExpr; #[pyclass( + from_py_object, frozen, name = "AggregateFunction", module = "datafusion.expr", diff --git a/src/expr/alias.rs b/src/expr/alias.rs index c6d486284..b76e82e22 100644 --- a/src/expr/alias.rs +++ b/src/expr/alias.rs @@ -22,7 +22,13 @@ use pyo3::prelude::*; use crate::expr::PyExpr; -#[pyclass(frozen, name = "Alias", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Alias", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyAlias { alias: Alias, diff --git a/src/expr/analyze.rs b/src/expr/analyze.rs index 3aae6f315..137765fe1 100644 --- a/src/expr/analyze.rs +++ b/src/expr/analyze.rs @@ -25,7 +25,13 @@ use super::logical_node::LogicalNode; use crate::common::df_schema::PyDFSchema; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "Analyze", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Analyze", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyAnalyze { analyze: Analyze, diff --git a/src/expr/between.rs b/src/expr/between.rs index 4f0b34add..6943b6c3b 100644 --- a/src/expr/between.rs +++ b/src/expr/between.rs @@ -22,7 +22,13 @@ use pyo3::prelude::*; use crate::expr::PyExpr; -#[pyclass(frozen, name = "Between", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Between", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyBetween { between: Between, diff --git a/src/expr/binary_expr.rs b/src/expr/binary_expr.rs index f67a08c7c..2326ba705 100644 --- a/src/expr/binary_expr.rs +++ b/src/expr/binary_expr.rs @@ -20,7 +20,13 @@ use pyo3::prelude::*; use crate::expr::PyExpr; -#[pyclass(frozen, name = "BinaryExpr", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "BinaryExpr", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyBinaryExpr { expr: BinaryExpr, diff --git a/src/expr/bool_expr.rs b/src/expr/bool_expr.rs index abd259409..9e374c7e2 100644 --- a/src/expr/bool_expr.rs +++ b/src/expr/bool_expr.rs @@ -22,7 +22,13 @@ use pyo3::prelude::*; use super::PyExpr; -#[pyclass(frozen, name = "Not", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Not", + module = "datafusion.expr", + subclass +)] #[derive(Clone, Debug)] pub struct PyNot { expr: Expr, @@ -52,7 +58,13 @@ impl PyNot { } } -#[pyclass(frozen, name = "IsNotNull", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "IsNotNull", + module = "datafusion.expr", + subclass +)] #[derive(Clone, Debug)] pub struct PyIsNotNull { expr: Expr, @@ -82,7 +94,13 @@ impl PyIsNotNull { } } -#[pyclass(frozen, name = "IsNull", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "IsNull", + module = "datafusion.expr", + subclass +)] #[derive(Clone, Debug)] pub struct PyIsNull { expr: Expr, @@ -112,7 +130,13 @@ impl PyIsNull { } } -#[pyclass(frozen, name = "IsTrue", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "IsTrue", + module = "datafusion.expr", + subclass +)] #[derive(Clone, Debug)] pub struct PyIsTrue { expr: Expr, @@ -142,7 +166,13 @@ impl PyIsTrue { } } -#[pyclass(frozen, name = "IsFalse", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "IsFalse", + module = "datafusion.expr", + subclass +)] #[derive(Clone, Debug)] pub struct PyIsFalse { expr: Expr, @@ -172,7 +202,13 @@ impl PyIsFalse { } } -#[pyclass(frozen, name = "IsUnknown", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "IsUnknown", + module = "datafusion.expr", + subclass +)] #[derive(Clone, Debug)] pub struct PyIsUnknown { expr: Expr, @@ -202,7 +238,13 @@ impl PyIsUnknown { } } -#[pyclass(frozen, name = "IsNotTrue", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "IsNotTrue", + module = "datafusion.expr", + subclass +)] #[derive(Clone, Debug)] pub struct PyIsNotTrue { expr: Expr, @@ -232,7 +274,13 @@ impl PyIsNotTrue { } } -#[pyclass(frozen, name = "IsNotFalse", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "IsNotFalse", + module = "datafusion.expr", + subclass +)] #[derive(Clone, Debug)] pub struct PyIsNotFalse { expr: Expr, @@ -262,7 +310,13 @@ impl PyIsNotFalse { } } -#[pyclass(frozen, name = "IsNotUnknown", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "IsNotUnknown", + module = "datafusion.expr", + subclass +)] #[derive(Clone, Debug)] pub struct PyIsNotUnknown { expr: Expr, @@ -292,7 +346,13 @@ impl PyIsNotUnknown { } } -#[pyclass(frozen, name = "Negative", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Negative", + module = "datafusion.expr", + subclass +)] #[derive(Clone, Debug)] pub struct PyNegative { expr: Expr, diff --git a/src/expr/case.rs b/src/expr/case.rs index b49c19081..4f00449d8 100644 --- a/src/expr/case.rs +++ b/src/expr/case.rs @@ -20,7 +20,13 @@ use pyo3::prelude::*; use crate::expr::PyExpr; -#[pyclass(frozen, name = "Case", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Case", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyCase { case: Case, diff --git a/src/expr/cast.rs b/src/expr/cast.rs index 1aca9ea95..37d603538 100644 --- a/src/expr/cast.rs +++ b/src/expr/cast.rs @@ -21,7 +21,13 @@ use pyo3::prelude::*; use crate::common::data_type::PyDataType; use crate::expr::PyExpr; -#[pyclass(frozen, name = "Cast", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Cast", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyCast { cast: Cast, @@ -50,7 +56,7 @@ impl PyCast { } } -#[pyclass(name = "TryCast", module = "datafusion.expr", subclass)] +#[pyclass(from_py_object, name = "TryCast", module = "datafusion.expr", subclass)] #[derive(Clone)] pub struct PyTryCast { try_cast: TryCast, diff --git a/src/expr/column.rs b/src/expr/column.rs index 300079481..c1238f98a 100644 --- a/src/expr/column.rs +++ b/src/expr/column.rs @@ -18,7 +18,13 @@ use datafusion::common::Column; use pyo3::prelude::*; -#[pyclass(frozen, name = "Column", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Column", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyColumn { pub col: Column, diff --git a/src/expr/conditional_expr.rs b/src/expr/conditional_expr.rs index da6102dbf..ea21fdb20 100644 --- a/src/expr/conditional_expr.rs +++ b/src/expr/conditional_expr.rs @@ -24,7 +24,13 @@ use crate::expr::PyExpr; // TODO(tsaucer) replace this all with CaseBuilder after it implements Clone #[derive(Clone, Debug)] -#[pyclass(name = "CaseBuilder", module = "datafusion.expr", subclass, frozen)] +#[pyclass( + from_py_object, + name = "CaseBuilder", + module = "datafusion.expr", + subclass, + frozen +)] pub struct PyCaseBuilder { expr: Option, when: Vec, diff --git a/src/expr/copy_to.rs b/src/expr/copy_to.rs index 807104fc1..78e53cdff 100644 --- a/src/expr/copy_to.rs +++ b/src/expr/copy_to.rs @@ -27,7 +27,13 @@ use pyo3::prelude::*; use super::logical_node::LogicalNode; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "CopyTo", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "CopyTo", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyCopyTo { copy: CopyTo, @@ -113,7 +119,13 @@ impl PyCopyTo { } } -#[pyclass(frozen, name = "FileType", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "FileType", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyFileType { file_type: Arc, diff --git a/src/expr/create_catalog.rs b/src/expr/create_catalog.rs index 0131014eb..fa95980c0 100644 --- a/src/expr/create_catalog.rs +++ b/src/expr/create_catalog.rs @@ -26,7 +26,13 @@ use super::logical_node::LogicalNode; use crate::common::df_schema::PyDFSchema; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "CreateCatalog", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "CreateCatalog", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyCreateCatalog { create: CreateCatalog, diff --git a/src/expr/create_catalog_schema.rs b/src/expr/create_catalog_schema.rs index 63ed3a2d2..d836284a0 100644 --- a/src/expr/create_catalog_schema.rs +++ b/src/expr/create_catalog_schema.rs @@ -27,6 +27,7 @@ use crate::common::df_schema::PyDFSchema; use crate::sql::logical::PyLogicalPlan; #[pyclass( + from_py_object, frozen, name = "CreateCatalogSchema", module = "datafusion.expr", diff --git a/src/expr/create_external_table.rs b/src/expr/create_external_table.rs index cb4bb781a..980eea131 100644 --- a/src/expr/create_external_table.rs +++ b/src/expr/create_external_table.rs @@ -31,6 +31,7 @@ use crate::expr::PyExpr; use crate::sql::logical::PyLogicalPlan; #[pyclass( + from_py_object, frozen, name = "CreateExternalTable", module = "datafusion.expr", diff --git a/src/expr/create_function.rs b/src/expr/create_function.rs index 3433363a0..622858913 100644 --- a/src/expr/create_function.rs +++ b/src/expr/create_function.rs @@ -30,7 +30,13 @@ use crate::common::data_type::PyDataType; use crate::common::df_schema::PyDFSchema; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "CreateFunction", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "CreateFunction", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyCreateFunction { create: CreateFunction, @@ -55,6 +61,7 @@ impl Display for PyCreateFunction { } #[pyclass( + from_py_object, frozen, name = "OperateFunctionArg", module = "datafusion.expr", @@ -66,7 +73,14 @@ pub struct PyOperateFunctionArg { } #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -#[pyclass(frozen, eq, eq_int, name = "Volatility", module = "datafusion.expr")] +#[pyclass( + from_py_object, + frozen, + eq, + eq_int, + name = "Volatility", + module = "datafusion.expr" +)] pub enum PyVolatility { Immutable, Stable, @@ -74,6 +88,7 @@ pub enum PyVolatility { } #[pyclass( + from_py_object, frozen, name = "CreateFunctionBody", module = "datafusion.expr", diff --git a/src/expr/create_index.rs b/src/expr/create_index.rs index ebe0620ca..5f9bd11e8 100644 --- a/src/expr/create_index.rs +++ b/src/expr/create_index.rs @@ -27,7 +27,13 @@ use super::sort_expr::PySortExpr; use crate::common::df_schema::PyDFSchema; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "CreateIndex", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "CreateIndex", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyCreateIndex { create: CreateIndex, diff --git a/src/expr/create_memory_table.rs b/src/expr/create_memory_table.rs index c11a816aa..3214dab0e 100644 --- a/src/expr/create_memory_table.rs +++ b/src/expr/create_memory_table.rs @@ -25,6 +25,7 @@ use super::logical_node::LogicalNode; use crate::sql::logical::PyLogicalPlan; #[pyclass( + from_py_object, frozen, name = "CreateMemoryTable", module = "datafusion.expr", diff --git a/src/expr/create_view.rs b/src/expr/create_view.rs index 52882ddbc..6941ef769 100644 --- a/src/expr/create_view.rs +++ b/src/expr/create_view.rs @@ -25,7 +25,13 @@ use super::logical_node::LogicalNode; use crate::errors::py_type_err; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "CreateView", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "CreateView", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyCreateView { create: CreateView, diff --git a/src/expr/describe_table.rs b/src/expr/describe_table.rs index 721c13ab9..73955bb34 100644 --- a/src/expr/describe_table.rs +++ b/src/expr/describe_table.rs @@ -28,7 +28,13 @@ use super::logical_node::LogicalNode; use crate::common::df_schema::PyDFSchema; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "DescribeTable", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "DescribeTable", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyDescribeTable { describe: DescribeTable, diff --git a/src/expr/distinct.rs b/src/expr/distinct.rs index c7da9c7f6..68c2a17fe 100644 --- a/src/expr/distinct.rs +++ b/src/expr/distinct.rs @@ -24,7 +24,13 @@ use pyo3::prelude::*; use super::logical_node::LogicalNode; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "Distinct", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Distinct", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyDistinct { distinct: Distinct, diff --git a/src/expr/dml.rs b/src/expr/dml.rs index 84a34e65f..26f975820 100644 --- a/src/expr/dml.rs +++ b/src/expr/dml.rs @@ -25,7 +25,13 @@ use crate::common::df_schema::PyDFSchema; use crate::common::schema::PyTableSource; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "DmlStatement", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "DmlStatement", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyDmlStatement { dml: DmlStatement, @@ -89,15 +95,21 @@ impl PyDmlStatement { } #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -#[pyclass(eq, eq_int, name = "WriteOp", module = "datafusion.expr")] +#[pyclass( + from_py_object, + eq, + eq_int, + name = "WriteOp", + module = "datafusion.expr" +)] pub enum PyWriteOp { Append, Overwrite, Replace, - Update, Delete, Ctas, + Truncate, } impl From for PyWriteOp { @@ -106,10 +118,10 @@ impl From for PyWriteOp { WriteOp::Insert(InsertOp::Append) => PyWriteOp::Append, WriteOp::Insert(InsertOp::Overwrite) => PyWriteOp::Overwrite, WriteOp::Insert(InsertOp::Replace) => PyWriteOp::Replace, - WriteOp::Update => PyWriteOp::Update, WriteOp::Delete => PyWriteOp::Delete, WriteOp::Ctas => PyWriteOp::Ctas, + WriteOp::Truncate => PyWriteOp::Truncate, } } } @@ -120,10 +132,10 @@ impl From for WriteOp { PyWriteOp::Append => WriteOp::Insert(InsertOp::Append), PyWriteOp::Overwrite => WriteOp::Insert(InsertOp::Overwrite), PyWriteOp::Replace => WriteOp::Insert(InsertOp::Replace), - PyWriteOp::Update => WriteOp::Update, PyWriteOp::Delete => WriteOp::Delete, PyWriteOp::Ctas => WriteOp::Ctas, + PyWriteOp::Truncate => WriteOp::Truncate, } } } diff --git a/src/expr/drop_catalog_schema.rs b/src/expr/drop_catalog_schema.rs index 339e11968..fd5105332 100644 --- a/src/expr/drop_catalog_schema.rs +++ b/src/expr/drop_catalog_schema.rs @@ -30,6 +30,7 @@ use crate::common::df_schema::PyDFSchema; use crate::sql::logical::PyLogicalPlan; #[pyclass( + from_py_object, frozen, name = "DropCatalogSchema", module = "datafusion.expr", diff --git a/src/expr/drop_function.rs b/src/expr/drop_function.rs index db0942a0c..0599dd49e 100644 --- a/src/expr/drop_function.rs +++ b/src/expr/drop_function.rs @@ -26,7 +26,13 @@ use super::logical_node::LogicalNode; use crate::common::df_schema::PyDFSchema; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "DropFunction", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "DropFunction", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyDropFunction { drop: DropFunction, diff --git a/src/expr/drop_table.rs b/src/expr/drop_table.rs index 9bf2c6530..46fe67465 100644 --- a/src/expr/drop_table.rs +++ b/src/expr/drop_table.rs @@ -24,7 +24,13 @@ use pyo3::prelude::*; use super::logical_node::LogicalNode; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "DropTable", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "DropTable", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyDropTable { drop: DropTable, diff --git a/src/expr/drop_view.rs b/src/expr/drop_view.rs index 187865f4f..0d0c51f13 100644 --- a/src/expr/drop_view.rs +++ b/src/expr/drop_view.rs @@ -26,7 +26,13 @@ use super::logical_node::LogicalNode; use crate::common::df_schema::PyDFSchema; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "DropView", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "DropView", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyDropView { drop: DropView, diff --git a/src/expr/empty_relation.rs b/src/expr/empty_relation.rs index 4a9e82358..f3c237731 100644 --- a/src/expr/empty_relation.rs +++ b/src/expr/empty_relation.rs @@ -25,7 +25,13 @@ use super::logical_node::LogicalNode; use crate::common::df_schema::PyDFSchema; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "EmptyRelation", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "EmptyRelation", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyEmptyRelation { empty: EmptyRelation, diff --git a/src/expr/exists.rs b/src/expr/exists.rs index 392bfcb9e..d2e816127 100644 --- a/src/expr/exists.rs +++ b/src/expr/exists.rs @@ -20,7 +20,13 @@ use pyo3::prelude::*; use super::subquery::PySubquery; -#[pyclass(frozen, name = "Exists", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Exists", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyExists { exists: Exists, diff --git a/src/expr/explain.rs b/src/expr/explain.rs index 2100df5c7..6259951de 100644 --- a/src/expr/explain.rs +++ b/src/expr/explain.rs @@ -27,7 +27,13 @@ use crate::common::df_schema::PyDFSchema; use crate::errors::py_type_err; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "Explain", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Explain", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyExplain { explain: Explain, diff --git a/src/expr/extension.rs b/src/expr/extension.rs index 2d9ac45f9..a0b617565 100644 --- a/src/expr/extension.rs +++ b/src/expr/extension.rs @@ -22,7 +22,13 @@ use pyo3::prelude::*; use super::logical_node::LogicalNode; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "Extension", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Extension", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyExtension { pub node: Extension, diff --git a/src/expr/filter.rs b/src/expr/filter.rs index bee180565..67426806d 100644 --- a/src/expr/filter.rs +++ b/src/expr/filter.rs @@ -26,7 +26,13 @@ use crate::expr::PyExpr; use crate::expr::logical_node::LogicalNode; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "Filter", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Filter", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyFilter { filter: Filter, diff --git a/src/expr/grouping_set.rs b/src/expr/grouping_set.rs index 107dd9370..549a866ed 100644 --- a/src/expr/grouping_set.rs +++ b/src/expr/grouping_set.rs @@ -18,7 +18,13 @@ use datafusion::logical_expr::GroupingSet; use pyo3::prelude::*; -#[pyclass(frozen, name = "GroupingSet", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "GroupingSet", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyGroupingSet { grouping_set: GroupingSet, diff --git a/src/expr/in_list.rs b/src/expr/in_list.rs index 128c3f4c2..0612cc21e 100644 --- a/src/expr/in_list.rs +++ b/src/expr/in_list.rs @@ -20,7 +20,13 @@ use pyo3::prelude::*; use crate::expr::PyExpr; -#[pyclass(frozen, name = "InList", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "InList", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyInList { in_list: InList, diff --git a/src/expr/in_subquery.rs b/src/expr/in_subquery.rs index 139e8376e..81a2c5794 100644 --- a/src/expr/in_subquery.rs +++ b/src/expr/in_subquery.rs @@ -21,7 +21,13 @@ use pyo3::prelude::*; use super::PyExpr; use super::subquery::PySubquery; -#[pyclass(frozen, name = "InSubquery", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "InSubquery", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyInSubquery { in_subquery: InSubquery, diff --git a/src/expr/indexed_field.rs b/src/expr/indexed_field.rs index 79f528179..98a90d8d4 100644 --- a/src/expr/indexed_field.rs +++ b/src/expr/indexed_field.rs @@ -23,7 +23,13 @@ use pyo3::prelude::*; use super::literal::PyLiteral; use crate::expr::PyExpr; -#[pyclass(frozen, name = "GetIndexedField", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "GetIndexedField", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyGetIndexedField { indexed_field: GetIndexedField, diff --git a/src/expr/join.rs b/src/expr/join.rs index 2cfd8cc28..b90f2f57d 100644 --- a/src/expr/join.rs +++ b/src/expr/join.rs @@ -28,7 +28,7 @@ use crate::expr::logical_node::LogicalNode; use crate::sql::logical::PyLogicalPlan; #[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[pyclass(frozen, name = "JoinType", module = "datafusion.expr")] +#[pyclass(from_py_object, frozen, name = "JoinType", module = "datafusion.expr")] pub struct PyJoinType { join_type: JoinType, } @@ -63,7 +63,12 @@ impl Display for PyJoinType { } #[derive(Debug, Clone, Copy)] -#[pyclass(frozen, name = "JoinConstraint", module = "datafusion.expr")] +#[pyclass( + from_py_object, + frozen, + name = "JoinConstraint", + module = "datafusion.expr" +)] pub struct PyJoinConstraint { join_constraint: JoinConstraint, } @@ -90,7 +95,13 @@ impl PyJoinConstraint { } } -#[pyclass(frozen, name = "Join", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Join", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyJoin { join: Join, diff --git a/src/expr/like.rs b/src/expr/like.rs index 94860bd6c..417dc9182 100644 --- a/src/expr/like.rs +++ b/src/expr/like.rs @@ -22,7 +22,13 @@ use pyo3::prelude::*; use crate::expr::PyExpr; -#[pyclass(frozen, name = "Like", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Like", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyLike { like: Like, @@ -80,7 +86,13 @@ impl PyLike { } } -#[pyclass(frozen, name = "ILike", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "ILike", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyILike { like: Like, @@ -138,7 +150,13 @@ impl PyILike { } } -#[pyclass(frozen, name = "SimilarTo", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "SimilarTo", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PySimilarTo { like: Like, diff --git a/src/expr/limit.rs b/src/expr/limit.rs index aad921605..c04b8bfa8 100644 --- a/src/expr/limit.rs +++ b/src/expr/limit.rs @@ -25,7 +25,13 @@ use crate::common::df_schema::PyDFSchema; use crate::expr::logical_node::LogicalNode; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "Limit", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Limit", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyLimit { limit: Limit, diff --git a/src/expr/literal.rs b/src/expr/literal.rs index f97c2b491..9db0f594b 100644 --- a/src/expr/literal.rs +++ b/src/expr/literal.rs @@ -22,7 +22,13 @@ use pyo3::prelude::*; use crate::errors::PyDataFusionError; -#[pyclass(name = "Literal", module = "datafusion.expr", subclass, frozen)] +#[pyclass( + from_py_object, + name = "Literal", + module = "datafusion.expr", + subclass, + frozen +)] #[derive(Clone)] pub struct PyLiteral { pub value: ScalarValue, diff --git a/src/expr/placeholder.rs b/src/expr/placeholder.rs index f1e8694a9..6bd88321c 100644 --- a/src/expr/placeholder.rs +++ b/src/expr/placeholder.rs @@ -22,7 +22,13 @@ use pyo3::prelude::*; use crate::common::data_type::PyDataType; -#[pyclass(frozen, name = "Placeholder", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Placeholder", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyPlaceholder { placeholder: Placeholder, diff --git a/src/expr/projection.rs b/src/expr/projection.rs index 1bce3389f..456e06412 100644 --- a/src/expr/projection.rs +++ b/src/expr/projection.rs @@ -27,7 +27,13 @@ use crate::expr::PyExpr; use crate::expr::logical_node::LogicalNode; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "Projection", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Projection", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyProjection { pub projection: Projection, diff --git a/src/expr/recursive_query.rs b/src/expr/recursive_query.rs index 3f6d06558..e03137b80 100644 --- a/src/expr/recursive_query.rs +++ b/src/expr/recursive_query.rs @@ -24,7 +24,13 @@ use pyo3::prelude::*; use super::logical_node::LogicalNode; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "RecursiveQuery", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "RecursiveQuery", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyRecursiveQuery { query: RecursiveQuery, diff --git a/src/expr/repartition.rs b/src/expr/repartition.rs index b4fd78bc3..be39b9978 100644 --- a/src/expr/repartition.rs +++ b/src/expr/repartition.rs @@ -27,13 +27,25 @@ use super::logical_node::LogicalNode; use crate::errors::py_type_err; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "Repartition", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Repartition", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyRepartition { repartition: Repartition, } -#[pyclass(frozen, name = "Partitioning", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Partitioning", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyPartitioning { partitioning: Partitioning, diff --git a/src/expr/scalar_subquery.rs b/src/expr/scalar_subquery.rs index e58d66e19..c7852a4c4 100644 --- a/src/expr/scalar_subquery.rs +++ b/src/expr/scalar_subquery.rs @@ -20,7 +20,13 @@ use pyo3::prelude::*; use super::subquery::PySubquery; -#[pyclass(frozen, name = "ScalarSubquery", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "ScalarSubquery", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyScalarSubquery { subquery: Subquery, diff --git a/src/expr/scalar_variable.rs b/src/expr/scalar_variable.rs index 5bc056e76..2d3bc4b76 100644 --- a/src/expr/scalar_variable.rs +++ b/src/expr/scalar_variable.rs @@ -20,7 +20,13 @@ use pyo3::prelude::*; use crate::common::data_type::PyDataType; -#[pyclass(frozen, name = "ScalarVariable", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "ScalarVariable", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyScalarVariable { field: FieldRef, diff --git a/src/expr/set_comparison.rs b/src/expr/set_comparison.rs new file mode 100644 index 000000000..9f0c077e1 --- /dev/null +++ b/src/expr/set_comparison.rs @@ -0,0 +1,59 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use datafusion::logical_expr::expr::SetComparison; +use pyo3::prelude::*; + +use super::subquery::PySubquery; +use crate::expr::PyExpr; + +#[pyclass( + from_py_object, + frozen, + name = "SetComparison", + module = "datafusion.set_comparison", + subclass +)] +#[derive(Clone)] +pub struct PySetComparison { + set_comparison: SetComparison, +} + +impl From for PySetComparison { + fn from(set_comparison: SetComparison) -> Self { + PySetComparison { set_comparison } + } +} + +#[pymethods] +impl PySetComparison { + fn expr(&self) -> PyExpr { + (*self.set_comparison.expr).clone().into() + } + + fn subquery(&self) -> PySubquery { + self.set_comparison.subquery.clone().into() + } + + fn op(&self) -> String { + format!("{}", self.set_comparison.op) + } + + fn quantifier(&self) -> String { + format!("{}", self.set_comparison.quantifier) + } +} diff --git a/src/expr/signature.rs b/src/expr/signature.rs index e2c23dce9..35268e3a9 100644 --- a/src/expr/signature.rs +++ b/src/expr/signature.rs @@ -19,7 +19,13 @@ use datafusion::logical_expr::{TypeSignature, Volatility}; use pyo3::prelude::*; #[allow(dead_code)] -#[pyclass(frozen, name = "Signature", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Signature", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PySignature { type_signature: TypeSignature, diff --git a/src/expr/sort.rs b/src/expr/sort.rs index db2f870b4..7c1e654c5 100644 --- a/src/expr/sort.rs +++ b/src/expr/sort.rs @@ -27,7 +27,13 @@ use crate::expr::logical_node::LogicalNode; use crate::expr::sort_expr::PySortExpr; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "Sort", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Sort", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PySort { sort: Sort, diff --git a/src/expr/sort_expr.rs b/src/expr/sort_expr.rs index 23c066156..3c3c86bc1 100644 --- a/src/expr/sort_expr.rs +++ b/src/expr/sort_expr.rs @@ -22,7 +22,13 @@ use pyo3::prelude::*; use crate::expr::PyExpr; -#[pyclass(frozen, name = "SortExpr", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "SortExpr", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PySortExpr { pub(crate) sort: SortExpr, diff --git a/src/expr/statement.rs b/src/expr/statement.rs index 8f362e608..5aa1e4e9c 100644 --- a/src/expr/statement.rs +++ b/src/expr/statement.rs @@ -31,6 +31,7 @@ use super::logical_node::LogicalNode; use crate::sql::logical::PyLogicalPlan; #[pyclass( + from_py_object, frozen, name = "TransactionStart", module = "datafusion.expr", @@ -67,6 +68,7 @@ impl LogicalNode for PyTransactionStart { #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[pyclass( + from_py_object, frozen, eq, eq_int, @@ -100,6 +102,7 @@ impl TryFrom for TransactionAccessMode { #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[pyclass( + from_py_object, frozen, eq, eq_int, @@ -178,7 +181,13 @@ impl PyTransactionStart { } } -#[pyclass(frozen, name = "TransactionEnd", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "TransactionEnd", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyTransactionEnd { transaction_end: TransactionEnd, @@ -210,6 +219,7 @@ impl LogicalNode for PyTransactionEnd { #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[pyclass( + from_py_object, frozen, eq, eq_int, @@ -259,7 +269,13 @@ impl PyTransactionEnd { } } -#[pyclass(frozen, name = "ResetVariable", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "ResetVariable", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyResetVariable { reset_variable: ResetVariable, @@ -303,7 +319,13 @@ impl PyResetVariable { } } -#[pyclass(frozen, name = "SetVariable", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "SetVariable", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PySetVariable { set_variable: SetVariable, @@ -351,7 +373,13 @@ impl PySetVariable { } } -#[pyclass(frozen, name = "Prepare", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Prepare", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyPrepare { prepare: Prepare, @@ -416,7 +444,13 @@ impl PyPrepare { } } -#[pyclass(frozen, name = "Execute", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Execute", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyExecute { execute: Execute, @@ -473,7 +507,13 @@ impl PyExecute { } } -#[pyclass(frozen, name = "Deallocate", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Deallocate", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyDeallocate { deallocate: Deallocate, diff --git a/src/expr/subquery.rs b/src/expr/subquery.rs index 5d50e8ed2..c6fa83db8 100644 --- a/src/expr/subquery.rs +++ b/src/expr/subquery.rs @@ -24,7 +24,13 @@ use pyo3::prelude::*; use super::logical_node::LogicalNode; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "Subquery", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Subquery", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PySubquery { subquery: Subquery, diff --git a/src/expr/subquery_alias.rs b/src/expr/subquery_alias.rs index 0170d1281..a6b09e842 100644 --- a/src/expr/subquery_alias.rs +++ b/src/expr/subquery_alias.rs @@ -25,7 +25,13 @@ use super::logical_node::LogicalNode; use crate::common::df_schema::PyDFSchema; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "SubqueryAlias", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "SubqueryAlias", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PySubqueryAlias { subquery_alias: SubqueryAlias, diff --git a/src/expr/table_scan.rs b/src/expr/table_scan.rs index 94bc37085..8ba7e4a69 100644 --- a/src/expr/table_scan.rs +++ b/src/expr/table_scan.rs @@ -27,7 +27,13 @@ use crate::expr::PyExpr; use crate::expr::logical_node::LogicalNode; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "TableScan", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "TableScan", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyTableScan { table_scan: TableScan, diff --git a/src/expr/union.rs b/src/expr/union.rs index 41370ab51..a3b9efe91 100644 --- a/src/expr/union.rs +++ b/src/expr/union.rs @@ -25,7 +25,13 @@ use crate::common::df_schema::PyDFSchema; use crate::expr::logical_node::LogicalNode; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "Union", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Union", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyUnion { union_: Union, diff --git a/src/expr/unnest.rs b/src/expr/unnest.rs index 9b7afb778..880d0a279 100644 --- a/src/expr/unnest.rs +++ b/src/expr/unnest.rs @@ -25,7 +25,13 @@ use crate::common::df_schema::PyDFSchema; use crate::expr::logical_node::LogicalNode; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "Unnest", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Unnest", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyUnnest { unnest_: Unnest, diff --git a/src/expr/unnest_expr.rs b/src/expr/unnest_expr.rs index dc6c4cb50..97feef1d1 100644 --- a/src/expr/unnest_expr.rs +++ b/src/expr/unnest_expr.rs @@ -22,7 +22,13 @@ use pyo3::prelude::*; use super::PyExpr; -#[pyclass(frozen, name = "UnnestExpr", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "UnnestExpr", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyUnnestExpr { unnest: Unnest, diff --git a/src/expr/values.rs b/src/expr/values.rs index d89d4f242..d40b0e7cf 100644 --- a/src/expr/values.rs +++ b/src/expr/values.rs @@ -26,7 +26,13 @@ use super::logical_node::LogicalNode; use crate::common::df_schema::PyDFSchema; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "Values", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Values", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyValues { values: Values, diff --git a/src/expr/window.rs b/src/expr/window.rs index c6972b20e..92d909bfc 100644 --- a/src/expr/window.rs +++ b/src/expr/window.rs @@ -32,13 +32,25 @@ use crate::expr::logical_node::LogicalNode; use crate::expr::sort_expr::{PySortExpr, py_sort_expr_list}; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "WindowExpr", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "WindowExpr", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyWindowExpr { window: Window, } -#[pyclass(frozen, name = "WindowFrame", module = "datafusion.expr", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "WindowFrame", + module = "datafusion.expr", + subclass +)] #[derive(Clone)] pub struct PyWindowFrame { window_frame: WindowFrame, @@ -57,6 +69,7 @@ impl From for PyWindowFrame { } #[pyclass( + from_py_object, frozen, name = "WindowFrameBound", module = "datafusion.expr", diff --git a/src/physical_plan.rs b/src/physical_plan.rs index 0069e5e6e..8674a8b55 100644 --- a/src/physical_plan.rs +++ b/src/physical_plan.rs @@ -27,7 +27,13 @@ use pyo3::types::PyBytes; use crate::context::PySessionContext; use crate::errors::PyDataFusionResult; -#[pyclass(frozen, name = "ExecutionPlan", module = "datafusion", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "ExecutionPlan", + module = "datafusion", + subclass +)] #[derive(Debug, Clone)] pub struct PyExecutionPlan { pub plan: Arc, @@ -77,7 +83,7 @@ impl PyExecutionPlan { ctx: PySessionContext, proto_msg: Bound<'_, PyBytes>, ) -> PyDataFusionResult { - let bytes: &[u8] = proto_msg.extract()?; + let bytes: &[u8] = proto_msg.extract().map_err(Into::::into)?; let proto_plan = datafusion_proto::protobuf::PhysicalPlanNode::decode(bytes).map_err(|e| { PyRuntimeError::new_err(format!( diff --git a/src/pyarrow_util.rs b/src/pyarrow_util.rs index 2a119274f..1401a4938 100644 --- a/src/pyarrow_util.rs +++ b/src/pyarrow_util.rs @@ -26,7 +26,7 @@ use arrow::pyarrow::{FromPyArrow, ToPyArrow}; use datafusion::common::exec_err; use datafusion::scalar::ScalarValue; use pyo3::types::{PyAnyMethods, PyList}; -use pyo3::{Bound, FromPyObject, PyAny, PyResult, Python}; +use pyo3::{Borrowed, Bound, FromPyObject, PyAny, PyErr, PyResult, Python}; use crate::common::data_type::PyScalarValue; use crate::errors::PyDataFusionError; @@ -142,9 +142,11 @@ impl FromPyArrow for PyScalarValue { } } -impl<'source> FromPyObject<'source> for PyScalarValue { - fn extract_bound(value: &Bound<'source, PyAny>) -> PyResult { - Self::from_pyarrow_bound(value) +impl<'source> FromPyObject<'_, 'source> for PyScalarValue { + type Error = PyErr; + + fn extract(value: Borrowed<'_, 'source, PyAny>) -> Result { + Self::from_pyarrow_bound(&value) } } diff --git a/src/sql/logical.rs b/src/sql/logical.rs index cd2ed73d3..631aa9b09 100644 --- a/src/sql/logical.rs +++ b/src/sql/logical.rs @@ -66,7 +66,14 @@ use crate::expr::unnest::PyUnnest; use crate::expr::values::PyValues; use crate::expr::window::PyWindowExpr; -#[pyclass(frozen, name = "LogicalPlan", module = "datafusion", subclass, eq)] +#[pyclass( + from_py_object, + frozen, + name = "LogicalPlan", + module = "datafusion", + subclass, + eq +)] #[derive(Debug, Clone, PartialEq, Eq)] pub struct PyLogicalPlan { pub(crate) plan: Arc, @@ -203,7 +210,7 @@ impl PyLogicalPlan { ctx: PySessionContext, proto_msg: Bound<'_, PyBytes>, ) -> PyDataFusionResult { - let bytes: &[u8] = proto_msg.extract()?; + let bytes: &[u8] = proto_msg.extract().map_err(Into::::into)?; let proto_plan = datafusion_proto::protobuf::LogicalPlanNode::decode(bytes).map_err(|e| { PyRuntimeError::new_err(format!( diff --git a/src/store.rs b/src/store.rs index 3eae866bc..8535e83b7 100644 --- a/src/store.rs +++ b/src/store.rs @@ -36,6 +36,7 @@ pub enum StorageContexts { } #[pyclass( + from_py_object, frozen, name = "LocalFileSystem", module = "datafusion.store", @@ -66,7 +67,13 @@ impl PyLocalFileSystemContext { } } -#[pyclass(frozen, name = "MicrosoftAzure", module = "datafusion.store", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "MicrosoftAzure", + module = "datafusion.store", + subclass +)] #[derive(Debug, Clone)] pub struct PyMicrosoftAzureContext { pub inner: Arc, @@ -143,7 +150,13 @@ impl PyMicrosoftAzureContext { } } -#[pyclass(frozen, name = "GoogleCloud", module = "datafusion.store", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "GoogleCloud", + module = "datafusion.store", + subclass +)] #[derive(Debug, Clone)] pub struct PyGoogleCloudContext { pub inner: Arc, @@ -173,7 +186,13 @@ impl PyGoogleCloudContext { } } -#[pyclass(frozen, name = "AmazonS3", module = "datafusion.store", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "AmazonS3", + module = "datafusion.store", + subclass +)] #[derive(Debug, Clone)] pub struct PyAmazonS3Context { pub inner: Arc, @@ -237,7 +256,13 @@ impl PyAmazonS3Context { } } -#[pyclass(frozen, name = "Http", module = "datafusion.store", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Http", + module = "datafusion.store", + subclass +)] #[derive(Debug, Clone)] pub struct PyHttpContext { pub url: String, diff --git a/src/substrait.rs b/src/substrait.rs index 1cbf3256c..c2f112520 100644 --- a/src/substrait.rs +++ b/src/substrait.rs @@ -27,7 +27,13 @@ use crate::errors::{PyDataFusionError, PyDataFusionResult, py_datafusion_err, to use crate::sql::logical::PyLogicalPlan; use crate::utils::wait_for_future; -#[pyclass(frozen, name = "Plan", module = "datafusion.substrait", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Plan", + module = "datafusion.substrait", + subclass +)] #[derive(Debug, Clone)] pub struct PyPlan { pub plan: Plan, @@ -72,7 +78,13 @@ impl From for PyPlan { /// A PySubstraitSerializer is a representation of a Serializer that is capable of both serializing /// a `LogicalPlan` instance to Substrait Protobuf bytes and also deserialize Substrait Protobuf bytes /// to a valid `LogicalPlan` instance. -#[pyclass(frozen, name = "Serde", module = "datafusion.substrait", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Serde", + module = "datafusion.substrait", + subclass +)] #[derive(Debug, Clone)] pub struct PySubstraitSerializer; @@ -96,7 +108,7 @@ impl PySubstraitSerializer { py: Python, ) -> PyDataFusionResult { PySubstraitSerializer::serialize_bytes(sql, ctx, py).and_then(|proto_bytes| { - let proto_bytes = proto_bytes.bind(py).downcast::().unwrap(); + let proto_bytes = proto_bytes.bind(py).cast::().unwrap(); PySubstraitSerializer::deserialize_bytes(proto_bytes.as_bytes().to_vec(), py) }) } @@ -125,7 +137,13 @@ impl PySubstraitSerializer { } } -#[pyclass(frozen, name = "Producer", module = "datafusion.substrait", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Producer", + module = "datafusion.substrait", + subclass +)] #[derive(Debug, Clone)] pub struct PySubstraitProducer; @@ -142,7 +160,13 @@ impl PySubstraitProducer { } } -#[pyclass(frozen, name = "Consumer", module = "datafusion.substrait", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Consumer", + module = "datafusion.substrait", + subclass +)] #[derive(Debug, Clone)] pub struct PySubstraitConsumer; diff --git a/src/table.rs b/src/table.rs index 0683b544d..b9f30af9c 100644 --- a/src/table.rs +++ b/src/table.rs @@ -38,7 +38,13 @@ use crate::utils::table_provider_from_pycapsule; /// This struct is used as a common method for all TableProviders, /// whether they refer to an FFI provider, an internally known /// implementation, a dataset, or a dataframe view. -#[pyclass(frozen, name = "RawTable", module = "datafusion.catalog", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "RawTable", + module = "datafusion.catalog", + subclass +)] #[derive(Clone)] pub struct PyTable { pub table: Arc, @@ -68,7 +74,7 @@ impl PyTable { Ok(py_table) } else if let Ok(py_table) = obj .getattr("_inner") - .and_then(|inner| inner.extract::()) + .and_then(|inner| inner.extract::().map_err(Into::::into)) { Ok(py_table) } else if let Ok(py_df) = obj.extract::() { @@ -76,7 +82,7 @@ impl PyTable { Ok(PyTable::from(provider)) } else if let Ok(py_df) = obj .getattr("df") - .and_then(|inner| inner.extract::()) + .and_then(|inner| inner.extract::().map_err(Into::::into)) { let provider = py_df.inner_df().as_ref().clone().into_view(); Ok(PyTable::from(provider)) diff --git a/src/udaf.rs b/src/udaf.rs index cc166035d..7ba499c66 100644 --- a/src/udaf.rs +++ b/src/udaf.rs @@ -15,6 +15,7 @@ // specific language governing permissions and limitations // under the License. +use std::ptr::NonNull; use std::sync::Arc; use datafusion::arrow::array::ArrayRef; @@ -26,6 +27,7 @@ use datafusion::logical_expr::{ Accumulator, AccumulatorFactoryFunction, AggregateUDF, AggregateUDFImpl, create_udaf, }; use datafusion_ffi::udaf::FFI_AggregateUDF; +use pyo3::ffi::c_str; use pyo3::prelude::*; use pyo3::types::{PyCapsule, PyTuple}; @@ -157,14 +159,23 @@ pub fn to_rust_accumulator(accum: Py) -> AccumulatorFactoryFunction { fn aggregate_udf_from_capsule(capsule: &Bound<'_, PyCapsule>) -> PyDataFusionResult { validate_pycapsule(capsule, "datafusion_aggregate_udf")?; - let udaf = unsafe { capsule.reference::() }; + let data: NonNull = capsule + .pointer_checked(Some(c_str!("datafusion_aggregate_udf")))? + .cast(); + let udaf = unsafe { data.as_ref() }; let udaf: Arc = udaf.into(); Ok(AggregateUDF::new_from_shared_impl(udaf)) } /// Represents an AggregateUDF -#[pyclass(frozen, name = "AggregateUDF", module = "datafusion", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "AggregateUDF", + module = "datafusion", + subclass +)] #[derive(Debug, Clone)] pub struct PyAggregateUDF { pub(crate) function: AggregateUDF, @@ -196,14 +207,14 @@ impl PyAggregateUDF { #[staticmethod] pub fn from_pycapsule(func: Bound<'_, PyAny>) -> PyDataFusionResult { if func.is_instance_of::() { - let capsule = func.downcast::().map_err(py_datafusion_err)?; + let capsule = func.cast::().map_err(py_datafusion_err)?; let function = aggregate_udf_from_capsule(capsule)?; return Ok(Self { function }); } if func.hasattr("__datafusion_aggregate_udf__")? { let capsule = func.getattr("__datafusion_aggregate_udf__")?.call0()?; - let capsule = capsule.downcast::().map_err(py_datafusion_err)?; + let capsule = capsule.cast::().map_err(py_datafusion_err)?; let function = aggregate_udf_from_capsule(capsule)?; return Ok(Self { function }); } diff --git a/src/udf.rs b/src/udf.rs index e1f8291d6..2d60abc09 100644 --- a/src/udf.rs +++ b/src/udf.rs @@ -17,6 +17,7 @@ use std::any::Any; use std::hash::{Hash, Hasher}; +use std::ptr::NonNull; use std::sync::Arc; use arrow::datatypes::{Field, FieldRef}; @@ -31,6 +32,7 @@ use datafusion::logical_expr::{ Volatility, }; use datafusion_ffi::udf::FFI_ScalarUDF; +use pyo3::ffi::c_str; use pyo3::prelude::*; use pyo3::types::{PyCapsule, PyTuple}; @@ -153,7 +155,13 @@ impl ScalarUDFImpl for PythonFunctionScalarUDF { } /// Represents a PyScalarUDF -#[pyclass(frozen, name = "ScalarUDF", module = "datafusion", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "ScalarUDF", + module = "datafusion", + subclass +)] #[derive(Debug, Clone)] pub struct PyScalarUDF { pub(crate) function: ScalarUDF, @@ -186,10 +194,13 @@ impl PyScalarUDF { pub fn from_pycapsule(func: Bound<'_, PyAny>) -> PyDataFusionResult { if func.hasattr("__datafusion_scalar_udf__")? { let capsule = func.getattr("__datafusion_scalar_udf__")?.call0()?; - let capsule = capsule.downcast::().map_err(py_datafusion_err)?; + let capsule = capsule.cast::().map_err(py_datafusion_err)?; validate_pycapsule(capsule, "datafusion_scalar_udf")?; - let udf = unsafe { capsule.reference::() }; + let data: NonNull = capsule + .pointer_checked(Some(c_str!("datafusion_scalar_udf")))? + .cast(); + let udf = unsafe { data.as_ref() }; let udf: Arc = udf.into(); Ok(Self { diff --git a/src/udtf.rs b/src/udtf.rs index 24cf25824..24df93e2b 100644 --- a/src/udtf.rs +++ b/src/udtf.rs @@ -15,6 +15,7 @@ // specific language governing permissions and limitations // under the License. +use std::ptr::NonNull; use std::sync::Arc; use datafusion::catalog::{TableFunctionImpl, TableProvider}; @@ -23,6 +24,7 @@ use datafusion::logical_expr::Expr; use datafusion_ffi::udtf::FFI_TableFunction; use pyo3::IntoPyObjectExt; use pyo3::exceptions::{PyImportError, PyTypeError}; +use pyo3::ffi::c_str; use pyo3::prelude::*; use pyo3::types::{PyCapsule, PyTuple, PyType}; @@ -33,7 +35,7 @@ use crate::table::PyTable; use crate::utils::validate_pycapsule; /// Represents a user defined table function -#[pyclass(frozen, name = "TableFunction", module = "datafusion")] +#[pyclass(from_py_object, frozen, name = "TableFunction", module = "datafusion")] #[derive(Debug, Clone)] pub struct PyTableFunction { pub(crate) name: String, @@ -71,10 +73,13 @@ impl PyTableFunction { err } })?; - let capsule = capsule.downcast::().map_err(py_datafusion_err)?; + let capsule = capsule.cast::().map_err(py_datafusion_err)?; validate_pycapsule(capsule, "datafusion_table_function")?; - let ffi_func = unsafe { capsule.reference::() }; + let data: NonNull = capsule + .pointer_checked(Some(c_str!("datafusion_table_function")))? + .cast(); + let ffi_func = unsafe { data.as_ref() }; let foreign_func: Arc = ffi_func.to_owned().into(); PyTableFunctionInner::FFIFunction(foreign_func) diff --git a/src/udwf.rs b/src/udwf.rs index 4bf55a850..de63e2f9a 100644 --- a/src/udwf.rs +++ b/src/udwf.rs @@ -17,6 +17,7 @@ use std::any::Any; use std::ops::Range; +use std::ptr::NonNull; use std::sync::Arc; use arrow::array::{Array, ArrayData, ArrayRef, make_array}; @@ -32,6 +33,7 @@ use datafusion::logical_expr::{ use datafusion::scalar::ScalarValue; use datafusion_ffi::udwf::FFI_WindowUDF; use pyo3::exceptions::PyValueError; +use pyo3::ffi::c_str; use pyo3::prelude::*; use pyo3::types::{PyCapsule, PyList, PyTuple}; @@ -209,7 +211,13 @@ pub fn to_rust_partition_evaluator(evaluator: Py) -> PartitionEvaluatorFa } /// Represents an WindowUDF -#[pyclass(frozen, name = "WindowUDF", module = "datafusion", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "WindowUDF", + module = "datafusion", + subclass +)] #[derive(Debug, Clone)] pub struct PyWindowUDF { pub(crate) function: WindowUDF, @@ -254,10 +262,13 @@ impl PyWindowUDF { func }; - let capsule = capsule.downcast::().map_err(py_datafusion_err)?; + let capsule = capsule.cast::().map_err(py_datafusion_err)?; validate_pycapsule(capsule, "datafusion_window_udf")?; - let udwf = unsafe { capsule.reference::() }; + let data: NonNull = capsule + .pointer_checked(Some(c_str!("datafusion_window_udf")))? + .cast(); + let udwf = unsafe { data.as_ref() }; let udwf: Arc = udwf.into(); Ok(Self { diff --git a/src/unparser/dialect.rs b/src/unparser/dialect.rs index 5df0a0c2e..52a2da00b 100644 --- a/src/unparser/dialect.rs +++ b/src/unparser/dialect.rs @@ -22,7 +22,13 @@ use datafusion::sql::unparser::dialect::{ }; use pyo3::prelude::*; -#[pyclass(frozen, name = "Dialect", module = "datafusion.unparser", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Dialect", + module = "datafusion.unparser", + subclass +)] #[derive(Clone)] pub struct PyDialect { pub dialect: Arc, diff --git a/src/unparser/mod.rs b/src/unparser/mod.rs index 203e334c3..5142b918e 100644 --- a/src/unparser/mod.rs +++ b/src/unparser/mod.rs @@ -27,7 +27,13 @@ use pyo3::prelude::*; use crate::sql::logical::PyLogicalPlan; -#[pyclass(frozen, name = "Unparser", module = "datafusion.unparser", subclass)] +#[pyclass( + from_py_object, + frozen, + name = "Unparser", + module = "datafusion.unparser", + subclass +)] #[derive(Clone)] pub struct PyUnparser { dialect: Arc, diff --git a/src/utils.rs b/src/utils.rs index 28b58ba0f..5085018f7 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -16,6 +16,7 @@ // under the License. use std::future::Future; +use std::ptr::NonNull; use std::sync::{Arc, OnceLock}; use std::time::Duration; @@ -26,6 +27,7 @@ use datafusion_ffi::proto::logical_extension_codec::FFI_LogicalExtensionCodec; use datafusion_ffi::table_provider::FFI_TableProvider; use pyo3::IntoPyObjectExt; use pyo3::exceptions::{PyImportError, PyTypeError, PyValueError}; +use pyo3::ffi::c_str; use pyo3::prelude::*; use pyo3::types::{PyCapsule, PyType}; use tokio::runtime::Runtime; @@ -158,7 +160,7 @@ pub(crate) fn validate_pycapsule(capsule: &Bound, name: &str) -> PyRe ))); } - let capsule_name = capsule_name.unwrap().to_str()?; + let capsule_name = unsafe { capsule_name.unwrap().as_cstr().to_str()? }; if capsule_name != name { return Err(PyValueError::new_err(format!( "Expected name '{name}' in PyCapsule, instead got '{capsule_name}'" @@ -185,10 +187,13 @@ pub(crate) fn table_provider_from_pycapsule<'py>( })?; } - if let Ok(capsule) = obj.downcast::().map_err(py_datafusion_err) { + if let Ok(capsule) = obj.cast::().map_err(py_datafusion_err) { validate_pycapsule(capsule, "datafusion_table_provider")?; - let provider = unsafe { capsule.reference::() }; + let data: NonNull = capsule + .pointer_checked(Some(c_str!("datafusion_table_provider")))? + .cast(); + let provider = unsafe { data.as_ref() }; let provider: Arc = provider.into(); Ok(Some(provider)) @@ -211,11 +216,14 @@ pub(crate) fn extract_logical_extension_codec( } else { obj }; - let capsule = capsule.downcast::().map_err(py_datafusion_err)?; + let capsule = capsule.cast::().map_err(py_datafusion_err)?; validate_pycapsule(capsule, "datafusion_logical_extension_codec")?; - let codec = unsafe { capsule.reference::() }; + let data: NonNull = capsule + .pointer_checked(Some(c_str!("datafusion_logical_extension_codec")))? + .cast(); + let codec = unsafe { data.as_ref() }; Ok(Arc::new(codec.clone())) } diff --git a/uv.lock b/uv.lock index 743611005..f4926521b 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14'", @@ -11,9 +12,71 @@ resolution-markers = [ name = "alabaster" version = "1.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210 } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929 }, + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + +[[package]] +name = "arro3-core" +version = "0.6.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/01/f06342d2eb822153f63d188153e41fbeabb29b48247f7a11ce76c538f7d1/arro3_core-0.6.5.tar.gz", hash = "sha256:768078887cd7ac82de4736f94bbd91f6d660f10779848bd5b019f511badd9d75", size = 107522, upload-time = "2025-10-13T23:12:38.872Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/8a/24b35cf01a68621f5f07e3191ca96f70a145022ca367347266901eb504a7/arro3_core-0.6.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:da193dc2fb8c2005d0b3887b09d1a90d42cec1f59f17a8a1a5791f0de90946ae", size = 2678116, upload-time = "2025-10-13T23:09:04.198Z" }, + { url = "https://files.pythonhosted.org/packages/5a/7a/4398bb0582fb22d575f256f2b9ac7be735c765222cc61fb214d606bdb77c/arro3_core-0.6.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed1a760ec39fe19c65e98f45515582408002d0212df5db227a5959ffeb07ad4a", size = 2383214, upload-time = "2025-10-13T23:09:06.841Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/a321501c5da4bf3ff7438c3e5eb6e63bcecb5630c0f4a89a017cbfa8e4a0/arro3_core-0.6.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6584a3d28007740afcef1e301332876e2b785bd8edd59a458a6bc9b051bce052", size = 2883536, upload-time = "2025-10-13T23:09:08.877Z" }, + { url = "https://files.pythonhosted.org/packages/0d/50/1d1e55b9a8c4cf2fdeb954947aa135010554a3333b709e8cad3d5d084be2/arro3_core-0.6.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8e0af4789618f02bead4a0cd4d0a54abd9c8aa4fcedf9872b4891d2e3e984161", size = 2908828, upload-time = "2025-10-13T23:09:10.958Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/b4b1de1ccb17890bada9a3f4131cf3137f145d5d10490db51de6b8799926/arro3_core-0.6.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c73f212e549e9b6d11cfe3f14bbf3fba9d0891426afb5916688d16d0df724085", size = 3145458, upload-time = "2025-10-13T23:09:13.275Z" }, + { url = "https://files.pythonhosted.org/packages/08/4f/f42ce1840490fd0863bfbc56f28eaaec3bcb4eb322079af9c070111657e5/arro3_core-0.6.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f88f62e4e276a9e84f250722d2e5ffc078af9a3f67ac691f572a0e05dd6095", size = 2775793, upload-time = "2025-10-13T23:09:15.342Z" }, + { url = "https://files.pythonhosted.org/packages/2b/aa/9637efc8d8733c34bedef44e5b2c170dea14d15ab56b3566d8d7963c2616/arro3_core-0.6.5-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:b2635e4c227f25ff8784dc8efb38cb7c1674646cfdc68ded53f2426289885f0e", size = 2516697, upload-time = "2025-10-13T23:09:17.584Z" }, + { url = "https://files.pythonhosted.org/packages/60/84/1fcfadf956bc25eb5251b1ea7a7099f05198a55764635d2fc9ceafdbdbd1/arro3_core-0.6.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a5f3e936686bcd8542fafc94c68fdb23ec42d1d51a4777967ae815c90aff7296", size = 3023625, upload-time = "2025-10-13T23:09:21.556Z" }, + { url = "https://files.pythonhosted.org/packages/58/d0/52d0cb3c0dfa8e94ba2118b7e91a70da76d6ede9de4e70374f831f38cfdf/arro3_core-0.6.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:705c32fec03dadc08f807d69ce557882005d43eb20ec62699f7036340f0d580f", size = 2701346, upload-time = "2025-10-13T23:09:25.031Z" }, + { url = "https://files.pythonhosted.org/packages/69/bf/42a6f6501805c31cb65d8a6e3379eeec4fa6c26dc07c9ce894f363ccad1c/arro3_core-0.6.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:56d8166235a4c54e4f7ba082ec76890c820fa8c1b6c995ec59cead62a9698e59", size = 3153207, upload-time = "2025-10-13T23:09:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e5/41fdee468b33759b42958347c2d70b0461bf8f70ba1762a94cdf2e9b0142/arro3_core-0.6.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1ba43ba9081c00767083195222b6be74913de668296f55599658c4b0bb7cd327", size = 3105033, upload-time = "2025-10-13T23:09:31.545Z" }, + { url = "https://files.pythonhosted.org/packages/03/e0/b6d733b4540c05bac546162e045b547031f4d88c67b7c864929d9bce29ad/arro3_core-0.6.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4f5df13c6742e3f0b494cfe9025dccdc8426a74cc9e3e5a1239311e07a4b24e0", size = 2954793, upload-time = "2025-10-13T23:09:34.988Z" }, + { url = "https://files.pythonhosted.org/packages/c0/34/8353ba79c8d0498eaacc077d58b384ef785e0b69c9cbff7c2580136b8fe3/arro3_core-0.6.5-cp310-cp310-win_amd64.whl", hash = "sha256:34676b728178236df63c9ea10b21432392d4b5bb51e2030e77c68eed4dede2ad", size = 2837495, upload-time = "2025-10-13T23:09:38.539Z" }, + { url = "https://files.pythonhosted.org/packages/78/85/20e46d3ed59d2f93be4a4d1abea4f6bef3e96acd59bf5a50726f84303c51/arro3_core-0.6.5-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:9d5999506daec1ab31096b3deb1e3573041d6ecadb4ca99c96f7ab26720c592c", size = 2685615, upload-time = "2025-10-13T23:09:41.793Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/427d578f7d2bf3149515a8b75217e7189e7b1d74e5c5609e1a7e7f0f8d3c/arro3_core-0.6.5-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:bd3e251184c2dd6ade81c5613256b6d85ab3ddbd5af838b1de657e0ddec017f8", size = 2391944, upload-time = "2025-10-13T23:09:45.266Z" }, + { url = "https://files.pythonhosted.org/packages/90/24/7e4af478eb889bfa401e1c1b8868048ca692e6205affbf81cf3666347852/arro3_core-0.6.5-cp311-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cadb29349960d3821b0515d9df80f2725cea155ad966c699f6084de32e313cb", size = 2888376, upload-time = "2025-10-13T23:09:48.737Z" }, + { url = "https://files.pythonhosted.org/packages/70/3b/01006a96bc980275aa4d2eb759c5f10afb7c85fcdce3c36ddb18635ad23b/arro3_core-0.6.5-cp311-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a922e560ed2ccee3293d51b39e013b51cc233895d25ddafcacfb83c540a19e6f", size = 2916568, upload-time = "2025-10-13T23:09:51.95Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/4e04c7f5687de6fb6f88aa7590b16bcf507ba17ddbd268525f27b70b7a68/arro3_core-0.6.5-cp311-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68fe6672bf51f039b12046a209cba0a9405e10ae44e5a0d557f091b356a62051", size = 3144223, upload-time = "2025-10-13T23:09:55.387Z" }, + { url = "https://files.pythonhosted.org/packages/31/4a/72dc383d1a0d14f1d453e334e3461e229762edb1bf3f75b3ab977e9386ed/arro3_core-0.6.5-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c3ee95603e375401a58ff763ce2c8aa858e0c4f757c1fb719f48fb070f540b2", size = 2781862, upload-time = "2025-10-13T23:09:59.035Z" }, + { url = "https://files.pythonhosted.org/packages/14/dc/0df7684b683114eaf8e57989b4230edb359cbfb6e98b8770d69128b27572/arro3_core-0.6.5-cp311-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:fbaf6b65213630007b798b565e0701c2092a330deeba16bd3d896d401f7e9f28", size = 2522442, upload-time = "2025-10-13T23:10:02.134Z" }, + { url = "https://files.pythonhosted.org/packages/c9/04/75f8627cd7fe4d103eca51760d50269cfbc0bf6beaf83a3cdefb4ebd37c7/arro3_core-0.6.5-cp311-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:20679f874558bb2113e96325522625ec64a72687000b7a9578031a4d082c6ef5", size = 3033454, upload-time = "2025-10-13T23:10:05.192Z" }, + { url = "https://files.pythonhosted.org/packages/ea/19/f2d54985da65bf6d3da76218bee56383285035541c8d0cadb53095845b3e/arro3_core-0.6.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d82d6ec32d5c7c73057fb9c528390289fd5bc94b8d8f28fca9c56fc8e41c412c", size = 2705984, upload-time = "2025-10-13T23:10:08.518Z" }, + { url = "https://files.pythonhosted.org/packages/6c/53/b1d7742d6db7b4aa44d3785956955d651b3ac36db321625fd15466be1aca/arro3_core-0.6.5-cp311-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:4cba4db0a4203a3ccf131c3fb7804d77f0740d6165ec9efa3aa3acbca87c43a3", size = 3157472, upload-time = "2025-10-13T23:10:11.976Z" }, + { url = "https://files.pythonhosted.org/packages/05/31/68711327dbdd480aed54158fc1c46ab245e860ab0286e0916ce788f9889e/arro3_core-0.6.5-cp311-abi3-musllinux_1_2_i686.whl", hash = "sha256:e358affc4a0fe5c1b5dccf4f92c43a836aaa4c4eab0906c83b00b60275de3b6d", size = 3117099, upload-time = "2025-10-13T23:10:15.374Z" }, + { url = "https://files.pythonhosted.org/packages/31/e3/15ffca0797d9500b23759ae4477cf052fde8dd47a3890f4e4e1d04639016/arro3_core-0.6.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:324e43f07b7681846d00a8995b78bdc4b4a719047aa0d34426b462b8f208ee98", size = 2963677, upload-time = "2025-10-13T23:10:18.828Z" }, + { url = "https://files.pythonhosted.org/packages/bc/02/69e60dbe3bbe2bfc8b6dfa4f4bfcb8d1dd240a137bf2a5f7bcc84703f05c/arro3_core-0.6.5-cp311-abi3-win_amd64.whl", hash = "sha256:285f802c8a42fe29ecb84584d1700bc4c4f974552b75f805e1f4362d28b97080", size = 2850445, upload-time = "2025-10-13T23:10:22.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/2e5b091f6b5cffb6489dbe7ed353841568dde8ac4d1232c77321da1d0925/arro3_core-0.6.5-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:8c20e69c3b3411fd6ed56091f388e699072651e880e682be5bd14f3a392ed3e8", size = 2671985, upload-time = "2025-10-13T23:10:25.515Z" }, + { url = "https://files.pythonhosted.org/packages/30/74/764ac4b58fef3fdfc655416c42349206156db5c687fa24a0674acaeaadbb/arro3_core-0.6.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:92211f1d03221ff74d0b535a576b39601083d8e98e9d47228314573f9d4f9ae2", size = 2382931, upload-time = "2025-10-13T23:10:29.893Z" }, + { url = "https://files.pythonhosted.org/packages/6a/07/bd8c92e218240ae8a30150a5d7a2dab359b452ab54a8bb7b90effe806e3d/arro3_core-0.6.5-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:280d933b75f2649779d76e32a07f91d2352a952f2c97ddf7b320e267f440cd42", size = 2879900, upload-time = "2025-10-13T23:10:33.238Z" }, + { url = "https://files.pythonhosted.org/packages/0f/d4/253725019fe2ae5f5fde87928118ffa568cc59f07b2d6a0e90620938c537/arro3_core-0.6.5-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfc3f6b93b924f43fb7985b06202343c30b43da6bd5055ba8b84eda431e494d4", size = 2904149, upload-time = "2025-10-13T23:10:36.547Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b0/7a3dea641ac8de041c1a34859a2f2a82d3cdf3c3360872101c1d198a1e24/arro3_core-0.6.5-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5963635eb698ebc7da689e641f68b3998864bab894cf0ca84bd058b8c60d97f", size = 3143477, upload-time = "2025-10-13T23:10:40.232Z" }, + { url = "https://files.pythonhosted.org/packages/a7/05/1a50575be33fe9240898a1b5a8574658a905b5675865285585e070dcf7e2/arro3_core-0.6.5-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac291b3e74b57e56e03373d57530540cbbbfd92e4219fe2778ea531006673fe9", size = 2776522, upload-time = "2025-10-13T23:10:43.413Z" }, + { url = "https://files.pythonhosted.org/packages/2e/bd/e7b03207e7906e94e327cd4190fdb2d26ae52bc4ee1edeb057fed760796b/arro3_core-0.6.5-cp313-cp313t-manylinux_2_24_aarch64.whl", hash = "sha256:5d3f4cc58a654037d61f61ba230419da2c8f88a0ac82b9d41fe307f7cf9fda97", size = 2515426, upload-time = "2025-10-13T23:10:46.926Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ed/82d1febd5c104eccdfb82434e3619125c328c36da143e19dfa3c86de4a81/arro3_core-0.6.5-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:93cddac90238d64451f5e66c630ded89d0b5fd6d2c099bf3a5151dde2c1ddf1d", size = 3024759, upload-time = "2025-10-13T23:10:50.281Z" }, + { url = "https://files.pythonhosted.org/packages/da/cd/00e06907e42e404c21eb08282dee94ac7a1961facfa9a96d116829031721/arro3_core-0.6.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1fa7ac10db5846c33f4e8b66a6eaa705d84998e38575a835acac9a6a6649933d", size = 2700191, upload-time = "2025-10-13T23:10:53.776Z" }, + { url = "https://files.pythonhosted.org/packages/a3/11/a4bb9a900f456a6905d481bd2289f7a2371dcde024de56779621fd6a92c3/arro3_core-0.6.5-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:ca69f698a065cdbf845d59d412bc204e8f8af12f93737d82e6a18f3cff812349", size = 3149963, upload-time = "2025-10-13T23:10:57.163Z" }, + { url = "https://files.pythonhosted.org/packages/28/8a/79c76ad88b16f2fac25684f7313593738f353355eb1af2307e43efd7b1ca/arro3_core-0.6.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:de74a2512e2e2366d4b064c498c38672bf6ddea38acec8b1999b4e66182dd001", size = 3104663, upload-time = "2025-10-13T23:11:00.582Z" }, + { url = "https://files.pythonhosted.org/packages/20/66/9152feaa87f851a37c1a2bd74fb89d7e82e4c76447ee590bf8e6fff5e9d8/arro3_core-0.6.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:806ca8e20507675b2de68b3d009f76e898cc3c3e441c834ea5220866f68aac50", size = 2956440, upload-time = "2025-10-13T23:11:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/ad/66/f4179ef64d5c18fe76ec93cfbff42c0f401438ef771c6766b880044d7e13/arro3_core-0.6.5-cp313-cp313t-win_amd64.whl", hash = "sha256:8f6f0cc78877ade7ad6e678a4671b191406547e7b407bc9637436869c017ed47", size = 2845345, upload-time = "2025-10-13T23:11:07.447Z" }, + { url = "https://files.pythonhosted.org/packages/10/ca/b2139dbb25f9fefb9b1cdce8a73785615de6763af6a16bf6ff96a3b630f2/arro3_core-0.6.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:26d5b50139f1a96727fa1760b4d70393acf5ee0fba45346ad2d4f69824d3bdc2", size = 2676788, upload-time = "2025-10-13T23:11:56.965Z" }, + { url = "https://files.pythonhosted.org/packages/34/a1/c68dde2944f493c8ccfcb91bf6da6d27a27c3674316dd09c9560f9e6ab1a/arro3_core-0.6.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b65b3d8d7f65f2f3c36002dc467380d7a31ea771132986dddc6341c5a9dc726f", size = 2382809, upload-time = "2025-10-13T23:12:00.175Z" }, + { url = "https://files.pythonhosted.org/packages/c6/fc/2fb81d42a3cecd632deace97dc23ac74083d60d158106440c783bae4ff01/arro3_core-0.6.5-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c3442a79a757ed3fbd7793de180019ae3201f04237537c2e2e3f1e3dd99b31c", size = 2882818, upload-time = "2025-10-13T23:12:03.721Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/16f741e1d49ba5c5a893ce6f8eb0283d64bc68d6cc9e07ac62f96eaadfae/arro3_core-0.6.5-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:def7b0065a684d6f903a658d2567da47e2fcecde716e0b34eff4d899c6468c8d", size = 2907503, upload-time = "2025-10-13T23:12:07.066Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/2eb7972e0bbec0ee0ab22b0f166ec1ea74b53bd76c93a18ced434713e495/arro3_core-0.6.5-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbfe2f2d4d0d393833cd6a4bd9c15266a02307a3028f159155a1c536469c3ae7", size = 3143706, upload-time = "2025-10-13T23:12:10.492Z" }, + { url = "https://files.pythonhosted.org/packages/2d/af/b78e28842faa675e4e6c4d82e861accf21ac08bbab80a65fa80c578f80a1/arro3_core-0.6.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a191a3e4f72c34f7ace7724a94f2d90b06c804a6cbece4ae0f18d36325479cf3", size = 2775462, upload-time = "2025-10-13T23:12:14.026Z" }, + { url = "https://files.pythonhosted.org/packages/45/df/950e57e4915e0457acadaaca13c4423d5e2652e403135eb7606d5e6e5443/arro3_core-0.6.5-pp310-pypy310_pp73-manylinux_2_24_aarch64.whl", hash = "sha256:e3f6ab4c6ea96c451eff72aa6c5b9835a0ea8a9847cfe3995c88cce0c7701fb5", size = 2516212, upload-time = "2025-10-13T23:12:17.548Z" }, + { url = "https://files.pythonhosted.org/packages/07/73/821640d0827a829ed2565c2d4812080ab7fb86f0d271b462f9b37e6d946e/arro3_core-0.6.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:27df5239835330299636a02977f2cb34d5c460cc03b2ae1d6ab6a03d28051b08", size = 3023342, upload-time = "2025-10-13T23:12:21.308Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/51302d2f4d1b627dd11e2be979f2c48550b782d8d58d0378316342e284a8/arro3_core-0.6.5-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:71dce89c0e91be4cfb42591f03809235bbc374c396e08acdf93c4d85b09e40f5", size = 2700740, upload-time = "2025-10-13T23:12:24.968Z" }, + { url = "https://files.pythonhosted.org/packages/1d/e8/0c8a345a013bb64abea60b4864bacc01e43b8699b8874794baec9c8a7e76/arro3_core-0.6.5-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:d380c28f85568ed99c1686fb9d64b5a811d76d569f367cbec8ef7e58f6e2fdf9", size = 3152749, upload-time = "2025-10-13T23:12:28.393Z" }, + { url = "https://files.pythonhosted.org/packages/6a/42/003b30c4da394366d5967a5b993f7471a74182c983d8f757891b3dd5d594/arro3_core-0.6.5-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:8e359c0c4fe9992f5a863a4a31502ea58eb2f92988fc2e501850540b3eff0328", size = 3104676, upload-time = "2025-10-13T23:12:31.711Z" }, + { url = "https://files.pythonhosted.org/packages/0b/fd/4f8dac58ea17e05978bf35cb9a3e485b1ff3cdd6e2cc29deb08f54080de4/arro3_core-0.6.5-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9a58acbc61480b533aa84d735db04b1e68fc7f6807ab694d606c03b5e694d83d", size = 2954405, upload-time = "2025-10-13T23:12:35.328Z" }, ] [[package]] @@ -23,27 +86,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/80/c5/5c83c48bbf547f3dd8b587529db7cf5a265a3368b33e85e76af8ff6061d3/astroid-3.3.8.tar.gz", hash = "sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b", size = 398196 } +sdist = { url = "https://files.pythonhosted.org/packages/80/c5/5c83c48bbf547f3dd8b587529db7cf5a265a3368b33e85e76af8ff6061d3/astroid-3.3.8.tar.gz", hash = "sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b", size = 398196, upload-time = "2024-12-24T01:13:05.59Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/28/0bc8a17d6cd4cc3c79ae41b7105a2b9a327c110e5ddd37a8a27b29a5c8a2/astroid-3.3.8-py3-none-any.whl", hash = "sha256:187ccc0c248bfbba564826c26f070494f7bc964fd286b6d9fff4420e55de828c", size = 275153 }, + { url = "https://files.pythonhosted.org/packages/07/28/0bc8a17d6cd4cc3c79ae41b7105a2b9a327c110e5ddd37a8a27b29a5c8a2/astroid-3.3.8-py3-none-any.whl", hash = "sha256:187ccc0c248bfbba564826c26f070494f7bc964fd286b6d9fff4420e55de828c", size = 275153, upload-time = "2024-12-24T01:13:02.726Z" }, ] [[package]] name = "asttokens" version = "3.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978 } +sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978, upload-time = "2024-11-30T04:30:14.439Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918 }, + { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918, upload-time = "2024-11-30T04:30:10.946Z" }, ] [[package]] name = "babel" version = "2.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } +sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104, upload-time = "2024-08-08T14:25:45.459Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, + { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599, upload-time = "2024-08-08T14:25:42.686Z" }, ] [[package]] @@ -53,18 +116,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/ca/824b1195773ce6166d388573fc106ce56d4a805bd7427b624e063596ec58/beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", size = 581181 } +sdist = { url = "https://files.pythonhosted.org/packages/b3/ca/824b1195773ce6166d388573fc106ce56d4a805bd7427b624e063596ec58/beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", size = 581181, upload-time = "2024-01-17T16:53:17.902Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/fe/e8c672695b37eecc5cbf43e1d0638d88d66ba3a44c4d321c796f4e59167f/beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed", size = 147925 }, + { url = "https://files.pythonhosted.org/packages/b1/fe/e8c672695b37eecc5cbf43e1d0638d88d66ba3a44c4d321c796f4e59167f/beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed", size = 147925, upload-time = "2024-01-17T16:53:12.779Z" }, ] [[package]] name = "certifi" version = "2024.12.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 } +sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010, upload-time = "2024-12-14T13:52:38.02Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 }, + { url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927, upload-time = "2024-12-14T13:52:36.114Z" }, ] [[package]] @@ -74,142 +137,142 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 }, - { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 }, - { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, - { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, - { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, - { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, - { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, - { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, - { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, - { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, - { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, - { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, - { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, - { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, - { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, - { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, - { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, - { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, - { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, - { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, - { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, - { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, - { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, - { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, - { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, - { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, - { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, - { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, - { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, - { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, - { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, - { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, - { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, - { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, - { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, - { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, - { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, - { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, - { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, - { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, - { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, - { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, - { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, - { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, - { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, - { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, ] [[package]] name = "cfgv" version = "3.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 }, - { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 }, - { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 }, - { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 }, - { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 }, - { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 }, - { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 }, - { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 }, - { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 }, - { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 }, - { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 }, - { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 }, - { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 }, - { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, - { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, - { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, - { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, - { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, - { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, - { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, - { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, - { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, - { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, - { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, - { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, - { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, - { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, - { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, - { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, - { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, - { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, - { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, - { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, - { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, - { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, - { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, - { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, - { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, - { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, - { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, - { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, - { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, - { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, - { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, - { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, - { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, - { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, - { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, - { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, - { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, - { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, - { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, - { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188, upload-time = "2024-12-24T18:12:35.43Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013, upload-time = "2024-12-24T18:09:43.671Z" }, + { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285, upload-time = "2024-12-24T18:09:48.113Z" }, + { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449, upload-time = "2024-12-24T18:09:50.845Z" }, + { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892, upload-time = "2024-12-24T18:09:52.078Z" }, + { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123, upload-time = "2024-12-24T18:09:54.575Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943, upload-time = "2024-12-24T18:09:57.324Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063, upload-time = "2024-12-24T18:09:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578, upload-time = "2024-12-24T18:10:02.357Z" }, + { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629, upload-time = "2024-12-24T18:10:03.678Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778, upload-time = "2024-12-24T18:10:06.197Z" }, + { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453, upload-time = "2024-12-24T18:10:08.848Z" }, + { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479, upload-time = "2024-12-24T18:10:10.044Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790, upload-time = "2024-12-24T18:10:11.323Z" }, + { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995, upload-time = "2024-12-24T18:10:12.838Z" }, + { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471, upload-time = "2024-12-24T18:10:14.101Z" }, + { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831, upload-time = "2024-12-24T18:10:15.512Z" }, + { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335, upload-time = "2024-12-24T18:10:18.369Z" }, + { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862, upload-time = "2024-12-24T18:10:19.743Z" }, + { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673, upload-time = "2024-12-24T18:10:21.139Z" }, + { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211, upload-time = "2024-12-24T18:10:22.382Z" }, + { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039, upload-time = "2024-12-24T18:10:24.802Z" }, + { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939, upload-time = "2024-12-24T18:10:26.124Z" }, + { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075, upload-time = "2024-12-24T18:10:30.027Z" }, + { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340, upload-time = "2024-12-24T18:10:32.679Z" }, + { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205, upload-time = "2024-12-24T18:10:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441, upload-time = "2024-12-24T18:10:37.574Z" }, + { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105, upload-time = "2024-12-24T18:10:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404, upload-time = "2024-12-24T18:10:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423, upload-time = "2024-12-24T18:10:45.492Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184, upload-time = "2024-12-24T18:10:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268, upload-time = "2024-12-24T18:10:50.589Z" }, + { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601, upload-time = "2024-12-24T18:10:52.541Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098, upload-time = "2024-12-24T18:10:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520, upload-time = "2024-12-24T18:10:55.048Z" }, + { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852, upload-time = "2024-12-24T18:10:57.647Z" }, + { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488, upload-time = "2024-12-24T18:10:59.43Z" }, + { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192, upload-time = "2024-12-24T18:11:00.676Z" }, + { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550, upload-time = "2024-12-24T18:11:01.952Z" }, + { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785, upload-time = "2024-12-24T18:11:03.142Z" }, + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698, upload-time = "2024-12-24T18:11:05.834Z" }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162, upload-time = "2024-12-24T18:11:07.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263, upload-time = "2024-12-24T18:11:08.374Z" }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966, upload-time = "2024-12-24T18:11:09.831Z" }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992, upload-time = "2024-12-24T18:11:12.03Z" }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162, upload-time = "2024-12-24T18:11:13.372Z" }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972, upload-time = "2024-12-24T18:11:14.628Z" }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095, upload-time = "2024-12-24T18:11:17.672Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668, upload-time = "2024-12-24T18:11:18.989Z" }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073, upload-time = "2024-12-24T18:11:21.507Z" }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732, upload-time = "2024-12-24T18:11:22.774Z" }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391, upload-time = "2024-12-24T18:11:24.139Z" }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702, upload-time = "2024-12-24T18:11:26.535Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767, upload-time = "2024-12-24T18:12:32.852Z" }, ] [[package]] name = "codespell" version = "2.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/e0/709453393c0ea77d007d907dd436b3ee262e28b30995ea1aa36c6ffbccaf/codespell-2.4.1.tar.gz", hash = "sha256:299fcdcb09d23e81e35a671bbe746d5ad7e8385972e65dbb833a2eaac33c01e5", size = 344740 } +sdist = { url = "https://files.pythonhosted.org/packages/15/e0/709453393c0ea77d007d907dd436b3ee262e28b30995ea1aa36c6ffbccaf/codespell-2.4.1.tar.gz", hash = "sha256:299fcdcb09d23e81e35a671bbe746d5ad7e8385972e65dbb833a2eaac33c01e5", size = 344740, upload-time = "2025-01-28T18:52:39.411Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl", hash = "sha256:3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425", size = 344501 }, + { url = "https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl", hash = "sha256:3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425", size = 344501, upload-time = "2025-01-28T18:52:37.057Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] @@ -219,34 +282,34 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/91/4c/45dfa6829acffa344e3967d6006ee4ae8be57af746ae2eba1c431949b32c/cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02", size = 710657 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/09/8cc67f9b84730ad330b3b72cf867150744bf07ff113cda21a15a1c6d2c7c/cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123", size = 6541833 }, - { url = "https://files.pythonhosted.org/packages/7e/5b/3759e30a103144e29632e7cb72aec28cedc79e514b2ea8896bb17163c19b/cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092", size = 3922710 }, - { url = "https://files.pythonhosted.org/packages/5f/58/3b14bf39f1a0cfd679e753e8647ada56cddbf5acebffe7db90e184c76168/cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f", size = 4137546 }, - { url = "https://files.pythonhosted.org/packages/98/65/13d9e76ca19b0ba5603d71ac8424b5694415b348e719db277b5edc985ff5/cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb", size = 3915420 }, - { url = "https://files.pythonhosted.org/packages/b1/07/40fe09ce96b91fc9276a9ad272832ead0fddedcba87f1190372af8e3039c/cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b", size = 4154498 }, - { url = "https://files.pythonhosted.org/packages/75/ea/af65619c800ec0a7e4034207aec543acdf248d9bffba0533342d1bd435e1/cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543", size = 3932569 }, - { url = "https://files.pythonhosted.org/packages/c7/af/d1deb0c04d59612e3d5e54203159e284d3e7a6921e565bb0eeb6269bdd8a/cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e", size = 4016721 }, - { url = "https://files.pythonhosted.org/packages/bd/69/7ca326c55698d0688db867795134bdfac87136b80ef373aaa42b225d6dd5/cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e", size = 4240915 }, - { url = "https://files.pythonhosted.org/packages/ef/d4/cae11bf68c0f981e0413906c6dd03ae7fa864347ed5fac40021df1ef467c/cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053", size = 2757925 }, - { url = "https://files.pythonhosted.org/packages/64/b1/50d7739254d2002acae64eed4fc43b24ac0cc44bf0a0d388d1ca06ec5bb1/cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd", size = 3202055 }, - { url = "https://files.pythonhosted.org/packages/11/18/61e52a3d28fc1514a43b0ac291177acd1b4de00e9301aaf7ef867076ff8a/cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591", size = 6542801 }, - { url = "https://files.pythonhosted.org/packages/1a/07/5f165b6c65696ef75601b781a280fc3b33f1e0cd6aa5a92d9fb96c410e97/cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7", size = 3922613 }, - { url = "https://files.pythonhosted.org/packages/28/34/6b3ac1d80fc174812486561cf25194338151780f27e438526f9c64e16869/cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc", size = 4137925 }, - { url = "https://files.pythonhosted.org/packages/d0/c7/c656eb08fd22255d21bc3129625ed9cd5ee305f33752ef2278711b3fa98b/cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289", size = 3915417 }, - { url = "https://files.pythonhosted.org/packages/ef/82/72403624f197af0db6bac4e58153bc9ac0e6020e57234115db9596eee85d/cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7", size = 4155160 }, - { url = "https://files.pythonhosted.org/packages/a2/cd/2f3c440913d4329ade49b146d74f2e9766422e1732613f57097fea61f344/cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c", size = 3932331 }, - { url = "https://files.pythonhosted.org/packages/7f/df/8be88797f0a1cca6e255189a57bb49237402b1880d6e8721690c5603ac23/cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64", size = 4017372 }, - { url = "https://files.pythonhosted.org/packages/af/36/5ccc376f025a834e72b8e52e18746b927f34e4520487098e283a719c205e/cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285", size = 4239657 }, - { url = "https://files.pythonhosted.org/packages/46/b0/f4f7d0d0bcfbc8dd6296c1449be326d04217c57afb8b2594f017eed95533/cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417", size = 2758672 }, - { url = "https://files.pythonhosted.org/packages/97/9b/443270b9210f13f6ef240eff73fd32e02d381e7103969dc66ce8e89ee901/cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede", size = 3202071 }, - { url = "https://files.pythonhosted.org/packages/77/d4/fea74422326388bbac0c37b7489a0fcb1681a698c3b875959430ba550daa/cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731", size = 3338857 }, - { url = "https://files.pythonhosted.org/packages/1a/aa/ba8a7467c206cb7b62f09b4168da541b5109838627f582843bbbe0235e8e/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4", size = 3850615 }, - { url = "https://files.pythonhosted.org/packages/89/fa/b160e10a64cc395d090105be14f399b94e617c879efd401188ce0fea39ee/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756", size = 4081622 }, - { url = "https://files.pythonhosted.org/packages/47/8f/20ff0656bb0cf7af26ec1d01f780c5cfbaa7666736063378c5f48558b515/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c", size = 3867546 }, - { url = "https://files.pythonhosted.org/packages/38/d9/28edf32ee2fcdca587146bcde90102a7319b2f2c690edfa627e46d586050/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa", size = 4090937 }, - { url = "https://files.pythonhosted.org/packages/cc/9d/37e5da7519de7b0b070a3fedd4230fe76d50d2a21403e0f2153d70ac4163/cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c", size = 3128774 }, +sdist = { url = "https://files.pythonhosted.org/packages/91/4c/45dfa6829acffa344e3967d6006ee4ae8be57af746ae2eba1c431949b32c/cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02", size = 710657, upload-time = "2024-11-27T18:07:10.168Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/09/8cc67f9b84730ad330b3b72cf867150744bf07ff113cda21a15a1c6d2c7c/cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123", size = 6541833, upload-time = "2024-11-27T18:05:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/7e/5b/3759e30a103144e29632e7cb72aec28cedc79e514b2ea8896bb17163c19b/cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092", size = 3922710, upload-time = "2024-11-27T18:05:58.621Z" }, + { url = "https://files.pythonhosted.org/packages/5f/58/3b14bf39f1a0cfd679e753e8647ada56cddbf5acebffe7db90e184c76168/cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f", size = 4137546, upload-time = "2024-11-27T18:06:01.062Z" }, + { url = "https://files.pythonhosted.org/packages/98/65/13d9e76ca19b0ba5603d71ac8424b5694415b348e719db277b5edc985ff5/cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb", size = 3915420, upload-time = "2024-11-27T18:06:03.487Z" }, + { url = "https://files.pythonhosted.org/packages/b1/07/40fe09ce96b91fc9276a9ad272832ead0fddedcba87f1190372af8e3039c/cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b", size = 4154498, upload-time = "2024-11-27T18:06:05.763Z" }, + { url = "https://files.pythonhosted.org/packages/75/ea/af65619c800ec0a7e4034207aec543acdf248d9bffba0533342d1bd435e1/cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543", size = 3932569, upload-time = "2024-11-27T18:06:07.489Z" }, + { url = "https://files.pythonhosted.org/packages/c7/af/d1deb0c04d59612e3d5e54203159e284d3e7a6921e565bb0eeb6269bdd8a/cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e", size = 4016721, upload-time = "2024-11-27T18:06:11.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/69/7ca326c55698d0688db867795134bdfac87136b80ef373aaa42b225d6dd5/cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e", size = 4240915, upload-time = "2024-11-27T18:06:13.515Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d4/cae11bf68c0f981e0413906c6dd03ae7fa864347ed5fac40021df1ef467c/cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053", size = 2757925, upload-time = "2024-11-27T18:06:16.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/b1/50d7739254d2002acae64eed4fc43b24ac0cc44bf0a0d388d1ca06ec5bb1/cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd", size = 3202055, upload-time = "2024-11-27T18:06:19.113Z" }, + { url = "https://files.pythonhosted.org/packages/11/18/61e52a3d28fc1514a43b0ac291177acd1b4de00e9301aaf7ef867076ff8a/cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591", size = 6542801, upload-time = "2024-11-27T18:06:21.431Z" }, + { url = "https://files.pythonhosted.org/packages/1a/07/5f165b6c65696ef75601b781a280fc3b33f1e0cd6aa5a92d9fb96c410e97/cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7", size = 3922613, upload-time = "2024-11-27T18:06:24.314Z" }, + { url = "https://files.pythonhosted.org/packages/28/34/6b3ac1d80fc174812486561cf25194338151780f27e438526f9c64e16869/cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc", size = 4137925, upload-time = "2024-11-27T18:06:27.079Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c7/c656eb08fd22255d21bc3129625ed9cd5ee305f33752ef2278711b3fa98b/cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289", size = 3915417, upload-time = "2024-11-27T18:06:28.959Z" }, + { url = "https://files.pythonhosted.org/packages/ef/82/72403624f197af0db6bac4e58153bc9ac0e6020e57234115db9596eee85d/cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7", size = 4155160, upload-time = "2024-11-27T18:06:30.866Z" }, + { url = "https://files.pythonhosted.org/packages/a2/cd/2f3c440913d4329ade49b146d74f2e9766422e1732613f57097fea61f344/cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c", size = 3932331, upload-time = "2024-11-27T18:06:33.432Z" }, + { url = "https://files.pythonhosted.org/packages/7f/df/8be88797f0a1cca6e255189a57bb49237402b1880d6e8721690c5603ac23/cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64", size = 4017372, upload-time = "2024-11-27T18:06:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/af/36/5ccc376f025a834e72b8e52e18746b927f34e4520487098e283a719c205e/cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285", size = 4239657, upload-time = "2024-11-27T18:06:41.045Z" }, + { url = "https://files.pythonhosted.org/packages/46/b0/f4f7d0d0bcfbc8dd6296c1449be326d04217c57afb8b2594f017eed95533/cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417", size = 2758672, upload-time = "2024-11-27T18:06:43.566Z" }, + { url = "https://files.pythonhosted.org/packages/97/9b/443270b9210f13f6ef240eff73fd32e02d381e7103969dc66ce8e89ee901/cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede", size = 3202071, upload-time = "2024-11-27T18:06:45.586Z" }, + { url = "https://files.pythonhosted.org/packages/77/d4/fea74422326388bbac0c37b7489a0fcb1681a698c3b875959430ba550daa/cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731", size = 3338857, upload-time = "2024-11-27T18:06:48.88Z" }, + { url = "https://files.pythonhosted.org/packages/1a/aa/ba8a7467c206cb7b62f09b4168da541b5109838627f582843bbbe0235e8e/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4", size = 3850615, upload-time = "2024-11-27T18:06:50.774Z" }, + { url = "https://files.pythonhosted.org/packages/89/fa/b160e10a64cc395d090105be14f399b94e617c879efd401188ce0fea39ee/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756", size = 4081622, upload-time = "2024-11-27T18:06:55.126Z" }, + { url = "https://files.pythonhosted.org/packages/47/8f/20ff0656bb0cf7af26ec1d01f780c5cfbaa7666736063378c5f48558b515/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c", size = 3867546, upload-time = "2024-11-27T18:06:57.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/d9/28edf32ee2fcdca587146bcde90102a7319b2f2c690edfa627e46d586050/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa", size = 4090937, upload-time = "2024-11-27T18:07:00.338Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9d/37e5da7519de7b0b070a3fedd4230fe76d50d2a21403e0f2153d70ac4163/cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c", size = 3128774, upload-time = "2024-11-27T18:07:02.157Z" }, ] [[package]] @@ -259,8 +322,10 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "arro3-core" }, { name = "codespell" }, { name = "maturin" }, + { name = "nanoarrow" }, { name = "numpy", version = "2.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "pre-commit" }, @@ -293,8 +358,10 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "arro3-core", specifier = "==0.6.5" }, { name = "codespell", specifier = "==2.4.1" }, { name = "maturin", specifier = ">=1.8.1" }, + { name = "nanoarrow", specifier = "==0.8.0" }, { name = "numpy", marker = "python_full_version < '3.14'", specifier = ">1.25.0" }, { name = "numpy", marker = "python_full_version >= '3.14'", specifier = ">=2.3.2" }, { name = "pre-commit", specifier = ">=4.3.0" }, @@ -322,9 +389,9 @@ docs = [ name = "decorator" version = "5.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/0c/8d907af351aa16b42caae42f9d6aa37b900c67308052d10fdce809f8d952/decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", size = 35016 } +sdist = { url = "https://files.pythonhosted.org/packages/66/0c/8d907af351aa16b42caae42f9d6aa37b900c67308052d10fdce809f8d952/decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", size = 35016, upload-time = "2022-01-07T08:20:05.666Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186", size = 9073 }, + { url = "https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186", size = 9073, upload-time = "2022-01-07T08:20:03.734Z" }, ] [[package]] @@ -334,90 +401,90 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744 } +sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744, upload-time = "2025-01-27T10:46:25.7Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998 }, + { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998, upload-time = "2025-01-27T10:46:09.186Z" }, ] [[package]] name = "distlib" version = "0.3.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923, upload-time = "2024-10-09T18:35:47.551Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" }, ] [[package]] name = "docutils" version = "0.21.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, ] [[package]] name = "exceptiongroup" version = "1.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883, upload-time = "2024-07-12T22:26:00.161Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload-time = "2024-07-12T22:25:58.476Z" }, ] [[package]] name = "executing" version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/e3/7d45f492c2c4a0e8e0fad57d081a7c8a0286cdd86372b070cca1ec0caa1e/executing-2.1.0.tar.gz", hash = "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab", size = 977485 } +sdist = { url = "https://files.pythonhosted.org/packages/8c/e3/7d45f492c2c4a0e8e0fad57d081a7c8a0286cdd86372b070cca1ec0caa1e/executing-2.1.0.tar.gz", hash = "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab", size = 977485, upload-time = "2024-09-01T12:37:35.708Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/fd/afcd0496feca3276f509df3dbd5dae726fcc756f1a08d9e25abe1733f962/executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf", size = 25805 }, + { url = "https://files.pythonhosted.org/packages/b5/fd/afcd0496feca3276f509df3dbd5dae726fcc756f1a08d9e25abe1733f962/executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf", size = 25805, upload-time = "2024-09-01T12:37:33.007Z" }, ] [[package]] name = "filelock" version = "3.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 }, + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, ] [[package]] name = "identify" version = "2.6.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6", size = 99254 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6", size = 99254, upload-time = "2025-05-23T20:37:53.3Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145 }, + { url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145, upload-time = "2025-05-23T20:37:51.495Z" }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] [[package]] name = "imagesize" version = "1.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026 } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769 }, + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, ] [[package]] name = "iniconfig" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" }, ] [[package]] @@ -437,9 +504,9 @@ dependencies = [ { name = "traitlets" }, { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/35/6f90fdddff7a08b7b715fccbd2427b5212c9525cd043d26fdc45bee0708d/ipython-8.31.0.tar.gz", hash = "sha256:b6a2274606bec6166405ff05e54932ed6e5cfecaca1fc05f2cacde7bb074d70b", size = 5501011 } +sdist = { url = "https://files.pythonhosted.org/packages/01/35/6f90fdddff7a08b7b715fccbd2427b5212c9525cd043d26fdc45bee0708d/ipython-8.31.0.tar.gz", hash = "sha256:b6a2274606bec6166405ff05e54932ed6e5cfecaca1fc05f2cacde7bb074d70b", size = 5501011, upload-time = "2024-12-20T12:34:22.61Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/60/d0feb6b6d9fe4ab89fe8fe5b47cbf6cd936bfd9f1e7ffa9d0015425aeed6/ipython-8.31.0-py3-none-any.whl", hash = "sha256:46ec58f8d3d076a61d128fe517a51eb730e3aaf0c184ea8c17d16e366660c6a6", size = 821583 }, + { url = "https://files.pythonhosted.org/packages/04/60/d0feb6b6d9fe4ab89fe8fe5b47cbf6cd936bfd9f1e7ffa9d0015425aeed6/ipython-8.31.0-py3-none-any.whl", hash = "sha256:46ec58f8d3d076a61d128fe517a51eb730e3aaf0c184ea8c17d16e366660c6a6", size = 821583, upload-time = "2024-12-20T12:34:17.106Z" }, ] [[package]] @@ -449,9 +516,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "parso" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287 } +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278 }, + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, ] [[package]] @@ -461,9 +528,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674 } +sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674, upload-time = "2024-12-21T18:30:22.828Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 }, + { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596, upload-time = "2024-12-21T18:30:19.133Z" }, ] [[package]] @@ -473,67 +540,67 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] [[package]] name = "markupsafe" version = "3.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, - { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, - { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, - { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, - { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, - { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, - { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, - { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, - { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, - { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, - { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, - { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, - { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, - { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, - { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, - { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, - { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, - { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, - { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, - { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, - { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, - { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, - { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, - { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, - { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, ] [[package]] @@ -543,9 +610,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159 } +sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159, upload-time = "2024-04-15T13:44:44.803Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 }, + { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899, upload-time = "2024-04-15T13:44:43.265Z" }, ] [[package]] @@ -555,20 +622,20 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/08/ccb0f917722a35ab0d758be9bb5edaf645c3a3d6170061f10d396ecd273f/maturin-1.8.1.tar.gz", hash = "sha256:49cd964aabf59f8b0a6969f9860d2cdf194ac331529caae14c884f5659568857", size = 197397 } +sdist = { url = "https://files.pythonhosted.org/packages/9a/08/ccb0f917722a35ab0d758be9bb5edaf645c3a3d6170061f10d396ecd273f/maturin-1.8.1.tar.gz", hash = "sha256:49cd964aabf59f8b0a6969f9860d2cdf194ac331529caae14c884f5659568857", size = 197397, upload-time = "2024-12-30T14:03:48.109Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/00/f34077315f34db8ad2ccf6bfe11b864ca27baab3a1320634da8e3cf89a48/maturin-1.8.1-py3-none-linux_armv6l.whl", hash = "sha256:7e590a23d9076b8a994f2e67bc63dc9a2d1c9a41b1e7b45ac354ba8275254e89", size = 7568415 }, - { url = "https://files.pythonhosted.org/packages/5c/07/9219976135ce0cb32d2fa6ea5c6d0ad709013d9a17967312e149b98153a6/maturin-1.8.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d8251a95682c83ea60988c804b620c181911cd824aa107b4a49ac5333c92968", size = 14527816 }, - { url = "https://files.pythonhosted.org/packages/e6/04/fa009a00903acdd1785d58322193140bfe358595347c39f315112dabdf9e/maturin-1.8.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b9fc1a4354cac5e32c190410208039812ea88c4a36bd2b6499268ec49ef5de00", size = 7580446 }, - { url = "https://files.pythonhosted.org/packages/9b/d4/414b2aab9bbfe88182b734d3aa1b4fef7d7701e50f6be48500378b8c8721/maturin-1.8.1-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:621e171c6b39f95f1d0df69a118416034fbd59c0f89dcaea8c2ea62019deecba", size = 7650535 }, - { url = "https://files.pythonhosted.org/packages/f0/64/879418a8a0196013ec1fb19eada0781c04a30e8d6d9227e80f91275a4f5b/maturin-1.8.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:98f638739a5132962347871b85c91f525c9246ef4d99796ae98a2031e3df029f", size = 8006702 }, - { url = "https://files.pythonhosted.org/packages/39/c2/605829324f8371294f70303aca130682df75318958efed246873d3d604ab/maturin-1.8.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:f9f5c47521924b6e515cbc652a042fe5f17f8747445be9d931048e5d8ddb50a4", size = 7368164 }, - { url = "https://files.pythonhosted.org/packages/be/6c/30e136d397bb146b94b628c0ef7f17708281611b97849e2cf37847025ac7/maturin-1.8.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:0f4407c7353c31bfbb8cdeb82bc2170e474cbfb97b5ba27568f440c9d6c1fdd4", size = 7450889 }, - { url = "https://files.pythonhosted.org/packages/1b/50/e1f5023512696d4e56096f702e2f68d6d9a30afe0a4eec82b0e27b8eb4e4/maturin-1.8.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:ec49cd70cad3c389946c6e2bc0bd50772a7fcb463040dd800720345897eec9bf", size = 9585819 }, - { url = "https://files.pythonhosted.org/packages/b7/80/b24b5248d89d2e5982553900237a337ea098ca9297b8369ca2aa95549e0f/maturin-1.8.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08767d794de8f8a11c5c8b1b47a4ff9fb6ae2d2d97679e27030f2f509c8c2a0", size = 10920801 }, - { url = "https://files.pythonhosted.org/packages/6e/f4/8ede7a662fabf93456b44390a5ad22630e25fb5ddaecf787251071b2e143/maturin-1.8.1-py3-none-win32.whl", hash = "sha256:d678407713f3e10df33c5b3d7a343ec0551eb7f14d8ad9ba6febeb96f4e4c75c", size = 6873556 }, - { url = "https://files.pythonhosted.org/packages/9c/22/757f093ed0e319e9648155b8c9d716765442bea5bc98ebc58ad4ad5b0524/maturin-1.8.1-py3-none-win_amd64.whl", hash = "sha256:a526f90fe0e5cb59ffb81f4ff547ddc42e823bbdeae4a31012c0893ca6dcaf46", size = 7823153 }, - { url = "https://files.pythonhosted.org/packages/a4/f5/051413e04f6da25069db5e76759ecdb8cd2a8ab4a94045b5a3bf548c66fa/maturin-1.8.1-py3-none-win_arm64.whl", hash = "sha256:e95f077fd2ddd2f048182880eed458c308571a534be3eb2add4d3dac55bf57f4", size = 6552131 }, + { url = "https://files.pythonhosted.org/packages/4c/00/f34077315f34db8ad2ccf6bfe11b864ca27baab3a1320634da8e3cf89a48/maturin-1.8.1-py3-none-linux_armv6l.whl", hash = "sha256:7e590a23d9076b8a994f2e67bc63dc9a2d1c9a41b1e7b45ac354ba8275254e89", size = 7568415, upload-time = "2024-12-30T14:03:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/5c/07/9219976135ce0cb32d2fa6ea5c6d0ad709013d9a17967312e149b98153a6/maturin-1.8.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d8251a95682c83ea60988c804b620c181911cd824aa107b4a49ac5333c92968", size = 14527816, upload-time = "2024-12-30T14:03:13.851Z" }, + { url = "https://files.pythonhosted.org/packages/e6/04/fa009a00903acdd1785d58322193140bfe358595347c39f315112dabdf9e/maturin-1.8.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b9fc1a4354cac5e32c190410208039812ea88c4a36bd2b6499268ec49ef5de00", size = 7580446, upload-time = "2024-12-30T14:03:17.64Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d4/414b2aab9bbfe88182b734d3aa1b4fef7d7701e50f6be48500378b8c8721/maturin-1.8.1-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:621e171c6b39f95f1d0df69a118416034fbd59c0f89dcaea8c2ea62019deecba", size = 7650535, upload-time = "2024-12-30T14:03:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/f0/64/879418a8a0196013ec1fb19eada0781c04a30e8d6d9227e80f91275a4f5b/maturin-1.8.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:98f638739a5132962347871b85c91f525c9246ef4d99796ae98a2031e3df029f", size = 8006702, upload-time = "2024-12-30T14:03:24.318Z" }, + { url = "https://files.pythonhosted.org/packages/39/c2/605829324f8371294f70303aca130682df75318958efed246873d3d604ab/maturin-1.8.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:f9f5c47521924b6e515cbc652a042fe5f17f8747445be9d931048e5d8ddb50a4", size = 7368164, upload-time = "2024-12-30T14:03:26.582Z" }, + { url = "https://files.pythonhosted.org/packages/be/6c/30e136d397bb146b94b628c0ef7f17708281611b97849e2cf37847025ac7/maturin-1.8.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:0f4407c7353c31bfbb8cdeb82bc2170e474cbfb97b5ba27568f440c9d6c1fdd4", size = 7450889, upload-time = "2024-12-30T14:03:28.893Z" }, + { url = "https://files.pythonhosted.org/packages/1b/50/e1f5023512696d4e56096f702e2f68d6d9a30afe0a4eec82b0e27b8eb4e4/maturin-1.8.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:ec49cd70cad3c389946c6e2bc0bd50772a7fcb463040dd800720345897eec9bf", size = 9585819, upload-time = "2024-12-30T14:03:31.125Z" }, + { url = "https://files.pythonhosted.org/packages/b7/80/b24b5248d89d2e5982553900237a337ea098ca9297b8369ca2aa95549e0f/maturin-1.8.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08767d794de8f8a11c5c8b1b47a4ff9fb6ae2d2d97679e27030f2f509c8c2a0", size = 10920801, upload-time = "2024-12-30T14:03:35.127Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f4/8ede7a662fabf93456b44390a5ad22630e25fb5ddaecf787251071b2e143/maturin-1.8.1-py3-none-win32.whl", hash = "sha256:d678407713f3e10df33c5b3d7a343ec0551eb7f14d8ad9ba6febeb96f4e4c75c", size = 6873556, upload-time = "2024-12-30T14:03:37.913Z" }, + { url = "https://files.pythonhosted.org/packages/9c/22/757f093ed0e319e9648155b8c9d716765442bea5bc98ebc58ad4ad5b0524/maturin-1.8.1-py3-none-win_amd64.whl", hash = "sha256:a526f90fe0e5cb59ffb81f4ff547ddc42e823bbdeae4a31012c0893ca6dcaf46", size = 7823153, upload-time = "2024-12-30T14:03:40.33Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f5/051413e04f6da25069db5e76759ecdb8cd2a8ab4a94045b5a3bf548c66fa/maturin-1.8.1-py3-none-win_arm64.whl", hash = "sha256:e95f077fd2ddd2f048182880eed458c308571a534be3eb2add4d3dac55bf57f4", size = 6552131, upload-time = "2024-12-30T14:03:45.203Z" }, ] [[package]] @@ -578,18 +645,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542 } +sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542, upload-time = "2024-09-09T20:27:49.564Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316 }, + { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316, upload-time = "2024-09-09T20:27:48.397Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] @@ -604,18 +671,88 @@ dependencies = [ { name = "pyyaml" }, { name = "sphinx" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/85/55/6d1741a1780e5e65038b74bce6689da15f620261c490c3511eb4c12bac4b/myst_parser-4.0.0.tar.gz", hash = "sha256:851c9dfb44e36e56d15d05e72f02b80da21a9e0d07cba96baf5e2d476bb91531", size = 93858 } +sdist = { url = "https://files.pythonhosted.org/packages/85/55/6d1741a1780e5e65038b74bce6689da15f620261c490c3511eb4c12bac4b/myst_parser-4.0.0.tar.gz", hash = "sha256:851c9dfb44e36e56d15d05e72f02b80da21a9e0d07cba96baf5e2d476bb91531", size = 93858, upload-time = "2024-08-05T14:02:45.798Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/b4/b036f8fdb667587bb37df29dc6644681dd78b7a2a6321a34684b79412b28/myst_parser-4.0.0-py3-none-any.whl", hash = "sha256:b9317997552424448c6096c2558872fdb6f81d3ecb3a40ce84a7518798f3f28d", size = 84563 }, + { url = "https://files.pythonhosted.org/packages/ca/b4/b036f8fdb667587bb37df29dc6644681dd78b7a2a6321a34684b79412b28/myst_parser-4.0.0-py3-none-any.whl", hash = "sha256:b9317997552424448c6096c2558872fdb6f81d3ecb3a40ce84a7518798f3f28d", size = 84563, upload-time = "2024-08-05T14:02:43.767Z" }, +] + +[[package]] +name = "nanoarrow" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/29/7b1ab53ed83fb70c80571a2487070113881b54067bda72cd87affc360ccc/nanoarrow-0.8.0.tar.gz", hash = "sha256:aa63e01e799380ec4f8adab88f4faac8d27bfb725fe1009fe73d7ce4efd9f7f6", size = 3508214, upload-time = "2026-02-10T03:36:02.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/27/7aece654f60453026fe36985291853243485ac41dfb9a69e421cdd2271fe/nanoarrow-0.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c5ea89651e49afa2674557005938963cb849d3c65f2f22ac6701c281a7e0244d", size = 834242, upload-time = "2026-02-10T03:33:40.25Z" }, + { url = "https://files.pythonhosted.org/packages/8b/63/2960ea0b1bfeec0381f01e6f7652c104683444b7c9902f42907c911630e9/nanoarrow-0.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7861371960d09adb377d05da73190103178410dc014369779734f2dbff0ac0ad", size = 741604, upload-time = "2026-02-10T03:33:29.29Z" }, + { url = "https://files.pythonhosted.org/packages/f0/6d/9de1da912da0356169836af8ccecac1664ee4d603b65b7067a27b85ebaf2/nanoarrow-0.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db62ea708c873980eeb0e711fa6162120d1e372b2404bb79ead69f9aa0560192", size = 970784, upload-time = "2026-02-10T03:32:30.293Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a9/5e62e7f1b9b41ff86d6025c57636246e1e8702b0cba322fab0272c3cc0f8/nanoarrow-0.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:178cc6d097b988d13354c6a48a873b4496c7bcedce43c55c6770186b6d1b4845", size = 1018693, upload-time = "2026-02-10T03:32:51.819Z" }, + { url = "https://files.pythonhosted.org/packages/68/4d/70eb2a672ca81d4385069eb6fc70fa6ab44a029d18df4da48e6691e6d8ba/nanoarrow-0.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5715e68cc17ccec23e1fcb9e828281bdf6afa11d78c8b0cd9a343c1ac48fb1d", size = 1145087, upload-time = "2026-02-10T03:32:52.801Z" }, + { url = "https://files.pythonhosted.org/packages/83/0e/02698dc0a4af10670822b949cdf0999134152347138d553d440b8f14f471/nanoarrow-0.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:958572c48d53f79693f30070fd4531f4d9643aa62e03ea1336ea2fc69e9e964d", size = 989528, upload-time = "2026-02-10T03:32:31.632Z" }, + { url = "https://files.pythonhosted.org/packages/e3/b4/1a5f3c10ad667ac9f0dfbde2416124025bdaf963d3915968b1ae6f5f9e85/nanoarrow-0.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dc1a1fe64c6b1177314eb4c36d9037268257d6699b052f9462a99e056703f4cb", size = 1159183, upload-time = "2026-02-10T03:32:54.157Z" }, + { url = "https://files.pythonhosted.org/packages/22/28/8c314b5f0bb5c27d1c6164fd8f90d52f02e08defc2d8880466610ecfefdc/nanoarrow-0.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:feae14c938fe2192f1bea1d0f8e87be9446703d2997bbd555c949df36eed6d32", size = 1031646, upload-time = "2026-02-10T03:32:55.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/98/3314109e7064f84e25cfc6b7d460177d92dab7eabd149a5b78c1463ad797/nanoarrow-0.8.0-cp310-cp310-win32.whl", hash = "sha256:491a8aedbbbe4dd06660d641762ad9cb9743c39b96259f7795a4ac22cc046f18", size = 566048, upload-time = "2026-02-10T03:35:53.806Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f1/602c7be675383f725382f4eed0019ba840a8354d2eb732e56e336245182f/nanoarrow-0.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:1c3b2c6ff46c9cd37350b9855829c0eed1256311e4fea0fcbc8aa9c2080b80ca", size = 658209, upload-time = "2026-02-10T03:33:50.792Z" }, + { url = "https://files.pythonhosted.org/packages/22/89/3ba932b982d26c7f38c1c54cf97dde05ad141045c106b6f1664151c22387/nanoarrow-0.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:31445b4cb891f77cb0261a0790222c9584c122f6d851e5818bc50a2678ae7bc4", size = 832763, upload-time = "2026-02-10T03:33:41.41Z" }, + { url = "https://files.pythonhosted.org/packages/91/1e/70ff64e9ecbf2744aa7527f721bed8f5e549dabbe1c02ceb6afafa651ba5/nanoarrow-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5a579bd43011d2f5cb5a9ba3a7352bd4e3783f3adedb59b93540af71949433cf", size = 739843, upload-time = "2026-02-10T03:33:30.318Z" }, + { url = "https://files.pythonhosted.org/packages/e7/06/3d88f0fb29b7343426b035f21d90d61c872b83243895e9548d880e08f60a/nanoarrow-0.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1a910eaae1c60838ea9d11d247aba88cb17c02b67430ec88569a1ae68a7bb25", size = 969064, upload-time = "2026-02-10T03:32:32.669Z" }, + { url = "https://files.pythonhosted.org/packages/a0/aa/e5655fd8d8a6defb0bed22e2de695f974a759798f10775de19f5a924156a/nanoarrow-0.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8520fe9ab310d855376e4faed742f683390bbab7b5dd230da398cb79f3deb29", size = 1018919, upload-time = "2026-02-10T03:32:56.317Z" }, + { url = "https://files.pythonhosted.org/packages/94/16/db9fedc1d916ba6f66537a992144fb08ddc2495dd5b61a4a2710e5518ec4/nanoarrow-0.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78a5cbd6f3664e552280688dcae765424587d7392577774f7cd7191f654e71ab", size = 1133563, upload-time = "2026-02-10T03:32:58.884Z" }, + { url = "https://files.pythonhosted.org/packages/6e/10/68374d91b1a55f38e4f96ef0f32ed6fd72826aeae6e3c7de45b635937244/nanoarrow-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8412b5594cef5e86f35a4a3eb05c25842c38f357926d13610b54dc1d99ffa2df", size = 991138, upload-time = "2026-02-10T03:32:34.182Z" }, + { url = "https://files.pythonhosted.org/packages/4c/eb/ec98442b8b03ce2e9c3150b6ead5c2475253c462ab2b54808be52f6596bd/nanoarrow-0.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d6adfdc1129d3351e6a64e09749c2460270a49eea46a9badff16a15f31104e59", size = 1153814, upload-time = "2026-02-10T03:33:00.013Z" }, + { url = "https://files.pythonhosted.org/packages/c6/74/a3573db8c4b1de39b2ccca439479e408d0b40fd411c501299c3836f43c95/nanoarrow-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebe2b9a7a25b3cc0f86f215e12f67bdfe8925a112ceda86c82d7633fc14fc52d", size = 1032100, upload-time = "2026-02-10T03:33:01.048Z" }, + { url = "https://files.pythonhosted.org/packages/24/29/df629c41d2246fb7d0ad5f191296e5957389774a83f8097357e3073cc0cf/nanoarrow-0.8.0-cp311-cp311-win32.whl", hash = "sha256:196b561557a26295862b181f204790c9fd308bdc78df30247b0e4c0b775b4a48", size = 563662, upload-time = "2026-02-10T03:35:56.048Z" }, + { url = "https://files.pythonhosted.org/packages/17/b8/54001df497f4fdbf7121db2d61090bf9986298a9eba4ed2cbfc9aad414f0/nanoarrow-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:2cc015aa3905c3f0b798570975297730d1428a23768805a23202bc48d0eaabcd", size = 658770, upload-time = "2026-02-10T03:33:51.924Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/02ef20b340c7f765192309b87685e56c88cda203a4effac04b5d9347626a/nanoarrow-0.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5f27749e2b5218e69b484e01f4c08007386e1333fbb110f400354bde0612799", size = 840224, upload-time = "2026-02-10T03:33:42.615Z" }, + { url = "https://files.pythonhosted.org/packages/94/84/b1b5d807483f882b7309799d96ec122daaa69890d80c2994f476d4e07c51/nanoarrow-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c84efa8efba413a1cecee7d10d9e5dfbf7651026538449c5d554c1af19932791", size = 732615, upload-time = "2026-02-10T03:33:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/51a6b437cf173728e03e16e32aee865b36f2043478f4e2688ea2187f63ad/nanoarrow-0.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0548987b4d32536768e092478e3fe8968f35f9624354e30efa622e32c5d87944", size = 955080, upload-time = "2026-02-10T03:32:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/c8/12/9fed89e0d76ad8c376fe74d12b7e1a7cbcb75ff8ebb242264a1d980f5529/nanoarrow-0.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ae9d43a358cd6f13e9569c010de36e7e3e98b7da059bdf83438d5e7ce2f77f4", size = 1009196, upload-time = "2026-02-10T03:33:02.057Z" }, + { url = "https://files.pythonhosted.org/packages/9f/1a/eb1a7036f2dbb30748eda66d479319cfe165eea6e6748c94488c484be7f4/nanoarrow-0.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc83b0b5636a3e558588c0eb6e3c32e295d0296909a08f3b4af17c81a2db8bf6", size = 1119470, upload-time = "2026-02-10T03:33:03.696Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/d2178bccb12aaeef5843c90e671faf1a6247bdb8b4d64454fc471e97eb71/nanoarrow-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:29e0783f9ff2b802cd883a41f5cc009f70efea519efcc710db7d143819d1d315", size = 979664, upload-time = "2026-02-10T03:32:37.594Z" }, + { url = "https://files.pythonhosted.org/packages/7e/34/f52319f9304659a5ed5db125b635316ce6d042767cde257fcf9c6a7f80e1/nanoarrow-0.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:20d07a0ac666e9563e132a2097de5e9fa26b4781c0f8edfbdce0da866c22faba", size = 1144197, upload-time = "2026-02-10T03:33:04.824Z" }, + { url = "https://files.pythonhosted.org/packages/76/45/3b56702078b7515ff9b74b215ea983358df11140a6c3b7056f55551828da/nanoarrow-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3706258dc89231ef27dee50a26d53ec96dba85dbafa8d6637276bd212be4bc1b", size = 1026594, upload-time = "2026-02-10T03:33:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f6/fe382bf2770a7e522f132e5310350fb0aecc3023f876d02265a7f40c7c79/nanoarrow-0.8.0-cp312-cp312-win32.whl", hash = "sha256:6ab8bd2db59388c6bd131c4d9e2649a6626ffe7434084cee6c22fdfbedfeda1b", size = 569212, upload-time = "2026-02-10T03:35:57.767Z" }, + { url = "https://files.pythonhosted.org/packages/c5/38/589e3c41490a742c639221eea655cf5d0a5972242efab8040a0c904a7dba/nanoarrow-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:22c3443ebc0b988dff06cb88d03cf9accbf85fdde905fb7d76b6e001561855a8", size = 645746, upload-time = "2026-02-10T03:33:53.147Z" }, + { url = "https://files.pythonhosted.org/packages/8e/af/b7df299b87348d396d049ef9fab6bef76d29c63288e5b54f752b97f7b3df/nanoarrow-0.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:605c7af9130599c40264d14c295dcc2a779402183c13f4189e7475b9dc52613a", size = 838886, upload-time = "2026-02-10T03:33:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/07/ec/02fd6979c35e347e6d5cf57757616a6d599d4ac6808bf0a37ca334639d07/nanoarrow-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:64d49118b5477bef8af5fba0b66ad032e1f9861f70d210c262b5393e5b62f47d", size = 730110, upload-time = "2026-02-10T03:33:32.771Z" }, + { url = "https://files.pythonhosted.org/packages/d2/04/64beb88b036a9d20d0f8be0846d9db7912c3332f3969ecd66144a4fd2021/nanoarrow-0.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1b64aa3739efbe85e775ba5733e37713521386d3014c866f9065815b7387114", size = 951234, upload-time = "2026-02-10T03:32:39.119Z" }, + { url = "https://files.pythonhosted.org/packages/53/3d/1850ef02a632fa5d65319c1155c326982896828ffbfd88c8fc44ee1a23aa/nanoarrow-0.8.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b4a363e697b3e852fd1f374840df22aaac0323fb8d0ab24a50c3ea1090b4594", size = 1005525, upload-time = "2026-02-10T03:33:07.588Z" }, + { url = "https://files.pythonhosted.org/packages/94/4b/3c671773e6dcce1784b4e42d0e5f5942fee49f6ddf7ae2567d36b3b4248e/nanoarrow-0.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:381a2a65b0bcfe36b267d506072a3a5c83b8326dfbb50dff2d7f55ac25159f69", size = 1120370, upload-time = "2026-02-10T03:33:08.715Z" }, + { url = "https://files.pythonhosted.org/packages/4a/79/bc49e7518ba9e5b377ca3670ceba5949cb3e20363ba7f091df62d84c4edd/nanoarrow-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cbca73fcb5c2c753ddac3695774e47cbed3b3bc64dba34866f3056e33a2a0ac2", size = 977504, upload-time = "2026-02-10T03:32:40.349Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/bb57665133351b042b4c25d549b21fc9bb9f56a3c5f4e5d561c41f5d705c/nanoarrow-0.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a4539c5767723cf0c0a21b401acc7d706ca7fd84302b6be514eeb5b8ee230903", size = 1141114, upload-time = "2026-02-10T03:33:10.575Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a0/2792c5e160d56b5abe782228a963ae3d7477727bf950f6b990ebcfed8f49/nanoarrow-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:be0899058f66d3b7e4e4b7cfe125e95625e109b4513a81fd9bc098abef55a381", size = 1025080, upload-time = "2026-02-10T03:33:11.661Z" }, + { url = "https://files.pythonhosted.org/packages/8e/45/5209dad8a3e4f460ca7d7d314ff34ef6426ced873655df1a469b0f91e01d/nanoarrow-0.8.0-cp313-cp313-win32.whl", hash = "sha256:7c227e1e68926b0ccde7336211dd7a11f8983098b3698ee548756bdb778b016d", size = 568315, upload-time = "2026-02-10T03:35:58.998Z" }, + { url = "https://files.pythonhosted.org/packages/d2/41/b2ad2b541b94422e4091a96192deb5c98d5a6b4c44ade37f5bd6d3efd83f/nanoarrow-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:1730cb374739459a925c590c32e07e213c9c6ddd2e12f44547e2bd70d29a7a9b", size = 644676, upload-time = "2026-02-10T03:33:54.301Z" }, + { url = "https://files.pythonhosted.org/packages/87/7a/5e2d1005f98cca18ebb289cffbb55fe0895465349affbe4cfb1321de9ad0/nanoarrow-0.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:dfa96964d2ccd762a5cb8e28eb0439b6c05b4f5090c4ca2d0207c32d8093cda5", size = 863391, upload-time = "2026-02-10T03:33:44.864Z" }, + { url = "https://files.pythonhosted.org/packages/5e/63/e45fd81a0a35bc782161801e2bec03794184504eedc7760fa79b33e333ca/nanoarrow-0.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:714b21daefe495d7cdd6dad34d3409ae42a77f4ef6bf648f4688d0abef8924c1", size = 779228, upload-time = "2026-02-10T03:33:33.9Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a0/f8173511a74b48d2c3b88f7a337faaca8c01b3255a53b065db171e63fa85/nanoarrow-0.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777298c348b268b3490504d9ba099dc6eede702bb9f337360dec6412944a187", size = 967376, upload-time = "2026-02-10T03:32:41.885Z" }, + { url = "https://files.pythonhosted.org/packages/f7/cf/4c885fb3a605a17607cfd8cc9f7b23aba19f9826c3bfe4dcf300b0a8e48c/nanoarrow-0.8.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e31ee3e3b1e94eccc4cc034f02956ecd15b4ae33ae8a1f999704871ea3b6dec", size = 1014554, upload-time = "2026-02-10T03:33:12.916Z" }, + { url = "https://files.pythonhosted.org/packages/43/07/190f7b4746b0d691dbea0f4c36c34012d916d3579af7ae83254a1d9f6f26/nanoarrow-0.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7bda72e24dd8e2abb3f445f129a422809d788db9cfbbfd247c32f5620e03128c", size = 1115168, upload-time = "2026-02-10T03:33:14.533Z" }, + { url = "https://files.pythonhosted.org/packages/8e/58/abd834fc30abcb053642e5935911be9a442c6c5d48c7c6f855c8de2f329d/nanoarrow-0.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2ee1a27a7210c8eba6ac6e8ab70b598da238348b125b53b16d9e1ae0313addc", size = 984855, upload-time = "2026-02-10T03:32:43.422Z" }, + { url = "https://files.pythonhosted.org/packages/18/62/ca4977054d7267ce3756409425b82fe1ea916871555f2512872ec8f7e0d4/nanoarrow-0.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:91466de52617b25dff7349dbf18cc612ce5ec35d09f025b37ea60be819808be8", size = 1122634, upload-time = "2026-02-10T03:33:15.699Z" }, + { url = "https://files.pythonhosted.org/packages/16/b3/75b71c46a3950b06ae3f63cb426ba92a9ebfe2aaa216845c8a4cc56b1bb7/nanoarrow-0.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4204e2b5f9cf895bcecfe432b03c346ec2bdadfda0174c8ab195acc6b4794986", size = 1022700, upload-time = "2026-02-10T03:33:16.802Z" }, + { url = "https://files.pythonhosted.org/packages/89/12/3a3337b17de7c3c3ff1bfc09a01c75d8f463e40e6850c8f5e42d4240c9a7/nanoarrow-0.8.0-cp313-cp313t-win32.whl", hash = "sha256:1fdc0c2508b53a83c9814fdcd2d4bac6d98ea989fb363e0d88d329a8cddd7d50", size = 624159, upload-time = "2026-02-10T03:36:00.135Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a8/80c9ed4718e253e7f19320fcd69ca8c7c9ed87d32848d3da97afee3d8b6b/nanoarrow-0.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b73748e0f39cd8dc1ce33eaad3215f2aff6aebb03e659c26d2a8df9277e7e509", size = 712076, upload-time = "2026-02-10T03:33:55.345Z" }, + { url = "https://files.pythonhosted.org/packages/a4/6f/167cbe632266e8e84d8965262a5e3121e073f593140701bc9be06062f8da/nanoarrow-0.8.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:51e9609efad27191e6506b9c224c90ae49a0c72f641c8094f168d4694b45a3ff", size = 775361, upload-time = "2026-02-10T03:33:47.176Z" }, + { url = "https://files.pythonhosted.org/packages/32/94/762f77b6b0fa7a6787316af297a239b59b1f36e37122b0770ff3cfe61e3d/nanoarrow-0.8.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:185726c467211592ba47933949cb62bc6e1797eefdd760a145b241c44377fba9", size = 692089, upload-time = "2026-02-10T03:33:36.46Z" }, + { url = "https://files.pythonhosted.org/packages/27/c3/75ac260a7e5cd00b72c35248897bc6f899d4e65457141160978ce6258601/nanoarrow-0.8.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c11edd20949a874afb0e50f08402ea3f5c5206d70ec7ed2c27d8064a36222038", size = 903435, upload-time = "2026-02-10T03:32:47.413Z" }, + { url = "https://files.pythonhosted.org/packages/70/ce/26d6673123afe22ad04b68ca90f800133f75c55792355959037e81ddc8a2/nanoarrow-0.8.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c2b31fdab6b5fb3d3c10f7597e16698c9d3db1bac4c645341e6e36320b78642", size = 948741, upload-time = "2026-02-10T03:33:22.331Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ad/f3b7b205ff1a2e755dcc90e7df4ede0f2a7eb6d217f2ab626ef2b00ee0e3/nanoarrow-0.8.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df0f118c7ba1036adf032d909674cb925a37ceeed83756c43d27ff9ad225b9e1", size = 1055379, upload-time = "2026-02-10T03:33:23.481Z" }, + { url = "https://files.pythonhosted.org/packages/9b/13/623183d5df76a4e3835af9e42a6d63dcc46d3d3e22d846d48b4458cf5cfb/nanoarrow-0.8.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:827b3e3f8ba81c3b2a9de72dd6cddd74afc7e4cf03aacb0b7f6f2ac06747ae88", size = 591477, upload-time = "2026-02-10T03:33:57.652Z" }, + { url = "https://files.pythonhosted.org/packages/96/97/6265c84c3c865d2fc1fd56954c60a9386e03ab9c9db11c5f2d57fafa1077/nanoarrow-0.8.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a765f955a6bfb989af1d37fa3d0c4f89c242fe12088b5e787748f995a5fa13fc", size = 775344, upload-time = "2026-02-10T03:33:48.237Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f2/daaf03224b88cb66b1a6a19da371386f875e95208a42c73b109f1d273166/nanoarrow-0.8.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b2023fa0d5d6992fd8a5a04452c559817c9805aea7391fa46291aaf381a6aa19", size = 692002, upload-time = "2026-02-10T03:33:37.991Z" }, + { url = "https://files.pythonhosted.org/packages/55/53/c058976db13e18106737a1fddf192e45022375628a38c2caaa51a9934ada/nanoarrow-0.8.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96c7d723b5615e2e9c5f7ba7b5af8d80ba90ecf9871ba005941ac80355ef556a", size = 904133, upload-time = "2026-02-10T03:32:49.155Z" }, + { url = "https://files.pythonhosted.org/packages/d5/3f/002a228af17ecba07ca9ff47628e97c73e336a72fd18ad5d78534a6497d8/nanoarrow-0.8.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c22b03d1ceca21aace2c8053ed43cac5566e69dd1660708783fe0e84dd35693e", size = 949714, upload-time = "2026-02-10T03:33:24.542Z" }, + { url = "https://files.pythonhosted.org/packages/28/5e/3bad2cfeb03d0682b93f13640ede98eb59cf15b4d868d5c9745118f59eb2/nanoarrow-0.8.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:999f906c297203b5430dc4e79e662301f5ab02a793b6fc67973ee3c0518fb936", size = 1056467, upload-time = "2026-02-10T03:33:25.617Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e5/c740ea047b5ada76175327360d0406ae283159cb1745cbcb51443d90d53b/nanoarrow-0.8.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5529abc4e75b7764ffc6d2fbabd0c676f75ca2ece71a8671c4724207cfb697", size = 591889, upload-time = "2026-02-10T03:33:58.891Z" }, ] [[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 } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, ] [[package]] @@ -627,62 +764,62 @@ resolution-markers = [ "python_full_version == '3.11.*'", "python_full_version < '3.11'", ] -sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/fdbf6a7871703df6160b5cf3dd774074b086d278172285c52c2758b76305/numpy-2.2.1.tar.gz", hash = "sha256:45681fd7128c8ad1c379f0ca0776a8b0c6583d2f69889ddac01559dfe4390918", size = 20227662 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/c4/5588367dc9f91e1a813beb77de46ea8cab13f778e1b3a0e661ab031aba44/numpy-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5edb4e4caf751c1518e6a26a83501fda79bff41cc59dac48d70e6d65d4ec4440", size = 21213214 }, - { url = "https://files.pythonhosted.org/packages/d8/8b/32dd9f08419023a4cf856c5ad0b4eba9b830da85eafdef841a104c4fc05a/numpy-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa3017c40d513ccac9621a2364f939d39e550c542eb2a894b4c8da92b38896ab", size = 14352248 }, - { url = "https://files.pythonhosted.org/packages/84/2d/0e895d02940ba6e12389f0ab5cac5afcf8dc2dc0ade4e8cad33288a721bd/numpy-2.2.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:61048b4a49b1c93fe13426e04e04fdf5a03f456616f6e98c7576144677598675", size = 5391007 }, - { url = "https://files.pythonhosted.org/packages/11/b9/7f1e64a0d46d9c2af6d17966f641fb12d5b8ea3003f31b2308f3e3b9a6aa/numpy-2.2.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:7671dc19c7019103ca44e8d94917eba8534c76133523ca8406822efdd19c9308", size = 6926174 }, - { url = "https://files.pythonhosted.org/packages/2e/8c/043fa4418bc9364e364ab7aba8ff6ef5f6b9171ade22de8fbcf0e2fa4165/numpy-2.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4250888bcb96617e00bfa28ac24850a83c9f3a16db471eca2ee1f1714df0f957", size = 14330914 }, - { url = "https://files.pythonhosted.org/packages/f7/b6/d8110985501ca8912dfc1c3bbef99d66e62d487f72e46b2337494df77364/numpy-2.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7746f235c47abc72b102d3bce9977714c2444bdfaea7888d241b4c4bb6a78bf", size = 16379607 }, - { url = "https://files.pythonhosted.org/packages/e2/57/bdca9fb8bdaa810c3a4ff2eb3231379b77f618a7c0d24be9f7070db50775/numpy-2.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:059e6a747ae84fce488c3ee397cee7e5f905fd1bda5fb18c66bc41807ff119b2", size = 15541760 }, - { url = "https://files.pythonhosted.org/packages/97/55/3b9147b3cbc3b6b1abc2a411dec5337a46c873deca0dd0bf5bef9d0579cc/numpy-2.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f62aa6ee4eb43b024b0e5a01cf65a0bb078ef8c395e8713c6e8a12a697144528", size = 18168476 }, - { url = "https://files.pythonhosted.org/packages/00/e7/7c2cde16c9b87a8e14fdd262ca7849c4681cf48c8a774505f7e6f5e3b643/numpy-2.2.1-cp310-cp310-win32.whl", hash = "sha256:48fd472630715e1c1c89bf1feab55c29098cb403cc184b4859f9c86d4fcb6a95", size = 6570985 }, - { url = "https://files.pythonhosted.org/packages/a1/a8/554b0e99fc4ac11ec481254781a10da180d0559c2ebf2c324232317349ee/numpy-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:b541032178a718c165a49638d28272b771053f628382d5e9d1c93df23ff58dbf", size = 12913384 }, - { url = "https://files.pythonhosted.org/packages/59/14/645887347124e101d983e1daf95b48dc3e136bf8525cb4257bf9eab1b768/numpy-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40f9e544c1c56ba8f1cf7686a8c9b5bb249e665d40d626a23899ba6d5d9e1484", size = 21217379 }, - { url = "https://files.pythonhosted.org/packages/9f/fd/2279000cf29f58ccfd3778cbf4670dfe3f7ce772df5e198c5abe9e88b7d7/numpy-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9b57eaa3b0cd8db52049ed0330747b0364e899e8a606a624813452b8203d5f7", size = 14388520 }, - { url = "https://files.pythonhosted.org/packages/58/b0/034eb5d5ba12d66ab658ff3455a31f20add0b78df8203c6a7451bd1bee21/numpy-2.2.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bc8a37ad5b22c08e2dbd27df2b3ef7e5c0864235805b1e718a235bcb200cf1cb", size = 5389286 }, - { url = "https://files.pythonhosted.org/packages/5d/69/6f3cccde92e82e7835fdb475c2bf439761cbf8a1daa7c07338e1e132dfec/numpy-2.2.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:9036d6365d13b6cbe8f27a0eaf73ddcc070cae584e5ff94bb45e3e9d729feab5", size = 6930345 }, - { url = "https://files.pythonhosted.org/packages/d1/72/1cd38e91ab563e67f584293fcc6aca855c9ae46dba42e6b5ff4600022899/numpy-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51faf345324db860b515d3f364eaa93d0e0551a88d6218a7d61286554d190d73", size = 14335748 }, - { url = "https://files.pythonhosted.org/packages/f2/d4/f999444e86986f3533e7151c272bd8186c55dda554284def18557e013a2a/numpy-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38efc1e56b73cc9b182fe55e56e63b044dd26a72128fd2fbd502f75555d92591", size = 16391057 }, - { url = "https://files.pythonhosted.org/packages/99/7b/85cef6a3ae1b19542b7afd97d0b296526b6ef9e3c43ea0c4d9c4404fb2d0/numpy-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:31b89fa67a8042e96715c68e071a1200c4e172f93b0fbe01a14c0ff3ff820fc8", size = 15556943 }, - { url = "https://files.pythonhosted.org/packages/69/7e/b83cc884c3508e91af78760f6b17ab46ad649831b1fa35acb3eb26d9e6d2/numpy-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4c86e2a209199ead7ee0af65e1d9992d1dce7e1f63c4b9a616500f93820658d0", size = 18180785 }, - { url = "https://files.pythonhosted.org/packages/b2/9f/eb4a9a38867de059dcd4b6e18d47c3867fbd3795d4c9557bb49278f94087/numpy-2.2.1-cp311-cp311-win32.whl", hash = "sha256:b34d87e8a3090ea626003f87f9392b3929a7bbf4104a05b6667348b6bd4bf1cd", size = 6568983 }, - { url = "https://files.pythonhosted.org/packages/6d/1e/be3b9f3073da2f8c7fa361fcdc231b548266b0781029fdbaf75eeab997fd/numpy-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:360137f8fb1b753c5cde3ac388597ad680eccbbbb3865ab65efea062c4a1fd16", size = 12917260 }, - { url = "https://files.pythonhosted.org/packages/62/12/b928871c570d4a87ab13d2cc19f8817f17e340d5481621930e76b80ffb7d/numpy-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:694f9e921a0c8f252980e85bce61ebbd07ed2b7d4fa72d0e4246f2f8aa6642ab", size = 20909861 }, - { url = "https://files.pythonhosted.org/packages/3d/c3/59df91ae1d8ad7c5e03efd63fd785dec62d96b0fe56d1f9ab600b55009af/numpy-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3683a8d166f2692664262fd4900f207791d005fb088d7fdb973cc8d663626faa", size = 14095776 }, - { url = "https://files.pythonhosted.org/packages/af/4e/8ed5868efc8e601fb69419644a280e9c482b75691466b73bfaab7d86922c/numpy-2.2.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:780077d95eafc2ccc3ced969db22377b3864e5b9a0ea5eb347cc93b3ea900315", size = 5126239 }, - { url = "https://files.pythonhosted.org/packages/1a/74/dd0bbe650d7bc0014b051f092f2de65e34a8155aabb1287698919d124d7f/numpy-2.2.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:55ba24ebe208344aa7a00e4482f65742969a039c2acfcb910bc6fcd776eb4355", size = 6659296 }, - { url = "https://files.pythonhosted.org/packages/7f/11/4ebd7a3f4a655764dc98481f97bd0a662fb340d1001be6050606be13e162/numpy-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b1d07b53b78bf84a96898c1bc139ad7f10fda7423f5fd158fd0f47ec5e01ac7", size = 14047121 }, - { url = "https://files.pythonhosted.org/packages/7f/a7/c1f1d978166eb6b98ad009503e4d93a8c1962d0eb14a885c352ee0276a54/numpy-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5062dc1a4e32a10dc2b8b13cedd58988261416e811c1dc4dbdea4f57eea61b0d", size = 16096599 }, - { url = "https://files.pythonhosted.org/packages/3d/6d/0e22afd5fcbb4d8d0091f3f46bf4e8906399c458d4293da23292c0ba5022/numpy-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fce4f615f8ca31b2e61aa0eb5865a21e14f5629515c9151850aa936c02a1ee51", size = 15243932 }, - { url = "https://files.pythonhosted.org/packages/03/39/e4e5832820131ba424092b9610d996b37e5557180f8e2d6aebb05c31ae54/numpy-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:67d4cda6fa6ffa073b08c8372aa5fa767ceb10c9a0587c707505a6d426f4e046", size = 17861032 }, - { url = "https://files.pythonhosted.org/packages/5f/8a/3794313acbf5e70df2d5c7d2aba8718676f8d054a05abe59e48417fb2981/numpy-2.2.1-cp312-cp312-win32.whl", hash = "sha256:32cb94448be47c500d2c7a95f93e2f21a01f1fd05dd2beea1ccd049bb6001cd2", size = 6274018 }, - { url = "https://files.pythonhosted.org/packages/17/c1/c31d3637f2641e25c7a19adf2ae822fdaf4ddd198b05d79a92a9ce7cb63e/numpy-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:ba5511d8f31c033a5fcbda22dd5c813630af98c70b2661f2d2c654ae3cdfcfc8", size = 12613843 }, - { url = "https://files.pythonhosted.org/packages/20/d6/91a26e671c396e0c10e327b763485ee295f5a5a7a48c553f18417e5a0ed5/numpy-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f1d09e520217618e76396377c81fba6f290d5f926f50c35f3a5f72b01a0da780", size = 20896464 }, - { url = "https://files.pythonhosted.org/packages/8c/40/5792ccccd91d45e87d9e00033abc4f6ca8a828467b193f711139ff1f1cd9/numpy-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ecc47cd7f6ea0336042be87d9e7da378e5c7e9b3c8ad0f7c966f714fc10d821", size = 14111350 }, - { url = "https://files.pythonhosted.org/packages/c0/2a/fb0a27f846cb857cef0c4c92bef89f133a3a1abb4e16bba1c4dace2e9b49/numpy-2.2.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f419290bc8968a46c4933158c91a0012b7a99bb2e465d5ef5293879742f8797e", size = 5111629 }, - { url = "https://files.pythonhosted.org/packages/eb/e5/8e81bb9d84db88b047baf4e8b681a3e48d6390bc4d4e4453eca428ecbb49/numpy-2.2.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b6c390bfaef8c45a260554888966618328d30e72173697e5cabe6b285fb2348", size = 6645865 }, - { url = "https://files.pythonhosted.org/packages/7a/1a/a90ceb191dd2f9e2897c69dde93ccc2d57dd21ce2acbd7b0333e8eea4e8d/numpy-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:526fc406ab991a340744aad7e25251dd47a6720a685fa3331e5c59fef5282a59", size = 14043508 }, - { url = "https://files.pythonhosted.org/packages/f1/5a/e572284c86a59dec0871a49cd4e5351e20b9c751399d5f1d79628c0542cb/numpy-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f74e6fdeb9a265624ec3a3918430205dff1df7e95a230779746a6af78bc615af", size = 16094100 }, - { url = "https://files.pythonhosted.org/packages/0c/2c/a79d24f364788386d85899dd280a94f30b0950be4b4a545f4fa4ed1d4ca7/numpy-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:53c09385ff0b72ba79d8715683c1168c12e0b6e84fb0372e97553d1ea91efe51", size = 15239691 }, - { url = "https://files.pythonhosted.org/packages/cf/79/1e20fd1c9ce5a932111f964b544facc5bb9bde7865f5b42f00b4a6a9192b/numpy-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f3eac17d9ec51be534685ba877b6ab5edc3ab7ec95c8f163e5d7b39859524716", size = 17856571 }, - { url = "https://files.pythonhosted.org/packages/be/5b/cc155e107f75d694f562bdc84a26cc930569f3dfdfbccb3420b626065777/numpy-2.2.1-cp313-cp313-win32.whl", hash = "sha256:9ad014faa93dbb52c80d8f4d3dcf855865c876c9660cb9bd7553843dd03a4b1e", size = 6270841 }, - { url = "https://files.pythonhosted.org/packages/44/be/0e5cd009d2162e4138d79a5afb3b5d2341f0fe4777ab6e675aa3d4a42e21/numpy-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:164a829b6aacf79ca47ba4814b130c4020b202522a93d7bff2202bfb33b61c60", size = 12606618 }, - { url = "https://files.pythonhosted.org/packages/a8/87/04ddf02dd86fb17c7485a5f87b605c4437966d53de1e3745d450343a6f56/numpy-2.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4dfda918a13cc4f81e9118dea249e192ab167a0bb1966272d5503e39234d694e", size = 20921004 }, - { url = "https://files.pythonhosted.org/packages/6e/3e/d0e9e32ab14005425d180ef950badf31b862f3839c5b927796648b11f88a/numpy-2.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:733585f9f4b62e9b3528dd1070ec4f52b8acf64215b60a845fa13ebd73cd0712", size = 14119910 }, - { url = "https://files.pythonhosted.org/packages/b5/5b/aa2d1905b04a8fb681e08742bb79a7bddfc160c7ce8e1ff6d5c821be0236/numpy-2.2.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:89b16a18e7bba224ce5114db863e7029803c179979e1af6ad6a6b11f70545008", size = 5153612 }, - { url = "https://files.pythonhosted.org/packages/ce/35/6831808028df0648d9b43c5df7e1051129aa0d562525bacb70019c5f5030/numpy-2.2.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:676f4eebf6b2d430300f1f4f4c2461685f8269f94c89698d832cdf9277f30b84", size = 6668401 }, - { url = "https://files.pythonhosted.org/packages/b1/38/10ef509ad63a5946cc042f98d838daebfe7eaf45b9daaf13df2086b15ff9/numpy-2.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f5cdf9f493b35f7e41e8368e7d7b4bbafaf9660cba53fb21d2cd174ec09631", size = 14014198 }, - { url = "https://files.pythonhosted.org/packages/df/f8/c80968ae01df23e249ee0a4487fae55a4c0fe2f838dfe9cc907aa8aea0fa/numpy-2.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1ad395cf254c4fbb5b2132fee391f361a6e8c1adbd28f2cd8e79308a615fe9d", size = 16076211 }, - { url = "https://files.pythonhosted.org/packages/09/69/05c169376016a0b614b432967ac46ff14269eaffab80040ec03ae1ae8e2c/numpy-2.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:08ef779aed40dbc52729d6ffe7dd51df85796a702afbf68a4f4e41fafdc8bda5", size = 15220266 }, - { url = "https://files.pythonhosted.org/packages/f1/ff/94a4ce67ea909f41cf7ea712aebbe832dc67decad22944a1020bb398a5ee/numpy-2.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:26c9c4382b19fcfbbed3238a14abf7ff223890ea1936b8890f058e7ba35e8d71", size = 17852844 }, - { url = "https://files.pythonhosted.org/packages/46/72/8a5dbce4020dfc595592333ef2fbb0a187d084ca243b67766d29d03e0096/numpy-2.2.1-cp313-cp313t-win32.whl", hash = "sha256:93cf4e045bae74c90ca833cba583c14b62cb4ba2cba0abd2b141ab52548247e2", size = 6326007 }, - { url = "https://files.pythonhosted.org/packages/7b/9c/4fce9cf39dde2562584e4cfd351a0140240f82c0e3569ce25a250f47037d/numpy-2.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bff7d8ec20f5f42607599f9994770fa65d76edca264a87b5e4ea5629bce12268", size = 12693107 }, - { url = "https://files.pythonhosted.org/packages/f1/65/d36a76b811ffe0a4515e290cb05cb0e22171b1b0f0db6bee9141cf023545/numpy-2.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7ba9cc93a91d86365a5d270dee221fdc04fb68d7478e6bf6af650de78a8339e3", size = 21044672 }, - { url = "https://files.pythonhosted.org/packages/aa/3f/b644199f165063154df486d95198d814578f13dd4d8c1651e075bf1cb8af/numpy-2.2.1-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:3d03883435a19794e41f147612a77a8f56d4e52822337844fff3d4040a142964", size = 6789873 }, - { url = "https://files.pythonhosted.org/packages/d7/df/2adb0bb98a3cbe8a6c3c6d1019aede1f1d8b83927ced228a46cc56c7a206/numpy-2.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4511d9e6071452b944207c8ce46ad2f897307910b402ea5fa975da32e0102800", size = 16194933 }, - { url = "https://files.pythonhosted.org/packages/13/3e/1959d5219a9e6d200638d924cedda6a606392f7186a4ed56478252e70d55/numpy-2.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5c5cc0cbabe9452038ed984d05ac87910f89370b9242371bd9079cb4af61811e", size = 12820057 }, +sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/fdbf6a7871703df6160b5cf3dd774074b086d278172285c52c2758b76305/numpy-2.2.1.tar.gz", hash = "sha256:45681fd7128c8ad1c379f0ca0776a8b0c6583d2f69889ddac01559dfe4390918", size = 20227662, upload-time = "2024-12-21T22:49:36.523Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/c4/5588367dc9f91e1a813beb77de46ea8cab13f778e1b3a0e661ab031aba44/numpy-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5edb4e4caf751c1518e6a26a83501fda79bff41cc59dac48d70e6d65d4ec4440", size = 21213214, upload-time = "2024-12-21T20:29:57.832Z" }, + { url = "https://files.pythonhosted.org/packages/d8/8b/32dd9f08419023a4cf856c5ad0b4eba9b830da85eafdef841a104c4fc05a/numpy-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa3017c40d513ccac9621a2364f939d39e550c542eb2a894b4c8da92b38896ab", size = 14352248, upload-time = "2024-12-21T20:30:32.954Z" }, + { url = "https://files.pythonhosted.org/packages/84/2d/0e895d02940ba6e12389f0ab5cac5afcf8dc2dc0ade4e8cad33288a721bd/numpy-2.2.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:61048b4a49b1c93fe13426e04e04fdf5a03f456616f6e98c7576144677598675", size = 5391007, upload-time = "2024-12-21T20:30:46.067Z" }, + { url = "https://files.pythonhosted.org/packages/11/b9/7f1e64a0d46d9c2af6d17966f641fb12d5b8ea3003f31b2308f3e3b9a6aa/numpy-2.2.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:7671dc19c7019103ca44e8d94917eba8534c76133523ca8406822efdd19c9308", size = 6926174, upload-time = "2024-12-21T20:31:07.682Z" }, + { url = "https://files.pythonhosted.org/packages/2e/8c/043fa4418bc9364e364ab7aba8ff6ef5f6b9171ade22de8fbcf0e2fa4165/numpy-2.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4250888bcb96617e00bfa28ac24850a83c9f3a16db471eca2ee1f1714df0f957", size = 14330914, upload-time = "2024-12-21T20:31:31.641Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b6/d8110985501ca8912dfc1c3bbef99d66e62d487f72e46b2337494df77364/numpy-2.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7746f235c47abc72b102d3bce9977714c2444bdfaea7888d241b4c4bb6a78bf", size = 16379607, upload-time = "2024-12-21T20:32:06.43Z" }, + { url = "https://files.pythonhosted.org/packages/e2/57/bdca9fb8bdaa810c3a4ff2eb3231379b77f618a7c0d24be9f7070db50775/numpy-2.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:059e6a747ae84fce488c3ee397cee7e5f905fd1bda5fb18c66bc41807ff119b2", size = 15541760, upload-time = "2024-12-21T20:32:46.421Z" }, + { url = "https://files.pythonhosted.org/packages/97/55/3b9147b3cbc3b6b1abc2a411dec5337a46c873deca0dd0bf5bef9d0579cc/numpy-2.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f62aa6ee4eb43b024b0e5a01cf65a0bb078ef8c395e8713c6e8a12a697144528", size = 18168476, upload-time = "2024-12-21T22:25:15.062Z" }, + { url = "https://files.pythonhosted.org/packages/00/e7/7c2cde16c9b87a8e14fdd262ca7849c4681cf48c8a774505f7e6f5e3b643/numpy-2.2.1-cp310-cp310-win32.whl", hash = "sha256:48fd472630715e1c1c89bf1feab55c29098cb403cc184b4859f9c86d4fcb6a95", size = 6570985, upload-time = "2024-12-21T22:25:31.2Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a8/554b0e99fc4ac11ec481254781a10da180d0559c2ebf2c324232317349ee/numpy-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:b541032178a718c165a49638d28272b771053f628382d5e9d1c93df23ff58dbf", size = 12913384, upload-time = "2024-12-21T22:25:54.717Z" }, + { url = "https://files.pythonhosted.org/packages/59/14/645887347124e101d983e1daf95b48dc3e136bf8525cb4257bf9eab1b768/numpy-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40f9e544c1c56ba8f1cf7686a8c9b5bb249e665d40d626a23899ba6d5d9e1484", size = 21217379, upload-time = "2024-12-21T22:26:52.153Z" }, + { url = "https://files.pythonhosted.org/packages/9f/fd/2279000cf29f58ccfd3778cbf4670dfe3f7ce772df5e198c5abe9e88b7d7/numpy-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9b57eaa3b0cd8db52049ed0330747b0364e899e8a606a624813452b8203d5f7", size = 14388520, upload-time = "2024-12-21T22:27:29.302Z" }, + { url = "https://files.pythonhosted.org/packages/58/b0/034eb5d5ba12d66ab658ff3455a31f20add0b78df8203c6a7451bd1bee21/numpy-2.2.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bc8a37ad5b22c08e2dbd27df2b3ef7e5c0864235805b1e718a235bcb200cf1cb", size = 5389286, upload-time = "2024-12-21T22:27:42.369Z" }, + { url = "https://files.pythonhosted.org/packages/5d/69/6f3cccde92e82e7835fdb475c2bf439761cbf8a1daa7c07338e1e132dfec/numpy-2.2.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:9036d6365d13b6cbe8f27a0eaf73ddcc070cae584e5ff94bb45e3e9d729feab5", size = 6930345, upload-time = "2024-12-21T22:28:02.349Z" }, + { url = "https://files.pythonhosted.org/packages/d1/72/1cd38e91ab563e67f584293fcc6aca855c9ae46dba42e6b5ff4600022899/numpy-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51faf345324db860b515d3f364eaa93d0e0551a88d6218a7d61286554d190d73", size = 14335748, upload-time = "2024-12-21T22:28:33.546Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d4/f999444e86986f3533e7151c272bd8186c55dda554284def18557e013a2a/numpy-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38efc1e56b73cc9b182fe55e56e63b044dd26a72128fd2fbd502f75555d92591", size = 16391057, upload-time = "2024-12-21T22:29:06.549Z" }, + { url = "https://files.pythonhosted.org/packages/99/7b/85cef6a3ae1b19542b7afd97d0b296526b6ef9e3c43ea0c4d9c4404fb2d0/numpy-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:31b89fa67a8042e96715c68e071a1200c4e172f93b0fbe01a14c0ff3ff820fc8", size = 15556943, upload-time = "2024-12-21T22:30:03.919Z" }, + { url = "https://files.pythonhosted.org/packages/69/7e/b83cc884c3508e91af78760f6b17ab46ad649831b1fa35acb3eb26d9e6d2/numpy-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4c86e2a209199ead7ee0af65e1d9992d1dce7e1f63c4b9a616500f93820658d0", size = 18180785, upload-time = "2024-12-21T22:30:41.924Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9f/eb4a9a38867de059dcd4b6e18d47c3867fbd3795d4c9557bb49278f94087/numpy-2.2.1-cp311-cp311-win32.whl", hash = "sha256:b34d87e8a3090ea626003f87f9392b3929a7bbf4104a05b6667348b6bd4bf1cd", size = 6568983, upload-time = "2024-12-21T22:30:56.619Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1e/be3b9f3073da2f8c7fa361fcdc231b548266b0781029fdbaf75eeab997fd/numpy-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:360137f8fb1b753c5cde3ac388597ad680eccbbbb3865ab65efea062c4a1fd16", size = 12917260, upload-time = "2024-12-21T22:31:22.151Z" }, + { url = "https://files.pythonhosted.org/packages/62/12/b928871c570d4a87ab13d2cc19f8817f17e340d5481621930e76b80ffb7d/numpy-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:694f9e921a0c8f252980e85bce61ebbd07ed2b7d4fa72d0e4246f2f8aa6642ab", size = 20909861, upload-time = "2024-12-21T22:32:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c3/59df91ae1d8ad7c5e03efd63fd785dec62d96b0fe56d1f9ab600b55009af/numpy-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3683a8d166f2692664262fd4900f207791d005fb088d7fdb973cc8d663626faa", size = 14095776, upload-time = "2024-12-21T22:32:37.312Z" }, + { url = "https://files.pythonhosted.org/packages/af/4e/8ed5868efc8e601fb69419644a280e9c482b75691466b73bfaab7d86922c/numpy-2.2.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:780077d95eafc2ccc3ced969db22377b3864e5b9a0ea5eb347cc93b3ea900315", size = 5126239, upload-time = "2024-12-21T22:32:59.288Z" }, + { url = "https://files.pythonhosted.org/packages/1a/74/dd0bbe650d7bc0014b051f092f2de65e34a8155aabb1287698919d124d7f/numpy-2.2.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:55ba24ebe208344aa7a00e4482f65742969a039c2acfcb910bc6fcd776eb4355", size = 6659296, upload-time = "2024-12-21T22:33:11.456Z" }, + { url = "https://files.pythonhosted.org/packages/7f/11/4ebd7a3f4a655764dc98481f97bd0a662fb340d1001be6050606be13e162/numpy-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b1d07b53b78bf84a96898c1bc139ad7f10fda7423f5fd158fd0f47ec5e01ac7", size = 14047121, upload-time = "2024-12-21T22:33:47.216Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a7/c1f1d978166eb6b98ad009503e4d93a8c1962d0eb14a885c352ee0276a54/numpy-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5062dc1a4e32a10dc2b8b13cedd58988261416e811c1dc4dbdea4f57eea61b0d", size = 16096599, upload-time = "2024-12-21T22:34:27.868Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6d/0e22afd5fcbb4d8d0091f3f46bf4e8906399c458d4293da23292c0ba5022/numpy-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fce4f615f8ca31b2e61aa0eb5865a21e14f5629515c9151850aa936c02a1ee51", size = 15243932, upload-time = "2024-12-21T22:35:05.318Z" }, + { url = "https://files.pythonhosted.org/packages/03/39/e4e5832820131ba424092b9610d996b37e5557180f8e2d6aebb05c31ae54/numpy-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:67d4cda6fa6ffa073b08c8372aa5fa767ceb10c9a0587c707505a6d426f4e046", size = 17861032, upload-time = "2024-12-21T22:35:37.77Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8a/3794313acbf5e70df2d5c7d2aba8718676f8d054a05abe59e48417fb2981/numpy-2.2.1-cp312-cp312-win32.whl", hash = "sha256:32cb94448be47c500d2c7a95f93e2f21a01f1fd05dd2beea1ccd049bb6001cd2", size = 6274018, upload-time = "2024-12-21T22:35:51.117Z" }, + { url = "https://files.pythonhosted.org/packages/17/c1/c31d3637f2641e25c7a19adf2ae822fdaf4ddd198b05d79a92a9ce7cb63e/numpy-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:ba5511d8f31c033a5fcbda22dd5c813630af98c70b2661f2d2c654ae3cdfcfc8", size = 12613843, upload-time = "2024-12-21T22:36:22.816Z" }, + { url = "https://files.pythonhosted.org/packages/20/d6/91a26e671c396e0c10e327b763485ee295f5a5a7a48c553f18417e5a0ed5/numpy-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f1d09e520217618e76396377c81fba6f290d5f926f50c35f3a5f72b01a0da780", size = 20896464, upload-time = "2024-12-21T22:37:01.393Z" }, + { url = "https://files.pythonhosted.org/packages/8c/40/5792ccccd91d45e87d9e00033abc4f6ca8a828467b193f711139ff1f1cd9/numpy-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ecc47cd7f6ea0336042be87d9e7da378e5c7e9b3c8ad0f7c966f714fc10d821", size = 14111350, upload-time = "2024-12-21T22:37:35.152Z" }, + { url = "https://files.pythonhosted.org/packages/c0/2a/fb0a27f846cb857cef0c4c92bef89f133a3a1abb4e16bba1c4dace2e9b49/numpy-2.2.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f419290bc8968a46c4933158c91a0012b7a99bb2e465d5ef5293879742f8797e", size = 5111629, upload-time = "2024-12-21T22:37:51.291Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e5/8e81bb9d84db88b047baf4e8b681a3e48d6390bc4d4e4453eca428ecbb49/numpy-2.2.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b6c390bfaef8c45a260554888966618328d30e72173697e5cabe6b285fb2348", size = 6645865, upload-time = "2024-12-21T22:38:03.738Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1a/a90ceb191dd2f9e2897c69dde93ccc2d57dd21ce2acbd7b0333e8eea4e8d/numpy-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:526fc406ab991a340744aad7e25251dd47a6720a685fa3331e5c59fef5282a59", size = 14043508, upload-time = "2024-12-21T22:38:41.854Z" }, + { url = "https://files.pythonhosted.org/packages/f1/5a/e572284c86a59dec0871a49cd4e5351e20b9c751399d5f1d79628c0542cb/numpy-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f74e6fdeb9a265624ec3a3918430205dff1df7e95a230779746a6af78bc615af", size = 16094100, upload-time = "2024-12-21T22:39:12.904Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2c/a79d24f364788386d85899dd280a94f30b0950be4b4a545f4fa4ed1d4ca7/numpy-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:53c09385ff0b72ba79d8715683c1168c12e0b6e84fb0372e97553d1ea91efe51", size = 15239691, upload-time = "2024-12-21T22:39:48.32Z" }, + { url = "https://files.pythonhosted.org/packages/cf/79/1e20fd1c9ce5a932111f964b544facc5bb9bde7865f5b42f00b4a6a9192b/numpy-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f3eac17d9ec51be534685ba877b6ab5edc3ab7ec95c8f163e5d7b39859524716", size = 17856571, upload-time = "2024-12-21T22:40:22.575Z" }, + { url = "https://files.pythonhosted.org/packages/be/5b/cc155e107f75d694f562bdc84a26cc930569f3dfdfbccb3420b626065777/numpy-2.2.1-cp313-cp313-win32.whl", hash = "sha256:9ad014faa93dbb52c80d8f4d3dcf855865c876c9660cb9bd7553843dd03a4b1e", size = 6270841, upload-time = "2024-12-21T22:45:15.101Z" }, + { url = "https://files.pythonhosted.org/packages/44/be/0e5cd009d2162e4138d79a5afb3b5d2341f0fe4777ab6e675aa3d4a42e21/numpy-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:164a829b6aacf79ca47ba4814b130c4020b202522a93d7bff2202bfb33b61c60", size = 12606618, upload-time = "2024-12-21T22:45:47.227Z" }, + { url = "https://files.pythonhosted.org/packages/a8/87/04ddf02dd86fb17c7485a5f87b605c4437966d53de1e3745d450343a6f56/numpy-2.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4dfda918a13cc4f81e9118dea249e192ab167a0bb1966272d5503e39234d694e", size = 20921004, upload-time = "2024-12-21T22:40:58.532Z" }, + { url = "https://files.pythonhosted.org/packages/6e/3e/d0e9e32ab14005425d180ef950badf31b862f3839c5b927796648b11f88a/numpy-2.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:733585f9f4b62e9b3528dd1070ec4f52b8acf64215b60a845fa13ebd73cd0712", size = 14119910, upload-time = "2024-12-21T22:41:41.298Z" }, + { url = "https://files.pythonhosted.org/packages/b5/5b/aa2d1905b04a8fb681e08742bb79a7bddfc160c7ce8e1ff6d5c821be0236/numpy-2.2.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:89b16a18e7bba224ce5114db863e7029803c179979e1af6ad6a6b11f70545008", size = 5153612, upload-time = "2024-12-21T22:41:52.23Z" }, + { url = "https://files.pythonhosted.org/packages/ce/35/6831808028df0648d9b43c5df7e1051129aa0d562525bacb70019c5f5030/numpy-2.2.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:676f4eebf6b2d430300f1f4f4c2461685f8269f94c89698d832cdf9277f30b84", size = 6668401, upload-time = "2024-12-21T22:42:05.378Z" }, + { url = "https://files.pythonhosted.org/packages/b1/38/10ef509ad63a5946cc042f98d838daebfe7eaf45b9daaf13df2086b15ff9/numpy-2.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f5cdf9f493b35f7e41e8368e7d7b4bbafaf9660cba53fb21d2cd174ec09631", size = 14014198, upload-time = "2024-12-21T22:42:36.414Z" }, + { url = "https://files.pythonhosted.org/packages/df/f8/c80968ae01df23e249ee0a4487fae55a4c0fe2f838dfe9cc907aa8aea0fa/numpy-2.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1ad395cf254c4fbb5b2132fee391f361a6e8c1adbd28f2cd8e79308a615fe9d", size = 16076211, upload-time = "2024-12-21T22:43:10.125Z" }, + { url = "https://files.pythonhosted.org/packages/09/69/05c169376016a0b614b432967ac46ff14269eaffab80040ec03ae1ae8e2c/numpy-2.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:08ef779aed40dbc52729d6ffe7dd51df85796a702afbf68a4f4e41fafdc8bda5", size = 15220266, upload-time = "2024-12-21T22:43:44.16Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ff/94a4ce67ea909f41cf7ea712aebbe832dc67decad22944a1020bb398a5ee/numpy-2.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:26c9c4382b19fcfbbed3238a14abf7ff223890ea1936b8890f058e7ba35e8d71", size = 17852844, upload-time = "2024-12-21T22:44:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/46/72/8a5dbce4020dfc595592333ef2fbb0a187d084ca243b67766d29d03e0096/numpy-2.2.1-cp313-cp313t-win32.whl", hash = "sha256:93cf4e045bae74c90ca833cba583c14b62cb4ba2cba0abd2b141ab52548247e2", size = 6326007, upload-time = "2024-12-21T22:44:34.097Z" }, + { url = "https://files.pythonhosted.org/packages/7b/9c/4fce9cf39dde2562584e4cfd351a0140240f82c0e3569ce25a250f47037d/numpy-2.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bff7d8ec20f5f42607599f9994770fa65d76edca264a87b5e4ea5629bce12268", size = 12693107, upload-time = "2024-12-21T22:44:57.542Z" }, + { url = "https://files.pythonhosted.org/packages/f1/65/d36a76b811ffe0a4515e290cb05cb0e22171b1b0f0db6bee9141cf023545/numpy-2.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7ba9cc93a91d86365a5d270dee221fdc04fb68d7478e6bf6af650de78a8339e3", size = 21044672, upload-time = "2024-12-21T22:46:49.317Z" }, + { url = "https://files.pythonhosted.org/packages/aa/3f/b644199f165063154df486d95198d814578f13dd4d8c1651e075bf1cb8af/numpy-2.2.1-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:3d03883435a19794e41f147612a77a8f56d4e52822337844fff3d4040a142964", size = 6789873, upload-time = "2024-12-21T22:47:10.519Z" }, + { url = "https://files.pythonhosted.org/packages/d7/df/2adb0bb98a3cbe8a6c3c6d1019aede1f1d8b83927ced228a46cc56c7a206/numpy-2.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4511d9e6071452b944207c8ce46ad2f897307910b402ea5fa975da32e0102800", size = 16194933, upload-time = "2024-12-21T22:47:47.113Z" }, + { url = "https://files.pythonhosted.org/packages/13/3e/1959d5219a9e6d200638d924cedda6a606392f7186a4ed56478252e70d55/numpy-2.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5c5cc0cbabe9452038ed984d05ac87910f89370b9242371bd9079cb4af61811e", size = 12820057, upload-time = "2024-12-21T22:48:36.421Z" }, ] [[package]] @@ -692,90 +829,90 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/f4/098d2270d52b41f1bd7db9fc288aaa0400cb48c2a3e2af6fa365d9720947/numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a", size = 20582187 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/e7/0e07379944aa8afb49a556a2b54587b828eb41dc9adc56fb7615b678ca53/numpy-2.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb", size = 21259519 }, - { url = "https://files.pythonhosted.org/packages/d0/cb/5a69293561e8819b09e34ed9e873b9a82b5f2ade23dce4c51dc507f6cfe1/numpy-2.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f", size = 14452796 }, - { url = "https://files.pythonhosted.org/packages/e4/04/ff11611200acd602a1e5129e36cfd25bf01ad8e5cf927baf2e90236eb02e/numpy-2.3.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1b219560ae2c1de48ead517d085bc2d05b9433f8e49d0955c82e8cd37bd7bf36", size = 5381639 }, - { url = "https://files.pythonhosted.org/packages/ea/77/e95c757a6fe7a48d28a009267408e8aa382630cc1ad1db7451b3bc21dbb4/numpy-2.3.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:bafa7d87d4c99752d07815ed7a2c0964f8ab311eb8168f41b910bd01d15b6032", size = 6914296 }, - { url = "https://files.pythonhosted.org/packages/a3/d2/137c7b6841c942124eae921279e5c41b1c34bab0e6fc60c7348e69afd165/numpy-2.3.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36dc13af226aeab72b7abad501d370d606326a0029b9f435eacb3b8c94b8a8b7", size = 14591904 }, - { url = "https://files.pythonhosted.org/packages/bb/32/67e3b0f07b0aba57a078c4ab777a9e8e6bc62f24fb53a2337f75f9691699/numpy-2.3.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7b2f9a18b5ff9824a6af80de4f37f4ec3c2aab05ef08f51c77a093f5b89adda", size = 16939602 }, - { url = "https://files.pythonhosted.org/packages/95/22/9639c30e32c93c4cee3ccdb4b09c2d0fbff4dcd06d36b357da06146530fb/numpy-2.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9984bd645a8db6ca15d850ff996856d8762c51a2239225288f08f9050ca240a0", size = 16372661 }, - { url = "https://files.pythonhosted.org/packages/12/e9/a685079529be2b0156ae0c11b13d6be647743095bb51d46589e95be88086/numpy-2.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:64c5825affc76942973a70acf438a8ab618dbd692b84cd5ec40a0a0509edc09a", size = 18884682 }, - { url = "https://files.pythonhosted.org/packages/cf/85/f6f00d019b0cc741e64b4e00ce865a57b6bed945d1bbeb1ccadbc647959b/numpy-2.3.4-cp311-cp311-win32.whl", hash = "sha256:ed759bf7a70342f7817d88376eb7142fab9fef8320d6019ef87fae05a99874e1", size = 6570076 }, - { url = "https://files.pythonhosted.org/packages/7d/10/f8850982021cb90e2ec31990291f9e830ce7d94eef432b15066e7cbe0bec/numpy-2.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:faba246fb30ea2a526c2e9645f61612341de1a83fb1e0c5edf4ddda5a9c10996", size = 13089358 }, - { url = "https://files.pythonhosted.org/packages/d1/ad/afdd8351385edf0b3445f9e24210a9c3971ef4de8fd85155462fc4321d79/numpy-2.3.4-cp311-cp311-win_arm64.whl", hash = "sha256:4c01835e718bcebe80394fd0ac66c07cbb90147ebbdad3dcecd3f25de2ae7e2c", size = 10462292 }, - { url = "https://files.pythonhosted.org/packages/96/7a/02420400b736f84317e759291b8edaeee9dc921f72b045475a9cbdb26b17/numpy-2.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11", size = 20957727 }, - { url = "https://files.pythonhosted.org/packages/18/90/a014805d627aa5750f6f0e878172afb6454552da929144b3c07fcae1bb13/numpy-2.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9", size = 14187262 }, - { url = "https://files.pythonhosted.org/packages/c7/e4/0a94b09abe89e500dc748e7515f21a13e30c5c3fe3396e6d4ac108c25fca/numpy-2.3.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667", size = 5115992 }, - { url = "https://files.pythonhosted.org/packages/88/dd/db77c75b055c6157cbd4f9c92c4458daef0dd9cbe6d8d2fe7f803cb64c37/numpy-2.3.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef", size = 6648672 }, - { url = "https://files.pythonhosted.org/packages/e1/e6/e31b0d713719610e406c0ea3ae0d90760465b086da8783e2fd835ad59027/numpy-2.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e", size = 14284156 }, - { url = "https://files.pythonhosted.org/packages/f9/58/30a85127bfee6f108282107caf8e06a1f0cc997cb6b52cdee699276fcce4/numpy-2.3.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a", size = 16641271 }, - { url = "https://files.pythonhosted.org/packages/06/f2/2e06a0f2adf23e3ae29283ad96959267938d0efd20a2e25353b70065bfec/numpy-2.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16", size = 16059531 }, - { url = "https://files.pythonhosted.org/packages/b0/e7/b106253c7c0d5dc352b9c8fab91afd76a93950998167fa3e5afe4ef3a18f/numpy-2.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786", size = 18578983 }, - { url = "https://files.pythonhosted.org/packages/73/e3/04ecc41e71462276ee867ccbef26a4448638eadecf1bc56772c9ed6d0255/numpy-2.3.4-cp312-cp312-win32.whl", hash = "sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc", size = 6291380 }, - { url = "https://files.pythonhosted.org/packages/3d/a8/566578b10d8d0e9955b1b6cd5db4e9d4592dd0026a941ff7994cedda030a/numpy-2.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32", size = 12787999 }, - { url = "https://files.pythonhosted.org/packages/58/22/9c903a957d0a8071b607f5b1bff0761d6e608b9a965945411f867d515db1/numpy-2.3.4-cp312-cp312-win_arm64.whl", hash = "sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db", size = 10197412 }, - { url = "https://files.pythonhosted.org/packages/57/7e/b72610cc91edf138bc588df5150957a4937221ca6058b825b4725c27be62/numpy-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966", size = 20950335 }, - { url = "https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3", size = 14179878 }, - { url = "https://files.pythonhosted.org/packages/ac/01/5a67cb785bda60f45415d09c2bc245433f1c68dd82eef9c9002c508b5a65/numpy-2.3.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197", size = 5108673 }, - { url = "https://files.pythonhosted.org/packages/c2/cd/8428e23a9fcebd33988f4cb61208fda832800ca03781f471f3727a820704/numpy-2.3.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e", size = 6641438 }, - { url = "https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7", size = 14281290 }, - { url = "https://files.pythonhosted.org/packages/9e/7e/7d306ff7cb143e6d975cfa7eb98a93e73495c4deabb7d1b5ecf09ea0fd69/numpy-2.3.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953", size = 16636543 }, - { url = "https://files.pythonhosted.org/packages/47/6a/8cfc486237e56ccfb0db234945552a557ca266f022d281a2f577b98e955c/numpy-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37", size = 16056117 }, - { url = "https://files.pythonhosted.org/packages/b1/0e/42cb5e69ea901e06ce24bfcc4b5664a56f950a70efdcf221f30d9615f3f3/numpy-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd", size = 18577788 }, - { url = "https://files.pythonhosted.org/packages/86/92/41c3d5157d3177559ef0a35da50f0cda7fa071f4ba2306dd36818591a5bc/numpy-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646", size = 6282620 }, - { url = "https://files.pythonhosted.org/packages/09/97/fd421e8bc50766665ad35536c2bb4ef916533ba1fdd053a62d96cc7c8b95/numpy-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d", size = 12784672 }, - { url = "https://files.pythonhosted.org/packages/ad/df/5474fb2f74970ca8eb978093969b125a84cc3d30e47f82191f981f13a8a0/numpy-2.3.4-cp313-cp313-win_arm64.whl", hash = "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc", size = 10196702 }, - { url = "https://files.pythonhosted.org/packages/11/83/66ac031464ec1767ea3ed48ce40f615eb441072945e98693bec0bcd056cc/numpy-2.3.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879", size = 21049003 }, - { url = "https://files.pythonhosted.org/packages/5f/99/5b14e0e686e61371659a1d5bebd04596b1d72227ce36eed121bb0aeab798/numpy-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562", size = 14302980 }, - { url = "https://files.pythonhosted.org/packages/2c/44/e9486649cd087d9fc6920e3fc3ac2aba10838d10804b1e179fb7cbc4e634/numpy-2.3.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a", size = 5231472 }, - { url = "https://files.pythonhosted.org/packages/3e/51/902b24fa8887e5fe2063fd61b1895a476d0bbf46811ab0c7fdf4bd127345/numpy-2.3.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6", size = 6739342 }, - { url = "https://files.pythonhosted.org/packages/34/f1/4de9586d05b1962acdcdb1dc4af6646361a643f8c864cef7c852bf509740/numpy-2.3.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7", size = 14354338 }, - { url = "https://files.pythonhosted.org/packages/1f/06/1c16103b425de7969d5a76bdf5ada0804b476fed05d5f9e17b777f1cbefd/numpy-2.3.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0", size = 16702392 }, - { url = "https://files.pythonhosted.org/packages/34/b2/65f4dc1b89b5322093572b6e55161bb42e3e0487067af73627f795cc9d47/numpy-2.3.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f", size = 16134998 }, - { url = "https://files.pythonhosted.org/packages/d4/11/94ec578896cdb973aaf56425d6c7f2aff4186a5c00fac15ff2ec46998b46/numpy-2.3.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64", size = 18651574 }, - { url = "https://files.pythonhosted.org/packages/62/b7/7efa763ab33dbccf56dade36938a77345ce8e8192d6b39e470ca25ff3cd0/numpy-2.3.4-cp313-cp313t-win32.whl", hash = "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb", size = 6413135 }, - { url = "https://files.pythonhosted.org/packages/43/70/aba4c38e8400abcc2f345e13d972fb36c26409b3e644366db7649015f291/numpy-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c", size = 12928582 }, - { url = "https://files.pythonhosted.org/packages/67/63/871fad5f0073fc00fbbdd7232962ea1ac40eeaae2bba66c76214f7954236/numpy-2.3.4-cp313-cp313t-win_arm64.whl", hash = "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40", size = 10266691 }, - { url = "https://files.pythonhosted.org/packages/72/71/ae6170143c115732470ae3a2d01512870dd16e0953f8a6dc89525696069b/numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e", size = 20955580 }, - { url = "https://files.pythonhosted.org/packages/af/39/4be9222ffd6ca8a30eda033d5f753276a9c3426c397bb137d8e19dedd200/numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff", size = 14188056 }, - { url = "https://files.pythonhosted.org/packages/6c/3d/d85f6700d0a4aa4f9491030e1021c2b2b7421b2b38d01acd16734a2bfdc7/numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f", size = 5116555 }, - { url = "https://files.pythonhosted.org/packages/bf/04/82c1467d86f47eee8a19a464c92f90a9bb68ccf14a54c5224d7031241ffb/numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b", size = 6643581 }, - { url = "https://files.pythonhosted.org/packages/0c/d3/c79841741b837e293f48bd7db89d0ac7a4f2503b382b78a790ef1dc778a5/numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7", size = 14299186 }, - { url = "https://files.pythonhosted.org/packages/e8/7e/4a14a769741fbf237eec5a12a2cbc7a4c4e061852b6533bcb9e9a796c908/numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2", size = 16638601 }, - { url = "https://files.pythonhosted.org/packages/93/87/1c1de269f002ff0a41173fe01dcc925f4ecff59264cd8f96cf3b60d12c9b/numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52", size = 16074219 }, - { url = "https://files.pythonhosted.org/packages/cd/28/18f72ee77408e40a76d691001ae599e712ca2a47ddd2c4f695b16c65f077/numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26", size = 18576702 }, - { url = "https://files.pythonhosted.org/packages/c3/76/95650169b465ececa8cf4b2e8f6df255d4bf662775e797ade2025cc51ae6/numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc", size = 6337136 }, - { url = "https://files.pythonhosted.org/packages/dc/89/a231a5c43ede5d6f77ba4a91e915a87dea4aeea76560ba4d2bf185c683f0/numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9", size = 12920542 }, - { url = "https://files.pythonhosted.org/packages/0d/0c/ae9434a888f717c5ed2ff2393b3f344f0ff6f1c793519fa0c540461dc530/numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868", size = 10480213 }, - { url = "https://files.pythonhosted.org/packages/83/4b/c4a5f0841f92536f6b9592694a5b5f68c9ab37b775ff342649eadf9055d3/numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec", size = 21052280 }, - { url = "https://files.pythonhosted.org/packages/3e/80/90308845fc93b984d2cc96d83e2324ce8ad1fd6efea81b324cba4b673854/numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3", size = 14302930 }, - { url = "https://files.pythonhosted.org/packages/3d/4e/07439f22f2a3b247cec4d63a713faae55e1141a36e77fb212881f7cda3fb/numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365", size = 5231504 }, - { url = "https://files.pythonhosted.org/packages/ab/de/1e11f2547e2fe3d00482b19721855348b94ada8359aef5d40dd57bfae9df/numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252", size = 6739405 }, - { url = "https://files.pythonhosted.org/packages/3b/40/8cd57393a26cebe2e923005db5134a946c62fa56a1087dc7c478f3e30837/numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e", size = 14354866 }, - { url = "https://files.pythonhosted.org/packages/93/39/5b3510f023f96874ee6fea2e40dfa99313a00bf3ab779f3c92978f34aace/numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0", size = 16703296 }, - { url = "https://files.pythonhosted.org/packages/41/0d/19bb163617c8045209c1996c4e427bccbc4bbff1e2c711f39203c8ddbb4a/numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0", size = 16136046 }, - { url = "https://files.pythonhosted.org/packages/e2/c1/6dba12fdf68b02a21ac411c9df19afa66bed2540f467150ca64d246b463d/numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f", size = 18652691 }, - { url = "https://files.pythonhosted.org/packages/f8/73/f85056701dbbbb910c51d846c58d29fd46b30eecd2b6ba760fc8b8a1641b/numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d", size = 6485782 }, - { url = "https://files.pythonhosted.org/packages/17/90/28fa6f9865181cb817c2471ee65678afa8a7e2a1fb16141473d5fa6bacc3/numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6", size = 13113301 }, - { url = "https://files.pythonhosted.org/packages/54/23/08c002201a8e7e1f9afba93b97deceb813252d9cfd0d3351caed123dcf97/numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29", size = 10547532 }, - { url = "https://files.pythonhosted.org/packages/b1/b6/64898f51a86ec88ca1257a59c1d7fd077b60082a119affefcdf1dd0df8ca/numpy-2.3.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6e274603039f924c0fe5cb73438fa9246699c78a6df1bd3decef9ae592ae1c05", size = 21131552 }, - { url = "https://files.pythonhosted.org/packages/ce/4c/f135dc6ebe2b6a3c77f4e4838fa63d350f85c99462012306ada1bd4bc460/numpy-2.3.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d149aee5c72176d9ddbc6803aef9c0f6d2ceeea7626574fc68518da5476fa346", size = 14377796 }, - { url = "https://files.pythonhosted.org/packages/d0/a4/f33f9c23fcc13dd8412fc8614559b5b797e0aba9d8e01dfa8bae10c84004/numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:6d34ed9db9e6395bb6cd33286035f73a59b058169733a9db9f85e650b88df37e", size = 5306904 }, - { url = "https://files.pythonhosted.org/packages/28/af/c44097f25f834360f9fb960fa082863e0bad14a42f36527b2a121abdec56/numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:fdebe771ca06bb8d6abce84e51dca9f7921fe6ad34a0c914541b063e9a68928b", size = 6819682 }, - { url = "https://files.pythonhosted.org/packages/c5/8c/cd283b54c3c2b77e188f63e23039844f56b23bba1712318288c13fe86baf/numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e92defe6c08211eb77902253b14fe5b480ebc5112bc741fd5e9cd0608f847", size = 14422300 }, - { url = "https://files.pythonhosted.org/packages/b0/f0/8404db5098d92446b3e3695cf41c6f0ecb703d701cb0b7566ee2177f2eee/numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13b9062e4f5c7ee5c7e5be96f29ba71bc5a37fed3d1d77c37390ae00724d296d", size = 16760806 }, - { url = "https://files.pythonhosted.org/packages/95/8e/2844c3959ce9a63acc7c8e50881133d86666f0420bcde695e115ced0920f/numpy-2.3.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f", size = 12973130 }, +sdist = { url = "https://files.pythonhosted.org/packages/b5/f4/098d2270d52b41f1bd7db9fc288aaa0400cb48c2a3e2af6fa365d9720947/numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a", size = 20582187, upload-time = "2025-10-15T16:18:11.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/e7/0e07379944aa8afb49a556a2b54587b828eb41dc9adc56fb7615b678ca53/numpy-2.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb", size = 21259519, upload-time = "2025-10-15T16:15:19.012Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cb/5a69293561e8819b09e34ed9e873b9a82b5f2ade23dce4c51dc507f6cfe1/numpy-2.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f", size = 14452796, upload-time = "2025-10-15T16:15:23.094Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/ff11611200acd602a1e5129e36cfd25bf01ad8e5cf927baf2e90236eb02e/numpy-2.3.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1b219560ae2c1de48ead517d085bc2d05b9433f8e49d0955c82e8cd37bd7bf36", size = 5381639, upload-time = "2025-10-15T16:15:25.572Z" }, + { url = "https://files.pythonhosted.org/packages/ea/77/e95c757a6fe7a48d28a009267408e8aa382630cc1ad1db7451b3bc21dbb4/numpy-2.3.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:bafa7d87d4c99752d07815ed7a2c0964f8ab311eb8168f41b910bd01d15b6032", size = 6914296, upload-time = "2025-10-15T16:15:27.079Z" }, + { url = "https://files.pythonhosted.org/packages/a3/d2/137c7b6841c942124eae921279e5c41b1c34bab0e6fc60c7348e69afd165/numpy-2.3.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36dc13af226aeab72b7abad501d370d606326a0029b9f435eacb3b8c94b8a8b7", size = 14591904, upload-time = "2025-10-15T16:15:29.044Z" }, + { url = "https://files.pythonhosted.org/packages/bb/32/67e3b0f07b0aba57a078c4ab777a9e8e6bc62f24fb53a2337f75f9691699/numpy-2.3.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7b2f9a18b5ff9824a6af80de4f37f4ec3c2aab05ef08f51c77a093f5b89adda", size = 16939602, upload-time = "2025-10-15T16:15:31.106Z" }, + { url = "https://files.pythonhosted.org/packages/95/22/9639c30e32c93c4cee3ccdb4b09c2d0fbff4dcd06d36b357da06146530fb/numpy-2.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9984bd645a8db6ca15d850ff996856d8762c51a2239225288f08f9050ca240a0", size = 16372661, upload-time = "2025-10-15T16:15:33.546Z" }, + { url = "https://files.pythonhosted.org/packages/12/e9/a685079529be2b0156ae0c11b13d6be647743095bb51d46589e95be88086/numpy-2.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:64c5825affc76942973a70acf438a8ab618dbd692b84cd5ec40a0a0509edc09a", size = 18884682, upload-time = "2025-10-15T16:15:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/cf/85/f6f00d019b0cc741e64b4e00ce865a57b6bed945d1bbeb1ccadbc647959b/numpy-2.3.4-cp311-cp311-win32.whl", hash = "sha256:ed759bf7a70342f7817d88376eb7142fab9fef8320d6019ef87fae05a99874e1", size = 6570076, upload-time = "2025-10-15T16:15:38.225Z" }, + { url = "https://files.pythonhosted.org/packages/7d/10/f8850982021cb90e2ec31990291f9e830ce7d94eef432b15066e7cbe0bec/numpy-2.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:faba246fb30ea2a526c2e9645f61612341de1a83fb1e0c5edf4ddda5a9c10996", size = 13089358, upload-time = "2025-10-15T16:15:40.404Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ad/afdd8351385edf0b3445f9e24210a9c3971ef4de8fd85155462fc4321d79/numpy-2.3.4-cp311-cp311-win_arm64.whl", hash = "sha256:4c01835e718bcebe80394fd0ac66c07cbb90147ebbdad3dcecd3f25de2ae7e2c", size = 10462292, upload-time = "2025-10-15T16:15:42.896Z" }, + { url = "https://files.pythonhosted.org/packages/96/7a/02420400b736f84317e759291b8edaeee9dc921f72b045475a9cbdb26b17/numpy-2.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11", size = 20957727, upload-time = "2025-10-15T16:15:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/18/90/a014805d627aa5750f6f0e878172afb6454552da929144b3c07fcae1bb13/numpy-2.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9", size = 14187262, upload-time = "2025-10-15T16:15:47.761Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e4/0a94b09abe89e500dc748e7515f21a13e30c5c3fe3396e6d4ac108c25fca/numpy-2.3.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667", size = 5115992, upload-time = "2025-10-15T16:15:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/db77c75b055c6157cbd4f9c92c4458daef0dd9cbe6d8d2fe7f803cb64c37/numpy-2.3.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef", size = 6648672, upload-time = "2025-10-15T16:15:52.442Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e6/e31b0d713719610e406c0ea3ae0d90760465b086da8783e2fd835ad59027/numpy-2.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e", size = 14284156, upload-time = "2025-10-15T16:15:54.351Z" }, + { url = "https://files.pythonhosted.org/packages/f9/58/30a85127bfee6f108282107caf8e06a1f0cc997cb6b52cdee699276fcce4/numpy-2.3.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a", size = 16641271, upload-time = "2025-10-15T16:15:56.67Z" }, + { url = "https://files.pythonhosted.org/packages/06/f2/2e06a0f2adf23e3ae29283ad96959267938d0efd20a2e25353b70065bfec/numpy-2.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16", size = 16059531, upload-time = "2025-10-15T16:15:59.412Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e7/b106253c7c0d5dc352b9c8fab91afd76a93950998167fa3e5afe4ef3a18f/numpy-2.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786", size = 18578983, upload-time = "2025-10-15T16:16:01.804Z" }, + { url = "https://files.pythonhosted.org/packages/73/e3/04ecc41e71462276ee867ccbef26a4448638eadecf1bc56772c9ed6d0255/numpy-2.3.4-cp312-cp312-win32.whl", hash = "sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc", size = 6291380, upload-time = "2025-10-15T16:16:03.938Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a8/566578b10d8d0e9955b1b6cd5db4e9d4592dd0026a941ff7994cedda030a/numpy-2.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32", size = 12787999, upload-time = "2025-10-15T16:16:05.801Z" }, + { url = "https://files.pythonhosted.org/packages/58/22/9c903a957d0a8071b607f5b1bff0761d6e608b9a965945411f867d515db1/numpy-2.3.4-cp312-cp312-win_arm64.whl", hash = "sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db", size = 10197412, upload-time = "2025-10-15T16:16:07.854Z" }, + { url = "https://files.pythonhosted.org/packages/57/7e/b72610cc91edf138bc588df5150957a4937221ca6058b825b4725c27be62/numpy-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966", size = 20950335, upload-time = "2025-10-15T16:16:10.304Z" }, + { url = "https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3", size = 14179878, upload-time = "2025-10-15T16:16:12.595Z" }, + { url = "https://files.pythonhosted.org/packages/ac/01/5a67cb785bda60f45415d09c2bc245433f1c68dd82eef9c9002c508b5a65/numpy-2.3.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197", size = 5108673, upload-time = "2025-10-15T16:16:14.877Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cd/8428e23a9fcebd33988f4cb61208fda832800ca03781f471f3727a820704/numpy-2.3.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e", size = 6641438, upload-time = "2025-10-15T16:16:16.805Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7", size = 14281290, upload-time = "2025-10-15T16:16:18.764Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7e/7d306ff7cb143e6d975cfa7eb98a93e73495c4deabb7d1b5ecf09ea0fd69/numpy-2.3.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953", size = 16636543, upload-time = "2025-10-15T16:16:21.072Z" }, + { url = "https://files.pythonhosted.org/packages/47/6a/8cfc486237e56ccfb0db234945552a557ca266f022d281a2f577b98e955c/numpy-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37", size = 16056117, upload-time = "2025-10-15T16:16:23.369Z" }, + { url = "https://files.pythonhosted.org/packages/b1/0e/42cb5e69ea901e06ce24bfcc4b5664a56f950a70efdcf221f30d9615f3f3/numpy-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd", size = 18577788, upload-time = "2025-10-15T16:16:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/86/92/41c3d5157d3177559ef0a35da50f0cda7fa071f4ba2306dd36818591a5bc/numpy-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646", size = 6282620, upload-time = "2025-10-15T16:16:29.811Z" }, + { url = "https://files.pythonhosted.org/packages/09/97/fd421e8bc50766665ad35536c2bb4ef916533ba1fdd053a62d96cc7c8b95/numpy-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d", size = 12784672, upload-time = "2025-10-15T16:16:31.589Z" }, + { url = "https://files.pythonhosted.org/packages/ad/df/5474fb2f74970ca8eb978093969b125a84cc3d30e47f82191f981f13a8a0/numpy-2.3.4-cp313-cp313-win_arm64.whl", hash = "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc", size = 10196702, upload-time = "2025-10-15T16:16:33.902Z" }, + { url = "https://files.pythonhosted.org/packages/11/83/66ac031464ec1767ea3ed48ce40f615eb441072945e98693bec0bcd056cc/numpy-2.3.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879", size = 21049003, upload-time = "2025-10-15T16:16:36.101Z" }, + { url = "https://files.pythonhosted.org/packages/5f/99/5b14e0e686e61371659a1d5bebd04596b1d72227ce36eed121bb0aeab798/numpy-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562", size = 14302980, upload-time = "2025-10-15T16:16:39.124Z" }, + { url = "https://files.pythonhosted.org/packages/2c/44/e9486649cd087d9fc6920e3fc3ac2aba10838d10804b1e179fb7cbc4e634/numpy-2.3.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a", size = 5231472, upload-time = "2025-10-15T16:16:41.168Z" }, + { url = "https://files.pythonhosted.org/packages/3e/51/902b24fa8887e5fe2063fd61b1895a476d0bbf46811ab0c7fdf4bd127345/numpy-2.3.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6", size = 6739342, upload-time = "2025-10-15T16:16:43.777Z" }, + { url = "https://files.pythonhosted.org/packages/34/f1/4de9586d05b1962acdcdb1dc4af6646361a643f8c864cef7c852bf509740/numpy-2.3.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7", size = 14354338, upload-time = "2025-10-15T16:16:46.081Z" }, + { url = "https://files.pythonhosted.org/packages/1f/06/1c16103b425de7969d5a76bdf5ada0804b476fed05d5f9e17b777f1cbefd/numpy-2.3.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0", size = 16702392, upload-time = "2025-10-15T16:16:48.455Z" }, + { url = "https://files.pythonhosted.org/packages/34/b2/65f4dc1b89b5322093572b6e55161bb42e3e0487067af73627f795cc9d47/numpy-2.3.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f", size = 16134998, upload-time = "2025-10-15T16:16:51.114Z" }, + { url = "https://files.pythonhosted.org/packages/d4/11/94ec578896cdb973aaf56425d6c7f2aff4186a5c00fac15ff2ec46998b46/numpy-2.3.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64", size = 18651574, upload-time = "2025-10-15T16:16:53.429Z" }, + { url = "https://files.pythonhosted.org/packages/62/b7/7efa763ab33dbccf56dade36938a77345ce8e8192d6b39e470ca25ff3cd0/numpy-2.3.4-cp313-cp313t-win32.whl", hash = "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb", size = 6413135, upload-time = "2025-10-15T16:16:55.992Z" }, + { url = "https://files.pythonhosted.org/packages/43/70/aba4c38e8400abcc2f345e13d972fb36c26409b3e644366db7649015f291/numpy-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c", size = 12928582, upload-time = "2025-10-15T16:16:57.943Z" }, + { url = "https://files.pythonhosted.org/packages/67/63/871fad5f0073fc00fbbdd7232962ea1ac40eeaae2bba66c76214f7954236/numpy-2.3.4-cp313-cp313t-win_arm64.whl", hash = "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40", size = 10266691, upload-time = "2025-10-15T16:17:00.048Z" }, + { url = "https://files.pythonhosted.org/packages/72/71/ae6170143c115732470ae3a2d01512870dd16e0953f8a6dc89525696069b/numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e", size = 20955580, upload-time = "2025-10-15T16:17:02.509Z" }, + { url = "https://files.pythonhosted.org/packages/af/39/4be9222ffd6ca8a30eda033d5f753276a9c3426c397bb137d8e19dedd200/numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff", size = 14188056, upload-time = "2025-10-15T16:17:04.873Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3d/d85f6700d0a4aa4f9491030e1021c2b2b7421b2b38d01acd16734a2bfdc7/numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f", size = 5116555, upload-time = "2025-10-15T16:17:07.499Z" }, + { url = "https://files.pythonhosted.org/packages/bf/04/82c1467d86f47eee8a19a464c92f90a9bb68ccf14a54c5224d7031241ffb/numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b", size = 6643581, upload-time = "2025-10-15T16:17:09.774Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d3/c79841741b837e293f48bd7db89d0ac7a4f2503b382b78a790ef1dc778a5/numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7", size = 14299186, upload-time = "2025-10-15T16:17:11.937Z" }, + { url = "https://files.pythonhosted.org/packages/e8/7e/4a14a769741fbf237eec5a12a2cbc7a4c4e061852b6533bcb9e9a796c908/numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2", size = 16638601, upload-time = "2025-10-15T16:17:14.391Z" }, + { url = "https://files.pythonhosted.org/packages/93/87/1c1de269f002ff0a41173fe01dcc925f4ecff59264cd8f96cf3b60d12c9b/numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52", size = 16074219, upload-time = "2025-10-15T16:17:17.058Z" }, + { url = "https://files.pythonhosted.org/packages/cd/28/18f72ee77408e40a76d691001ae599e712ca2a47ddd2c4f695b16c65f077/numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26", size = 18576702, upload-time = "2025-10-15T16:17:19.379Z" }, + { url = "https://files.pythonhosted.org/packages/c3/76/95650169b465ececa8cf4b2e8f6df255d4bf662775e797ade2025cc51ae6/numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc", size = 6337136, upload-time = "2025-10-15T16:17:22.886Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/a231a5c43ede5d6f77ba4a91e915a87dea4aeea76560ba4d2bf185c683f0/numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9", size = 12920542, upload-time = "2025-10-15T16:17:24.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/0c/ae9434a888f717c5ed2ff2393b3f344f0ff6f1c793519fa0c540461dc530/numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868", size = 10480213, upload-time = "2025-10-15T16:17:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/83/4b/c4a5f0841f92536f6b9592694a5b5f68c9ab37b775ff342649eadf9055d3/numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec", size = 21052280, upload-time = "2025-10-15T16:17:29.638Z" }, + { url = "https://files.pythonhosted.org/packages/3e/80/90308845fc93b984d2cc96d83e2324ce8ad1fd6efea81b324cba4b673854/numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3", size = 14302930, upload-time = "2025-10-15T16:17:32.384Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4e/07439f22f2a3b247cec4d63a713faae55e1141a36e77fb212881f7cda3fb/numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365", size = 5231504, upload-time = "2025-10-15T16:17:34.515Z" }, + { url = "https://files.pythonhosted.org/packages/ab/de/1e11f2547e2fe3d00482b19721855348b94ada8359aef5d40dd57bfae9df/numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252", size = 6739405, upload-time = "2025-10-15T16:17:36.128Z" }, + { url = "https://files.pythonhosted.org/packages/3b/40/8cd57393a26cebe2e923005db5134a946c62fa56a1087dc7c478f3e30837/numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e", size = 14354866, upload-time = "2025-10-15T16:17:38.884Z" }, + { url = "https://files.pythonhosted.org/packages/93/39/5b3510f023f96874ee6fea2e40dfa99313a00bf3ab779f3c92978f34aace/numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0", size = 16703296, upload-time = "2025-10-15T16:17:41.564Z" }, + { url = "https://files.pythonhosted.org/packages/41/0d/19bb163617c8045209c1996c4e427bccbc4bbff1e2c711f39203c8ddbb4a/numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0", size = 16136046, upload-time = "2025-10-15T16:17:43.901Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c1/6dba12fdf68b02a21ac411c9df19afa66bed2540f467150ca64d246b463d/numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f", size = 18652691, upload-time = "2025-10-15T16:17:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/f8/73/f85056701dbbbb910c51d846c58d29fd46b30eecd2b6ba760fc8b8a1641b/numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d", size = 6485782, upload-time = "2025-10-15T16:17:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/17/90/28fa6f9865181cb817c2471ee65678afa8a7e2a1fb16141473d5fa6bacc3/numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6", size = 13113301, upload-time = "2025-10-15T16:17:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/54/23/08c002201a8e7e1f9afba93b97deceb813252d9cfd0d3351caed123dcf97/numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29", size = 10547532, upload-time = "2025-10-15T16:17:53.48Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b6/64898f51a86ec88ca1257a59c1d7fd077b60082a119affefcdf1dd0df8ca/numpy-2.3.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6e274603039f924c0fe5cb73438fa9246699c78a6df1bd3decef9ae592ae1c05", size = 21131552, upload-time = "2025-10-15T16:17:55.845Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4c/f135dc6ebe2b6a3c77f4e4838fa63d350f85c99462012306ada1bd4bc460/numpy-2.3.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d149aee5c72176d9ddbc6803aef9c0f6d2ceeea7626574fc68518da5476fa346", size = 14377796, upload-time = "2025-10-15T16:17:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a4/f33f9c23fcc13dd8412fc8614559b5b797e0aba9d8e01dfa8bae10c84004/numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:6d34ed9db9e6395bb6cd33286035f73a59b058169733a9db9f85e650b88df37e", size = 5306904, upload-time = "2025-10-15T16:18:00.596Z" }, + { url = "https://files.pythonhosted.org/packages/28/af/c44097f25f834360f9fb960fa082863e0bad14a42f36527b2a121abdec56/numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:fdebe771ca06bb8d6abce84e51dca9f7921fe6ad34a0c914541b063e9a68928b", size = 6819682, upload-time = "2025-10-15T16:18:02.32Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8c/cd283b54c3c2b77e188f63e23039844f56b23bba1712318288c13fe86baf/numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e92defe6c08211eb77902253b14fe5b480ebc5112bc741fd5e9cd0608f847", size = 14422300, upload-time = "2025-10-15T16:18:04.271Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f0/8404db5098d92446b3e3695cf41c6f0ecb703d701cb0b7566ee2177f2eee/numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13b9062e4f5c7ee5c7e5be96f29ba71bc5a37fed3d1d77c37390ae00724d296d", size = 16760806, upload-time = "2025-10-15T16:18:06.668Z" }, + { url = "https://files.pythonhosted.org/packages/95/8e/2844c3959ce9a63acc7c8e50881133d86666f0420bcde695e115ced0920f/numpy-2.3.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f", size = 12973130, upload-time = "2025-10-15T16:18:09.397Z" }, ] [[package]] name = "packaging" version = "24.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, ] [[package]] @@ -789,51 +926,51 @@ dependencies = [ { name = "pytz" }, { name = "tzdata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/70/c853aec59839bceed032d52010ff5f1b8d87dc3114b762e4ba2727661a3b/pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5", size = 12580827 }, - { url = "https://files.pythonhosted.org/packages/99/f2/c4527768739ffa4469b2b4fff05aa3768a478aed89a2f271a79a40eee984/pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348", size = 11303897 }, - { url = "https://files.pythonhosted.org/packages/ed/12/86c1747ea27989d7a4064f806ce2bae2c6d575b950be087837bdfcabacc9/pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed", size = 66480908 }, - { url = "https://files.pythonhosted.org/packages/44/50/7db2cd5e6373ae796f0ddad3675268c8d59fb6076e66f0c339d61cea886b/pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57", size = 13064210 }, - { url = "https://files.pythonhosted.org/packages/61/61/a89015a6d5536cb0d6c3ba02cebed51a95538cf83472975275e28ebf7d0c/pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42", size = 16754292 }, - { url = "https://files.pythonhosted.org/packages/ce/0d/4cc7b69ce37fac07645a94e1d4b0880b15999494372c1523508511b09e40/pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f", size = 14416379 }, - { url = "https://files.pythonhosted.org/packages/31/9e/6ebb433de864a6cd45716af52a4d7a8c3c9aaf3a98368e61db9e69e69a9c/pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645", size = 11598471 }, - { url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222 }, - { url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274 }, - { url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836 }, - { url = "https://files.pythonhosted.org/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505 }, - { url = "https://files.pythonhosted.org/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420 }, - { url = "https://files.pythonhosted.org/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457 }, - { url = "https://files.pythonhosted.org/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166 }, - { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893 }, - { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475 }, - { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645 }, - { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445 }, - { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235 }, - { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756 }, - { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248 }, - { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643 }, - { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573 }, - { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085 }, - { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809 }, - { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316 }, - { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055 }, - { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175 }, - { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650 }, - { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177 }, - { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526 }, - { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013 }, - { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620 }, - { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 }, +sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213, upload-time = "2024-09-20T13:10:04.827Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/70/c853aec59839bceed032d52010ff5f1b8d87dc3114b762e4ba2727661a3b/pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5", size = 12580827, upload-time = "2024-09-20T13:08:42.347Z" }, + { url = "https://files.pythonhosted.org/packages/99/f2/c4527768739ffa4469b2b4fff05aa3768a478aed89a2f271a79a40eee984/pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348", size = 11303897, upload-time = "2024-09-20T13:08:45.807Z" }, + { url = "https://files.pythonhosted.org/packages/ed/12/86c1747ea27989d7a4064f806ce2bae2c6d575b950be087837bdfcabacc9/pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed", size = 66480908, upload-time = "2024-09-20T18:37:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/44/50/7db2cd5e6373ae796f0ddad3675268c8d59fb6076e66f0c339d61cea886b/pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57", size = 13064210, upload-time = "2024-09-20T13:08:48.325Z" }, + { url = "https://files.pythonhosted.org/packages/61/61/a89015a6d5536cb0d6c3ba02cebed51a95538cf83472975275e28ebf7d0c/pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42", size = 16754292, upload-time = "2024-09-20T19:01:54.443Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0d/4cc7b69ce37fac07645a94e1d4b0880b15999494372c1523508511b09e40/pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f", size = 14416379, upload-time = "2024-09-20T13:08:50.882Z" }, + { url = "https://files.pythonhosted.org/packages/31/9e/6ebb433de864a6cd45716af52a4d7a8c3c9aaf3a98368e61db9e69e69a9c/pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645", size = 11598471, upload-time = "2024-09-20T13:08:53.332Z" }, + { url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222, upload-time = "2024-09-20T13:08:56.254Z" }, + { url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274, upload-time = "2024-09-20T13:08:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836, upload-time = "2024-09-20T19:01:57.571Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505, upload-time = "2024-09-20T13:09:01.501Z" }, + { url = "https://files.pythonhosted.org/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420, upload-time = "2024-09-20T19:02:00.678Z" }, + { url = "https://files.pythonhosted.org/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457, upload-time = "2024-09-20T13:09:04.105Z" }, + { url = "https://files.pythonhosted.org/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166, upload-time = "2024-09-20T13:09:06.917Z" }, + { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893, upload-time = "2024-09-20T13:09:09.655Z" }, + { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475, upload-time = "2024-09-20T13:09:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645, upload-time = "2024-09-20T19:02:03.88Z" }, + { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445, upload-time = "2024-09-20T13:09:17.621Z" }, + { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235, upload-time = "2024-09-20T19:02:07.094Z" }, + { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756, upload-time = "2024-09-20T13:09:20.474Z" }, + { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248, upload-time = "2024-09-20T13:09:23.137Z" }, + { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643, upload-time = "2024-09-20T13:09:25.522Z" }, + { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573, upload-time = "2024-09-20T13:09:28.012Z" }, + { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085, upload-time = "2024-09-20T19:02:10.451Z" }, + { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809, upload-time = "2024-09-20T13:09:30.814Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316, upload-time = "2024-09-20T19:02:13.825Z" }, + { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055, upload-time = "2024-09-20T13:09:33.462Z" }, + { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175, upload-time = "2024-09-20T13:09:35.871Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650, upload-time = "2024-09-20T13:09:38.685Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177, upload-time = "2024-09-20T13:09:41.141Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526, upload-time = "2024-09-20T19:02:16.905Z" }, + { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013, upload-time = "2024-09-20T13:09:44.39Z" }, + { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620, upload-time = "2024-09-20T19:02:20.639Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436, upload-time = "2024-09-20T13:09:48.112Z" }, ] [[package]] name = "parso" version = "0.8.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609 } +sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609, upload-time = "2024-04-05T09:43:55.897Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 }, + { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650, upload-time = "2024-04-05T09:43:53.299Z" }, ] [[package]] @@ -843,36 +980,36 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ptyprocess" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, ] [[package]] name = "pickleshare" version = "0.7.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/b6/df3c1c9b616e9c0edbc4fbab6ddd09df9535849c64ba51fcb6531c32d4d8/pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", size = 6161 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/b6/df3c1c9b616e9c0edbc4fbab6ddd09df9535849c64ba51fcb6531c32d4d8/pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", size = 6161, upload-time = "2018-09-25T19:17:37.249Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/41/220f49aaea88bc6fa6cba8d05ecf24676326156c23b991e80b3f2fc24c77/pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56", size = 6877 }, + { url = "https://files.pythonhosted.org/packages/9a/41/220f49aaea88bc6fa6cba8d05ecf24676326156c23b991e80b3f2fc24c77/pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56", size = 6877, upload-time = "2018-09-25T19:17:35.817Z" }, ] [[package]] name = "platformdirs" version = "4.3.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362 } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 }, + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, ] [[package]] name = "pluggy" version = "1.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, ] [[package]] @@ -886,9 +1023,9 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792 } +sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965 }, + { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, ] [[package]] @@ -898,93 +1035,93 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2d/4f/feb5e137aff82f7c7f3248267b97451da3644f6cdc218edfe549fb354127/prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90", size = 424684 } +sdist = { url = "https://files.pythonhosted.org/packages/2d/4f/feb5e137aff82f7c7f3248267b97451da3644f6cdc218edfe549fb354127/prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90", size = 424684, upload-time = "2024-09-25T10:20:57.609Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/6a/fd08d94654f7e67c52ca30523a178b3f8ccc4237fce4be90d39c938a831a/prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e", size = 386595 }, + { url = "https://files.pythonhosted.org/packages/a9/6a/fd08d94654f7e67c52ca30523a178b3f8ccc4237fce4be90d39c938a831a/prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e", size = 386595, upload-time = "2024-09-25T10:20:53.932Z" }, ] [[package]] name = "ptyprocess" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, ] [[package]] name = "pure-eval" version = "0.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752 } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842 }, + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, ] [[package]] name = "pyarrow" version = "22.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/9b/cb3f7e0a345353def531ca879053e9ef6b9f38ed91aebcf68b09ba54dec0/pyarrow-22.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:77718810bd3066158db1e95a63c160ad7ce08c6b0710bc656055033e39cdad88", size = 34223968 }, - { url = "https://files.pythonhosted.org/packages/6c/41/3184b8192a120306270c5307f105b70320fdaa592c99843c5ef78aaefdcf/pyarrow-22.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:44d2d26cda26d18f7af7db71453b7b783788322d756e81730acb98f24eb90ace", size = 35942085 }, - { url = "https://files.pythonhosted.org/packages/d9/3d/a1eab2f6f08001f9fb714b8ed5cfb045e2fe3e3e3c0c221f2c9ed1e6d67d/pyarrow-22.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b9d71701ce97c95480fecb0039ec5bb889e75f110da72005743451339262f4ce", size = 44964613 }, - { url = "https://files.pythonhosted.org/packages/46/46/a1d9c24baf21cfd9ce994ac820a24608decf2710521b29223d4334985127/pyarrow-22.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:710624ab925dc2b05a6229d47f6f0dac1c1155e6ed559be7109f684eba048a48", size = 47627059 }, - { url = "https://files.pythonhosted.org/packages/3a/4c/f711acb13075c1391fd54bc17e078587672c575f8de2a6e62509af026dcf/pyarrow-22.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f963ba8c3b0199f9d6b794c90ec77545e05eadc83973897a4523c9e8d84e9340", size = 47947043 }, - { url = "https://files.pythonhosted.org/packages/4e/70/1f3180dd7c2eab35c2aca2b29ace6c519f827dcd4cfeb8e0dca41612cf7a/pyarrow-22.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd0d42297ace400d8febe55f13fdf46e86754842b860c978dfec16f081e5c653", size = 50206505 }, - { url = "https://files.pythonhosted.org/packages/80/07/fea6578112c8c60ffde55883a571e4c4c6bc7049f119d6b09333b5cc6f73/pyarrow-22.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:00626d9dc0f5ef3a75fe63fd68b9c7c8302d2b5bbc7f74ecaedba83447a24f84", size = 28101641 }, - { url = "https://files.pythonhosted.org/packages/2e/b7/18f611a8cdc43417f9394a3ccd3eace2f32183c08b9eddc3d17681819f37/pyarrow-22.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:3e294c5eadfb93d78b0763e859a0c16d4051fc1c5231ae8956d61cb0b5666f5a", size = 34272022 }, - { url = "https://files.pythonhosted.org/packages/26/5c/f259e2526c67eb4b9e511741b19870a02363a47a35edbebc55c3178db22d/pyarrow-22.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:69763ab2445f632d90b504a815a2a033f74332997052b721002298ed6de40f2e", size = 35995834 }, - { url = "https://files.pythonhosted.org/packages/50/8d/281f0f9b9376d4b7f146913b26fac0aa2829cd1ee7e997f53a27411bbb92/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b41f37cabfe2463232684de44bad753d6be08a7a072f6a83447eeaf0e4d2a215", size = 45030348 }, - { url = "https://files.pythonhosted.org/packages/f5/e5/53c0a1c428f0976bf22f513d79c73000926cb00b9c138d8e02daf2102e18/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:35ad0f0378c9359b3f297299c3309778bb03b8612f987399a0333a560b43862d", size = 47699480 }, - { url = "https://files.pythonhosted.org/packages/95/e1/9dbe4c465c3365959d183e6345d0a8d1dc5b02ca3f8db4760b3bc834cf25/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8382ad21458075c2e66a82a29d650f963ce51c7708c7c0ff313a8c206c4fd5e8", size = 48011148 }, - { url = "https://files.pythonhosted.org/packages/c5/b4/7caf5d21930061444c3cf4fa7535c82faf5263e22ce43af7c2759ceb5b8b/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a812a5b727bc09c3d7ea072c4eebf657c2f7066155506ba31ebf4792f88f016", size = 50276964 }, - { url = "https://files.pythonhosted.org/packages/ae/f3/cec89bd99fa3abf826f14d4e53d3d11340ce6f6af4d14bdcd54cd83b6576/pyarrow-22.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ec5d40dd494882704fb876c16fa7261a69791e784ae34e6b5992e977bd2e238c", size = 28106517 }, - { url = "https://files.pythonhosted.org/packages/af/63/ba23862d69652f85b615ca14ad14f3bcfc5bf1b99ef3f0cd04ff93fdad5a/pyarrow-22.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bea79263d55c24a32b0d79c00a1c58bb2ee5f0757ed95656b01c0fb310c5af3d", size = 34211578 }, - { url = "https://files.pythonhosted.org/packages/b1/d0/f9ad86fe809efd2bcc8be32032fa72e8b0d112b01ae56a053006376c5930/pyarrow-22.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:12fe549c9b10ac98c91cf791d2945e878875d95508e1a5d14091a7aaa66d9cf8", size = 35989906 }, - { url = "https://files.pythonhosted.org/packages/b4/a8/f910afcb14630e64d673f15904ec27dd31f1e009b77033c365c84e8c1e1d/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:334f900ff08ce0423407af97e6c26ad5d4e3b0763645559ece6fbf3747d6a8f5", size = 45021677 }, - { url = "https://files.pythonhosted.org/packages/13/95/aec81f781c75cd10554dc17a25849c720d54feafb6f7847690478dcf5ef8/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c6c791b09c57ed76a18b03f2631753a4960eefbbca80f846da8baefc6491fcfe", size = 47726315 }, - { url = "https://files.pythonhosted.org/packages/bb/d4/74ac9f7a54cfde12ee42734ea25d5a3c9a45db78f9def949307a92720d37/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c3200cb41cdbc65156e5f8c908d739b0dfed57e890329413da2748d1a2cd1a4e", size = 47990906 }, - { url = "https://files.pythonhosted.org/packages/2e/71/fedf2499bf7a95062eafc989ace56572f3343432570e1c54e6599d5b88da/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ac93252226cf288753d8b46280f4edf3433bf9508b6977f8dd8526b521a1bbb9", size = 50306783 }, - { url = "https://files.pythonhosted.org/packages/68/ed/b202abd5a5b78f519722f3d29063dda03c114711093c1995a33b8e2e0f4b/pyarrow-22.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:44729980b6c50a5f2bfcc2668d36c569ce17f8b17bccaf470c4313dcbbf13c9d", size = 27972883 }, - { url = "https://files.pythonhosted.org/packages/a6/d6/d0fac16a2963002fc22c8fa75180a838737203d558f0ed3b564c4a54eef5/pyarrow-22.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6e95176209257803a8b3d0394f21604e796dadb643d2f7ca21b66c9c0b30c9a", size = 34204629 }, - { url = "https://files.pythonhosted.org/packages/c6/9c/1d6357347fbae062ad3f17082f9ebc29cc733321e892c0d2085f42a2212b/pyarrow-22.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:001ea83a58024818826a9e3f89bf9310a114f7e26dfe404a4c32686f97bd7901", size = 35985783 }, - { url = "https://files.pythonhosted.org/packages/ff/c0/782344c2ce58afbea010150df07e3a2f5fdad299cd631697ae7bd3bac6e3/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ce20fe000754f477c8a9125543f1936ea5b8867c5406757c224d745ed033e691", size = 45020999 }, - { url = "https://files.pythonhosted.org/packages/1b/8b/5362443737a5307a7b67c1017c42cd104213189b4970bf607e05faf9c525/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e0a15757fccb38c410947df156f9749ae4a3c89b2393741a50521f39a8cf202a", size = 47724601 }, - { url = "https://files.pythonhosted.org/packages/69/4d/76e567a4fc2e190ee6072967cb4672b7d9249ac59ae65af2d7e3047afa3b/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cedb9dd9358e4ea1d9bce3665ce0797f6adf97ff142c8e25b46ba9cdd508e9b6", size = 48001050 }, - { url = "https://files.pythonhosted.org/packages/01/5e/5653f0535d2a1aef8223cee9d92944cb6bccfee5cf1cd3f462d7cb022790/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:252be4a05f9d9185bb8c18e83764ebcfea7185076c07a7a662253af3a8c07941", size = 50307877 }, - { url = "https://files.pythonhosted.org/packages/2d/f8/1d0bd75bf9328a3b826e24a16e5517cd7f9fbf8d34a3184a4566ef5a7f29/pyarrow-22.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:a4893d31e5ef780b6edcaf63122df0f8d321088bb0dee4c8c06eccb1ca28d145", size = 27977099 }, - { url = "https://files.pythonhosted.org/packages/90/81/db56870c997805bf2b0f6eeeb2d68458bf4654652dccdcf1bf7a42d80903/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:f7fe3dbe871294ba70d789be16b6e7e52b418311e166e0e3cba9522f0f437fb1", size = 34336685 }, - { url = "https://files.pythonhosted.org/packages/1c/98/0727947f199aba8a120f47dfc229eeb05df15bcd7a6f1b669e9f882afc58/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:ba95112d15fd4f1105fb2402c4eab9068f0554435e9b7085924bcfaac2cc306f", size = 36032158 }, - { url = "https://files.pythonhosted.org/packages/96/b4/9babdef9c01720a0785945c7cf550e4acd0ebcd7bdd2e6f0aa7981fa85e2/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c064e28361c05d72eed8e744c9605cbd6d2bb7481a511c74071fd9b24bc65d7d", size = 44892060 }, - { url = "https://files.pythonhosted.org/packages/f8/ca/2f8804edd6279f78a37062d813de3f16f29183874447ef6d1aadbb4efa0f/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6f9762274496c244d951c819348afbcf212714902742225f649cf02823a6a10f", size = 47504395 }, - { url = "https://files.pythonhosted.org/packages/b9/f0/77aa5198fd3943682b2e4faaf179a674f0edea0d55d326d83cb2277d9363/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a9d9ffdc2ab696f6b15b4d1f7cec6658e1d788124418cb30030afbae31c64746", size = 48066216 }, - { url = "https://files.pythonhosted.org/packages/79/87/a1937b6e78b2aff18b706d738c9e46ade5bfcf11b294e39c87706a0089ac/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ec1a15968a9d80da01e1d30349b2b0d7cc91e96588ee324ce1b5228175043e95", size = 50288552 }, - { url = "https://files.pythonhosted.org/packages/60/ae/b5a5811e11f25788ccfdaa8f26b6791c9807119dffcf80514505527c384c/pyarrow-22.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bba208d9c7decf9961998edf5c65e3ea4355d5818dd6cd0f6809bec1afb951cc", size = 28262504 }, - { url = "https://files.pythonhosted.org/packages/bd/b0/0fa4d28a8edb42b0a7144edd20befd04173ac79819547216f8a9f36f9e50/pyarrow-22.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9bddc2cade6561f6820d4cd73f99a0243532ad506bc510a75a5a65a522b2d74d", size = 34224062 }, - { url = "https://files.pythonhosted.org/packages/0f/a8/7a719076b3c1be0acef56a07220c586f25cd24de0e3f3102b438d18ae5df/pyarrow-22.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e70ff90c64419709d38c8932ea9fe1cc98415c4f87ea8da81719e43f02534bc9", size = 35990057 }, - { url = "https://files.pythonhosted.org/packages/89/3c/359ed54c93b47fb6fe30ed16cdf50e3f0e8b9ccfb11b86218c3619ae50a8/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:92843c305330aa94a36e706c16209cd4df274693e777ca47112617db7d0ef3d7", size = 45068002 }, - { url = "https://files.pythonhosted.org/packages/55/fc/4945896cc8638536ee787a3bd6ce7cec8ec9acf452d78ec39ab328efa0a1/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:6dda1ddac033d27421c20d7a7943eec60be44e0db4e079f33cc5af3b8280ccde", size = 47737765 }, - { url = "https://files.pythonhosted.org/packages/cd/5e/7cb7edeb2abfaa1f79b5d5eb89432356155c8426f75d3753cbcb9592c0fd/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:84378110dd9a6c06323b41b56e129c504d157d1a983ce8f5443761eb5256bafc", size = 48048139 }, - { url = "https://files.pythonhosted.org/packages/88/c6/546baa7c48185f5e9d6e59277c4b19f30f48c94d9dd938c2a80d4d6b067c/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:854794239111d2b88b40b6ef92aa478024d1e5074f364033e73e21e3f76b25e0", size = 50314244 }, - { url = "https://files.pythonhosted.org/packages/3c/79/755ff2d145aafec8d347bf18f95e4e81c00127f06d080135dfc86aea417c/pyarrow-22.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:b883fe6fd85adad7932b3271c38ac289c65b7337c2c132e9569f9d3940620730", size = 28757501 }, - { url = "https://files.pythonhosted.org/packages/0e/d2/237d75ac28ced3147912954e3c1a174df43a95f4f88e467809118a8165e0/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7a820d8ae11facf32585507c11f04e3f38343c1e784c9b5a8b1da5c930547fe2", size = 34355506 }, - { url = "https://files.pythonhosted.org/packages/1e/2c/733dfffe6d3069740f98e57ff81007809067d68626c5faef293434d11bd6/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:c6ec3675d98915bf1ec8b3c7986422682f7232ea76cad276f4c8abd5b7319b70", size = 36047312 }, - { url = "https://files.pythonhosted.org/packages/7c/2b/29d6e3782dc1f299727462c1543af357a0f2c1d3c160ce199950d9ca51eb/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3e739edd001b04f654b166204fc7a9de896cf6007eaff33409ee9e50ceaff754", size = 45081609 }, - { url = "https://files.pythonhosted.org/packages/8d/42/aa9355ecc05997915af1b7b947a7f66c02dcaa927f3203b87871c114ba10/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7388ac685cab5b279a41dfe0a6ccd99e4dbf322edfb63e02fc0443bf24134e91", size = 47703663 }, - { url = "https://files.pythonhosted.org/packages/ee/62/45abedde480168e83a1de005b7b7043fd553321c1e8c5a9a114425f64842/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f633074f36dbc33d5c05b5dc75371e5660f1dbf9c8b1d95669def05e5425989c", size = 48066543 }, - { url = "https://files.pythonhosted.org/packages/84/e9/7878940a5b072e4f3bf998770acafeae13b267f9893af5f6d4ab3904b67e/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4c19236ae2402a8663a2c8f21f1870a03cc57f0bef7e4b6eb3238cc82944de80", size = 50288838 }, - { url = "https://files.pythonhosted.org/packages/7b/03/f335d6c52b4a4761bcc83499789a1e2e16d9d201a58c327a9b5cc9a41bd9/pyarrow-22.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0c34fe18094686194f204a3b1787a27456897d8a2d62caf84b61e8dfbc0252ae", size = 29185594 }, +sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151, upload-time = "2025-10-24T12:30:00.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/9b/cb3f7e0a345353def531ca879053e9ef6b9f38ed91aebcf68b09ba54dec0/pyarrow-22.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:77718810bd3066158db1e95a63c160ad7ce08c6b0710bc656055033e39cdad88", size = 34223968, upload-time = "2025-10-24T10:03:31.21Z" }, + { url = "https://files.pythonhosted.org/packages/6c/41/3184b8192a120306270c5307f105b70320fdaa592c99843c5ef78aaefdcf/pyarrow-22.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:44d2d26cda26d18f7af7db71453b7b783788322d756e81730acb98f24eb90ace", size = 35942085, upload-time = "2025-10-24T10:03:38.146Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3d/a1eab2f6f08001f9fb714b8ed5cfb045e2fe3e3e3c0c221f2c9ed1e6d67d/pyarrow-22.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b9d71701ce97c95480fecb0039ec5bb889e75f110da72005743451339262f4ce", size = 44964613, upload-time = "2025-10-24T10:03:46.516Z" }, + { url = "https://files.pythonhosted.org/packages/46/46/a1d9c24baf21cfd9ce994ac820a24608decf2710521b29223d4334985127/pyarrow-22.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:710624ab925dc2b05a6229d47f6f0dac1c1155e6ed559be7109f684eba048a48", size = 47627059, upload-time = "2025-10-24T10:03:55.353Z" }, + { url = "https://files.pythonhosted.org/packages/3a/4c/f711acb13075c1391fd54bc17e078587672c575f8de2a6e62509af026dcf/pyarrow-22.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f963ba8c3b0199f9d6b794c90ec77545e05eadc83973897a4523c9e8d84e9340", size = 47947043, upload-time = "2025-10-24T10:04:05.408Z" }, + { url = "https://files.pythonhosted.org/packages/4e/70/1f3180dd7c2eab35c2aca2b29ace6c519f827dcd4cfeb8e0dca41612cf7a/pyarrow-22.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd0d42297ace400d8febe55f13fdf46e86754842b860c978dfec16f081e5c653", size = 50206505, upload-time = "2025-10-24T10:04:15.786Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/fea6578112c8c60ffde55883a571e4c4c6bc7049f119d6b09333b5cc6f73/pyarrow-22.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:00626d9dc0f5ef3a75fe63fd68b9c7c8302d2b5bbc7f74ecaedba83447a24f84", size = 28101641, upload-time = "2025-10-24T10:04:22.57Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b7/18f611a8cdc43417f9394a3ccd3eace2f32183c08b9eddc3d17681819f37/pyarrow-22.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:3e294c5eadfb93d78b0763e859a0c16d4051fc1c5231ae8956d61cb0b5666f5a", size = 34272022, upload-time = "2025-10-24T10:04:28.973Z" }, + { url = "https://files.pythonhosted.org/packages/26/5c/f259e2526c67eb4b9e511741b19870a02363a47a35edbebc55c3178db22d/pyarrow-22.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:69763ab2445f632d90b504a815a2a033f74332997052b721002298ed6de40f2e", size = 35995834, upload-time = "2025-10-24T10:04:35.467Z" }, + { url = "https://files.pythonhosted.org/packages/50/8d/281f0f9b9376d4b7f146913b26fac0aa2829cd1ee7e997f53a27411bbb92/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b41f37cabfe2463232684de44bad753d6be08a7a072f6a83447eeaf0e4d2a215", size = 45030348, upload-time = "2025-10-24T10:04:43.366Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e5/53c0a1c428f0976bf22f513d79c73000926cb00b9c138d8e02daf2102e18/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:35ad0f0378c9359b3f297299c3309778bb03b8612f987399a0333a560b43862d", size = 47699480, upload-time = "2025-10-24T10:04:51.486Z" }, + { url = "https://files.pythonhosted.org/packages/95/e1/9dbe4c465c3365959d183e6345d0a8d1dc5b02ca3f8db4760b3bc834cf25/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8382ad21458075c2e66a82a29d650f963ce51c7708c7c0ff313a8c206c4fd5e8", size = 48011148, upload-time = "2025-10-24T10:04:59.585Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b4/7caf5d21930061444c3cf4fa7535c82faf5263e22ce43af7c2759ceb5b8b/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a812a5b727bc09c3d7ea072c4eebf657c2f7066155506ba31ebf4792f88f016", size = 50276964, upload-time = "2025-10-24T10:05:08.175Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f3/cec89bd99fa3abf826f14d4e53d3d11340ce6f6af4d14bdcd54cd83b6576/pyarrow-22.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ec5d40dd494882704fb876c16fa7261a69791e784ae34e6b5992e977bd2e238c", size = 28106517, upload-time = "2025-10-24T10:05:14.314Z" }, + { url = "https://files.pythonhosted.org/packages/af/63/ba23862d69652f85b615ca14ad14f3bcfc5bf1b99ef3f0cd04ff93fdad5a/pyarrow-22.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bea79263d55c24a32b0d79c00a1c58bb2ee5f0757ed95656b01c0fb310c5af3d", size = 34211578, upload-time = "2025-10-24T10:05:21.583Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d0/f9ad86fe809efd2bcc8be32032fa72e8b0d112b01ae56a053006376c5930/pyarrow-22.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:12fe549c9b10ac98c91cf791d2945e878875d95508e1a5d14091a7aaa66d9cf8", size = 35989906, upload-time = "2025-10-24T10:05:29.485Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a8/f910afcb14630e64d673f15904ec27dd31f1e009b77033c365c84e8c1e1d/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:334f900ff08ce0423407af97e6c26ad5d4e3b0763645559ece6fbf3747d6a8f5", size = 45021677, upload-time = "2025-10-24T10:05:38.274Z" }, + { url = "https://files.pythonhosted.org/packages/13/95/aec81f781c75cd10554dc17a25849c720d54feafb6f7847690478dcf5ef8/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c6c791b09c57ed76a18b03f2631753a4960eefbbca80f846da8baefc6491fcfe", size = 47726315, upload-time = "2025-10-24T10:05:47.314Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d4/74ac9f7a54cfde12ee42734ea25d5a3c9a45db78f9def949307a92720d37/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c3200cb41cdbc65156e5f8c908d739b0dfed57e890329413da2748d1a2cd1a4e", size = 47990906, upload-time = "2025-10-24T10:05:58.254Z" }, + { url = "https://files.pythonhosted.org/packages/2e/71/fedf2499bf7a95062eafc989ace56572f3343432570e1c54e6599d5b88da/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ac93252226cf288753d8b46280f4edf3433bf9508b6977f8dd8526b521a1bbb9", size = 50306783, upload-time = "2025-10-24T10:06:08.08Z" }, + { url = "https://files.pythonhosted.org/packages/68/ed/b202abd5a5b78f519722f3d29063dda03c114711093c1995a33b8e2e0f4b/pyarrow-22.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:44729980b6c50a5f2bfcc2668d36c569ce17f8b17bccaf470c4313dcbbf13c9d", size = 27972883, upload-time = "2025-10-24T10:06:14.204Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d6/d0fac16a2963002fc22c8fa75180a838737203d558f0ed3b564c4a54eef5/pyarrow-22.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6e95176209257803a8b3d0394f21604e796dadb643d2f7ca21b66c9c0b30c9a", size = 34204629, upload-time = "2025-10-24T10:06:20.274Z" }, + { url = "https://files.pythonhosted.org/packages/c6/9c/1d6357347fbae062ad3f17082f9ebc29cc733321e892c0d2085f42a2212b/pyarrow-22.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:001ea83a58024818826a9e3f89bf9310a114f7e26dfe404a4c32686f97bd7901", size = 35985783, upload-time = "2025-10-24T10:06:27.301Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/782344c2ce58afbea010150df07e3a2f5fdad299cd631697ae7bd3bac6e3/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ce20fe000754f477c8a9125543f1936ea5b8867c5406757c224d745ed033e691", size = 45020999, upload-time = "2025-10-24T10:06:35.387Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8b/5362443737a5307a7b67c1017c42cd104213189b4970bf607e05faf9c525/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e0a15757fccb38c410947df156f9749ae4a3c89b2393741a50521f39a8cf202a", size = 47724601, upload-time = "2025-10-24T10:06:43.551Z" }, + { url = "https://files.pythonhosted.org/packages/69/4d/76e567a4fc2e190ee6072967cb4672b7d9249ac59ae65af2d7e3047afa3b/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cedb9dd9358e4ea1d9bce3665ce0797f6adf97ff142c8e25b46ba9cdd508e9b6", size = 48001050, upload-time = "2025-10-24T10:06:52.284Z" }, + { url = "https://files.pythonhosted.org/packages/01/5e/5653f0535d2a1aef8223cee9d92944cb6bccfee5cf1cd3f462d7cb022790/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:252be4a05f9d9185bb8c18e83764ebcfea7185076c07a7a662253af3a8c07941", size = 50307877, upload-time = "2025-10-24T10:07:02.405Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f8/1d0bd75bf9328a3b826e24a16e5517cd7f9fbf8d34a3184a4566ef5a7f29/pyarrow-22.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:a4893d31e5ef780b6edcaf63122df0f8d321088bb0dee4c8c06eccb1ca28d145", size = 27977099, upload-time = "2025-10-24T10:08:07.259Z" }, + { url = "https://files.pythonhosted.org/packages/90/81/db56870c997805bf2b0f6eeeb2d68458bf4654652dccdcf1bf7a42d80903/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:f7fe3dbe871294ba70d789be16b6e7e52b418311e166e0e3cba9522f0f437fb1", size = 34336685, upload-time = "2025-10-24T10:07:11.47Z" }, + { url = "https://files.pythonhosted.org/packages/1c/98/0727947f199aba8a120f47dfc229eeb05df15bcd7a6f1b669e9f882afc58/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:ba95112d15fd4f1105fb2402c4eab9068f0554435e9b7085924bcfaac2cc306f", size = 36032158, upload-time = "2025-10-24T10:07:18.626Z" }, + { url = "https://files.pythonhosted.org/packages/96/b4/9babdef9c01720a0785945c7cf550e4acd0ebcd7bdd2e6f0aa7981fa85e2/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c064e28361c05d72eed8e744c9605cbd6d2bb7481a511c74071fd9b24bc65d7d", size = 44892060, upload-time = "2025-10-24T10:07:26.002Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ca/2f8804edd6279f78a37062d813de3f16f29183874447ef6d1aadbb4efa0f/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6f9762274496c244d951c819348afbcf212714902742225f649cf02823a6a10f", size = 47504395, upload-time = "2025-10-24T10:07:34.09Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f0/77aa5198fd3943682b2e4faaf179a674f0edea0d55d326d83cb2277d9363/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a9d9ffdc2ab696f6b15b4d1f7cec6658e1d788124418cb30030afbae31c64746", size = 48066216, upload-time = "2025-10-24T10:07:43.528Z" }, + { url = "https://files.pythonhosted.org/packages/79/87/a1937b6e78b2aff18b706d738c9e46ade5bfcf11b294e39c87706a0089ac/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ec1a15968a9d80da01e1d30349b2b0d7cc91e96588ee324ce1b5228175043e95", size = 50288552, upload-time = "2025-10-24T10:07:53.519Z" }, + { url = "https://files.pythonhosted.org/packages/60/ae/b5a5811e11f25788ccfdaa8f26b6791c9807119dffcf80514505527c384c/pyarrow-22.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bba208d9c7decf9961998edf5c65e3ea4355d5818dd6cd0f6809bec1afb951cc", size = 28262504, upload-time = "2025-10-24T10:08:00.932Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b0/0fa4d28a8edb42b0a7144edd20befd04173ac79819547216f8a9f36f9e50/pyarrow-22.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9bddc2cade6561f6820d4cd73f99a0243532ad506bc510a75a5a65a522b2d74d", size = 34224062, upload-time = "2025-10-24T10:08:14.101Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a8/7a719076b3c1be0acef56a07220c586f25cd24de0e3f3102b438d18ae5df/pyarrow-22.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e70ff90c64419709d38c8932ea9fe1cc98415c4f87ea8da81719e43f02534bc9", size = 35990057, upload-time = "2025-10-24T10:08:21.842Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/359ed54c93b47fb6fe30ed16cdf50e3f0e8b9ccfb11b86218c3619ae50a8/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:92843c305330aa94a36e706c16209cd4df274693e777ca47112617db7d0ef3d7", size = 45068002, upload-time = "2025-10-24T10:08:29.034Z" }, + { url = "https://files.pythonhosted.org/packages/55/fc/4945896cc8638536ee787a3bd6ce7cec8ec9acf452d78ec39ab328efa0a1/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:6dda1ddac033d27421c20d7a7943eec60be44e0db4e079f33cc5af3b8280ccde", size = 47737765, upload-time = "2025-10-24T10:08:38.559Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5e/7cb7edeb2abfaa1f79b5d5eb89432356155c8426f75d3753cbcb9592c0fd/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:84378110dd9a6c06323b41b56e129c504d157d1a983ce8f5443761eb5256bafc", size = 48048139, upload-time = "2025-10-24T10:08:46.784Z" }, + { url = "https://files.pythonhosted.org/packages/88/c6/546baa7c48185f5e9d6e59277c4b19f30f48c94d9dd938c2a80d4d6b067c/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:854794239111d2b88b40b6ef92aa478024d1e5074f364033e73e21e3f76b25e0", size = 50314244, upload-time = "2025-10-24T10:08:55.771Z" }, + { url = "https://files.pythonhosted.org/packages/3c/79/755ff2d145aafec8d347bf18f95e4e81c00127f06d080135dfc86aea417c/pyarrow-22.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:b883fe6fd85adad7932b3271c38ac289c65b7337c2c132e9569f9d3940620730", size = 28757501, upload-time = "2025-10-24T10:09:59.891Z" }, + { url = "https://files.pythonhosted.org/packages/0e/d2/237d75ac28ced3147912954e3c1a174df43a95f4f88e467809118a8165e0/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7a820d8ae11facf32585507c11f04e3f38343c1e784c9b5a8b1da5c930547fe2", size = 34355506, upload-time = "2025-10-24T10:09:02.953Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/733dfffe6d3069740f98e57ff81007809067d68626c5faef293434d11bd6/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:c6ec3675d98915bf1ec8b3c7986422682f7232ea76cad276f4c8abd5b7319b70", size = 36047312, upload-time = "2025-10-24T10:09:10.334Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2b/29d6e3782dc1f299727462c1543af357a0f2c1d3c160ce199950d9ca51eb/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3e739edd001b04f654b166204fc7a9de896cf6007eaff33409ee9e50ceaff754", size = 45081609, upload-time = "2025-10-24T10:09:18.61Z" }, + { url = "https://files.pythonhosted.org/packages/8d/42/aa9355ecc05997915af1b7b947a7f66c02dcaa927f3203b87871c114ba10/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7388ac685cab5b279a41dfe0a6ccd99e4dbf322edfb63e02fc0443bf24134e91", size = 47703663, upload-time = "2025-10-24T10:09:27.369Z" }, + { url = "https://files.pythonhosted.org/packages/ee/62/45abedde480168e83a1de005b7b7043fd553321c1e8c5a9a114425f64842/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f633074f36dbc33d5c05b5dc75371e5660f1dbf9c8b1d95669def05e5425989c", size = 48066543, upload-time = "2025-10-24T10:09:34.908Z" }, + { url = "https://files.pythonhosted.org/packages/84/e9/7878940a5b072e4f3bf998770acafeae13b267f9893af5f6d4ab3904b67e/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4c19236ae2402a8663a2c8f21f1870a03cc57f0bef7e4b6eb3238cc82944de80", size = 50288838, upload-time = "2025-10-24T10:09:44.394Z" }, + { url = "https://files.pythonhosted.org/packages/7b/03/f335d6c52b4a4761bcc83499789a1e2e16d9d201a58c327a9b5cc9a41bd9/pyarrow-22.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0c34fe18094686194f204a3b1787a27456897d8a2d62caf84b61e8dfbc0252ae", size = 29185594, upload-time = "2025-10-24T10:09:53.111Z" }, ] [[package]] name = "pycparser" version = "2.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, ] [[package]] @@ -996,9 +1133,9 @@ dependencies = [ { name = "docutils" }, { name = "sphinx" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/d6/3921de802cf1ee771f0e76c9068b52498aeb8eeec6b830ff931c81c7ecf3/pydata_sphinx_theme-0.8.0.tar.gz", hash = "sha256:9f72015d9c572ea92e3007ab221a8325767c426783b6b9941813e65fa988dc90", size = 1123746 } +sdist = { url = "https://files.pythonhosted.org/packages/fc/d6/3921de802cf1ee771f0e76c9068b52498aeb8eeec6b830ff931c81c7ecf3/pydata_sphinx_theme-0.8.0.tar.gz", hash = "sha256:9f72015d9c572ea92e3007ab221a8325767c426783b6b9941813e65fa988dc90", size = 1123746, upload-time = "2022-01-15T19:25:25.712Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/26/0694318d46c7d90ab602ae27b24431e939f1600f9a4c69d1e727ec57289f/pydata_sphinx_theme-0.8.0-py3-none-any.whl", hash = "sha256:fbcbb833a07d3ad8dd997dd40dc94da18d98b41c68123ab0182b58fe92271204", size = 3284997 }, + { url = "https://files.pythonhosted.org/packages/91/26/0694318d46c7d90ab602ae27b24431e939f1600f9a4c69d1e727ec57289f/pydata_sphinx_theme-0.8.0-py3-none-any.whl", hash = "sha256:fbcbb833a07d3ad8dd997dd40dc94da18d98b41c68123ab0182b58fe92271204", size = 3284997, upload-time = "2022-01-15T19:25:23.807Z" }, ] [[package]] @@ -1013,27 +1150,27 @@ dependencies = [ { name = "typing-extensions" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/16/ce/aa91d30040d9552c274e7ea8bd10a977600d508d579a4bb262b95eccf961/pygithub-2.5.0.tar.gz", hash = "sha256:e1613ac508a9be710920d26eb18b1905ebd9926aa49398e88151c1b526aad3cf", size = 3552804 } +sdist = { url = "https://files.pythonhosted.org/packages/16/ce/aa91d30040d9552c274e7ea8bd10a977600d508d579a4bb262b95eccf961/pygithub-2.5.0.tar.gz", hash = "sha256:e1613ac508a9be710920d26eb18b1905ebd9926aa49398e88151c1b526aad3cf", size = 3552804, upload-time = "2024-11-06T20:50:07.168Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/05/bfbdbbc5d8aafd8dae9b3b6877edca561fccd8528ef5edc4e7b6d23721b5/PyGithub-2.5.0-py3-none-any.whl", hash = "sha256:b0b635999a658ab8e08720bdd3318893ff20e2275f6446fcf35bf3f44f2c0fd2", size = 375935 }, + { url = "https://files.pythonhosted.org/packages/37/05/bfbdbbc5d8aafd8dae9b3b6877edca561fccd8528ef5edc4e7b6d23721b5/PyGithub-2.5.0-py3-none-any.whl", hash = "sha256:b0b635999a658ab8e08720bdd3318893ff20e2275f6446fcf35bf3f44f2c0fd2", size = 375935, upload-time = "2024-11-06T20:50:04.931Z" }, ] [[package]] name = "pygments" version = "2.19.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, ] [[package]] name = "pyjwt" version = "2.10.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 }, + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, ] [package.optional-dependencies] @@ -1048,17 +1185,17 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/22/27582568be639dfe22ddb3902225f91f2f17ceff88ce80e4db396c8986da/PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", size = 3392854 } +sdist = { url = "https://files.pythonhosted.org/packages/a7/22/27582568be639dfe22ddb3902225f91f2f17ceff88ce80e4db396c8986da/PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", size = 3392854, upload-time = "2022-01-07T22:05:41.134Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/75/0b8ede18506041c0bf23ac4d8e2971b4161cd6ce630b177d0a08eb0d8857/PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", size = 349920 }, - { url = "https://files.pythonhosted.org/packages/59/bb/fddf10acd09637327a97ef89d2a9d621328850a72f1fdc8c08bdf72e385f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", size = 601722 }, - { url = "https://files.pythonhosted.org/packages/5d/70/87a065c37cca41a75f2ce113a5a2c2aa7533be648b184ade58971b5f7ccc/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", size = 680087 }, - { url = "https://files.pythonhosted.org/packages/ee/87/f1bb6a595f14a327e8285b9eb54d41fef76c585a0edef0a45f6fc95de125/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", size = 856678 }, - { url = "https://files.pythonhosted.org/packages/66/28/ca86676b69bf9f90e710571b67450508484388bfce09acf8a46f0b8c785f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", size = 1133660 }, - { url = "https://files.pythonhosted.org/packages/3d/85/c262db650e86812585e2bc59e497a8f59948a005325a11bbbc9ecd3fe26b/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", size = 663824 }, - { url = "https://files.pythonhosted.org/packages/fd/1a/cc308a884bd299b651f1633acb978e8596c71c33ca85e9dc9fa33a5399b9/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", size = 1117912 }, - { url = "https://files.pythonhosted.org/packages/25/2d/b7df6ddb0c2a33afdb358f8af6ea3b8c4d1196ca45497dd37a56f0c122be/PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543", size = 204624 }, - { url = "https://files.pythonhosted.org/packages/5e/22/d3db169895faaf3e2eda892f005f433a62db2decbcfbc2f61e6517adfa87/PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", size = 212141 }, + { url = "https://files.pythonhosted.org/packages/ce/75/0b8ede18506041c0bf23ac4d8e2971b4161cd6ce630b177d0a08eb0d8857/PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", size = 349920, upload-time = "2022-01-07T22:05:49.156Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/fddf10acd09637327a97ef89d2a9d621328850a72f1fdc8c08bdf72e385f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", size = 601722, upload-time = "2022-01-07T22:05:50.989Z" }, + { url = "https://files.pythonhosted.org/packages/5d/70/87a065c37cca41a75f2ce113a5a2c2aa7533be648b184ade58971b5f7ccc/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", size = 680087, upload-time = "2022-01-07T22:05:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/ee/87/f1bb6a595f14a327e8285b9eb54d41fef76c585a0edef0a45f6fc95de125/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", size = 856678, upload-time = "2022-01-07T22:05:54.251Z" }, + { url = "https://files.pythonhosted.org/packages/66/28/ca86676b69bf9f90e710571b67450508484388bfce09acf8a46f0b8c785f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", size = 1133660, upload-time = "2022-01-07T22:05:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/3d/85/c262db650e86812585e2bc59e497a8f59948a005325a11bbbc9ecd3fe26b/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", size = 663824, upload-time = "2022-01-07T22:05:57.434Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1a/cc308a884bd299b651f1633acb978e8596c71c33ca85e9dc9fa33a5399b9/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", size = 1117912, upload-time = "2022-01-07T22:05:58.665Z" }, + { url = "https://files.pythonhosted.org/packages/25/2d/b7df6ddb0c2a33afdb358f8af6ea3b8c4d1196ca45497dd37a56f0c122be/PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543", size = 204624, upload-time = "2022-01-07T22:06:00.085Z" }, + { url = "https://files.pythonhosted.org/packages/5e/22/d3db169895faaf3e2eda892f005f433a62db2decbcfbc2f61e6517adfa87/PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", size = 212141, upload-time = "2022-01-07T22:06:01.861Z" }, ] [[package]] @@ -1073,9 +1210,9 @@ dependencies = [ { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } +sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919, upload-time = "2024-12-01T12:54:25.98Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, + { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083, upload-time = "2024-12-01T12:54:19.735Z" }, ] [[package]] @@ -1085,9 +1222,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f2/a8/ecbc8ede70921dd2f544ab1cadd3ff3bf842af27f87bbdea774c7baa1d38/pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a", size = 54239 } +sdist = { url = "https://files.pythonhosted.org/packages/f2/a8/ecbc8ede70921dd2f544ab1cadd3ff3bf842af27f87bbdea774c7baa1d38/pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a", size = 54239, upload-time = "2025-01-28T18:37:58.729Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/17/3493c5624e48fd97156ebaec380dcaafee9506d7e2c46218ceebbb57d7de/pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3", size = 19467 }, + { url = "https://files.pythonhosted.org/packages/67/17/3493c5624e48fd97156ebaec380dcaafee9506d7e2c46218ceebbb57d7de/pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3", size = 19467, upload-time = "2025-01-28T18:37:56.798Z" }, ] [[package]] @@ -1097,82 +1234,82 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "pytz" version = "2024.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 } +sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692, upload-time = "2024-09-11T02:24:47.91Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 }, + { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002, upload-time = "2024-09-11T02:24:45.8Z" }, ] [[package]] name = "pyyaml" version = "6.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227 }, - { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019 }, - { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646 }, - { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793 }, - { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293 }, - { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872 }, - { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828 }, - { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415 }, - { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561 }, - { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826 }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577 }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556 }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114 }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638 }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463 }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986 }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543 }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763 }, - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063 }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973 }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116 }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011 }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870 }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089 }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181 }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658 }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003 }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344 }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669 }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252 }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081 }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159 }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626 }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613 }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115 }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814 }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809 }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454 }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355 }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175 }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228 }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194 }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429 }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912 }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108 }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641 }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901 }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132 }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261 }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272 }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923 }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062 }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 }, +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] @@ -1185,70 +1322,70 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, ] [[package]] name = "ruff" version = "0.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/67/3e/e89f736f01aa9517a97e2e7e0ce8d34a4d8207087b3cfdec95133fee13b5/ruff-0.9.1.tar.gz", hash = "sha256:fd2b25ecaf907d6458fa842675382c8597b3c746a2dde6717fe3415425df0c17", size = 3498844 } +sdist = { url = "https://files.pythonhosted.org/packages/67/3e/e89f736f01aa9517a97e2e7e0ce8d34a4d8207087b3cfdec95133fee13b5/ruff-0.9.1.tar.gz", hash = "sha256:fd2b25ecaf907d6458fa842675382c8597b3c746a2dde6717fe3415425df0c17", size = 3498844, upload-time = "2025-01-10T18:57:53.896Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/05/c3a2e0feb3d5d394cdfd552de01df9d3ec8a3a3771bbff247fab7e668653/ruff-0.9.1-py3-none-linux_armv6l.whl", hash = "sha256:84330dda7abcc270e6055551aca93fdde1b0685fc4fd358f26410f9349cf1743", size = 10645241 }, - { url = "https://files.pythonhosted.org/packages/dd/da/59f0a40e5f88ee5c054ad175caaa2319fc96571e1d29ab4730728f2aad4f/ruff-0.9.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3cae39ba5d137054b0e5b472aee3b78a7c884e61591b100aeb544bcd1fc38d4f", size = 10391066 }, - { url = "https://files.pythonhosted.org/packages/b7/fe/85e1c1acf0ba04a3f2d54ae61073da030f7a5dc386194f96f3c6ca444a78/ruff-0.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:50c647ff96f4ba288db0ad87048257753733763b409b2faf2ea78b45c8bb7fcb", size = 10012308 }, - { url = "https://files.pythonhosted.org/packages/6f/9b/780aa5d4bdca8dcea4309264b8faa304bac30e1ce0bcc910422bfcadd203/ruff-0.9.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0c8b149e9c7353cace7d698e1656ffcf1e36e50f8ea3b5d5f7f87ff9986a7ca", size = 10881960 }, - { url = "https://files.pythonhosted.org/packages/12/f4/dac4361afbfe520afa7186439e8094e4884ae3b15c8fc75fb2e759c1f267/ruff-0.9.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:beb3298604540c884d8b282fe7625651378e1986c25df51dec5b2f60cafc31ce", size = 10414803 }, - { url = "https://files.pythonhosted.org/packages/f0/a2/057a3cb7999513cb78d6cb33a7d1cc6401c82d7332583786e4dad9e38e44/ruff-0.9.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39d0174ccc45c439093971cc06ed3ac4dc545f5e8bdacf9f067adf879544d969", size = 11464929 }, - { url = "https://files.pythonhosted.org/packages/eb/c6/1ccfcc209bee465ced4874dcfeaadc88aafcc1ea9c9f31ef66f063c187f0/ruff-0.9.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:69572926c0f0c9912288915214ca9b2809525ea263603370b9e00bed2ba56dbd", size = 12170717 }, - { url = "https://files.pythonhosted.org/packages/84/97/4a524027518525c7cf6931e9fd3b2382be5e4b75b2b61bec02681a7685a5/ruff-0.9.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:937267afce0c9170d6d29f01fcd1f4378172dec6760a9f4dface48cdabf9610a", size = 11708921 }, - { url = "https://files.pythonhosted.org/packages/a6/a4/4e77cf6065c700d5593b25fca6cf725b1ab6d70674904f876254d0112ed0/ruff-0.9.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:186c2313de946f2c22bdf5954b8dd083e124bcfb685732cfb0beae0c47233d9b", size = 13058074 }, - { url = "https://files.pythonhosted.org/packages/f9/d6/fcb78e0531e863d0a952c4c5600cc5cd317437f0e5f031cd2288b117bb37/ruff-0.9.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f94942a3bb767675d9a051867c036655fe9f6c8a491539156a6f7e6b5f31831", size = 11281093 }, - { url = "https://files.pythonhosted.org/packages/e4/3b/7235bbeff00c95dc2d073cfdbf2b871b5bbf476754c5d277815d286b4328/ruff-0.9.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:728d791b769cc28c05f12c280f99e8896932e9833fef1dd8756a6af2261fd1ab", size = 10882610 }, - { url = "https://files.pythonhosted.org/packages/2a/66/5599d23257c61cf038137f82999ca8f9d0080d9d5134440a461bef85b461/ruff-0.9.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2f312c86fb40c5c02b44a29a750ee3b21002bd813b5233facdaf63a51d9a85e1", size = 10489273 }, - { url = "https://files.pythonhosted.org/packages/78/85/de4aa057e2532db0f9761e2c2c13834991e087787b93e4aeb5f1cb10d2df/ruff-0.9.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ae017c3a29bee341ba584f3823f805abbe5fe9cd97f87ed07ecbf533c4c88366", size = 11003314 }, - { url = "https://files.pythonhosted.org/packages/00/42/afedcaa089116d81447347f76041ff46025849fedb0ed2b187d24cf70fca/ruff-0.9.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5dc40a378a0e21b4cfe2b8a0f1812a6572fc7b230ef12cd9fac9161aa91d807f", size = 11342982 }, - { url = "https://files.pythonhosted.org/packages/39/c6/fe45f3eb27e3948b41a305d8b768e949bf6a39310e9df73f6c576d7f1d9f/ruff-0.9.1-py3-none-win32.whl", hash = "sha256:46ebf5cc106cf7e7378ca3c28ce4293b61b449cd121b98699be727d40b79ba72", size = 8819750 }, - { url = "https://files.pythonhosted.org/packages/38/8d/580db77c3b9d5c3d9479e55b0b832d279c30c8f00ab0190d4cd8fc67831c/ruff-0.9.1-py3-none-win_amd64.whl", hash = "sha256:342a824b46ddbcdddd3abfbb332fa7fcaac5488bf18073e841236aadf4ad5c19", size = 9701331 }, - { url = "https://files.pythonhosted.org/packages/b2/94/0498cdb7316ed67a1928300dd87d659c933479f44dec51b4f62bfd1f8028/ruff-0.9.1-py3-none-win_arm64.whl", hash = "sha256:1cd76c7f9c679e6e8f2af8f778367dca82b95009bc7b1a85a47f1521ae524fa7", size = 9145708 }, + { url = "https://files.pythonhosted.org/packages/dc/05/c3a2e0feb3d5d394cdfd552de01df9d3ec8a3a3771bbff247fab7e668653/ruff-0.9.1-py3-none-linux_armv6l.whl", hash = "sha256:84330dda7abcc270e6055551aca93fdde1b0685fc4fd358f26410f9349cf1743", size = 10645241, upload-time = "2025-01-10T18:56:45.897Z" }, + { url = "https://files.pythonhosted.org/packages/dd/da/59f0a40e5f88ee5c054ad175caaa2319fc96571e1d29ab4730728f2aad4f/ruff-0.9.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3cae39ba5d137054b0e5b472aee3b78a7c884e61591b100aeb544bcd1fc38d4f", size = 10391066, upload-time = "2025-01-10T18:56:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/85e1c1acf0ba04a3f2d54ae61073da030f7a5dc386194f96f3c6ca444a78/ruff-0.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:50c647ff96f4ba288db0ad87048257753733763b409b2faf2ea78b45c8bb7fcb", size = 10012308, upload-time = "2025-01-10T18:56:55.426Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9b/780aa5d4bdca8dcea4309264b8faa304bac30e1ce0bcc910422bfcadd203/ruff-0.9.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0c8b149e9c7353cace7d698e1656ffcf1e36e50f8ea3b5d5f7f87ff9986a7ca", size = 10881960, upload-time = "2025-01-10T18:56:59.539Z" }, + { url = "https://files.pythonhosted.org/packages/12/f4/dac4361afbfe520afa7186439e8094e4884ae3b15c8fc75fb2e759c1f267/ruff-0.9.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:beb3298604540c884d8b282fe7625651378e1986c25df51dec5b2f60cafc31ce", size = 10414803, upload-time = "2025-01-10T18:57:04.919Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a2/057a3cb7999513cb78d6cb33a7d1cc6401c82d7332583786e4dad9e38e44/ruff-0.9.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39d0174ccc45c439093971cc06ed3ac4dc545f5e8bdacf9f067adf879544d969", size = 11464929, upload-time = "2025-01-10T18:57:08.146Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c6/1ccfcc209bee465ced4874dcfeaadc88aafcc1ea9c9f31ef66f063c187f0/ruff-0.9.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:69572926c0f0c9912288915214ca9b2809525ea263603370b9e00bed2ba56dbd", size = 12170717, upload-time = "2025-01-10T18:57:12.564Z" }, + { url = "https://files.pythonhosted.org/packages/84/97/4a524027518525c7cf6931e9fd3b2382be5e4b75b2b61bec02681a7685a5/ruff-0.9.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:937267afce0c9170d6d29f01fcd1f4378172dec6760a9f4dface48cdabf9610a", size = 11708921, upload-time = "2025-01-10T18:57:17.216Z" }, + { url = "https://files.pythonhosted.org/packages/a6/a4/4e77cf6065c700d5593b25fca6cf725b1ab6d70674904f876254d0112ed0/ruff-0.9.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:186c2313de946f2c22bdf5954b8dd083e124bcfb685732cfb0beae0c47233d9b", size = 13058074, upload-time = "2025-01-10T18:57:20.57Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d6/fcb78e0531e863d0a952c4c5600cc5cd317437f0e5f031cd2288b117bb37/ruff-0.9.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f94942a3bb767675d9a051867c036655fe9f6c8a491539156a6f7e6b5f31831", size = 11281093, upload-time = "2025-01-10T18:57:25.526Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3b/7235bbeff00c95dc2d073cfdbf2b871b5bbf476754c5d277815d286b4328/ruff-0.9.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:728d791b769cc28c05f12c280f99e8896932e9833fef1dd8756a6af2261fd1ab", size = 10882610, upload-time = "2025-01-10T18:57:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/2a/66/5599d23257c61cf038137f82999ca8f9d0080d9d5134440a461bef85b461/ruff-0.9.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2f312c86fb40c5c02b44a29a750ee3b21002bd813b5233facdaf63a51d9a85e1", size = 10489273, upload-time = "2025-01-10T18:57:32.219Z" }, + { url = "https://files.pythonhosted.org/packages/78/85/de4aa057e2532db0f9761e2c2c13834991e087787b93e4aeb5f1cb10d2df/ruff-0.9.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ae017c3a29bee341ba584f3823f805abbe5fe9cd97f87ed07ecbf533c4c88366", size = 11003314, upload-time = "2025-01-10T18:57:35.431Z" }, + { url = "https://files.pythonhosted.org/packages/00/42/afedcaa089116d81447347f76041ff46025849fedb0ed2b187d24cf70fca/ruff-0.9.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5dc40a378a0e21b4cfe2b8a0f1812a6572fc7b230ef12cd9fac9161aa91d807f", size = 11342982, upload-time = "2025-01-10T18:57:38.642Z" }, + { url = "https://files.pythonhosted.org/packages/39/c6/fe45f3eb27e3948b41a305d8b768e949bf6a39310e9df73f6c576d7f1d9f/ruff-0.9.1-py3-none-win32.whl", hash = "sha256:46ebf5cc106cf7e7378ca3c28ce4293b61b449cd121b98699be727d40b79ba72", size = 8819750, upload-time = "2025-01-10T18:57:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/38/8d/580db77c3b9d5c3d9479e55b0b832d279c30c8f00ab0190d4cd8fc67831c/ruff-0.9.1-py3-none-win_amd64.whl", hash = "sha256:342a824b46ddbcdddd3abfbb332fa7fcaac5488bf18073e841236aadf4ad5c19", size = 9701331, upload-time = "2025-01-10T18:57:46.334Z" }, + { url = "https://files.pythonhosted.org/packages/b2/94/0498cdb7316ed67a1928300dd87d659c933479f44dec51b4f62bfd1f8028/ruff-0.9.1-py3-none-win_arm64.whl", hash = "sha256:1cd76c7f9c679e6e8f2af8f778367dca82b95009bc7b1a85a47f1521ae524fa7", size = 9145708, upload-time = "2025-01-10T18:57:51.308Z" }, ] [[package]] name = "setuptools" version = "75.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/92/ec/089608b791d210aec4e7f97488e67ab0d33add3efccb83a056cbafe3a2a6/setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6", size = 1343222 } +sdist = { url = "https://files.pythonhosted.org/packages/92/ec/089608b791d210aec4e7f97488e67ab0d33add3efccb83a056cbafe3a2a6/setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6", size = 1343222, upload-time = "2025-01-08T18:28:23.98Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/8a/b9dc7678803429e4a3bc9ba462fa3dd9066824d3c607490235c6a796be5a/setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3", size = 1228782 }, + { url = "https://files.pythonhosted.org/packages/69/8a/b9dc7678803429e4a3bc9ba462fa3dd9066824d3c607490235c6a796be5a/setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3", size = 1228782, upload-time = "2025-01-08T18:28:20.912Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "snowballstemmer" version = "2.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/7b/af302bebf22c749c56c9c3e8ae13190b5b5db37a33d9068652e8f73b7089/snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", size = 86699 } +sdist = { url = "https://files.pythonhosted.org/packages/44/7b/af302bebf22c749c56c9c3e8ae13190b5b5db37a33d9068652e8f73b7089/snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", size = 86699, upload-time = "2021-11-16T18:38:38.009Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002 }, + { url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002, upload-time = "2021-11-16T18:38:34.792Z" }, ] [[package]] name = "soupsieve" version = "2.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 } +sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569, upload-time = "2024-08-13T13:39:12.166Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 }, + { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186, upload-time = "2024-08-13T13:39:10.986Z" }, ] [[package]] @@ -1274,9 +1411,9 @@ dependencies = [ { name = "sphinxcontrib-serializinghtml" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611 } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125 }, + { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" }, ] [[package]] @@ -1289,63 +1426,63 @@ dependencies = [ { name = "pyyaml" }, { name = "sphinx" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4a/eb/cc243583bb1d518ca3b10998c203d919a8ed90affd4831f2b61ad09043d2/sphinx_autoapi-3.4.0.tar.gz", hash = "sha256:e6d5371f9411bbb9fca358c00a9e57aef3ac94cbfc5df4bab285946462f69e0c", size = 29292 } +sdist = { url = "https://files.pythonhosted.org/packages/4a/eb/cc243583bb1d518ca3b10998c203d919a8ed90affd4831f2b61ad09043d2/sphinx_autoapi-3.4.0.tar.gz", hash = "sha256:e6d5371f9411bbb9fca358c00a9e57aef3ac94cbfc5df4bab285946462f69e0c", size = 29292, upload-time = "2024-11-30T01:09:40.956Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/d6/f2acdc2567337fd5f5dc091a4e58d8a0fb14927b9779fc1e5ecee96d9824/sphinx_autoapi-3.4.0-py3-none-any.whl", hash = "sha256:4027fef2875a22c5f2a57107c71641d82f6166bf55beb407a47aaf3ef14e7b92", size = 34095 }, + { url = "https://files.pythonhosted.org/packages/de/d6/f2acdc2567337fd5f5dc091a4e58d8a0fb14927b9779fc1e5ecee96d9824/sphinx_autoapi-3.4.0-py3-none-any.whl", hash = "sha256:4027fef2875a22c5f2a57107c71641d82f6166bf55beb407a47aaf3ef14e7b92", size = 34095, upload-time = "2024-11-30T01:09:17.272Z" }, ] [[package]] name = "sphinxcontrib-applehelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053 } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300 }, + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, ] [[package]] name = "sphinxcontrib-devhelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967 } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530 }, + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, ] [[package]] name = "sphinxcontrib-htmlhelp" version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617 } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705 }, + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, ] [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787 } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071 }, + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, ] [[package]] name = "sphinxcontrib-qthelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165 } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743 }, + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, ] [[package]] name = "sphinxcontrib-serializinghtml" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080 } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072 }, + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, ] [[package]] @@ -1357,93 +1494,93 @@ dependencies = [ { name = "executing" }, { name = "pure-eval" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707 } +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521 }, + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] [[package]] name = "toml" version = "0.10.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 }, + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, ] [[package]] name = "tomli" version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, - { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, - { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, - { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, - { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, - { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, - { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, - { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, - { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, - { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, - { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, - { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, - { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, - { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, - { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, - { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, - { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, - { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, - { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, - { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, - { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] [[package]] name = "traitlets" version = "5.14.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621 } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359 }, + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, ] [[package]] name = "typing-extensions" version = "4.12.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321, upload-time = "2024-06-07T18:52:15.995Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438, upload-time = "2024-06-07T18:52:13.582Z" }, ] [[package]] name = "tzdata" version = "2024.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/34/943888654477a574a86a98e9896bae89c7aa15078ec29f490fef2f1e5384/tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", size = 193282 } +sdist = { url = "https://files.pythonhosted.org/packages/e1/34/943888654477a574a86a98e9896bae89c7aa15078ec29f490fef2f1e5384/tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", size = 193282, upload-time = "2024-09-23T18:56:46.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586 }, + { url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586, upload-time = "2024-09-23T18:56:45.478Z" }, ] [[package]] name = "urllib3" version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268, upload-time = "2024-12-22T07:47:30.032Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, + { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369, upload-time = "2024-12-22T07:47:28.074Z" }, ] [[package]] @@ -1455,80 +1592,80 @@ dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316 } +sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316, upload-time = "2025-05-08T17:58:23.811Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982 }, + { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" }, ] [[package]] name = "wcwidth" version = "0.2.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, ] [[package]] name = "wrapt" version = "1.17.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/d1/1daec934997e8b160040c78d7b31789f19b122110a75eca3d4e8da0049e1/wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984", size = 53307 }, - { url = "https://files.pythonhosted.org/packages/1b/7b/13369d42651b809389c1a7153baa01d9700430576c81a2f5c5e460df0ed9/wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22", size = 38486 }, - { url = "https://files.pythonhosted.org/packages/62/bf/e0105016f907c30b4bd9e377867c48c34dc9c6c0c104556c9c9126bd89ed/wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7", size = 38777 }, - { url = "https://files.pythonhosted.org/packages/27/70/0f6e0679845cbf8b165e027d43402a55494779295c4b08414097b258ac87/wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c", size = 83314 }, - { url = "https://files.pythonhosted.org/packages/0f/77/0576d841bf84af8579124a93d216f55d6f74374e4445264cb378a6ed33eb/wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72", size = 74947 }, - { url = "https://files.pythonhosted.org/packages/90/ec/00759565518f268ed707dcc40f7eeec38637d46b098a1f5143bff488fe97/wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061", size = 82778 }, - { url = "https://files.pythonhosted.org/packages/f8/5a/7cffd26b1c607b0b0c8a9ca9d75757ad7620c9c0a9b4a25d3f8a1480fafc/wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2", size = 81716 }, - { url = "https://files.pythonhosted.org/packages/7e/09/dccf68fa98e862df7e6a60a61d43d644b7d095a5fc36dbb591bbd4a1c7b2/wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c", size = 74548 }, - { url = "https://files.pythonhosted.org/packages/b7/8e/067021fa3c8814952c5e228d916963c1115b983e21393289de15128e867e/wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62", size = 81334 }, - { url = "https://files.pythonhosted.org/packages/4b/0d/9d4b5219ae4393f718699ca1c05f5ebc0c40d076f7e65fd48f5f693294fb/wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563", size = 36427 }, - { url = "https://files.pythonhosted.org/packages/72/6a/c5a83e8f61aec1e1aeef939807602fb880e5872371e95df2137142f5c58e/wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f", size = 38774 }, - { url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308 }, - { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488 }, - { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776 }, - { url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776 }, - { url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420 }, - { url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199 }, - { url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307 }, - { url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025 }, - { url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879 }, - { url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419 }, - { url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773 }, - { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799 }, - { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821 }, - { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919 }, - { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721 }, - { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899 }, - { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222 }, - { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707 }, - { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685 }, - { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567 }, - { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672 }, - { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865 }, - { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800 }, - { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824 }, - { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920 }, - { url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690 }, - { url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861 }, - { url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174 }, - { url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721 }, - { url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763 }, - { url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585 }, - { url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676 }, - { url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871 }, - { url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312 }, - { url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062 }, - { url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155 }, - { url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471 }, - { url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208 }, - { url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339 }, - { url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232 }, - { url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476 }, - { url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377 }, - { url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986 }, - { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750 }, - { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 }, +sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531, upload-time = "2025-01-14T10:35:45.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/d1/1daec934997e8b160040c78d7b31789f19b122110a75eca3d4e8da0049e1/wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984", size = 53307, upload-time = "2025-01-14T10:33:13.616Z" }, + { url = "https://files.pythonhosted.org/packages/1b/7b/13369d42651b809389c1a7153baa01d9700430576c81a2f5c5e460df0ed9/wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22", size = 38486, upload-time = "2025-01-14T10:33:15.947Z" }, + { url = "https://files.pythonhosted.org/packages/62/bf/e0105016f907c30b4bd9e377867c48c34dc9c6c0c104556c9c9126bd89ed/wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7", size = 38777, upload-time = "2025-01-14T10:33:17.462Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/0f6e0679845cbf8b165e027d43402a55494779295c4b08414097b258ac87/wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c", size = 83314, upload-time = "2025-01-14T10:33:21.282Z" }, + { url = "https://files.pythonhosted.org/packages/0f/77/0576d841bf84af8579124a93d216f55d6f74374e4445264cb378a6ed33eb/wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72", size = 74947, upload-time = "2025-01-14T10:33:24.414Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/00759565518f268ed707dcc40f7eeec38637d46b098a1f5143bff488fe97/wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061", size = 82778, upload-time = "2025-01-14T10:33:26.152Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5a/7cffd26b1c607b0b0c8a9ca9d75757ad7620c9c0a9b4a25d3f8a1480fafc/wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2", size = 81716, upload-time = "2025-01-14T10:33:27.372Z" }, + { url = "https://files.pythonhosted.org/packages/7e/09/dccf68fa98e862df7e6a60a61d43d644b7d095a5fc36dbb591bbd4a1c7b2/wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c", size = 74548, upload-time = "2025-01-14T10:33:28.52Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/067021fa3c8814952c5e228d916963c1115b983e21393289de15128e867e/wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62", size = 81334, upload-time = "2025-01-14T10:33:29.643Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0d/9d4b5219ae4393f718699ca1c05f5ebc0c40d076f7e65fd48f5f693294fb/wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563", size = 36427, upload-time = "2025-01-14T10:33:30.832Z" }, + { url = "https://files.pythonhosted.org/packages/72/6a/c5a83e8f61aec1e1aeef939807602fb880e5872371e95df2137142f5c58e/wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f", size = 38774, upload-time = "2025-01-14T10:33:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308, upload-time = "2025-01-14T10:33:33.992Z" }, + { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488, upload-time = "2025-01-14T10:33:35.264Z" }, + { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776, upload-time = "2025-01-14T10:33:38.28Z" }, + { url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776, upload-time = "2025-01-14T10:33:40.678Z" }, + { url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420, upload-time = "2025-01-14T10:33:41.868Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199, upload-time = "2025-01-14T10:33:43.598Z" }, + { url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307, upload-time = "2025-01-14T10:33:48.499Z" }, + { url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025, upload-time = "2025-01-14T10:33:51.191Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879, upload-time = "2025-01-14T10:33:52.328Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419, upload-time = "2025-01-14T10:33:53.551Z" }, + { url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773, upload-time = "2025-01-14T10:33:56.323Z" }, + { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799, upload-time = "2025-01-14T10:33:57.4Z" }, + { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821, upload-time = "2025-01-14T10:33:59.334Z" }, + { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919, upload-time = "2025-01-14T10:34:04.093Z" }, + { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721, upload-time = "2025-01-14T10:34:07.163Z" }, + { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899, upload-time = "2025-01-14T10:34:09.82Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222, upload-time = "2025-01-14T10:34:11.258Z" }, + { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707, upload-time = "2025-01-14T10:34:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685, upload-time = "2025-01-14T10:34:15.043Z" }, + { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567, upload-time = "2025-01-14T10:34:16.563Z" }, + { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672, upload-time = "2025-01-14T10:34:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865, upload-time = "2025-01-14T10:34:19.577Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800, upload-time = "2025-01-14T10:34:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824, upload-time = "2025-01-14T10:34:22.999Z" }, + { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920, upload-time = "2025-01-14T10:34:25.386Z" }, + { url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690, upload-time = "2025-01-14T10:34:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861, upload-time = "2025-01-14T10:34:29.167Z" }, + { url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174, upload-time = "2025-01-14T10:34:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721, upload-time = "2025-01-14T10:34:32.91Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763, upload-time = "2025-01-14T10:34:34.903Z" }, + { url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585, upload-time = "2025-01-14T10:34:36.13Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676, upload-time = "2025-01-14T10:34:37.962Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871, upload-time = "2025-01-14T10:34:39.13Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312, upload-time = "2025-01-14T10:34:40.604Z" }, + { url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062, upload-time = "2025-01-14T10:34:45.011Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155, upload-time = "2025-01-14T10:34:47.25Z" }, + { url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471, upload-time = "2025-01-14T10:34:50.934Z" }, + { url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208, upload-time = "2025-01-14T10:34:52.297Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339, upload-time = "2025-01-14T10:34:53.489Z" }, + { url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232, upload-time = "2025-01-14T10:34:55.327Z" }, + { url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476, upload-time = "2025-01-14T10:34:58.055Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377, upload-time = "2025-01-14T10:34:59.3Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986, upload-time = "2025-01-14T10:35:00.498Z" }, + { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750, upload-time = "2025-01-14T10:35:03.378Z" }, + { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594, upload-time = "2025-01-14T10:35:44.018Z" }, ] From f914fc854a54ba133ee8eb0c3cb0e9845a5d7f7f Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Fri, 6 Mar 2026 21:12:56 -0500 Subject: [PATCH 07/56] Catch warnings in FFI unit tests (#1410) * Failed pytest in ffi crate when warnings are generated * Bump DF53 version --- Cargo.lock | 72 ++++++++--------- Cargo.toml | 8 +- examples/datafusion-ffi-example/Cargo.lock | 79 ++++++++++++------- examples/datafusion-ffi-example/Cargo.toml | 13 +-- .../python/tests/conftest.py | 42 ++++++++++ examples/datafusion-ffi-example/src/lib.rs | 2 + examples/datafusion-ffi-example/src/utils.rs | 3 +- src/expr.rs | 3 +- 8 files changed, 145 insertions(+), 77 deletions(-) create mode 100644 examples/datafusion-ffi-example/python/tests/conftest.py diff --git a/Cargo.lock b/Cargo.lock index 82d5c66d0..40b1ba7f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -901,7 +901,7 @@ dependencies = [ [[package]] name = "datafusion" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "arrow-schema", @@ -956,7 +956,7 @@ dependencies = [ [[package]] name = "datafusion-catalog" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "async-trait", @@ -980,7 +980,7 @@ dependencies = [ [[package]] name = "datafusion-catalog-listing" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "async-trait", @@ -1002,7 +1002,7 @@ dependencies = [ [[package]] name = "datafusion-common" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "ahash", "apache-avro", @@ -1027,7 +1027,7 @@ dependencies = [ [[package]] name = "datafusion-common-runtime" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "futures", "log", @@ -1037,7 +1037,7 @@ dependencies = [ [[package]] name = "datafusion-datasource" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "async-compression", @@ -1071,7 +1071,7 @@ dependencies = [ [[package]] name = "datafusion-datasource-arrow" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "arrow-ipc", @@ -1094,7 +1094,7 @@ dependencies = [ [[package]] name = "datafusion-datasource-avro" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "apache-avro", "arrow", @@ -1113,7 +1113,7 @@ dependencies = [ [[package]] name = "datafusion-datasource-csv" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "async-trait", @@ -1135,7 +1135,7 @@ dependencies = [ [[package]] name = "datafusion-datasource-json" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "async-trait", @@ -1158,7 +1158,7 @@ dependencies = [ [[package]] name = "datafusion-datasource-parquet" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "async-trait", @@ -1187,12 +1187,12 @@ dependencies = [ [[package]] name = "datafusion-doc" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" [[package]] name = "datafusion-execution" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "arrow-buffer", @@ -1214,7 +1214,7 @@ dependencies = [ [[package]] name = "datafusion-expr" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "async-trait", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "datafusion-expr-common" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "datafusion-common", @@ -1248,7 +1248,7 @@ dependencies = [ [[package]] name = "datafusion-ffi" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "abi_stable", "arrow", @@ -1277,7 +1277,7 @@ dependencies = [ [[package]] name = "datafusion-functions" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "arrow-buffer", @@ -1308,7 +1308,7 @@ dependencies = [ [[package]] name = "datafusion-functions-aggregate" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "ahash", "arrow", @@ -1329,7 +1329,7 @@ dependencies = [ [[package]] name = "datafusion-functions-aggregate-common" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "ahash", "arrow", @@ -1341,7 +1341,7 @@ dependencies = [ [[package]] name = "datafusion-functions-nested" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "arrow-ord", @@ -1365,7 +1365,7 @@ dependencies = [ [[package]] name = "datafusion-functions-table" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "async-trait", @@ -1380,7 +1380,7 @@ dependencies = [ [[package]] name = "datafusion-functions-window" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "datafusion-common", @@ -1397,7 +1397,7 @@ dependencies = [ [[package]] name = "datafusion-functions-window-common" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "datafusion-common", "datafusion-physical-expr-common", @@ -1406,7 +1406,7 @@ dependencies = [ [[package]] name = "datafusion-macros" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "datafusion-doc", "quote", @@ -1416,7 +1416,7 @@ dependencies = [ [[package]] name = "datafusion-optimizer" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "chrono", @@ -1435,7 +1435,7 @@ dependencies = [ [[package]] name = "datafusion-physical-expr" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "ahash", "arrow", @@ -1458,7 +1458,7 @@ dependencies = [ [[package]] name = "datafusion-physical-expr-adapter" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "datafusion-common", @@ -1472,7 +1472,7 @@ dependencies = [ [[package]] name = "datafusion-physical-expr-common" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "ahash", "arrow", @@ -1488,7 +1488,7 @@ dependencies = [ [[package]] name = "datafusion-physical-optimizer" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "datafusion-common", @@ -1506,7 +1506,7 @@ dependencies = [ [[package]] name = "datafusion-physical-plan" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "ahash", "arrow", @@ -1537,7 +1537,7 @@ dependencies = [ [[package]] name = "datafusion-proto" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "chrono", @@ -1564,7 +1564,7 @@ dependencies = [ [[package]] name = "datafusion-proto-common" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "datafusion-common", @@ -1574,7 +1574,7 @@ dependencies = [ [[package]] name = "datafusion-pruning" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "datafusion-common", @@ -1619,7 +1619,7 @@ dependencies = [ [[package]] name = "datafusion-session" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "async-trait", "datafusion-common", @@ -1632,7 +1632,7 @@ dependencies = [ [[package]] name = "datafusion-sql" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "bigdecimal", @@ -1650,7 +1650,7 @@ dependencies = [ [[package]] name = "datafusion-substrait" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "async-recursion", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index dfc7e4b60..b584470d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -95,7 +95,7 @@ codegen-units = 1 # We cannot publish to crates.io with any patches in the below section. Developers # must remove any entries in this section before creating a release candidate. [patch.crates-io] -datafusion = { git = "https://github.com/apache/datafusion.git", rev = "518560246e87d489eba6d511fa167aa429b06728" } -datafusion-substrait = { git = "https://github.com/apache/datafusion.git", rev = "518560246e87d489eba6d511fa167aa429b06728" } -datafusion-proto = { git = "https://github.com/apache/datafusion.git", rev = "518560246e87d489eba6d511fa167aa429b06728" } -datafusion-ffi = { git = "https://github.com/apache/datafusion.git", rev = "518560246e87d489eba6d511fa167aa429b06728" } +datafusion = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } +datafusion-substrait = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } +datafusion-proto = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } +datafusion-ffi = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } diff --git a/examples/datafusion-ffi-example/Cargo.lock b/examples/datafusion-ffi-example/Cargo.lock index a5dda6575..ede9b446b 100644 --- a/examples/datafusion-ffi-example/Cargo.lock +++ b/examples/datafusion-ffi-example/Cargo.lock @@ -115,6 +115,15 @@ version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +[[package]] +name = "arc-swap" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +dependencies = [ + "rustversion", +] + [[package]] name = "arrow" version = "58.0.0" @@ -592,7 +601,7 @@ dependencies = [ [[package]] name = "datafusion-catalog" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "async-trait", @@ -616,7 +625,7 @@ dependencies = [ [[package]] name = "datafusion-catalog-listing" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "async-trait", @@ -638,7 +647,7 @@ dependencies = [ [[package]] name = "datafusion-common" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "ahash", "arrow", @@ -660,7 +669,7 @@ dependencies = [ [[package]] name = "datafusion-common-runtime" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "futures", "log", @@ -670,7 +679,7 @@ dependencies = [ [[package]] name = "datafusion-datasource" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "async-trait", @@ -698,7 +707,7 @@ dependencies = [ [[package]] name = "datafusion-datasource-arrow" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "arrow-ipc", @@ -721,7 +730,7 @@ dependencies = [ [[package]] name = "datafusion-datasource-csv" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "async-trait", @@ -743,7 +752,7 @@ dependencies = [ [[package]] name = "datafusion-datasource-json" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "async-trait", @@ -766,7 +775,7 @@ dependencies = [ [[package]] name = "datafusion-datasource-parquet" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "async-trait", @@ -795,12 +804,12 @@ dependencies = [ [[package]] name = "datafusion-doc" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" [[package]] name = "datafusion-execution" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "arrow-buffer", @@ -822,7 +831,7 @@ dependencies = [ [[package]] name = "datafusion-expr" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "async-trait", @@ -843,7 +852,7 @@ dependencies = [ [[package]] name = "datafusion-expr-common" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "datafusion-common", @@ -855,7 +864,7 @@ dependencies = [ [[package]] name = "datafusion-ffi" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "abi_stable", "arrow", @@ -897,12 +906,13 @@ dependencies = [ "datafusion-functions-window", "pyo3", "pyo3-build-config", + "pyo3-log", ] [[package]] name = "datafusion-functions" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "arrow-buffer", @@ -929,7 +939,7 @@ dependencies = [ [[package]] name = "datafusion-functions-aggregate" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "ahash", "arrow", @@ -950,7 +960,7 @@ dependencies = [ [[package]] name = "datafusion-functions-aggregate-common" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "ahash", "arrow", @@ -962,7 +972,7 @@ dependencies = [ [[package]] name = "datafusion-functions-table" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "async-trait", @@ -977,7 +987,7 @@ dependencies = [ [[package]] name = "datafusion-functions-window" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "datafusion-common", @@ -994,7 +1004,7 @@ dependencies = [ [[package]] name = "datafusion-functions-window-common" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "datafusion-common", "datafusion-physical-expr-common", @@ -1003,7 +1013,7 @@ dependencies = [ [[package]] name = "datafusion-macros" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "datafusion-doc", "quote", @@ -1013,7 +1023,7 @@ dependencies = [ [[package]] name = "datafusion-physical-expr" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "ahash", "arrow", @@ -1035,7 +1045,7 @@ dependencies = [ [[package]] name = "datafusion-physical-expr-adapter" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "datafusion-common", @@ -1049,7 +1059,7 @@ dependencies = [ [[package]] name = "datafusion-physical-expr-common" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "ahash", "arrow", @@ -1065,7 +1075,7 @@ dependencies = [ [[package]] name = "datafusion-physical-plan" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "ahash", "arrow", @@ -1096,7 +1106,7 @@ dependencies = [ [[package]] name = "datafusion-proto" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "chrono", @@ -1123,7 +1133,7 @@ dependencies = [ [[package]] name = "datafusion-proto-common" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "datafusion-common", @@ -1133,7 +1143,7 @@ dependencies = [ [[package]] name = "datafusion-pruning" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "arrow", "datafusion-common", @@ -1149,7 +1159,7 @@ dependencies = [ [[package]] name = "datafusion-session" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=518560246e87d489eba6d511fa167aa429b06728#518560246e87d489eba6d511fa167aa429b06728" +source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" dependencies = [ "async-trait", "datafusion-common", @@ -2067,6 +2077,17 @@ dependencies = [ "pyo3-build-config", ] +[[package]] +name = "pyo3-log" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c2ec80932c5c3b2d4fbc578c9b56b2d4502098587edb8bef5b6bfcad43682e" +dependencies = [ + "arc-swap", + "log", + "pyo3", +] + [[package]] name = "pyo3-macros" version = "0.28.2" diff --git a/examples/datafusion-ffi-example/Cargo.toml b/examples/datafusion-ffi-example/Cargo.toml index 483838e31..be6096faf 100644 --- a/examples/datafusion-ffi-example/Cargo.toml +++ b/examples/datafusion-ffi-example/Cargo.toml @@ -37,6 +37,7 @@ arrow = { version = "58" } arrow-array = { version = "58" } arrow-schema = { version = "58" } async-trait = "0.1.89" +pyo3-log = "0.13.2" [build-dependencies] pyo3-build-config = "0.28" @@ -47,9 +48,9 @@ crate-type = ["cdylib", "rlib"] # TODO: remove when datafusion-53 is released [patch.crates-io] -datafusion-catalog = { git = "https://github.com/apache/datafusion.git", rev = "518560246e87d489eba6d511fa167aa429b06728" } -datafusion-common = { git = "https://github.com/apache/datafusion.git", rev = "518560246e87d489eba6d511fa167aa429b06728" } -datafusion-functions-aggregate = { git = "https://github.com/apache/datafusion.git", rev = "518560246e87d489eba6d511fa167aa429b06728" } -datafusion-functions-window = { git = "https://github.com/apache/datafusion.git", rev = "518560246e87d489eba6d511fa167aa429b06728" } -datafusion-expr = { git = "https://github.com/apache/datafusion.git", rev = "518560246e87d489eba6d511fa167aa429b06728" } -datafusion-ffi = { git = "https://github.com/apache/datafusion.git", rev = "518560246e87d489eba6d511fa167aa429b06728" } +datafusion-catalog = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } +datafusion-common = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } +datafusion-functions-aggregate = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } +datafusion-functions-window = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } +datafusion-expr = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } +datafusion-ffi = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } diff --git a/examples/datafusion-ffi-example/python/tests/conftest.py b/examples/datafusion-ffi-example/python/tests/conftest.py new file mode 100644 index 000000000..68f8057af --- /dev/null +++ b/examples/datafusion-ffi-example/python/tests/conftest.py @@ -0,0 +1,42 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +import pytest + +if TYPE_CHECKING: + from collections.abc import Generator + from typing import Any + + +class _FailOnWarning(logging.Handler): + def emit(self, record: logging.LogRecord) -> None: + if record.levelno >= logging.WARNING: + err = f"Unexpected log warning from '{record.name}': {self.format(record)}" + raise AssertionError(err) + + +@pytest.fixture(autouse=True) +def fail_on_log_warnings() -> Generator[None, Any, None]: + handler = _FailOnWarning() + logging.root.addHandler(handler) + yield + logging.root.removeHandler(handler) diff --git a/examples/datafusion-ffi-example/src/lib.rs b/examples/datafusion-ffi-example/src/lib.rs index 6c64c9fe5..23f2001a2 100644 --- a/examples/datafusion-ffi-example/src/lib.rs +++ b/examples/datafusion-ffi-example/src/lib.rs @@ -34,6 +34,8 @@ pub(crate) mod window_udf; #[pymodule] fn datafusion_ffi_example(m: &Bound<'_, PyModule>) -> PyResult<()> { + pyo3_log::init(); + m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/examples/datafusion-ffi-example/src/utils.rs b/examples/datafusion-ffi-example/src/utils.rs index 5b0666837..5f2865aa2 100644 --- a/examples/datafusion-ffi-example/src/utils.rs +++ b/examples/datafusion-ffi-example/src/utils.rs @@ -15,13 +15,14 @@ // specific language governing permissions and limitations // under the License. +use std::ptr::NonNull; + use datafusion_ffi::proto::logical_extension_codec::FFI_LogicalExtensionCodec; use pyo3::exceptions::PyValueError; use pyo3::ffi::c_str; use pyo3::prelude::{PyAnyMethods, PyCapsuleMethods}; use pyo3::types::PyCapsule; use pyo3::{Bound, PyAny, PyResult}; -use std::ptr::NonNull; pub(crate) fn ffi_logical_codec_from_pycapsule( obj: Bound, diff --git a/src/expr.rs b/src/expr.rs index 4d4a73ff3..c4f2a12da 100644 --- a/src/expr.rs +++ b/src/expr.rs @@ -779,7 +779,8 @@ impl PyExpr { | Operator::AtQuestion | Operator::Question | Operator::QuestionAnd - | Operator::QuestionPipe => Err(py_type_err(format!("Unsupported expr: ${op}"))), + | Operator::QuestionPipe + | Operator::Colon => Err(py_type_err(format!("Unsupported expr: ${op}"))), }, Expr::Cast(Cast { expr: _, data_type }) => DataTypeMap::map_from_arrow_type(data_type), Expr::Literal(scalar_value, _) => DataTypeMap::map_from_scalar_value(scalar_value), From d322b7b7bfd527370f03854717661488737c9f8b Mon Sep 17 00:00:00 2001 From: Daniel Mesejo Date: Mon, 9 Mar 2026 07:52:47 +0100 Subject: [PATCH 08/56] feat: feat: add to_time, to_local_time, to_date functions (#1387) * feat: add to_time, to_local_time, to_date, to_char functions Additionally fix conditional on formatters (since it is *args it cannot be None) Refactor name to avoid possible collision with f. * address comments in PR * chore: add tests for today --- python/datafusion/functions.py | 80 +++++++++++++++++++++++++++------- python/tests/test_functions.py | 75 ++++++++++++++++++++++++++++++- src/functions.rs | 8 ++++ 3 files changed, 146 insertions(+), 17 deletions(-) diff --git a/python/datafusion/functions.py b/python/datafusion/functions.py index 431afcc30..9723be5ce 100644 --- a/python/datafusion/functions.py +++ b/python/datafusion/functions.py @@ -42,7 +42,6 @@ if TYPE_CHECKING: from datafusion.context import SessionContext - __all__ = [ "abs", "acos", @@ -268,13 +267,18 @@ "sum", "tan", "tanh", + "to_char", + "to_date", "to_hex", + "to_local_time", + "to_time", "to_timestamp", "to_timestamp_micros", "to_timestamp_millis", "to_timestamp_nanos", "to_timestamp_seconds", "to_unixtime", + "today", "translate", "trim", "trunc", @@ -1010,6 +1014,56 @@ def now() -> Expr: return Expr(f.now()) +def to_char(arg: Expr, formatter: Expr) -> Expr: + """Returns a string representation of a date, time, timestamp or duration. + + For usage of ``formatter`` see the rust chrono package ``strftime`` package. + + [Documentation here.](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) + """ + return Expr(f.to_char(arg.expr, formatter.expr)) + + +def _unwrap_exprs(args: tuple[Expr, ...]) -> list: + return [arg.expr for arg in args] + + +def to_date(arg: Expr, *formatters: Expr) -> Expr: + """Converts a value to a date (YYYY-MM-DD). + + Supports strings, numeric and timestamp types as input. + Integers and doubles are interpreted as days since the unix epoch. + Strings are parsed as YYYY-MM-DD (e.g. '2023-07-20') + if ``formatters`` are not provided. + + For usage of ``formatters`` see the rust chrono package ``strftime`` package. + + [Documentation here.](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) + """ + return Expr(f.to_date(arg.expr, *_unwrap_exprs(formatters))) + + +def to_local_time(*args: Expr) -> Expr: + """Converts a timestamp with a timezone to a timestamp without a timezone. + + This function handles daylight saving time changes. + """ + return Expr(f.to_local_time(*_unwrap_exprs(args))) + + +def to_time(arg: Expr, *formatters: Expr) -> Expr: + """Converts a value to a time. Supports strings and timestamps as input. + + If ``formatters`` is not provided strings are parsed as HH:MM:SS, HH:MM or + HH:MM:SS.nnnnnnnnn; + + For usage of ``formatters`` see the rust chrono package ``strftime`` package. + + [Documentation here.](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) + """ + return Expr(f.to_time(arg.expr, *_unwrap_exprs(formatters))) + + def to_timestamp(arg: Expr, *formatters: Expr) -> Expr: """Converts a string and optional formats to a ``Timestamp`` in nanoseconds. @@ -1017,11 +1071,7 @@ def to_timestamp(arg: Expr, *formatters: Expr) -> Expr: [Documentation here.](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) """ - if formatters is None: - return f.to_timestamp(arg.expr) - - formatters = [f.expr for f in formatters] - return Expr(f.to_timestamp(arg.expr, *formatters)) + return Expr(f.to_timestamp(arg.expr, *_unwrap_exprs(formatters))) def to_timestamp_millis(arg: Expr, *formatters: Expr) -> Expr: @@ -1029,8 +1079,7 @@ def to_timestamp_millis(arg: Expr, *formatters: Expr) -> Expr: See :py:func:`to_timestamp` for a description on how to use formatters. """ - formatters = [f.expr for f in formatters] - return Expr(f.to_timestamp_millis(arg.expr, *formatters)) + return Expr(f.to_timestamp_millis(arg.expr, *_unwrap_exprs(formatters))) def to_timestamp_micros(arg: Expr, *formatters: Expr) -> Expr: @@ -1038,8 +1087,7 @@ def to_timestamp_micros(arg: Expr, *formatters: Expr) -> Expr: See :py:func:`to_timestamp` for a description on how to use formatters. """ - formatters = [f.expr for f in formatters] - return Expr(f.to_timestamp_micros(arg.expr, *formatters)) + return Expr(f.to_timestamp_micros(arg.expr, *_unwrap_exprs(formatters))) def to_timestamp_nanos(arg: Expr, *formatters: Expr) -> Expr: @@ -1047,8 +1095,7 @@ def to_timestamp_nanos(arg: Expr, *formatters: Expr) -> Expr: See :py:func:`to_timestamp` for a description on how to use formatters. """ - formatters = [f.expr for f in formatters] - return Expr(f.to_timestamp_nanos(arg.expr, *formatters)) + return Expr(f.to_timestamp_nanos(arg.expr, *_unwrap_exprs(formatters))) def to_timestamp_seconds(arg: Expr, *formatters: Expr) -> Expr: @@ -1056,14 +1103,12 @@ def to_timestamp_seconds(arg: Expr, *formatters: Expr) -> Expr: See :py:func:`to_timestamp` for a description on how to use formatters. """ - formatters = [f.expr for f in formatters] - return Expr(f.to_timestamp_seconds(arg.expr, *formatters)) + return Expr(f.to_timestamp_seconds(arg.expr, *_unwrap_exprs(formatters))) def to_unixtime(string: Expr, *format_arguments: Expr) -> Expr: """Converts a string and optional formats to a Unixtime.""" - args = [f.expr for f in format_arguments] - return Expr(f.to_unixtime(string.expr, *args)) + return Expr(f.to_unixtime(string.expr, *_unwrap_exprs(format_arguments))) def current_date() -> Expr: @@ -1071,6 +1116,9 @@ def current_date() -> Expr: return Expr(f.current_date()) +today = current_date + + def current_time() -> Expr: """Returns current UTC time as a Time64 value.""" return Expr(f.current_time()) diff --git a/python/tests/test_functions.py b/python/tests/test_functions.py index 7d642b722..37d349c58 100644 --- a/python/tests/test_functions.py +++ b/python/tests/test_functions.py @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. import math -from datetime import datetime, timezone +from datetime import date, datetime, time, timezone import numpy as np import pyarrow as pa @@ -958,6 +958,12 @@ def test_temporal_functions(df): f.to_timestamp_nanos( literal("2023-09-07 05:06:14.523952000"), literal("%Y-%m-%d %H:%M:%S.%f") ), + f.to_time(literal("12:30:45")), + f.to_time(literal("12-30-45"), literal("%H-%M-%S")), + f.to_date(literal("2017-05-31")), + f.to_date(literal("2017-05-31"), literal("%Y-%m-%d")), + f.to_local_time(column("d")), + f.to_char(column("d"), literal("%d-%m-%Y")), ) result = df.collect() assert len(result) == 1 @@ -1032,6 +1038,73 @@ def test_temporal_functions(df): [datetime(2023, 9, 7, 5, 6, 14, 523952, tzinfo=DEFAULT_TZ)] * 3, type=pa.timestamp("ns"), ) + assert result.column(17) == pa.array( + [time(12, 30, 45)] * 3, + type=pa.time64("ns"), + ) + assert result.column(18) == pa.array( + [time(12, 30, 45)] * 3, + type=pa.time64("ns"), + ) + assert result.column(19) == pa.array( + [date(2017, 5, 31)] * 3, + type=pa.date32(), + ) + assert result.column(20) == pa.array( + [date(2017, 5, 31)] * 3, + type=pa.date32(), + ) + assert result.column(21) == pa.array( + [ + datetime(2022, 12, 31, tzinfo=DEFAULT_TZ), + datetime(2027, 6, 26, tzinfo=DEFAULT_TZ), + datetime(2020, 7, 2, tzinfo=DEFAULT_TZ), + ], + type=pa.timestamp("us"), + ) + + assert result.column(22) == pa.array( + [ + "31-12-2022", + "26-06-2027", + "02-07-2020", + ], + type=pa.string(), + ) + + +def test_to_time_invalid_input(df): + with pytest.raises(Exception, match=r"Error parsing 'not-a-time' as time"): + df.select(f.to_time(literal("not-a-time"))).collect() + + +def test_to_time_mismatched_formatter(df): + with pytest.raises(Exception, match=r"Error parsing '12:30:45' as time"): + df.select(f.to_time(literal("12:30:45"), literal("%Y-%m-%d"))).collect() + + +def test_to_date_invalid_input(df): + with pytest.raises(Exception, match=r"Date32"): + df.select(f.to_date(literal("not-a-date"))).collect() + + +def test_temporal_formatter_requires_expr(): + with pytest.raises(AttributeError, match="'str' object has no attribute 'expr'"): + f.to_time(literal("12:30:45"), "not-an-expr") + + +def test_today_returns_date32(df): + result = df.select(f.today().alias("today")).collect()[0] + assert result.column(0).type == pa.date32() + + +def test_today_alias_matches_current_date(df): + result = df.select( + f.current_date().alias("current_date"), + f.today().alias("today"), + ).collect()[0] + + assert result.column(0) == result.column(1) def test_arrow_cast(df): diff --git a/src/functions.rs b/src/functions.rs index 90b3a0a4b..c32134054 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -601,6 +601,9 @@ expr_fn!( "Converts the number to its equivalent hexadecimal representation." ); expr_fn!(now); +expr_fn_vec!(to_date); +expr_fn_vec!(to_local_time); +expr_fn_vec!(to_time); expr_fn_vec!(to_timestamp); expr_fn_vec!(to_timestamp_millis); expr_fn_vec!(to_timestamp_nanos); @@ -613,6 +616,7 @@ expr_fn!(date_part, part date); expr_fn!(date_trunc, part date); expr_fn!(date_bin, stride source origin); expr_fn!(make_date, year month day); +expr_fn!(to_char, datetime format); expr_fn!(translate, string from to, "Replaces each character in string that matches a character in the from set with the corresponding character in the to set. If from is longer than to, occurrences of the extra characters in from are deleted."); expr_fn_vec!( @@ -1045,6 +1049,10 @@ pub(crate) fn init_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(tan))?; m.add_wrapped(wrap_pyfunction!(tanh))?; m.add_wrapped(wrap_pyfunction!(to_hex))?; + m.add_wrapped(wrap_pyfunction!(to_char))?; + m.add_wrapped(wrap_pyfunction!(to_date))?; + m.add_wrapped(wrap_pyfunction!(to_local_time))?; + m.add_wrapped(wrap_pyfunction!(to_time))?; m.add_wrapped(wrap_pyfunction!(to_timestamp))?; m.add_wrapped(wrap_pyfunction!(to_timestamp_millis))?; m.add_wrapped(wrap_pyfunction!(to_timestamp_nanos))?; From 1160d5a91d586927dc6e466829965770c3fa299a Mon Sep 17 00:00:00 2001 From: Nick <24689722+ntjohnson1@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:01:18 +0100 Subject: [PATCH 09/56] Add docstring examples for Scalar trigonometric functions (#1411) * Add docstring examples for Scalar trigonometric functions Add example usage to docstrings for Scalar trigonometric functions to improve documentation. Co-Authored-By: Claude Opus 4.6 * Remove weird artifact * Move conftest so it doesn't get packaged in release --------- Co-authored-by: Claude Opus 4.6 --- conftest.py | 29 +++++ python/datafusion/functions.py | 190 +++++++++++++++++++++++++++++---- 2 files changed, 200 insertions(+), 19 deletions(-) create mode 100644 conftest.py diff --git a/conftest.py b/conftest.py new file mode 100644 index 000000000..1c89f92bc --- /dev/null +++ b/conftest.py @@ -0,0 +1,29 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""Pytest configuration for doctest namespace injection.""" + +import datafusion as dfn +import numpy as np +import pytest + + +@pytest.fixture(autouse=True) +def _doctest_namespace(doctest_namespace: dict) -> None: + """Add common imports to the doctest namespace.""" + doctest_namespace["dfn"] = dfn + doctest_namespace["np"] = np diff --git a/python/datafusion/functions.py b/python/datafusion/functions.py index 9723be5ce..fd116254b 100644 --- a/python/datafusion/functions.py +++ b/python/datafusion/functions.py @@ -495,16 +495,28 @@ def abs(arg: Expr) -> Expr: def acos(arg: Expr) -> Expr: """Returns the arc cosine or inverse cosine of a number. - Returns: - -------- - Expr - A new expression representing the arc cosine of the input expression. + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0]}) + >>> result = df.select(dfn.functions.acos(dfn.col("a")).alias("acos")) + >>> result.collect_column("acos")[0].as_py() + 0.0 """ return Expr(f.acos(arg.expr)) def acosh(arg: Expr) -> Expr: - """Returns inverse hyperbolic cosine.""" + """Returns inverse hyperbolic cosine. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0]}) + >>> result = df.select(dfn.functions.acosh(dfn.col("a")).alias("acosh")) + >>> result.collect_column("acosh")[0].as_py() + 0.0 + """ return Expr(f.acosh(arg.expr)) @@ -514,27 +526,73 @@ def ascii(arg: Expr) -> Expr: def asin(arg: Expr) -> Expr: - """Returns the arc sine or inverse sine of a number.""" + """Returns the arc sine or inverse sine of a number. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0.0]}) + >>> result = df.select(dfn.functions.asin(dfn.col("a")).alias("asin")) + >>> result.collect_column("asin")[0].as_py() + 0.0 + """ return Expr(f.asin(arg.expr)) def asinh(arg: Expr) -> Expr: - """Returns inverse hyperbolic sine.""" + """Returns inverse hyperbolic sine. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0.0]}) + >>> result = df.select(dfn.functions.asinh(dfn.col("a")).alias("asinh")) + >>> result.collect_column("asinh")[0].as_py() + 0.0 + """ return Expr(f.asinh(arg.expr)) def atan(arg: Expr) -> Expr: - """Returns inverse tangent of a number.""" + """Returns inverse tangent of a number. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0.0]}) + >>> result = df.select(dfn.functions.atan(dfn.col("a")).alias("atan")) + >>> result.collect_column("atan")[0].as_py() + 0.0 + """ return Expr(f.atan(arg.expr)) def atanh(arg: Expr) -> Expr: - """Returns inverse hyperbolic tangent.""" + """Returns inverse hyperbolic tangent. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0.0]}) + >>> result = df.select(dfn.functions.atanh(dfn.col("a")).alias("atanh")) + >>> result.collect_column("atanh")[0].as_py() + 0.0 + """ return Expr(f.atanh(arg.expr)) def atan2(y: Expr, x: Expr) -> Expr: - """Returns inverse tangent of a division given in the argument.""" + """Returns inverse tangent of a division given in the argument. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"y": [0.0], "x": [1.0]}) + >>> result = df.select( + ... dfn.functions.atan2(dfn.col("y"), dfn.col("x")).alias("atan2")) + >>> result.collect_column("atan2")[0].as_py() + 0.0 + """ return Expr(f.atan2(y.expr, x.expr)) @@ -585,22 +643,65 @@ def coalesce(*args: Expr) -> Expr: def cos(arg: Expr) -> Expr: - """Returns the cosine of the argument.""" + """Returns the cosine of the argument. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0,-1,1]}) + >>> cos_df = df.select(dfn.functions.cos(dfn.col("a")).alias("cos")) + >>> cos_df.collect_column("cos")[0].as_py() + 1.0 + """ return Expr(f.cos(arg.expr)) def cosh(arg: Expr) -> Expr: - """Returns the hyperbolic cosine of the argument.""" + """Returns the hyperbolic cosine of the argument. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0,-1,1]}) + >>> cosh_df = df.select(dfn.functions.cosh(dfn.col("a")).alias("cosh")) + >>> cosh_df.collect_column("cosh")[0].as_py() + 1.0 + """ return Expr(f.cosh(arg.expr)) def cot(arg: Expr) -> Expr: - """Returns the cotangent of the argument.""" + """Returns the cotangent of the argument. + + Examples: + --------- + >>> from math import pi + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [pi / 4]}) + >>> import builtins + >>> result = df.select( + ... dfn.functions.cot(dfn.col("a")).alias("cot") + ... ) + >>> builtins.round( + ... result.collect_column("cot")[0].as_py(), 1 + ... ) + 1.0 + """ return Expr(f.cot(arg.expr)) def degrees(arg: Expr) -> Expr: - """Converts the argument from radians to degrees.""" + """Converts the argument from radians to degrees. + + Examples: + --------- + >>> from math import pi + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0,pi,2*pi]}) + >>> deg_df = df.select(dfn.functions.degrees(dfn.col("a")).alias("deg")) + >>> deg_df.collect_column("deg")[2].as_py() + 360.0 + """ return Expr(f.degrees(arg.expr)) @@ -778,7 +879,22 @@ def pow(base: Expr, exponent: Expr) -> Expr: def radians(arg: Expr) -> Expr: - """Converts the argument from degrees to radians.""" + """Converts the argument from degrees to radians. + + Examples: + --------- + >>> from math import pi + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [180.0]}) + >>> import builtins + >>> result = df.select( + ... dfn.functions.radians(dfn.col("a")).alias("rad") + ... ) + >>> builtins.round( + ... result.collect_column("rad")[0].as_py(), 6 + ... ) + 3.141593 + """ return Expr(f.radians(arg.expr)) @@ -939,12 +1055,30 @@ def signum(arg: Expr) -> Expr: def sin(arg: Expr) -> Expr: - """Returns the sine of the argument.""" + """Returns the sine of the argument. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0.0]}) + >>> result = df.select(dfn.functions.sin(dfn.col("a")).alias("sin")) + >>> result.collect_column("sin")[0].as_py() + 0.0 + """ return Expr(f.sin(arg.expr)) def sinh(arg: Expr) -> Expr: - """Returns the hyperbolic sine of the argument.""" + """Returns the hyperbolic sine of the argument. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0.0]}) + >>> result = df.select(dfn.functions.sinh(dfn.col("a")).alias("sinh")) + >>> result.collect_column("sinh")[0].as_py() + 0.0 + """ return Expr(f.sinh(arg.expr)) @@ -992,12 +1126,30 @@ def substring(string: Expr, position: Expr, length: Expr) -> Expr: def tan(arg: Expr) -> Expr: - """Returns the tangent of the argument.""" + """Returns the tangent of the argument. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0.0]}) + >>> result = df.select(dfn.functions.tan(dfn.col("a")).alias("tan")) + >>> result.collect_column("tan")[0].as_py() + 0.0 + """ return Expr(f.tan(arg.expr)) def tanh(arg: Expr) -> Expr: - """Returns the hyperbolic tangent of the argument.""" + """Returns the hyperbolic tangent of the argument. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0.0]}) + >>> result = df.select(dfn.functions.tanh(dfn.col("a")).alias("tanh")) + >>> result.collect_column("tanh")[0].as_py() + 0.0 + """ return Expr(f.tanh(arg.expr)) From 9af1681f203ec2a21b64371a1dc6361641ffb2f9 Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Mon, 16 Mar 2026 12:05:00 +0100 Subject: [PATCH 10/56] Create workspace with core and util crates (#1414) * Break up the repository into a workspace with three crates * We have a workspace cargo lock file now so this is not needed * Cleanup * These files should be redundant because of the build.rs file * More moving around of utils to clean up * Add note on how to run FFI example tests * Add back in dep removed during rebase * taplo fmt * Since we have a workspace we know the example version is in sync so we do not need this test * Add description, homepage, and repository to Cargo.toml Co-authored-by: Kevin Liu * Add description, homepage, and repository to Cargo.toml Co-authored-by: Kevin Liu * Add description, homepage, and repository to Cargo.toml Co-authored-by: Kevin Liu * Removed unused include Co-authored-by: Kevin Liu --------- Co-authored-by: Kevin Liu --- .cargo/config.toml | 5 - .github/workflows/test.yml | 12 - Cargo.lock | 33 + Cargo.toml | 78 +- README.md | 9 + crates/core/Cargo.toml | 82 + build.rs => crates/core/build.rs | 0 {src => crates/core/src}/array.rs | 2 +- {src => crates/core/src}/catalog.rs | 20 +- {src => crates/core/src}/common.rs | 0 {src => crates/core/src}/common/data_type.rs | 0 {src => crates/core/src}/common/df_schema.rs | 0 {src => crates/core/src}/common/function.rs | 0 {src => crates/core/src}/common/schema.rs | 0 {src => crates/core/src}/config.rs | 0 {src => crates/core/src}/context.rs | 13 +- {src => crates/core/src}/dataframe.rs | 2 +- {src => crates/core/src}/dataset.rs | 0 {src => crates/core/src}/dataset_exec.rs | 0 crates/core/src/errors.rs | 18 + {src => crates/core/src}/expr.rs | 0 {src => crates/core/src}/expr/aggregate.rs | 0 .../core/src}/expr/aggregate_expr.rs | 0 {src => crates/core/src}/expr/alias.rs | 0 {src => crates/core/src}/expr/analyze.rs | 0 {src => crates/core/src}/expr/between.rs | 0 {src => crates/core/src}/expr/binary_expr.rs | 0 {src => crates/core/src}/expr/bool_expr.rs | 0 {src => crates/core/src}/expr/case.rs | 0 {src => crates/core/src}/expr/cast.rs | 0 {src => crates/core/src}/expr/column.rs | 0 .../core/src}/expr/conditional_expr.rs | 0 {src => crates/core/src}/expr/copy_to.rs | 0 .../core/src}/expr/create_catalog.rs | 0 .../core/src}/expr/create_catalog_schema.rs | 0 .../core/src}/expr/create_external_table.rs | 0 .../core/src}/expr/create_function.rs | 0 {src => crates/core/src}/expr/create_index.rs | 0 .../core/src}/expr/create_memory_table.rs | 0 {src => crates/core/src}/expr/create_view.rs | 0 .../core/src}/expr/describe_table.rs | 0 {src => crates/core/src}/expr/distinct.rs | 0 {src => crates/core/src}/expr/dml.rs | 0 .../core/src}/expr/drop_catalog_schema.rs | 0 .../core/src}/expr/drop_function.rs | 0 {src => crates/core/src}/expr/drop_table.rs | 0 {src => crates/core/src}/expr/drop_view.rs | 0 .../core/src}/expr/empty_relation.rs | 0 {src => crates/core/src}/expr/exists.rs | 0 {src => crates/core/src}/expr/explain.rs | 0 {src => crates/core/src}/expr/extension.rs | 0 {src => crates/core/src}/expr/filter.rs | 0 {src => crates/core/src}/expr/grouping_set.rs | 0 {src => crates/core/src}/expr/in_list.rs | 0 {src => crates/core/src}/expr/in_subquery.rs | 0 .../core/src}/expr/indexed_field.rs | 0 {src => crates/core/src}/expr/join.rs | 0 {src => crates/core/src}/expr/like.rs | 0 {src => crates/core/src}/expr/limit.rs | 0 {src => crates/core/src}/expr/literal.rs | 0 {src => crates/core/src}/expr/logical_node.rs | 0 {src => crates/core/src}/expr/placeholder.rs | 0 {src => crates/core/src}/expr/projection.rs | 0 .../core/src}/expr/recursive_query.rs | 0 {src => crates/core/src}/expr/repartition.rs | 0 .../core/src}/expr/scalar_subquery.rs | 0 .../core/src}/expr/scalar_variable.rs | 0 .../core/src}/expr/set_comparison.rs | 0 {src => crates/core/src}/expr/signature.rs | 0 {src => crates/core/src}/expr/sort.rs | 0 {src => crates/core/src}/expr/sort_expr.rs | 0 {src => crates/core/src}/expr/statement.rs | 0 {src => crates/core/src}/expr/subquery.rs | 0 .../core/src}/expr/subquery_alias.rs | 0 {src => crates/core/src}/expr/table_scan.rs | 0 {src => crates/core/src}/expr/union.rs | 0 {src => crates/core/src}/expr/unnest.rs | 0 {src => crates/core/src}/expr/unnest_expr.rs | 0 {src => crates/core/src}/expr/values.rs | 0 {src => crates/core/src}/expr/window.rs | 0 {src => crates/core/src}/functions.rs | 0 {src => crates/core/src}/lib.rs | 4 - {src => crates/core/src}/options.rs | 0 {src => crates/core/src}/physical_plan.rs | 0 .../core/src}/pyarrow_filter_expression.rs | 0 {src => crates/core/src}/pyarrow_util.rs | 0 {src => crates/core/src}/record_batch.rs | 2 +- {src => crates/core/src}/sql.rs | 0 {src => crates/core/src}/sql/exceptions.rs | 0 {src => crates/core/src}/sql/logical.rs | 0 {src => crates/core/src}/sql/util.rs | 0 {src => crates/core/src}/store.rs | 0 {src => crates/core/src}/substrait.rs | 2 +- {src => crates/core/src}/table.rs | 2 +- {src => crates/core/src}/udaf.rs | 2 +- {src => crates/core/src}/udf.rs | 2 +- {src => crates/core/src}/udtf.rs | 2 +- {src => crates/core/src}/udwf.rs | 2 +- {src => crates/core/src}/unparser/dialect.rs | 0 {src => crates/core/src}/unparser/mod.rs | 0 crates/util/Cargo.toml | 34 + {src => crates/util/src}/errors.rs | 0 src/utils.rs => crates/util/src/lib.rs | 65 +- .../datafusion-ffi-example/.cargo/config.toml | 5 - examples/datafusion-ffi-example/Cargo.lock | 3127 ----------------- examples/datafusion-ffi-example/Cargo.toml | 47 +- .../src/catalog_provider.rs | 3 +- examples/datafusion-ffi-example/src/lib.rs | 1 - .../src/table_function.rs | 2 +- .../src/table_provider.rs | 3 +- examples/datafusion-ffi-example/src/utils.rs | 64 - pyproject.toml | 1 + 112 files changed, 291 insertions(+), 3353 deletions(-) delete mode 100644 .cargo/config.toml create mode 100644 crates/core/Cargo.toml rename build.rs => crates/core/build.rs (100%) rename {src => crates/core/src}/array.rs (98%) rename {src => crates/core/src}/catalog.rs (97%) rename {src => crates/core/src}/common.rs (100%) rename {src => crates/core/src}/common/data_type.rs (100%) rename {src => crates/core/src}/common/df_schema.rs (100%) rename {src => crates/core/src}/common/function.rs (100%) rename {src => crates/core/src}/common/schema.rs (100%) rename {src => crates/core/src}/config.rs (100%) rename {src => crates/core/src}/context.rs (99%) rename {src => crates/core/src}/dataframe.rs (99%) rename {src => crates/core/src}/dataset.rs (100%) rename {src => crates/core/src}/dataset_exec.rs (100%) create mode 100644 crates/core/src/errors.rs rename {src => crates/core/src}/expr.rs (100%) rename {src => crates/core/src}/expr/aggregate.rs (100%) rename {src => crates/core/src}/expr/aggregate_expr.rs (100%) rename {src => crates/core/src}/expr/alias.rs (100%) rename {src => crates/core/src}/expr/analyze.rs (100%) rename {src => crates/core/src}/expr/between.rs (100%) rename {src => crates/core/src}/expr/binary_expr.rs (100%) rename {src => crates/core/src}/expr/bool_expr.rs (100%) rename {src => crates/core/src}/expr/case.rs (100%) rename {src => crates/core/src}/expr/cast.rs (100%) rename {src => crates/core/src}/expr/column.rs (100%) rename {src => crates/core/src}/expr/conditional_expr.rs (100%) rename {src => crates/core/src}/expr/copy_to.rs (100%) rename {src => crates/core/src}/expr/create_catalog.rs (100%) rename {src => crates/core/src}/expr/create_catalog_schema.rs (100%) rename {src => crates/core/src}/expr/create_external_table.rs (100%) rename {src => crates/core/src}/expr/create_function.rs (100%) rename {src => crates/core/src}/expr/create_index.rs (100%) rename {src => crates/core/src}/expr/create_memory_table.rs (100%) rename {src => crates/core/src}/expr/create_view.rs (100%) rename {src => crates/core/src}/expr/describe_table.rs (100%) rename {src => crates/core/src}/expr/distinct.rs (100%) rename {src => crates/core/src}/expr/dml.rs (100%) rename {src => crates/core/src}/expr/drop_catalog_schema.rs (100%) rename {src => crates/core/src}/expr/drop_function.rs (100%) rename {src => crates/core/src}/expr/drop_table.rs (100%) rename {src => crates/core/src}/expr/drop_view.rs (100%) rename {src => crates/core/src}/expr/empty_relation.rs (100%) rename {src => crates/core/src}/expr/exists.rs (100%) rename {src => crates/core/src}/expr/explain.rs (100%) rename {src => crates/core/src}/expr/extension.rs (100%) rename {src => crates/core/src}/expr/filter.rs (100%) rename {src => crates/core/src}/expr/grouping_set.rs (100%) rename {src => crates/core/src}/expr/in_list.rs (100%) rename {src => crates/core/src}/expr/in_subquery.rs (100%) rename {src => crates/core/src}/expr/indexed_field.rs (100%) rename {src => crates/core/src}/expr/join.rs (100%) rename {src => crates/core/src}/expr/like.rs (100%) rename {src => crates/core/src}/expr/limit.rs (100%) rename {src => crates/core/src}/expr/literal.rs (100%) rename {src => crates/core/src}/expr/logical_node.rs (100%) rename {src => crates/core/src}/expr/placeholder.rs (100%) rename {src => crates/core/src}/expr/projection.rs (100%) rename {src => crates/core/src}/expr/recursive_query.rs (100%) rename {src => crates/core/src}/expr/repartition.rs (100%) rename {src => crates/core/src}/expr/scalar_subquery.rs (100%) rename {src => crates/core/src}/expr/scalar_variable.rs (100%) rename {src => crates/core/src}/expr/set_comparison.rs (100%) rename {src => crates/core/src}/expr/signature.rs (100%) rename {src => crates/core/src}/expr/sort.rs (100%) rename {src => crates/core/src}/expr/sort_expr.rs (100%) rename {src => crates/core/src}/expr/statement.rs (100%) rename {src => crates/core/src}/expr/subquery.rs (100%) rename {src => crates/core/src}/expr/subquery_alias.rs (100%) rename {src => crates/core/src}/expr/table_scan.rs (100%) rename {src => crates/core/src}/expr/union.rs (100%) rename {src => crates/core/src}/expr/unnest.rs (100%) rename {src => crates/core/src}/expr/unnest_expr.rs (100%) rename {src => crates/core/src}/expr/values.rs (100%) rename {src => crates/core/src}/expr/window.rs (100%) rename {src => crates/core/src}/functions.rs (100%) rename {src => crates/core/src}/lib.rs (97%) rename {src => crates/core/src}/options.rs (100%) rename {src => crates/core/src}/physical_plan.rs (100%) rename {src => crates/core/src}/pyarrow_filter_expression.rs (100%) rename {src => crates/core/src}/pyarrow_util.rs (100%) rename {src => crates/core/src}/record_batch.rs (98%) rename {src => crates/core/src}/sql.rs (100%) rename {src => crates/core/src}/sql/exceptions.rs (100%) rename {src => crates/core/src}/sql/logical.rs (100%) rename {src => crates/core/src}/sql/util.rs (100%) rename {src => crates/core/src}/store.rs (100%) rename {src => crates/core/src}/substrait.rs (99%) rename {src => crates/core/src}/table.rs (99%) rename {src => crates/core/src}/udaf.rs (99%) rename {src => crates/core/src}/udf.rs (99%) rename {src => crates/core/src}/udtf.rs (99%) rename {src => crates/core/src}/udwf.rs (99%) rename {src => crates/core/src}/unparser/dialect.rs (100%) rename {src => crates/core/src}/unparser/mod.rs (100%) create mode 100644 crates/util/Cargo.toml rename {src => crates/util/src}/errors.rs (100%) rename src/utils.rs => crates/util/src/lib.rs (85%) delete mode 100644 examples/datafusion-ffi-example/.cargo/config.toml delete mode 100644 examples/datafusion-ffi-example/Cargo.lock delete mode 100644 examples/datafusion-ffi-example/src/utils.rs diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index af951327f..000000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,5 +0,0 @@ -[target.x86_64-apple-darwin] -rustflags = ["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"] - -[target.aarch64-apple-darwin] -rustflags = ["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"] diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 692563019..a2f304aa5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,18 +41,6 @@ jobs: steps: - uses: actions/checkout@v6 - - name: Verify example datafusion version - run: | - MAIN_VERSION=$(grep -A 1 "name = \"datafusion-common\"" Cargo.lock | grep "version = " | head -1 | sed 's/.*version = "\(.*\)"/\1/') - EXAMPLE_VERSION=$(grep -A 1 "name = \"datafusion-common\"" examples/datafusion-ffi-example/Cargo.lock | grep "version = " | head -1 | sed 's/.*version = "\(.*\)"/\1/') - echo "Main crate datafusion version: $MAIN_VERSION" - echo "FFI example datafusion version: $EXAMPLE_VERSION" - - if [ "$MAIN_VERSION" != "$EXAMPLE_VERSION" ]; then - echo "❌ Error: FFI example datafusion versions don't match!" - exit 1 - fi - - name: Setup Python uses: actions/setup-python@v6 with: diff --git a/Cargo.lock b/Cargo.lock index 40b1ba7f1..e44c84b97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1274,6 +1274,26 @@ dependencies = [ "tokio", ] +[[package]] +name = "datafusion-ffi-example" +version = "52.0.0" +dependencies = [ + "arrow", + "arrow-array", + "arrow-schema", + "async-trait", + "datafusion-catalog", + "datafusion-common", + "datafusion-expr", + "datafusion-ffi", + "datafusion-functions-aggregate", + "datafusion-functions-window", + "datafusion-python-util", + "pyo3", + "pyo3-build-config", + "pyo3-log", +] + [[package]] name = "datafusion-functions" version = "53.0.0" @@ -1598,6 +1618,7 @@ dependencies = [ "datafusion", "datafusion-ffi", "datafusion-proto", + "datafusion-python-util", "datafusion-substrait", "futures", "log", @@ -1616,6 +1637,18 @@ dependencies = [ "uuid", ] +[[package]] +name = "datafusion-python-util" +version = "52.0.0" +dependencies = [ + "arrow", + "datafusion", + "datafusion-ffi", + "prost", + "pyo3", + "tokio", +] + [[package]] name = "datafusion-session" version = "53.0.0" diff --git a/Cargo.toml b/Cargo.toml index b584470d6..aa8aa6b38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,7 @@ # specific language governing permissions and limitations # under the License. -[package] -name = "datafusion-python" +[workspace.package] version = "52.0.0" homepage = "https://datafusion.apache.org/python" repository = "https://github.com/apache/datafusion-python" @@ -26,67 +25,43 @@ readme = "README.md" license = "Apache-2.0" edition = "2024" rust-version = "1.88" -include = [ - "/src", - "/datafusion", - "/LICENSE.txt", - "build.rs", - "pyproject.toml", - "Cargo.toml", - "Cargo.lock", -] -[features] -default = ["mimalloc"] -protoc = ["datafusion-substrait/protoc"] -substrait = ["dep:datafusion-substrait"] +[workspace] +members = ["crates/core", "crates/util", "examples/datafusion-ffi-example"] +resolver = "3" -[dependencies] -tokio = { version = "1.49", features = [ - "macros", - "rt", - "rt-multi-thread", - "sync", -] } -pyo3 = { version = "0.28", features = [ - "extension-module", - "abi3", - "abi3-py310", -] } -pyo3-async-runtimes = { version = "0.28", features = ["tokio-runtime"] } +[workspace.dependencies] +tokio = { version = "1.49" } +pyo3 = { version = "0.28" } +pyo3-async-runtimes = { version = "0.28" } pyo3-log = "0.13.3" -arrow = { version = "58", features = ["pyarrow"] } +arrow = { version = "58" } +arrow-array = { version = "58" } +arrow-schema = { version = "58" } arrow-select = { version = "58" } -datafusion = { version = "53", features = ["avro", "unicode_expressions"] } -datafusion-substrait = { version = "53", optional = true } +datafusion = { version = "53" } +datafusion-substrait = { version = "53" } datafusion-proto = { version = "53" } datafusion-ffi = { version = "53" } -prost = "0.14.3" # keep in line with `datafusion-substrait` +datafusion-catalog = { version = "53", default-features = false } +datafusion-common = { version = "53", default-features = false } +datafusion-functions-aggregate = { version = "53" } +datafusion-functions-window = { version = "53" } +datafusion-expr = { version = "53" } +prost = "0.14.3" serde_json = "1" -uuid = { version = "1.21", features = ["v4"] } -mimalloc = { version = "0.1", optional = true, default-features = false, features = [ - "local_dynamic_tls", -] } +uuid = { version = "1.21" } +mimalloc = { version = "0.1", default-features = false } async-trait = "0.1.89" futures = "0.3" cstr = "0.2" -object_store = { version = "0.13.1", features = [ - "aws", - "gcp", - "azure", - "http", -] } +object_store = { version = "0.13.1" } url = "2" log = "0.4.29" parking_lot = "0.12" - -[build-dependencies] -prost-types = "0.14.3" # keep in line with `datafusion-substrait` +prost-types = "0.14.3" # keep in line with `datafusion-substrait` pyo3-build-config = "0.28" - -[lib] -name = "datafusion_python" -crate-type = ["cdylib", "rlib"] +datafusion-python-util = { path = "crates/util" } [profile.release] lto = true @@ -99,3 +74,8 @@ datafusion = { git = "https://github.com/apache/datafusion.git", rev = "35749607 datafusion-substrait = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } datafusion-proto = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } datafusion-ffi = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } +datafusion-catalog = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } +datafusion-common = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } +datafusion-functions-aggregate = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } +datafusion-functions-window = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } +datafusion-expr = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } diff --git a/README.md b/README.md index 810ac8710..c24257876 100644 --- a/README.md +++ b/README.md @@ -278,6 +278,15 @@ uv run --no-project maturin develop --uv uv run --no-project pytest . ``` +To run the FFI tests within the examples folder, after you have built +`datafusion-python` with the previous commands: + +```bash +cd examples/datafusion-ffi-example +uv run --no-project maturin develop --uv +uv run --no-project pytest python/tests/_test_*py +``` + ### Running & Installing pre-commit hooks `datafusion-python` takes advantage of [pre-commit](https://pre-commit.com/) to assist developers with code linting to help reduce diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml new file mode 100644 index 000000000..3e2b01c8e --- /dev/null +++ b/crates/core/Cargo.toml @@ -0,0 +1,82 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +[package] +name = "datafusion-python" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +description.workspace = true +homepage.workspace = true +repository.workspace = true +include = [ + "src", + "../LICENSE.txt", + "build.rs", + "../pyproject.toml", + "Cargo.toml", + "../Cargo.lock", +] + +[dependencies] +tokio = { workspace = true, features = [ + "macros", + "rt", + "rt-multi-thread", + "sync", +] } +pyo3 = { workspace = true, features = [ + "extension-module", + "abi3", + "abi3-py310", +] } +pyo3-async-runtimes = { workspace = true, features = ["tokio-runtime"] } +pyo3-log = { workspace = true } +arrow = { workspace = true, features = ["pyarrow"] } +arrow-select = { workspace = true } +datafusion = { workspace = true, features = ["avro", "unicode_expressions"] } +datafusion-substrait = { workspace = true, optional = true } +datafusion-proto = { workspace = true } +datafusion-ffi = { workspace = true } +prost = { workspace = true } # keep in line with `datafusion-substrait` +serde_json = { workspace = true } +uuid = { workspace = true, features = ["v4"] } +mimalloc = { workspace = true, optional = true, features = [ + "local_dynamic_tls", +] } +async-trait = { workspace = true } +futures = { workspace = true } +cstr = { workspace = true } +object_store = { workspace = true, features = ["aws", "gcp", "azure", "http"] } +url = { workspace = true } +log = { workspace = true } +parking_lot = { workspace = true } +datafusion-python-util = { workspace = true } + +[build-dependencies] +prost-types = { workspace = true } +pyo3-build-config = { workspace = true } + +[features] +default = ["mimalloc"] +protoc = ["datafusion-substrait/protoc"] +substrait = ["dep:datafusion-substrait"] + +[lib] +name = "datafusion_python" +crate-type = ["cdylib", "rlib"] diff --git a/build.rs b/crates/core/build.rs similarity index 100% rename from build.rs rename to crates/core/build.rs diff --git a/src/array.rs b/crates/core/src/array.rs similarity index 98% rename from src/array.rs rename to crates/core/src/array.rs index 1ff08dfb2..99e63ef50 100644 --- a/src/array.rs +++ b/crates/core/src/array.rs @@ -22,13 +22,13 @@ use arrow::array::{Array, ArrayRef}; use arrow::datatypes::{Field, FieldRef}; use arrow::ffi::{FFI_ArrowArray, FFI_ArrowSchema}; use arrow::pyarrow::ToPyArrow; +use datafusion_python_util::validate_pycapsule; use pyo3::ffi::c_str; use pyo3::prelude::{PyAnyMethods, PyCapsuleMethods}; use pyo3::types::PyCapsule; use pyo3::{Bound, PyAny, PyResult, Python, pyclass, pymethods}; use crate::errors::PyDataFusionResult; -use crate::utils::validate_pycapsule; /// A Python object which implements the Arrow PyCapsule for importing /// into other libraries. diff --git a/src/catalog.rs b/crates/core/src/catalog.rs similarity index 97% rename from src/catalog.rs rename to crates/core/src/catalog.rs index 43325c30d..f707e7e5c 100644 --- a/src/catalog.rs +++ b/crates/core/src/catalog.rs @@ -30,19 +30,20 @@ use datafusion::datasource::TableProvider; use datafusion_ffi::catalog_provider::FFI_CatalogProvider; use datafusion_ffi::proto::logical_extension_codec::FFI_LogicalExtensionCodec; use datafusion_ffi::schema_provider::FFI_SchemaProvider; +use datafusion_python_util::{ + create_logical_extension_capsule, ffi_logical_codec_from_pycapsule, validate_pycapsule, + wait_for_future, +}; use pyo3::IntoPyObjectExt; use pyo3::exceptions::PyKeyError; use pyo3::ffi::c_str; use pyo3::prelude::*; use pyo3::types::PyCapsule; +use crate::context::PySessionContext; use crate::dataset::Dataset; use crate::errors::{PyDataFusionError, PyDataFusionResult, py_datafusion_err, to_datafusion_err}; use crate::table::PyTable; -use crate::utils::{ - create_logical_extension_capsule, extract_logical_extension_codec, validate_pycapsule, - wait_for_future, -}; #[pyclass( from_py_object, @@ -710,6 +711,17 @@ fn extract_schema_provider_from_pyobj( Ok(provider) } +fn extract_logical_extension_codec( + py: Python, + obj: Option>, +) -> PyResult> { + let obj = match obj { + Some(obj) => obj, + None => PySessionContext::global_ctx()?.into_bound_py_any(py)?, + }; + ffi_logical_codec_from_pycapsule(obj).map(Arc::new) +} + pub(crate) fn init_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; diff --git a/src/common.rs b/crates/core/src/common.rs similarity index 100% rename from src/common.rs rename to crates/core/src/common.rs diff --git a/src/common/data_type.rs b/crates/core/src/common/data_type.rs similarity index 100% rename from src/common/data_type.rs rename to crates/core/src/common/data_type.rs diff --git a/src/common/df_schema.rs b/crates/core/src/common/df_schema.rs similarity index 100% rename from src/common/df_schema.rs rename to crates/core/src/common/df_schema.rs diff --git a/src/common/function.rs b/crates/core/src/common/function.rs similarity index 100% rename from src/common/function.rs rename to crates/core/src/common/function.rs diff --git a/src/common/schema.rs b/crates/core/src/common/schema.rs similarity index 100% rename from src/common/schema.rs rename to crates/core/src/common/schema.rs diff --git a/src/config.rs b/crates/core/src/config.rs similarity index 100% rename from src/config.rs rename to crates/core/src/config.rs diff --git a/src/context.rs b/crates/core/src/context.rs similarity index 99% rename from src/context.rs rename to crates/core/src/context.rs index 2eaf5a737..709fdc5a2 100644 --- a/src/context.rs +++ b/crates/core/src/context.rs @@ -52,6 +52,10 @@ use datafusion_ffi::catalog_provider_list::FFI_CatalogProviderList; use datafusion_ffi::execution::FFI_TaskContextProvider; use datafusion_ffi::proto::logical_extension_codec::FFI_LogicalExtensionCodec; use datafusion_proto::logical_plan::DefaultLogicalExtensionCodec; +use datafusion_python_util::{ + create_logical_extension_capsule, ffi_logical_codec_from_pycapsule, get_global_ctx, + get_tokio_runtime, spawn_future, validate_pycapsule, wait_for_future, +}; use object_store::ObjectStore; use pyo3::IntoPyObjectExt; use pyo3::exceptions::{PyKeyError, PyValueError}; @@ -82,10 +86,6 @@ use crate::udaf::PyAggregateUDF; use crate::udf::PyScalarUDF; use crate::udtf::PyTableFunction; use crate::udwf::PyWindowUDF; -use crate::utils::{ - create_logical_extension_capsule, extract_logical_extension_codec, get_global_ctx, - get_tokio_runtime, spawn_future, validate_pycapsule, wait_for_future, -}; /// Configuration options for a SessionContext #[pyclass( @@ -1187,8 +1187,7 @@ impl PySessionContext { &self, codec: Bound<'py, PyAny>, ) -> PyDataFusionResult { - let py = codec.py(); - let logical_codec = extract_logical_extension_codec(py, Some(codec))?; + let logical_codec = Arc::new(ffi_logical_codec_from_pycapsule(codec)?); Ok({ Self { @@ -1246,7 +1245,7 @@ impl PySessionContext { fn default_logical_codec(ctx: &Arc) -> Arc { let codec = Arc::new(DefaultLogicalExtensionCodec {}); - let runtime = get_tokio_runtime().0.handle().clone(); + let runtime = get_tokio_runtime().handle().clone(); let ctx_provider = Arc::clone(ctx) as Arc; Arc::new(FFI_LogicalExtensionCodec::new( codec, diff --git a/src/dataframe.rs b/crates/core/src/dataframe.rs similarity index 99% rename from src/dataframe.rs rename to crates/core/src/dataframe.rs index eb1fa4a81..29fc05ed3 100644 --- a/src/dataframe.rs +++ b/crates/core/src/dataframe.rs @@ -41,6 +41,7 @@ use datafusion::logical_expr::SortExpr; use datafusion::logical_expr::dml::InsertOp; use datafusion::parquet::basic::{BrotliLevel, Compression, GzipLevel, ZstdLevel}; use datafusion::prelude::*; +use datafusion_python_util::{is_ipython_env, spawn_future, validate_pycapsule, wait_for_future}; use futures::{StreamExt, TryStreamExt}; use parking_lot::Mutex; use pyo3::PyErr; @@ -58,7 +59,6 @@ use crate::physical_plan::PyExecutionPlan; use crate::record_batch::{PyRecordBatchStream, poll_next_batch}; use crate::sql::logical::PyLogicalPlan; use crate::table::{PyTable, TempViewTable}; -use crate::utils::{is_ipython_env, spawn_future, validate_pycapsule, wait_for_future}; /// File-level static CStr for the Arrow array stream capsule name. static ARROW_ARRAY_STREAM_NAME: &CStr = cstr!("arrow_array_stream"); diff --git a/src/dataset.rs b/crates/core/src/dataset.rs similarity index 100% rename from src/dataset.rs rename to crates/core/src/dataset.rs diff --git a/src/dataset_exec.rs b/crates/core/src/dataset_exec.rs similarity index 100% rename from src/dataset_exec.rs rename to crates/core/src/dataset_exec.rs diff --git a/crates/core/src/errors.rs b/crates/core/src/errors.rs new file mode 100644 index 000000000..8babc5a56 --- /dev/null +++ b/crates/core/src/errors.rs @@ -0,0 +1,18 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +pub use datafusion_python_util::errors::*; diff --git a/src/expr.rs b/crates/core/src/expr.rs similarity index 100% rename from src/expr.rs rename to crates/core/src/expr.rs diff --git a/src/expr/aggregate.rs b/crates/core/src/expr/aggregate.rs similarity index 100% rename from src/expr/aggregate.rs rename to crates/core/src/expr/aggregate.rs diff --git a/src/expr/aggregate_expr.rs b/crates/core/src/expr/aggregate_expr.rs similarity index 100% rename from src/expr/aggregate_expr.rs rename to crates/core/src/expr/aggregate_expr.rs diff --git a/src/expr/alias.rs b/crates/core/src/expr/alias.rs similarity index 100% rename from src/expr/alias.rs rename to crates/core/src/expr/alias.rs diff --git a/src/expr/analyze.rs b/crates/core/src/expr/analyze.rs similarity index 100% rename from src/expr/analyze.rs rename to crates/core/src/expr/analyze.rs diff --git a/src/expr/between.rs b/crates/core/src/expr/between.rs similarity index 100% rename from src/expr/between.rs rename to crates/core/src/expr/between.rs diff --git a/src/expr/binary_expr.rs b/crates/core/src/expr/binary_expr.rs similarity index 100% rename from src/expr/binary_expr.rs rename to crates/core/src/expr/binary_expr.rs diff --git a/src/expr/bool_expr.rs b/crates/core/src/expr/bool_expr.rs similarity index 100% rename from src/expr/bool_expr.rs rename to crates/core/src/expr/bool_expr.rs diff --git a/src/expr/case.rs b/crates/core/src/expr/case.rs similarity index 100% rename from src/expr/case.rs rename to crates/core/src/expr/case.rs diff --git a/src/expr/cast.rs b/crates/core/src/expr/cast.rs similarity index 100% rename from src/expr/cast.rs rename to crates/core/src/expr/cast.rs diff --git a/src/expr/column.rs b/crates/core/src/expr/column.rs similarity index 100% rename from src/expr/column.rs rename to crates/core/src/expr/column.rs diff --git a/src/expr/conditional_expr.rs b/crates/core/src/expr/conditional_expr.rs similarity index 100% rename from src/expr/conditional_expr.rs rename to crates/core/src/expr/conditional_expr.rs diff --git a/src/expr/copy_to.rs b/crates/core/src/expr/copy_to.rs similarity index 100% rename from src/expr/copy_to.rs rename to crates/core/src/expr/copy_to.rs diff --git a/src/expr/create_catalog.rs b/crates/core/src/expr/create_catalog.rs similarity index 100% rename from src/expr/create_catalog.rs rename to crates/core/src/expr/create_catalog.rs diff --git a/src/expr/create_catalog_schema.rs b/crates/core/src/expr/create_catalog_schema.rs similarity index 100% rename from src/expr/create_catalog_schema.rs rename to crates/core/src/expr/create_catalog_schema.rs diff --git a/src/expr/create_external_table.rs b/crates/core/src/expr/create_external_table.rs similarity index 100% rename from src/expr/create_external_table.rs rename to crates/core/src/expr/create_external_table.rs diff --git a/src/expr/create_function.rs b/crates/core/src/expr/create_function.rs similarity index 100% rename from src/expr/create_function.rs rename to crates/core/src/expr/create_function.rs diff --git a/src/expr/create_index.rs b/crates/core/src/expr/create_index.rs similarity index 100% rename from src/expr/create_index.rs rename to crates/core/src/expr/create_index.rs diff --git a/src/expr/create_memory_table.rs b/crates/core/src/expr/create_memory_table.rs similarity index 100% rename from src/expr/create_memory_table.rs rename to crates/core/src/expr/create_memory_table.rs diff --git a/src/expr/create_view.rs b/crates/core/src/expr/create_view.rs similarity index 100% rename from src/expr/create_view.rs rename to crates/core/src/expr/create_view.rs diff --git a/src/expr/describe_table.rs b/crates/core/src/expr/describe_table.rs similarity index 100% rename from src/expr/describe_table.rs rename to crates/core/src/expr/describe_table.rs diff --git a/src/expr/distinct.rs b/crates/core/src/expr/distinct.rs similarity index 100% rename from src/expr/distinct.rs rename to crates/core/src/expr/distinct.rs diff --git a/src/expr/dml.rs b/crates/core/src/expr/dml.rs similarity index 100% rename from src/expr/dml.rs rename to crates/core/src/expr/dml.rs diff --git a/src/expr/drop_catalog_schema.rs b/crates/core/src/expr/drop_catalog_schema.rs similarity index 100% rename from src/expr/drop_catalog_schema.rs rename to crates/core/src/expr/drop_catalog_schema.rs diff --git a/src/expr/drop_function.rs b/crates/core/src/expr/drop_function.rs similarity index 100% rename from src/expr/drop_function.rs rename to crates/core/src/expr/drop_function.rs diff --git a/src/expr/drop_table.rs b/crates/core/src/expr/drop_table.rs similarity index 100% rename from src/expr/drop_table.rs rename to crates/core/src/expr/drop_table.rs diff --git a/src/expr/drop_view.rs b/crates/core/src/expr/drop_view.rs similarity index 100% rename from src/expr/drop_view.rs rename to crates/core/src/expr/drop_view.rs diff --git a/src/expr/empty_relation.rs b/crates/core/src/expr/empty_relation.rs similarity index 100% rename from src/expr/empty_relation.rs rename to crates/core/src/expr/empty_relation.rs diff --git a/src/expr/exists.rs b/crates/core/src/expr/exists.rs similarity index 100% rename from src/expr/exists.rs rename to crates/core/src/expr/exists.rs diff --git a/src/expr/explain.rs b/crates/core/src/expr/explain.rs similarity index 100% rename from src/expr/explain.rs rename to crates/core/src/expr/explain.rs diff --git a/src/expr/extension.rs b/crates/core/src/expr/extension.rs similarity index 100% rename from src/expr/extension.rs rename to crates/core/src/expr/extension.rs diff --git a/src/expr/filter.rs b/crates/core/src/expr/filter.rs similarity index 100% rename from src/expr/filter.rs rename to crates/core/src/expr/filter.rs diff --git a/src/expr/grouping_set.rs b/crates/core/src/expr/grouping_set.rs similarity index 100% rename from src/expr/grouping_set.rs rename to crates/core/src/expr/grouping_set.rs diff --git a/src/expr/in_list.rs b/crates/core/src/expr/in_list.rs similarity index 100% rename from src/expr/in_list.rs rename to crates/core/src/expr/in_list.rs diff --git a/src/expr/in_subquery.rs b/crates/core/src/expr/in_subquery.rs similarity index 100% rename from src/expr/in_subquery.rs rename to crates/core/src/expr/in_subquery.rs diff --git a/src/expr/indexed_field.rs b/crates/core/src/expr/indexed_field.rs similarity index 100% rename from src/expr/indexed_field.rs rename to crates/core/src/expr/indexed_field.rs diff --git a/src/expr/join.rs b/crates/core/src/expr/join.rs similarity index 100% rename from src/expr/join.rs rename to crates/core/src/expr/join.rs diff --git a/src/expr/like.rs b/crates/core/src/expr/like.rs similarity index 100% rename from src/expr/like.rs rename to crates/core/src/expr/like.rs diff --git a/src/expr/limit.rs b/crates/core/src/expr/limit.rs similarity index 100% rename from src/expr/limit.rs rename to crates/core/src/expr/limit.rs diff --git a/src/expr/literal.rs b/crates/core/src/expr/literal.rs similarity index 100% rename from src/expr/literal.rs rename to crates/core/src/expr/literal.rs diff --git a/src/expr/logical_node.rs b/crates/core/src/expr/logical_node.rs similarity index 100% rename from src/expr/logical_node.rs rename to crates/core/src/expr/logical_node.rs diff --git a/src/expr/placeholder.rs b/crates/core/src/expr/placeholder.rs similarity index 100% rename from src/expr/placeholder.rs rename to crates/core/src/expr/placeholder.rs diff --git a/src/expr/projection.rs b/crates/core/src/expr/projection.rs similarity index 100% rename from src/expr/projection.rs rename to crates/core/src/expr/projection.rs diff --git a/src/expr/recursive_query.rs b/crates/core/src/expr/recursive_query.rs similarity index 100% rename from src/expr/recursive_query.rs rename to crates/core/src/expr/recursive_query.rs diff --git a/src/expr/repartition.rs b/crates/core/src/expr/repartition.rs similarity index 100% rename from src/expr/repartition.rs rename to crates/core/src/expr/repartition.rs diff --git a/src/expr/scalar_subquery.rs b/crates/core/src/expr/scalar_subquery.rs similarity index 100% rename from src/expr/scalar_subquery.rs rename to crates/core/src/expr/scalar_subquery.rs diff --git a/src/expr/scalar_variable.rs b/crates/core/src/expr/scalar_variable.rs similarity index 100% rename from src/expr/scalar_variable.rs rename to crates/core/src/expr/scalar_variable.rs diff --git a/src/expr/set_comparison.rs b/crates/core/src/expr/set_comparison.rs similarity index 100% rename from src/expr/set_comparison.rs rename to crates/core/src/expr/set_comparison.rs diff --git a/src/expr/signature.rs b/crates/core/src/expr/signature.rs similarity index 100% rename from src/expr/signature.rs rename to crates/core/src/expr/signature.rs diff --git a/src/expr/sort.rs b/crates/core/src/expr/sort.rs similarity index 100% rename from src/expr/sort.rs rename to crates/core/src/expr/sort.rs diff --git a/src/expr/sort_expr.rs b/crates/core/src/expr/sort_expr.rs similarity index 100% rename from src/expr/sort_expr.rs rename to crates/core/src/expr/sort_expr.rs diff --git a/src/expr/statement.rs b/crates/core/src/expr/statement.rs similarity index 100% rename from src/expr/statement.rs rename to crates/core/src/expr/statement.rs diff --git a/src/expr/subquery.rs b/crates/core/src/expr/subquery.rs similarity index 100% rename from src/expr/subquery.rs rename to crates/core/src/expr/subquery.rs diff --git a/src/expr/subquery_alias.rs b/crates/core/src/expr/subquery_alias.rs similarity index 100% rename from src/expr/subquery_alias.rs rename to crates/core/src/expr/subquery_alias.rs diff --git a/src/expr/table_scan.rs b/crates/core/src/expr/table_scan.rs similarity index 100% rename from src/expr/table_scan.rs rename to crates/core/src/expr/table_scan.rs diff --git a/src/expr/union.rs b/crates/core/src/expr/union.rs similarity index 100% rename from src/expr/union.rs rename to crates/core/src/expr/union.rs diff --git a/src/expr/unnest.rs b/crates/core/src/expr/unnest.rs similarity index 100% rename from src/expr/unnest.rs rename to crates/core/src/expr/unnest.rs diff --git a/src/expr/unnest_expr.rs b/crates/core/src/expr/unnest_expr.rs similarity index 100% rename from src/expr/unnest_expr.rs rename to crates/core/src/expr/unnest_expr.rs diff --git a/src/expr/values.rs b/crates/core/src/expr/values.rs similarity index 100% rename from src/expr/values.rs rename to crates/core/src/expr/values.rs diff --git a/src/expr/window.rs b/crates/core/src/expr/window.rs similarity index 100% rename from src/expr/window.rs rename to crates/core/src/expr/window.rs diff --git a/src/functions.rs b/crates/core/src/functions.rs similarity index 100% rename from src/functions.rs rename to crates/core/src/functions.rs diff --git a/src/lib.rs b/crates/core/src/lib.rs similarity index 97% rename from src/lib.rs rename to crates/core/src/lib.rs index 468243a3d..fc2d006d3 100644 --- a/src/lib.rs +++ b/crates/core/src/lib.rs @@ -62,15 +62,11 @@ mod udaf; mod udf; pub mod udtf; mod udwf; -pub mod utils; #[cfg(feature = "mimalloc")] #[global_allocator] static GLOBAL: MiMalloc = MiMalloc; -// Used to define Tokio Runtime as a Python module attribute -pub(crate) struct TokioRuntime(tokio::runtime::Runtime); - /// Low-level DataFusion internal package. /// /// The higher-level public API is defined in pure python files under the diff --git a/src/options.rs b/crates/core/src/options.rs similarity index 100% rename from src/options.rs rename to crates/core/src/options.rs diff --git a/src/physical_plan.rs b/crates/core/src/physical_plan.rs similarity index 100% rename from src/physical_plan.rs rename to crates/core/src/physical_plan.rs diff --git a/src/pyarrow_filter_expression.rs b/crates/core/src/pyarrow_filter_expression.rs similarity index 100% rename from src/pyarrow_filter_expression.rs rename to crates/core/src/pyarrow_filter_expression.rs diff --git a/src/pyarrow_util.rs b/crates/core/src/pyarrow_util.rs similarity index 100% rename from src/pyarrow_util.rs rename to crates/core/src/pyarrow_util.rs diff --git a/src/record_batch.rs b/crates/core/src/record_batch.rs similarity index 98% rename from src/record_batch.rs rename to crates/core/src/record_batch.rs index e8abc641b..0492c6c76 100644 --- a/src/record_batch.rs +++ b/crates/core/src/record_batch.rs @@ -20,6 +20,7 @@ use std::sync::Arc; use datafusion::arrow::pyarrow::ToPyArrow; use datafusion::arrow::record_batch::RecordBatch; use datafusion::physical_plan::SendableRecordBatchStream; +use datafusion_python_util::wait_for_future; use futures::StreamExt; use pyo3::exceptions::{PyStopAsyncIteration, PyStopIteration}; use pyo3::prelude::*; @@ -27,7 +28,6 @@ use pyo3::{PyAny, PyResult, Python, pyclass, pymethods}; use tokio::sync::Mutex; use crate::errors::PyDataFusionError; -use crate::utils::wait_for_future; #[pyclass(name = "RecordBatch", module = "datafusion", subclass, frozen)] pub struct PyRecordBatch { diff --git a/src/sql.rs b/crates/core/src/sql.rs similarity index 100% rename from src/sql.rs rename to crates/core/src/sql.rs diff --git a/src/sql/exceptions.rs b/crates/core/src/sql/exceptions.rs similarity index 100% rename from src/sql/exceptions.rs rename to crates/core/src/sql/exceptions.rs diff --git a/src/sql/logical.rs b/crates/core/src/sql/logical.rs similarity index 100% rename from src/sql/logical.rs rename to crates/core/src/sql/logical.rs diff --git a/src/sql/util.rs b/crates/core/src/sql/util.rs similarity index 100% rename from src/sql/util.rs rename to crates/core/src/sql/util.rs diff --git a/src/store.rs b/crates/core/src/store.rs similarity index 100% rename from src/store.rs rename to crates/core/src/store.rs diff --git a/src/substrait.rs b/crates/core/src/substrait.rs similarity index 99% rename from src/substrait.rs rename to crates/core/src/substrait.rs index c2f112520..27e446f48 100644 --- a/src/substrait.rs +++ b/crates/core/src/substrait.rs @@ -15,6 +15,7 @@ // specific language governing permissions and limitations // under the License. +use datafusion_python_util::wait_for_future; use datafusion_substrait::logical_plan::{consumer, producer}; use datafusion_substrait::serializer; use datafusion_substrait::substrait::proto::Plan; @@ -25,7 +26,6 @@ use pyo3::types::PyBytes; use crate::context::PySessionContext; use crate::errors::{PyDataFusionError, PyDataFusionResult, py_datafusion_err, to_datafusion_err}; use crate::sql::logical::PyLogicalPlan; -use crate::utils::wait_for_future; #[pyclass( from_py_object, diff --git a/src/table.rs b/crates/core/src/table.rs similarity index 99% rename from src/table.rs rename to crates/core/src/table.rs index b9f30af9c..3dfe3e9cf 100644 --- a/src/table.rs +++ b/crates/core/src/table.rs @@ -27,13 +27,13 @@ use datafusion::datasource::{TableProvider, TableType}; use datafusion::logical_expr::{Expr, LogicalPlanBuilder, TableProviderFilterPushDown}; use datafusion::physical_plan::ExecutionPlan; use datafusion::prelude::DataFrame; +use datafusion_python_util::table_provider_from_pycapsule; use pyo3::IntoPyObjectExt; use pyo3::prelude::*; use crate::context::PySessionContext; use crate::dataframe::PyDataFrame; use crate::dataset::Dataset; -use crate::utils::table_provider_from_pycapsule; /// This struct is used as a common method for all TableProviders, /// whether they refer to an FFI provider, an internally known diff --git a/src/udaf.rs b/crates/core/src/udaf.rs similarity index 99% rename from src/udaf.rs rename to crates/core/src/udaf.rs index 7ba499c66..ed26c79cc 100644 --- a/src/udaf.rs +++ b/crates/core/src/udaf.rs @@ -27,6 +27,7 @@ use datafusion::logical_expr::{ Accumulator, AccumulatorFactoryFunction, AggregateUDF, AggregateUDFImpl, create_udaf, }; use datafusion_ffi::udaf::FFI_AggregateUDF; +use datafusion_python_util::{parse_volatility, validate_pycapsule}; use pyo3::ffi::c_str; use pyo3::prelude::*; use pyo3::types::{PyCapsule, PyTuple}; @@ -34,7 +35,6 @@ use pyo3::types::{PyCapsule, PyTuple}; use crate::common::data_type::PyScalarValue; use crate::errors::{PyDataFusionResult, py_datafusion_err, to_datafusion_err}; use crate::expr::PyExpr; -use crate::utils::{parse_volatility, validate_pycapsule}; #[derive(Debug)] struct RustAccumulator { diff --git a/src/udf.rs b/crates/core/src/udf.rs similarity index 99% rename from src/udf.rs rename to crates/core/src/udf.rs index 2d60abc09..7543f96d4 100644 --- a/src/udf.rs +++ b/crates/core/src/udf.rs @@ -32,6 +32,7 @@ use datafusion::logical_expr::{ Volatility, }; use datafusion_ffi::udf::FFI_ScalarUDF; +use datafusion_python_util::{parse_volatility, validate_pycapsule}; use pyo3::ffi::c_str; use pyo3::prelude::*; use pyo3::types::{PyCapsule, PyTuple}; @@ -39,7 +40,6 @@ use pyo3::types::{PyCapsule, PyTuple}; use crate::array::PyArrowArrayExportable; use crate::errors::{PyDataFusionResult, py_datafusion_err, to_datafusion_err}; use crate::expr::PyExpr; -use crate::utils::{parse_volatility, validate_pycapsule}; /// This struct holds the Python written function that is a /// ScalarUDF. diff --git a/src/udtf.rs b/crates/core/src/udtf.rs similarity index 99% rename from src/udtf.rs rename to crates/core/src/udtf.rs index 24df93e2b..77c5ffbbc 100644 --- a/src/udtf.rs +++ b/crates/core/src/udtf.rs @@ -22,6 +22,7 @@ use datafusion::catalog::{TableFunctionImpl, TableProvider}; use datafusion::error::Result as DataFusionResult; use datafusion::logical_expr::Expr; use datafusion_ffi::udtf::FFI_TableFunction; +use datafusion_python_util::validate_pycapsule; use pyo3::IntoPyObjectExt; use pyo3::exceptions::{PyImportError, PyTypeError}; use pyo3::ffi::c_str; @@ -32,7 +33,6 @@ use crate::context::PySessionContext; use crate::errors::{py_datafusion_err, to_datafusion_err}; use crate::expr::PyExpr; use crate::table::PyTable; -use crate::utils::validate_pycapsule; /// Represents a user defined table function #[pyclass(from_py_object, frozen, name = "TableFunction", module = "datafusion")] diff --git a/src/udwf.rs b/crates/core/src/udwf.rs similarity index 99% rename from src/udwf.rs rename to crates/core/src/udwf.rs index de63e2f9a..ff7ab0352 100644 --- a/src/udwf.rs +++ b/crates/core/src/udwf.rs @@ -32,6 +32,7 @@ use datafusion::logical_expr::{ }; use datafusion::scalar::ScalarValue; use datafusion_ffi::udwf::FFI_WindowUDF; +use datafusion_python_util::{parse_volatility, validate_pycapsule}; use pyo3::exceptions::PyValueError; use pyo3::ffi::c_str; use pyo3::prelude::*; @@ -40,7 +41,6 @@ use pyo3::types::{PyCapsule, PyList, PyTuple}; use crate::common::data_type::PyScalarValue; use crate::errors::{PyDataFusionResult, py_datafusion_err, to_datafusion_err}; use crate::expr::PyExpr; -use crate::utils::{parse_volatility, validate_pycapsule}; #[derive(Debug)] struct RustPartitionEvaluator { diff --git a/src/unparser/dialect.rs b/crates/core/src/unparser/dialect.rs similarity index 100% rename from src/unparser/dialect.rs rename to crates/core/src/unparser/dialect.rs diff --git a/src/unparser/mod.rs b/crates/core/src/unparser/mod.rs similarity index 100% rename from src/unparser/mod.rs rename to crates/core/src/unparser/mod.rs diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml new file mode 100644 index 000000000..00d5946a5 --- /dev/null +++ b/crates/util/Cargo.toml @@ -0,0 +1,34 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +[package] +name = "datafusion-python-util" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +description.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread"] } +pyo3 = { workspace = true } +datafusion = { workspace = true } +datafusion-ffi = { workspace = true } +arrow = { workspace = true } +prost = { workspace = true } diff --git a/src/errors.rs b/crates/util/src/errors.rs similarity index 100% rename from src/errors.rs rename to crates/util/src/errors.rs diff --git a/src/utils.rs b/crates/util/src/lib.rs similarity index 85% rename from src/utils.rs rename to crates/util/src/lib.rs index 5085018f7..2678a6b9a 100644 --- a/src/utils.rs +++ b/crates/util/src/lib.rs @@ -25,7 +25,6 @@ use datafusion::execution::context::SessionContext; use datafusion::logical_expr::Volatility; use datafusion_ffi::proto::logical_extension_codec::FFI_LogicalExtensionCodec; use datafusion_ffi::table_provider::FFI_TableProvider; -use pyo3::IntoPyObjectExt; use pyo3::exceptions::{PyImportError, PyTypeError, PyValueError}; use pyo3::ffi::c_str; use pyo3::prelude::*; @@ -34,24 +33,24 @@ use tokio::runtime::Runtime; use tokio::task::JoinHandle; use tokio::time::sleep; -use crate::TokioRuntime; -use crate::context::PySessionContext; use crate::errors::{PyDataFusionError, PyDataFusionResult, py_datafusion_err, to_datafusion_err}; +pub mod errors; + /// Utility to get the Tokio Runtime from Python #[inline] -pub(crate) fn get_tokio_runtime() -> &'static TokioRuntime { +pub fn get_tokio_runtime() -> &'static Runtime { // NOTE: Other pyo3 python libraries have had issues with using tokio // behind a forking app-server like `gunicorn` // If we run into that problem, in the future we can look to `delta-rs` // which adds a check in that disallows calls from a forked process // https://github.com/delta-io/delta-rs/blob/87010461cfe01563d91a4b9cd6fa468e2ad5f283/python/src/utils.rs#L10-L31 - static RUNTIME: OnceLock = OnceLock::new(); - RUNTIME.get_or_init(|| TokioRuntime(tokio::runtime::Runtime::new().unwrap())) + static RUNTIME: OnceLock = OnceLock::new(); + RUNTIME.get_or_init(|| Runtime::new().unwrap()) } #[inline] -pub(crate) fn is_ipython_env(py: Python) -> &'static bool { +pub fn is_ipython_env(py: Python) -> &'static bool { static IS_IPYTHON_ENV: OnceLock = OnceLock::new(); IS_IPYTHON_ENV.get_or_init(|| { py.import("IPython") @@ -63,7 +62,7 @@ pub(crate) fn is_ipython_env(py: Python) -> &'static bool { /// Utility to get the Global Datafussion CTX #[inline] -pub(crate) fn get_global_ctx() -> &'static Arc { +pub fn get_global_ctx() -> &'static Arc { static CTX: OnceLock> = OnceLock::new(); CTX.get_or_init(|| Arc::new(SessionContext::new())) } @@ -77,7 +76,7 @@ where F: Future + Send, F::Output: Send, { - let runtime: &Runtime = &get_tokio_runtime().0; + let runtime: &Runtime = get_tokio_runtime(); const INTERVAL_CHECK_SIGNALS: Duration = Duration::from_millis(1_000); // Some fast running processes that generate many `wait_for_future` calls like @@ -111,12 +110,12 @@ where /// Spawn a [`Future`] on the Tokio runtime and wait for completion /// while respecting Python signal handling. -pub(crate) fn spawn_future(py: Python, fut: F) -> PyDataFusionResult +pub fn spawn_future(py: Python, fut: F) -> PyDataFusionResult where F: Future> + Send + 'static, T: Send + 'static, { - let rt = &get_tokio_runtime().0; + let rt = get_tokio_runtime(); let handle: JoinHandle> = rt.spawn(fut); // Wait for the join handle while respecting Python signal handling. // We handle errors in two steps so `?` maps the error types correctly: @@ -138,7 +137,7 @@ where Ok(inner_result?) } -pub(crate) fn parse_volatility(value: &str) -> PyDataFusionResult { +pub fn parse_volatility(value: &str) -> PyDataFusionResult { Ok(match value { "immutable" => Volatility::Immutable, "stable" => Volatility::Stable, @@ -152,7 +151,7 @@ pub(crate) fn parse_volatility(value: &str) -> PyDataFusionResult { }) } -pub(crate) fn validate_pycapsule(capsule: &Bound, name: &str) -> PyResult<()> { +pub fn validate_pycapsule(capsule: &Bound, name: &str) -> PyResult<()> { let capsule_name = capsule.name()?; if capsule_name.is_none() { return Err(PyValueError::new_err(format!( @@ -170,7 +169,7 @@ pub(crate) fn validate_pycapsule(capsule: &Bound, name: &str) -> PyRe Ok(()) } -pub(crate) fn table_provider_from_pycapsule<'py>( +pub fn table_provider_from_pycapsule<'py>( mut obj: Bound<'py, PyAny>, session: Bound<'py, PyAny>, ) -> PyResult>> { @@ -202,37 +201,31 @@ pub(crate) fn table_provider_from_pycapsule<'py>( } } -pub(crate) fn extract_logical_extension_codec( - py: Python, - obj: Option>, -) -> PyResult> { - let obj = match obj { - Some(obj) => obj, - None => PySessionContext::global_ctx()?.into_bound_py_any(py)?, - }; - let capsule = if obj.hasattr("__datafusion_logical_extension_codec__")? { - obj.getattr("__datafusion_logical_extension_codec__")? - .call0()? +pub fn create_logical_extension_capsule<'py>( + py: Python<'py>, + codec: &FFI_LogicalExtensionCodec, +) -> PyResult> { + let name = cr"datafusion_logical_extension_codec".into(); + let codec = codec.clone(); + + PyCapsule::new(py, codec, Some(name)) +} + +pub fn ffi_logical_codec_from_pycapsule(obj: Bound) -> PyResult { + let attr_name = "__datafusion_logical_extension_codec__"; + let capsule = if obj.hasattr(attr_name)? { + obj.getattr(attr_name)?.call0()? } else { obj }; - let capsule = capsule.cast::().map_err(py_datafusion_err)?; + let capsule = capsule.cast::()?; validate_pycapsule(capsule, "datafusion_logical_extension_codec")?; let data: NonNull = capsule .pointer_checked(Some(c_str!("datafusion_logical_extension_codec")))? .cast(); let codec = unsafe { data.as_ref() }; - Ok(Arc::new(codec.clone())) -} -pub(crate) fn create_logical_extension_capsule<'py>( - py: Python<'py>, - codec: &FFI_LogicalExtensionCodec, -) -> PyResult> { - let name = cr"datafusion_logical_extension_codec".into(); - let codec = codec.clone(); - - PyCapsule::new(py, codec, Some(name)) + Ok(codec.clone()) } diff --git a/examples/datafusion-ffi-example/.cargo/config.toml b/examples/datafusion-ffi-example/.cargo/config.toml deleted file mode 100644 index af951327f..000000000 --- a/examples/datafusion-ffi-example/.cargo/config.toml +++ /dev/null @@ -1,5 +0,0 @@ -[target.x86_64-apple-darwin] -rustflags = ["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"] - -[target.aarch64-apple-darwin] -rustflags = ["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"] diff --git a/examples/datafusion-ffi-example/Cargo.lock b/examples/datafusion-ffi-example/Cargo.lock deleted file mode 100644 index ede9b446b..000000000 --- a/examples/datafusion-ffi-example/Cargo.lock +++ /dev/null @@ -1,3127 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "abi_stable" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d6512d3eb05ffe5004c59c206de7f99c34951504056ce23fc953842f12c445" -dependencies = [ - "abi_stable_derive", - "abi_stable_shared", - "const_panic", - "core_extensions", - "crossbeam-channel", - "generational-arena", - "libloading", - "lock_api", - "parking_lot", - "paste", - "repr_offset", - "rustc_version", - "serde", - "serde_derive", - "serde_json", -] - -[[package]] -name = "abi_stable_derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7178468b407a4ee10e881bc7a328a65e739f0863615cca4429d43916b05e898" -dependencies = [ - "abi_stable_shared", - "as_derive_utils", - "core_extensions", - "proc-macro2", - "quote", - "rustc_version", - "syn 1.0.109", - "typed-arena", -] - -[[package]] -name = "abi_stable_shared" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b5df7688c123e63f4d4d649cba63f2967ba7f7861b1664fca3f77d3dad2b63" -dependencies = [ - "core_extensions", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "const-random", - "getrandom 0.3.4", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" - -[[package]] -name = "arc-swap" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" -dependencies = [ - "rustversion", -] - -[[package]] -name = "arrow" -version = "58.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602268ce9f569f282cedb9a9f6bac569b680af47b9b077d515900c03c5d190da" -dependencies = [ - "arrow-arith", - "arrow-array", - "arrow-buffer", - "arrow-cast", - "arrow-csv", - "arrow-data", - "arrow-ipc", - "arrow-json", - "arrow-ord", - "arrow-row", - "arrow-schema", - "arrow-select", - "arrow-string", -] - -[[package]] -name = "arrow-arith" -version = "58.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd53c6bf277dea91f136ae8e3a5d7041b44b5e489e244e637d00ae302051f56f" -dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "chrono", - "num-traits", -] - -[[package]] -name = "arrow-array" -version = "58.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e53796e07a6525edaf7dc28b540d477a934aff14af97967ad1d5550878969b9e" -dependencies = [ - "ahash", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "chrono", - "chrono-tz", - "half", - "hashbrown 0.16.1", - "num-complex", - "num-integer", - "num-traits", -] - -[[package]] -name = "arrow-buffer" -version = "58.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c1a85bb2e94ee10b76531d8bc3ce9b7b4c0d508cabfb17d477f63f2617bd20" -dependencies = [ - "bytes", - "half", - "num-bigint", - "num-traits", -] - -[[package]] -name = "arrow-cast" -version = "58.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89fb245db6b0e234ed8e15b644edb8664673fefe630575e94e62cd9d489a8a26" -dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-ord", - "arrow-schema", - "arrow-select", - "atoi", - "base64", - "chrono", - "comfy-table", - "half", - "lexical-core", - "num-traits", - "ryu", -] - -[[package]] -name = "arrow-csv" -version = "58.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d374882fb465a194462527c0c15a93aa19a554cf690a6b77a26b2a02539937a7" -dependencies = [ - "arrow-array", - "arrow-cast", - "arrow-schema", - "chrono", - "csv", - "csv-core", - "regex", -] - -[[package]] -name = "arrow-data" -version = "58.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "189d210bc4244c715fa3ed9e6e22864673cccb73d5da28c2723fb2e527329b33" -dependencies = [ - "arrow-buffer", - "arrow-schema", - "half", - "num-integer", - "num-traits", -] - -[[package]] -name = "arrow-ipc" -version = "58.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7968c2e5210c41f4909b2ef76f6e05e172b99021c2def5edf3cc48fdd39d1d6c" -dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "arrow-select", - "flatbuffers", - "lz4_flex", -] - -[[package]] -name = "arrow-json" -version = "58.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92111dba5bf900f443488e01f00d8c4ddc2f47f5c50039d18120287b580baa22" -dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-cast", - "arrow-data", - "arrow-schema", - "chrono", - "half", - "indexmap", - "itoa", - "lexical-core", - "memchr", - "num-traits", - "ryu", - "serde_core", - "serde_json", - "simdutf8", -] - -[[package]] -name = "arrow-ord" -version = "58.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "211136cb253577ee1a6665f741a13136d4e563f64f5093ffd6fb837af90b9495" -dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "arrow-select", -] - -[[package]] -name = "arrow-row" -version = "58.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e0f20145f9f5ea3fe383e2ba7a7487bf19be36aa9dbf5dd6a1f92f657179663" -dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "half", -] - -[[package]] -name = "arrow-schema" -version = "58.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b47e0ca91cc438d2c7879fe95e0bca5329fff28649e30a88c6f760b1faeddcb" -dependencies = [ - "bitflags", -] - -[[package]] -name = "arrow-select" -version = "58.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "750a7d1dda177735f5e82a314485b6915c7cccdbb278262ac44090f4aba4a325" -dependencies = [ - "ahash", - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "num-traits", -] - -[[package]] -name = "arrow-string" -version = "58.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1eab1208bc4fe55d768cdc9b9f3d9df5a794cdb3ee2586bf89f9b30dc31ad8c" -dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "arrow-select", - "memchr", - "num-traits", - "regex", - "regex-syntax", -] - -[[package]] -name = "as_derive_utils" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff3c96645900a44cf11941c111bd08a6573b0e2f9f69bc9264b179d8fae753c4" -dependencies = [ - "core_extensions", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "async-ffi" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4de21c0feef7e5a556e51af767c953f0501f7f300ba785cc99c47bdc8081a50" -dependencies = [ - "abi_stable", -] - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "atoi" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" -dependencies = [ - "num-traits", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bitflags" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" - -[[package]] -name = "brotli" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bumpalo" -version = "3.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c81d250916401487680ed13b8b675660281dcfc3ab0121fe44c94bcab9eae2fb" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - -[[package]] -name = "cc" -version = "1.2.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chrono" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" -dependencies = [ - "iana-time-zone", - "num-traits", - "windows-link", -] - -[[package]] -name = "chrono-tz" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" -dependencies = [ - "chrono", - "phf", -] - -[[package]] -name = "comfy-table" -version = "7.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47" -dependencies = [ - "unicode-segmentation", - "unicode-width", -] - -[[package]] -name = "const-random" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] - -[[package]] -name = "const-random-macro" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom 0.2.17", - "once_cell", - "tiny-keccak", -] - -[[package]] -name = "const_panic" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e262cdaac42494e3ae34c43969f9cdeb7da178bdb4b66fa6a1ea2edb4c8ae652" -dependencies = [ - "typewit", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core_extensions" -version = "1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42bb5e5d0269fd4f739ea6cedaf29c16d81c27a7ce7582008e90eb50dcd57003" -dependencies = [ - "core_extensions_proc_macros", -] - -[[package]] -name = "core_extensions_proc_macros" -version = "1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533d38ecd2709b7608fb8e18e4504deb99e9a72879e6aa66373a76d8dc4259ea" - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "csv" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" -dependencies = [ - "csv-core", - "itoa", - "ryu", - "serde_core", -] - -[[package]] -name = "csv-core" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" -dependencies = [ - "memchr", -] - -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - -[[package]] -name = "datafusion-catalog" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "arrow", - "async-trait", - "dashmap", - "datafusion-common", - "datafusion-common-runtime", - "datafusion-datasource", - "datafusion-execution", - "datafusion-expr", - "datafusion-physical-expr", - "datafusion-physical-plan", - "datafusion-session", - "futures", - "itertools", - "log", - "object_store", - "parking_lot", - "tokio", -] - -[[package]] -name = "datafusion-catalog-listing" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "arrow", - "async-trait", - "datafusion-catalog", - "datafusion-common", - "datafusion-datasource", - "datafusion-execution", - "datafusion-expr", - "datafusion-physical-expr", - "datafusion-physical-expr-adapter", - "datafusion-physical-expr-common", - "datafusion-physical-plan", - "futures", - "itertools", - "log", - "object_store", -] - -[[package]] -name = "datafusion-common" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "ahash", - "arrow", - "arrow-ipc", - "chrono", - "half", - "hashbrown 0.16.1", - "indexmap", - "itertools", - "libc", - "log", - "object_store", - "parquet", - "paste", - "tokio", - "web-time", -] - -[[package]] -name = "datafusion-common-runtime" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "futures", - "log", - "tokio", -] - -[[package]] -name = "datafusion-datasource" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "arrow", - "async-trait", - "bytes", - "chrono", - "datafusion-common", - "datafusion-common-runtime", - "datafusion-execution", - "datafusion-expr", - "datafusion-physical-expr", - "datafusion-physical-expr-adapter", - "datafusion-physical-expr-common", - "datafusion-physical-plan", - "datafusion-session", - "futures", - "glob", - "itertools", - "log", - "object_store", - "rand", - "tokio", - "url", -] - -[[package]] -name = "datafusion-datasource-arrow" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "arrow", - "arrow-ipc", - "async-trait", - "bytes", - "datafusion-common", - "datafusion-common-runtime", - "datafusion-datasource", - "datafusion-execution", - "datafusion-expr", - "datafusion-physical-expr-common", - "datafusion-physical-plan", - "datafusion-session", - "futures", - "itertools", - "object_store", - "tokio", -] - -[[package]] -name = "datafusion-datasource-csv" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "arrow", - "async-trait", - "bytes", - "datafusion-common", - "datafusion-common-runtime", - "datafusion-datasource", - "datafusion-execution", - "datafusion-expr", - "datafusion-physical-expr-common", - "datafusion-physical-plan", - "datafusion-session", - "futures", - "object_store", - "regex", - "tokio", -] - -[[package]] -name = "datafusion-datasource-json" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "arrow", - "async-trait", - "bytes", - "datafusion-common", - "datafusion-common-runtime", - "datafusion-datasource", - "datafusion-execution", - "datafusion-expr", - "datafusion-physical-expr-common", - "datafusion-physical-plan", - "datafusion-session", - "futures", - "object_store", - "serde_json", - "tokio", - "tokio-stream", -] - -[[package]] -name = "datafusion-datasource-parquet" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "arrow", - "async-trait", - "bytes", - "datafusion-common", - "datafusion-common-runtime", - "datafusion-datasource", - "datafusion-execution", - "datafusion-expr", - "datafusion-functions-aggregate-common", - "datafusion-physical-expr", - "datafusion-physical-expr-adapter", - "datafusion-physical-expr-common", - "datafusion-physical-plan", - "datafusion-pruning", - "datafusion-session", - "futures", - "itertools", - "log", - "object_store", - "parking_lot", - "parquet", - "tokio", -] - -[[package]] -name = "datafusion-doc" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" - -[[package]] -name = "datafusion-execution" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "arrow", - "arrow-buffer", - "async-trait", - "chrono", - "dashmap", - "datafusion-common", - "datafusion-expr", - "datafusion-physical-expr-common", - "futures", - "log", - "object_store", - "parking_lot", - "rand", - "tempfile", - "url", -] - -[[package]] -name = "datafusion-expr" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "arrow", - "async-trait", - "chrono", - "datafusion-common", - "datafusion-doc", - "datafusion-expr-common", - "datafusion-functions-aggregate-common", - "datafusion-functions-window-common", - "datafusion-physical-expr-common", - "indexmap", - "itertools", - "paste", - "serde_json", - "sqlparser", -] - -[[package]] -name = "datafusion-expr-common" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "arrow", - "datafusion-common", - "indexmap", - "itertools", - "paste", -] - -[[package]] -name = "datafusion-ffi" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "abi_stable", - "arrow", - "arrow-schema", - "async-ffi", - "async-trait", - "datafusion-catalog", - "datafusion-common", - "datafusion-datasource", - "datafusion-execution", - "datafusion-expr", - "datafusion-functions-aggregate-common", - "datafusion-physical-expr", - "datafusion-physical-expr-common", - "datafusion-physical-plan", - "datafusion-proto", - "datafusion-proto-common", - "datafusion-session", - "futures", - "log", - "prost", - "semver", - "tokio", -] - -[[package]] -name = "datafusion-ffi-example" -version = "0.2.0" -dependencies = [ - "arrow", - "arrow-array", - "arrow-schema", - "async-trait", - "datafusion-catalog", - "datafusion-common", - "datafusion-expr", - "datafusion-ffi", - "datafusion-functions-aggregate", - "datafusion-functions-window", - "pyo3", - "pyo3-build-config", - "pyo3-log", -] - -[[package]] -name = "datafusion-functions" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "arrow", - "arrow-buffer", - "base64", - "chrono", - "chrono-tz", - "datafusion-common", - "datafusion-doc", - "datafusion-execution", - "datafusion-expr", - "datafusion-expr-common", - "datafusion-macros", - "hex", - "itertools", - "log", - "memchr", - "num-traits", - "rand", - "regex", - "unicode-segmentation", - "uuid", -] - -[[package]] -name = "datafusion-functions-aggregate" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "ahash", - "arrow", - "datafusion-common", - "datafusion-doc", - "datafusion-execution", - "datafusion-expr", - "datafusion-functions-aggregate-common", - "datafusion-macros", - "datafusion-physical-expr", - "datafusion-physical-expr-common", - "half", - "log", - "num-traits", - "paste", -] - -[[package]] -name = "datafusion-functions-aggregate-common" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "ahash", - "arrow", - "datafusion-common", - "datafusion-expr-common", - "datafusion-physical-expr-common", -] - -[[package]] -name = "datafusion-functions-table" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "arrow", - "async-trait", - "datafusion-catalog", - "datafusion-common", - "datafusion-expr", - "datafusion-physical-plan", - "parking_lot", - "paste", -] - -[[package]] -name = "datafusion-functions-window" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "arrow", - "datafusion-common", - "datafusion-doc", - "datafusion-expr", - "datafusion-functions-window-common", - "datafusion-macros", - "datafusion-physical-expr", - "datafusion-physical-expr-common", - "log", - "paste", -] - -[[package]] -name = "datafusion-functions-window-common" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "datafusion-common", - "datafusion-physical-expr-common", -] - -[[package]] -name = "datafusion-macros" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "datafusion-doc", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "datafusion-physical-expr" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "ahash", - "arrow", - "datafusion-common", - "datafusion-expr", - "datafusion-expr-common", - "datafusion-functions-aggregate-common", - "datafusion-physical-expr-common", - "half", - "hashbrown 0.16.1", - "indexmap", - "itertools", - "parking_lot", - "paste", - "petgraph", - "tokio", -] - -[[package]] -name = "datafusion-physical-expr-adapter" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "arrow", - "datafusion-common", - "datafusion-expr", - "datafusion-functions", - "datafusion-physical-expr", - "datafusion-physical-expr-common", - "itertools", -] - -[[package]] -name = "datafusion-physical-expr-common" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "ahash", - "arrow", - "chrono", - "datafusion-common", - "datafusion-expr-common", - "hashbrown 0.16.1", - "indexmap", - "itertools", - "parking_lot", -] - -[[package]] -name = "datafusion-physical-plan" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "ahash", - "arrow", - "arrow-ord", - "arrow-schema", - "async-trait", - "datafusion-common", - "datafusion-common-runtime", - "datafusion-execution", - "datafusion-expr", - "datafusion-functions", - "datafusion-functions-aggregate-common", - "datafusion-functions-window-common", - "datafusion-physical-expr", - "datafusion-physical-expr-common", - "futures", - "half", - "hashbrown 0.16.1", - "indexmap", - "itertools", - "log", - "num-traits", - "parking_lot", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "datafusion-proto" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "arrow", - "chrono", - "datafusion-catalog", - "datafusion-catalog-listing", - "datafusion-common", - "datafusion-datasource", - "datafusion-datasource-arrow", - "datafusion-datasource-csv", - "datafusion-datasource-json", - "datafusion-datasource-parquet", - "datafusion-execution", - "datafusion-expr", - "datafusion-functions-table", - "datafusion-physical-expr", - "datafusion-physical-expr-common", - "datafusion-physical-plan", - "datafusion-proto-common", - "object_store", - "prost", - "rand", -] - -[[package]] -name = "datafusion-proto-common" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "arrow", - "datafusion-common", - "prost", -] - -[[package]] -name = "datafusion-pruning" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "arrow", - "datafusion-common", - "datafusion-datasource", - "datafusion-expr-common", - "datafusion-physical-expr", - "datafusion-physical-expr-common", - "datafusion-physical-plan", - "itertools", - "log", -] - -[[package]] -name = "datafusion-session" -version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" -dependencies = [ - "async-trait", - "datafusion-common", - "datafusion-execution", - "datafusion-expr", - "datafusion-physical-plan", - "parking_lot", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - -[[package]] -name = "flatbuffers" -version = "25.12.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35f6839d7b3b98adde531effaf34f0c2badc6f4735d26fe74709d8e513a96ef3" -dependencies = [ - "bitflags", - "rustc_version", -] - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "miniz_oxide", - "zlib-rs", -] - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "generational-arena" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877e94aff08e743b651baaea359664321055749b398adff8740a7399af7796e7" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "getrandom" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", - "wasip3", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "num-traits", - "zerocopy", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "humantime" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "integer-encoding" -version = "3.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "lexical-core" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d8d125a277f807e55a77304455eb7b1cb52f2b18c143b60e766c120bd64a594" -dependencies = [ - "lexical-parse-float", - "lexical-parse-integer", - "lexical-util", - "lexical-write-float", - "lexical-write-integer", -] - -[[package]] -name = "lexical-parse-float" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56" -dependencies = [ - "lexical-parse-integer", - "lexical-util", -] - -[[package]] -name = "lexical-parse-integer" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34" -dependencies = [ - "lexical-util", -] - -[[package]] -name = "lexical-util" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17" - -[[package]] -name = "lexical-write-float" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c438c87c013188d415fbabbb1dceb44249ab81664efbd31b14ae55dabb6361" -dependencies = [ - "lexical-util", - "lexical-write-integer", -] - -[[package]] -name = "lexical-write-integer" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "409851a618475d2d5796377cad353802345cba92c867d9fbcde9cf4eac4e14df" -dependencies = [ - "lexical-util", -] - -[[package]] -name = "libc" -version = "0.2.182" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" - -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "lz4_flex" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab6473172471198271ff72e9379150e9dfd70d8e533e0752a27e515b48dd375e" -dependencies = [ - "twox-hash", -] - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "object_store" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2858065e55c148d294a9f3aae3b0fa9458edadb41a108397094566f4e3c0dfb" -dependencies = [ - "async-trait", - "bytes", - "chrono", - "futures", - "http", - "humantime", - "itertools", - "parking_lot", - "percent-encoding", - "thiserror", - "tokio", - "tracing", - "url", - "walkdir", - "wasm-bindgen-futures", - "web-time", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "ordered-float" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" -dependencies = [ - "num-traits", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - -[[package]] -name = "parquet" -version = "58.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f491d0ef1b510194426ee67ddc18a9b747ef3c42050c19322a2cd2e1666c29b" -dependencies = [ - "ahash", - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-ipc", - "arrow-schema", - "arrow-select", - "base64", - "brotli", - "bytes", - "chrono", - "flate2", - "futures", - "half", - "hashbrown 0.16.1", - "lz4_flex", - "num-bigint", - "num-integer", - "num-traits", - "object_store", - "paste", - "seq-macro", - "simdutf8", - "snap", - "thrift", - "tokio", - "twox-hash", - "zstd", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "petgraph" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" -dependencies = [ - "fixedbitset", - "hashbrown 0.15.5", - "indexmap", - "serde", -] - -[[package]] -name = "phf" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" -dependencies = [ - "phf_shared", -] - -[[package]] -name = "phf_shared" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "prost" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-derive" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" -dependencies = [ - "anyhow", - "itertools", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pyo3" -version = "0.28.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf85e27e86080aafd5a22eae58a162e133a589551542b3e5cee4beb27e54f8e1" -dependencies = [ - "libc", - "once_cell", - "portable-atomic", - "pyo3-build-config", - "pyo3-ffi", - "pyo3-macros", -] - -[[package]] -name = "pyo3-build-config" -version = "0.28.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf94ee265674bf76c09fa430b0e99c26e319c945d96ca0d5a8215f31bf81cf7" -dependencies = [ - "target-lexicon", -] - -[[package]] -name = "pyo3-ffi" -version = "0.28.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "491aa5fc66d8059dd44a75f4580a2962c1862a1c2945359db36f6c2818b748dc" -dependencies = [ - "libc", - "pyo3-build-config", -] - -[[package]] -name = "pyo3-log" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c2ec80932c5c3b2d4fbc578c9b56b2d4502098587edb8bef5b6bfcad43682e" -dependencies = [ - "arc-swap", - "log", - "pyo3", -] - -[[package]] -name = "pyo3-macros" -version = "0.28.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d671734e9d7a43449f8480f8b38115df67bef8d21f76837fa75ee7aaa5e52e" -dependencies = [ - "proc-macro2", - "pyo3-macros-backend", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pyo3-macros-backend" -version = "0.28.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22faaa1ce6c430a1f71658760497291065e6450d7b5dc2bcf254d49f66ee700a" -dependencies = [ - "heck", - "proc-macro2", - "pyo3-build-config", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "quote" -version = "1.0.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" - -[[package]] -name = "repr_offset" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb1070755bd29dffc19d0971cab794e607839ba2ef4b69a9e6fbc8733c1b72ea" -dependencies = [ - "tstr", -] - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "seq-macro" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "simd-adler32" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "siphasher" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "snap" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" - -[[package]] -name = "sqlparser" -version = "0.61.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf5ea8d4d7c808e1af1cbabebca9a2abe603bcefc22294c5b95018d53200cb7" -dependencies = [ - "log", - "sqlparser_derive", -] - -[[package]] -name = "sqlparser_derive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6dd45d8fc1c79299bfbb7190e42ccbbdf6a5f52e4a6ad98d92357ea965bd289" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "target-lexicon" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" - -[[package]] -name = "tempfile" -version = "3.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" -dependencies = [ - "fastrand", - "getrandom 0.4.1", - "once_cell", - "rustix", - "windows-sys", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "thrift" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e54bc85fc7faa8bc175c4bab5b92ba8d9a3ce893d0e9f42cc455c8ab16a9e09" -dependencies = [ - "byteorder", - "integer-encoding", - "ordered-float", -] - -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tokio" -version = "1.49.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" -dependencies = [ - "bytes", - "pin-project-lite", - "tokio-macros", -] - -[[package]] -name = "tokio-macros" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tokio-stream" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "tstr" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f8e0294f14baae476d0dd0a2d780b2e24d66e349a9de876f5126777a37bdba7" -dependencies = [ - "tstr_proc_macros", -] - -[[package]] -name = "tstr_proc_macros" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78122066b0cb818b8afd08f7ed22f7fdbc3e90815035726f0840d0d26c0747a" - -[[package]] -name = "twox-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" - -[[package]] -name = "typed-arena" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" - -[[package]] -name = "typewit" -version = "1.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c1ae7cc0fdb8b842d65d127cb981574b0d2b249b74d1c7a2986863dc134f71" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "uuid" -version = "1.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" -dependencies = [ - "getrandom 0.4.1", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" -dependencies = [ - "cfg-if", - "futures-util", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.117", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "web-sys" -version = "0.3.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zlib-rs" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c745c48e1007337ed136dc99df34128b9faa6ed542d80a1c673cf55a6d7236c8" - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/examples/datafusion-ffi-example/Cargo.toml b/examples/datafusion-ffi-example/Cargo.toml index be6096faf..178dce9f9 100644 --- a/examples/datafusion-ffi-example/Cargo.toml +++ b/examples/datafusion-ffi-example/Cargo.toml @@ -17,40 +17,37 @@ [package] name = "datafusion-ffi-example" -version = "0.2.0" -edition = "2024" +version.workspace = true +edition.workspace = true +license.workspace = true +description.workspace = true +homepage.workspace = true +repository.workspace = true +publish = false [dependencies] -datafusion-catalog = { version = "53", default-features = false } -datafusion-common = { version = "53", default-features = false } -datafusion-functions-aggregate = { version = "53" } -datafusion-functions-window = { version = "53" } -datafusion-expr = { version = "53" } -datafusion-ffi = { version = "53" } +datafusion-catalog = { workspace = true, default-features = false } +datafusion-common = { workspace = true, default-features = false } +datafusion-functions-aggregate = { workspace = true } +datafusion-functions-window = { workspace = true } +datafusion-expr = { workspace = true } +datafusion-ffi = { workspace = true } -pyo3 = { version = "0.28", features = [ +arrow = { workspace = true } +arrow-array = { workspace = true } +arrow-schema = { workspace = true } +async-trait = { workspace = true } +datafusion-python-util.workspace = true +pyo3 = { workspace = true, features = [ "extension-module", "abi3", - "abi3-py39", + "abi3-py310", ] } -arrow = { version = "58" } -arrow-array = { version = "58" } -arrow-schema = { version = "58" } -async-trait = "0.1.89" -pyo3-log = "0.13.2" +pyo3-log = { workspace = true } [build-dependencies] -pyo3-build-config = "0.28" +pyo3-build-config = { workspace = true } [lib] name = "datafusion_ffi_example" crate-type = ["cdylib", "rlib"] - -# TODO: remove when datafusion-53 is released -[patch.crates-io] -datafusion-catalog = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } -datafusion-common = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } -datafusion-functions-aggregate = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } -datafusion-functions-window = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } -datafusion-expr = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } -datafusion-ffi = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } diff --git a/examples/datafusion-ffi-example/src/catalog_provider.rs b/examples/datafusion-ffi-example/src/catalog_provider.rs index d0e07c787..bd5da1e4d 100644 --- a/examples/datafusion-ffi-example/src/catalog_provider.rs +++ b/examples/datafusion-ffi-example/src/catalog_provider.rs @@ -29,11 +29,10 @@ use datafusion_common::error::{DataFusionError, Result}; use datafusion_ffi::catalog_provider::FFI_CatalogProvider; use datafusion_ffi::catalog_provider_list::FFI_CatalogProviderList; use datafusion_ffi::schema_provider::FFI_SchemaProvider; +use datafusion_python_util::ffi_logical_codec_from_pycapsule; use pyo3::types::PyCapsule; use pyo3::{Bound, PyAny, PyResult, Python, pyclass, pymethods}; -use crate::utils::ffi_logical_codec_from_pycapsule; - pub fn my_table() -> Arc { use arrow::datatypes::{DataType, Field}; use datafusion_common::record_batch; diff --git a/examples/datafusion-ffi-example/src/lib.rs b/examples/datafusion-ffi-example/src/lib.rs index 23f2001a2..3627c149e 100644 --- a/examples/datafusion-ffi-example/src/lib.rs +++ b/examples/datafusion-ffi-example/src/lib.rs @@ -29,7 +29,6 @@ pub(crate) mod catalog_provider; pub(crate) mod scalar_udf; pub(crate) mod table_function; pub(crate) mod table_provider; -pub(crate) mod utils; pub(crate) mod window_udf; #[pymodule] diff --git a/examples/datafusion-ffi-example/src/table_function.rs b/examples/datafusion-ffi-example/src/table_function.rs index 0914e161c..79c13f64d 100644 --- a/examples/datafusion-ffi-example/src/table_function.rs +++ b/examples/datafusion-ffi-example/src/table_function.rs @@ -21,11 +21,11 @@ use datafusion_catalog::{TableFunctionImpl, TableProvider}; use datafusion_common::error::Result as DataFusionResult; use datafusion_expr::Expr; use datafusion_ffi::udtf::FFI_TableFunction; +use datafusion_python_util::ffi_logical_codec_from_pycapsule; use pyo3::types::PyCapsule; use pyo3::{Bound, PyAny, PyResult, Python, pyclass, pymethods}; use crate::table_provider::MyTableProvider; -use crate::utils::ffi_logical_codec_from_pycapsule; #[pyclass( from_py_object, diff --git a/examples/datafusion-ffi-example/src/table_provider.rs b/examples/datafusion-ffi-example/src/table_provider.rs index 2c79e6ef9..358ef7402 100644 --- a/examples/datafusion-ffi-example/src/table_provider.rs +++ b/examples/datafusion-ffi-example/src/table_provider.rs @@ -22,12 +22,11 @@ use arrow_schema::{DataType, Field, Schema}; use datafusion_catalog::MemTable; use datafusion_common::error::{DataFusionError, Result as DataFusionResult}; use datafusion_ffi::table_provider::FFI_TableProvider; +use datafusion_python_util::ffi_logical_codec_from_pycapsule; use pyo3::exceptions::PyRuntimeError; use pyo3::types::PyCapsule; use pyo3::{Bound, PyAny, PyResult, Python, pyclass, pymethods}; -use crate::utils::ffi_logical_codec_from_pycapsule; - /// In order to provide a test that demonstrates different sized record batches, /// the first batch will have num_rows, the second batch num_rows+1, and so on. #[pyclass( diff --git a/examples/datafusion-ffi-example/src/utils.rs b/examples/datafusion-ffi-example/src/utils.rs deleted file mode 100644 index 5f2865aa2..000000000 --- a/examples/datafusion-ffi-example/src/utils.rs +++ /dev/null @@ -1,64 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -use std::ptr::NonNull; - -use datafusion_ffi::proto::logical_extension_codec::FFI_LogicalExtensionCodec; -use pyo3::exceptions::PyValueError; -use pyo3::ffi::c_str; -use pyo3::prelude::{PyAnyMethods, PyCapsuleMethods}; -use pyo3::types::PyCapsule; -use pyo3::{Bound, PyAny, PyResult}; - -pub(crate) fn ffi_logical_codec_from_pycapsule( - obj: Bound, -) -> PyResult { - let attr_name = "__datafusion_logical_extension_codec__"; - let capsule = if obj.hasattr(attr_name)? { - obj.getattr(attr_name)?.call0()? - } else { - obj - }; - - let capsule = capsule.cast::()?; - validate_pycapsule(capsule, "datafusion_logical_extension_codec")?; - - let data: NonNull = capsule - .pointer_checked(Some(c_str!("datafusion_logical_extension_codec")))? - .cast(); - let codec = unsafe { data.as_ref() }; - - Ok(codec.clone()) -} - -pub(crate) fn validate_pycapsule(capsule: &Bound, name: &str) -> PyResult<()> { - let capsule_name = capsule.name()?; - if capsule_name.is_none() { - return Err(PyValueError::new_err(format!( - "Expected {name} PyCapsule to have name set." - ))); - } - - let capsule_name = unsafe { capsule_name.unwrap().as_cstr().to_str()? }; - if capsule_name != name { - return Err(PyValueError::new_err(format!( - "Expected name '{name}' in PyCapsule, instead got '{capsule_name}'" - ))); - } - - Ok(()) -} diff --git a/pyproject.toml b/pyproject.toml index b238e049e..117aeefc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,6 +59,7 @@ repository = "https://github.com/apache/datafusion-python" profile = "black" [tool.maturin] +manifest-path = "crates/core/Cargo.toml" python-source = "python" module-name = "datafusion._internal" include = [{ path = "Cargo.lock", format = "sdist" }] From 21990b0bb01599fb67dbd8686c907e5f810aace3 Mon Sep 17 00:00:00 2001 From: "Paul J. Davis" Date: Mon, 16 Mar 2026 07:07:50 -0500 Subject: [PATCH 11/56] feat: Add FFI_TableProviderFactory support (#1396) * feat: Add FFI_TableProviderFactory support This wraps the new FFI_TableProviderFactory APIs in datafusion-ffi. * Address PR comments * Add support for Python based TableProviderFactory This adds the ability to register Python based TableProviderFactory instances to the SessionContext. * Correction after rebase --------- Co-authored-by: Tim Saucer --- crates/core/src/context.rs | 42 ++++++++- crates/core/src/table.rs | 59 ++++++++++++- .../tests/_test_table_provider_factory.py | 41 +++++++++ examples/datafusion-ffi-example/src/lib.rs | 3 + .../src/table_provider_factory.rs | 87 +++++++++++++++++++ python/datafusion/catalog.py | 19 ++++ python/datafusion/context.py | 18 ++++ python/tests/test_catalog.py | 27 ++++++ 8 files changed, 291 insertions(+), 5 deletions(-) create mode 100644 examples/datafusion-ffi-example/python/tests/_test_table_provider_factory.py create mode 100644 examples/datafusion-ffi-example/src/table_provider_factory.rs diff --git a/crates/core/src/context.rs b/crates/core/src/context.rs index 709fdc5a2..200b6470b 100644 --- a/crates/core/src/context.rs +++ b/crates/core/src/context.rs @@ -27,7 +27,7 @@ use arrow::pyarrow::FromPyArrow; use datafusion::arrow::datatypes::{DataType, Schema, SchemaRef}; use datafusion::arrow::pyarrow::PyArrowType; use datafusion::arrow::record_batch::RecordBatch; -use datafusion::catalog::{CatalogProvider, CatalogProviderList}; +use datafusion::catalog::{CatalogProvider, CatalogProviderList, TableProviderFactory}; use datafusion::common::{ScalarValue, TableReference, exec_err}; use datafusion::datasource::file_format::file_compression_type::FileCompressionType; use datafusion::datasource::file_format::parquet::ParquetFormat; @@ -51,6 +51,7 @@ use datafusion_ffi::catalog_provider::FFI_CatalogProvider; use datafusion_ffi::catalog_provider_list::FFI_CatalogProviderList; use datafusion_ffi::execution::FFI_TaskContextProvider; use datafusion_ffi::proto::logical_extension_codec::FFI_LogicalExtensionCodec; +use datafusion_ffi::table_provider_factory::FFI_TableProviderFactory; use datafusion_proto::logical_plan::DefaultLogicalExtensionCodec; use datafusion_python_util::{ create_logical_extension_capsule, ffi_logical_codec_from_pycapsule, get_global_ctx, @@ -81,7 +82,7 @@ use crate::record_batch::PyRecordBatchStream; use crate::sql::logical::PyLogicalPlan; use crate::sql::util::replace_placeholders_with_strings; use crate::store::StorageContexts; -use crate::table::PyTable; +use crate::table::{PyTable, RustWrappedPyTableProviderFactory}; use crate::udaf::PyAggregateUDF; use crate::udf::PyScalarUDF; use crate::udtf::PyTableFunction; @@ -659,6 +660,43 @@ impl PySessionContext { Ok(()) } + pub fn register_table_factory( + &self, + format: &str, + mut factory: Bound<'_, PyAny>, + ) -> PyDataFusionResult<()> { + if factory.hasattr("__datafusion_table_provider_factory__")? { + let py = factory.py(); + let codec_capsule = create_logical_extension_capsule(py, self.logical_codec.as_ref())?; + factory = factory + .getattr("__datafusion_table_provider_factory__")? + .call1((codec_capsule,))?; + } + + let factory: Arc = + if let Ok(capsule) = factory.cast::().map_err(py_datafusion_err) { + validate_pycapsule(capsule, "datafusion_table_provider_factory")?; + + let data: NonNull = capsule + .pointer_checked(Some(c_str!("datafusion_table_provider_factory")))? + .cast(); + let factory = unsafe { data.as_ref() }; + factory.into() + } else { + Arc::new(RustWrappedPyTableProviderFactory::new( + factory.into(), + self.logical_codec.clone(), + )) + }; + + let st = self.ctx.state_ref(); + let mut lock = st.write(); + lock.table_factories_mut() + .insert(format.to_owned(), factory); + + Ok(()) + } + pub fn register_catalog_provider_list( &self, mut provider: Bound, diff --git a/crates/core/src/table.rs b/crates/core/src/table.rs index 3dfe3e9cf..623349771 100644 --- a/crates/core/src/table.rs +++ b/crates/core/src/table.rs @@ -21,19 +21,24 @@ use std::sync::Arc; use arrow::datatypes::SchemaRef; use arrow::pyarrow::ToPyArrow; use async_trait::async_trait; -use datafusion::catalog::Session; +use datafusion::catalog::{Session, TableProviderFactory}; use datafusion::common::Column; use datafusion::datasource::{TableProvider, TableType}; -use datafusion::logical_expr::{Expr, LogicalPlanBuilder, TableProviderFilterPushDown}; +use datafusion::logical_expr::{ + CreateExternalTable, Expr, LogicalPlanBuilder, TableProviderFilterPushDown, +}; use datafusion::physical_plan::ExecutionPlan; use datafusion::prelude::DataFrame; -use datafusion_python_util::table_provider_from_pycapsule; +use datafusion_ffi::proto::logical_extension_codec::FFI_LogicalExtensionCodec; +use datafusion_python_util::{create_logical_extension_capsule, table_provider_from_pycapsule}; use pyo3::IntoPyObjectExt; use pyo3::prelude::*; use crate::context::PySessionContext; use crate::dataframe::PyDataFrame; use crate::dataset::Dataset; +use crate::errors; +use crate::expr::create_external_table::PyCreateExternalTable; /// This struct is used as a common method for all TableProviders, /// whether they refer to an FFI provider, an internally known @@ -206,3 +211,51 @@ impl TableProvider for TempViewTable { Ok(vec![TableProviderFilterPushDown::Exact; filters.len()]) } } + +#[derive(Debug)] +pub(crate) struct RustWrappedPyTableProviderFactory { + pub(crate) table_provider_factory: Py, + pub(crate) codec: Arc, +} + +impl RustWrappedPyTableProviderFactory { + pub fn new(table_provider_factory: Py, codec: Arc) -> Self { + Self { + table_provider_factory, + codec, + } + } + + fn create_inner( + &self, + cmd: CreateExternalTable, + codec: Bound, + ) -> PyResult> { + Python::attach(|py| { + let provider = self.table_provider_factory.bind(py); + let cmd = PyCreateExternalTable::from(cmd); + + provider + .call_method1("create", (cmd,)) + .and_then(|t| PyTable::new(t, Some(codec))) + .map(|t| t.table()) + }) + } +} + +#[async_trait] +impl TableProviderFactory for RustWrappedPyTableProviderFactory { + async fn create( + &self, + _: &dyn Session, + cmd: &CreateExternalTable, + ) -> datafusion::common::Result> { + Python::attach(|py| { + let codec = create_logical_extension_capsule(py, self.codec.as_ref()) + .map_err(errors::to_datafusion_err)?; + + self.create_inner(cmd.clone(), codec.into_any()) + .map_err(errors::to_datafusion_err) + }) + } +} diff --git a/examples/datafusion-ffi-example/python/tests/_test_table_provider_factory.py b/examples/datafusion-ffi-example/python/tests/_test_table_provider_factory.py new file mode 100644 index 000000000..b1e94ec73 --- /dev/null +++ b/examples/datafusion-ffi-example/python/tests/_test_table_provider_factory.py @@ -0,0 +1,41 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from __future__ import annotations + +from datafusion import SessionContext +from datafusion_ffi_example import MyTableProviderFactory + + +def test_table_provider_factory_ffi() -> None: + ctx = SessionContext() + table = MyTableProviderFactory() + + ctx.register_table_factory("MY_FORMAT", table) + + # Create a new external table + ctx.sql(""" + CREATE EXTERNAL TABLE + foo + STORED AS my_format + LOCATION ''; + """).collect() + + # Query the pre-populated table + result = ctx.sql("SELECT * FROM foo;").collect() + assert len(result) == 2 + assert result[0].num_columns == 2 diff --git a/examples/datafusion-ffi-example/src/lib.rs b/examples/datafusion-ffi-example/src/lib.rs index 3627c149e..68120a4cd 100644 --- a/examples/datafusion-ffi-example/src/lib.rs +++ b/examples/datafusion-ffi-example/src/lib.rs @@ -22,6 +22,7 @@ use crate::catalog_provider::{FixedSchemaProvider, MyCatalogProvider, MyCatalogP use crate::scalar_udf::IsNullUDF; use crate::table_function::MyTableFunction; use crate::table_provider::MyTableProvider; +use crate::table_provider_factory::MyTableProviderFactory; use crate::window_udf::MyRankUDF; pub(crate) mod aggregate_udf; @@ -29,6 +30,7 @@ pub(crate) mod catalog_provider; pub(crate) mod scalar_udf; pub(crate) mod table_function; pub(crate) mod table_provider; +pub(crate) mod table_provider_factory; pub(crate) mod window_udf; #[pymodule] @@ -36,6 +38,7 @@ fn datafusion_ffi_example(m: &Bound<'_, PyModule>) -> PyResult<()> { pyo3_log::init(); m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/examples/datafusion-ffi-example/src/table_provider_factory.rs b/examples/datafusion-ffi-example/src/table_provider_factory.rs new file mode 100644 index 000000000..53248a905 --- /dev/null +++ b/examples/datafusion-ffi-example/src/table_provider_factory.rs @@ -0,0 +1,87 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::sync::Arc; + +use async_trait::async_trait; +use datafusion_catalog::{Session, TableProvider, TableProviderFactory}; +use datafusion_common::error::Result as DataFusionResult; +use datafusion_expr::CreateExternalTable; +use datafusion_ffi::table_provider_factory::FFI_TableProviderFactory; +use datafusion_python_util::ffi_logical_codec_from_pycapsule; +use pyo3::types::PyCapsule; +use pyo3::{Bound, PyAny, PyResult, Python, pyclass, pymethods}; + +use crate::catalog_provider; + +#[derive(Debug)] +pub(crate) struct ExampleTableProviderFactory {} + +impl ExampleTableProviderFactory { + fn new() -> Self { + Self {} + } +} + +#[async_trait] +impl TableProviderFactory for ExampleTableProviderFactory { + async fn create( + &self, + _state: &dyn Session, + _cmd: &CreateExternalTable, + ) -> DataFusionResult> { + Ok(catalog_provider::my_table()) + } +} + +#[pyclass( + name = "MyTableProviderFactory", + module = "datafusion_ffi_example", + subclass +)] +#[derive(Debug)] +pub struct MyTableProviderFactory { + inner: Arc, +} + +impl Default for MyTableProviderFactory { + fn default() -> Self { + let inner = Arc::new(ExampleTableProviderFactory::new()); + Self { inner } + } +} + +#[pymethods] +impl MyTableProviderFactory { + #[new] + pub fn new() -> Self { + Self::default() + } + + pub fn __datafusion_table_provider_factory__<'py>( + &self, + py: Python<'py>, + codec: Bound, + ) -> PyResult> { + let name = cr"datafusion_table_provider_factory".into(); + let codec = ffi_logical_codec_from_pycapsule(codec)?; + let factory = Arc::clone(&self.inner) as Arc; + let factory = FFI_TableProviderFactory::new_with_ffi_codec(factory, None, codec); + + PyCapsule::new(py, factory, Some(name)) + } +} diff --git a/python/datafusion/catalog.py b/python/datafusion/catalog.py index bc43cf349..03c0ddc68 100644 --- a/python/datafusion/catalog.py +++ b/python/datafusion/catalog.py @@ -29,6 +29,7 @@ from datafusion import DataFrame, SessionContext from datafusion.context import TableProviderExportable + from datafusion.expr import CreateExternalTable try: from warnings import deprecated # Python 3.13+ @@ -243,6 +244,24 @@ def kind(self) -> str: return self._inner.kind +class TableProviderFactory(ABC): + """Abstract class for defining a Python based Table Provider Factory.""" + + @abstractmethod + def create(self, cmd: CreateExternalTable) -> Table: + """Create a table using the :class:`CreateExternalTable`.""" + ... + + +class TableProviderFactoryExportable(Protocol): + """Type hint for object that has __datafusion_table_provider_factory__ PyCapsule. + + https://docs.rs/datafusion/latest/datafusion/catalog/trait.TableProviderFactory.html + """ + + def __datafusion_table_provider_factory__(self, session: Any) -> object: ... + + class CatalogProviderList(ABC): """Abstract class for defining a Python based Catalog Provider List.""" diff --git a/python/datafusion/context.py b/python/datafusion/context.py index 0d8259774..ba9290a58 100644 --- a/python/datafusion/context.py +++ b/python/datafusion/context.py @@ -37,6 +37,8 @@ CatalogProviderExportable, CatalogProviderList, CatalogProviderListExportable, + TableProviderFactory, + TableProviderFactoryExportable, ) from datafusion.dataframe import DataFrame from datafusion.expr import sort_list_to_raw_sort_list @@ -830,6 +832,22 @@ def deregister_table(self, name: str) -> None: """Remove a table from the session.""" self.ctx.deregister_table(name) + def register_table_factory( + self, + format: str, + factory: TableProviderFactory | TableProviderFactoryExportable, + ) -> None: + """Register a :py:class:`~datafusion.TableProviderFactoryExportable`. + + The registered factory can be referenced from SQL DDL statements executed + against this context. + + Args: + format: The value to be used in `STORED AS ${format}` clause. + factory: A PyCapsule that implements :class:`TableProviderFactoryExportable` + """ + self.ctx.register_table_factory(format, factory) + def catalog_names(self) -> set[str]: """Returns the list of catalogs in this context.""" return self.ctx.catalog_names() diff --git a/python/tests/test_catalog.py b/python/tests/test_catalog.py index 9310da506..c89da36bf 100644 --- a/python/tests/test_catalog.py +++ b/python/tests/test_catalog.py @@ -120,6 +120,12 @@ def register_catalog( self.catalogs[name] = catalog +class CustomTableProviderFactory(dfn.catalog.TableProviderFactory): + def create(self, cmd: dfn.expr.CreateExternalTable): + assert cmd.name() == "test_table_factory" + return create_dataset() + + def test_python_catalog_provider_list(ctx: SessionContext): ctx.register_catalog_provider_list(CustomCatalogProviderList()) @@ -314,3 +320,24 @@ def my_table_function_udtf() -> Table: assert len(result[0]) == 1 assert len(result[0][0]) == 1 assert result[0][0][0].as_py() == 3 + + +def test_register_python_table_provider_factory(ctx: SessionContext): + ctx.register_table_factory("CUSTOM_FACTORY", CustomTableProviderFactory()) + + ctx.sql(""" + CREATE EXTERNAL TABLE test_table_factory + STORED AS CUSTOM_FACTORY + LOCATION foo; + """).collect() + + result = ctx.sql("SELECT * FROM test_table_factory;").collect() + + expect = [ + pa.RecordBatch.from_arrays( + [pa.array([1, 2, 3]), pa.array([4, 5, 6])], + names=["a", "b"], + ) + ] + + assert result == expect From 89751b552e8c5388e9cc994acadf1de5b896422f Mon Sep 17 00:00:00 2001 From: Nick <24689722+ntjohnson1@users.noreply.github.com> Date: Tue, 17 Mar 2026 02:12:28 -0400 Subject: [PATCH 12/56] Add docstring examples for Scalar regex, crypto, struct and other (#1422) * Add docstring examples for Scalar regex, crypto, struct and other functions Add example usage to docstrings for Scalar regex, crypto, struct and other functions to improve documentation. Co-Authored-By: Claude Opus 4.6 * Fix typo * Fix docstring already broken that I added an example to * Add sha outputs * clarify struct results * Examples should follow google docstyle --------- Co-authored-by: Claude Opus 4.6 --- python/datafusion/functions.py | 426 +++++++++++++++++++++++---------- 1 file changed, 296 insertions(+), 130 deletions(-) diff --git a/python/datafusion/functions.py b/python/datafusion/functions.py index fd116254b..73df56643 100644 --- a/python/datafusion/functions.py +++ b/python/datafusion/functions.py @@ -496,12 +496,11 @@ def acos(arg: Expr) -> Expr: """Returns the arc cosine or inverse cosine of a number. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [1.0]}) - >>> result = df.select(dfn.functions.acos(dfn.col("a")).alias("acos")) - >>> result.collect_column("acos")[0].as_py() - 0.0 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0]}) + >>> result = df.select(dfn.functions.acos(dfn.col("a")).alias("acos")) + >>> result.collect_column("acos")[0].as_py() + 0.0 """ return Expr(f.acos(arg.expr)) @@ -510,12 +509,11 @@ def acosh(arg: Expr) -> Expr: """Returns inverse hyperbolic cosine. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [1.0]}) - >>> result = df.select(dfn.functions.acosh(dfn.col("a")).alias("acosh")) - >>> result.collect_column("acosh")[0].as_py() - 0.0 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0]}) + >>> result = df.select(dfn.functions.acosh(dfn.col("a")).alias("acosh")) + >>> result.collect_column("acosh")[0].as_py() + 0.0 """ return Expr(f.acosh(arg.expr)) @@ -529,12 +527,11 @@ def asin(arg: Expr) -> Expr: """Returns the arc sine or inverse sine of a number. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [0.0]}) - >>> result = df.select(dfn.functions.asin(dfn.col("a")).alias("asin")) - >>> result.collect_column("asin")[0].as_py() - 0.0 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0.0]}) + >>> result = df.select(dfn.functions.asin(dfn.col("a")).alias("asin")) + >>> result.collect_column("asin")[0].as_py() + 0.0 """ return Expr(f.asin(arg.expr)) @@ -543,12 +540,11 @@ def asinh(arg: Expr) -> Expr: """Returns inverse hyperbolic sine. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [0.0]}) - >>> result = df.select(dfn.functions.asinh(dfn.col("a")).alias("asinh")) - >>> result.collect_column("asinh")[0].as_py() - 0.0 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0.0]}) + >>> result = df.select(dfn.functions.asinh(dfn.col("a")).alias("asinh")) + >>> result.collect_column("asinh")[0].as_py() + 0.0 """ return Expr(f.asinh(arg.expr)) @@ -557,12 +553,11 @@ def atan(arg: Expr) -> Expr: """Returns inverse tangent of a number. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [0.0]}) - >>> result = df.select(dfn.functions.atan(dfn.col("a")).alias("atan")) - >>> result.collect_column("atan")[0].as_py() - 0.0 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0.0]}) + >>> result = df.select(dfn.functions.atan(dfn.col("a")).alias("atan")) + >>> result.collect_column("atan")[0].as_py() + 0.0 """ return Expr(f.atan(arg.expr)) @@ -571,12 +566,11 @@ def atanh(arg: Expr) -> Expr: """Returns inverse hyperbolic tangent. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [0.0]}) - >>> result = df.select(dfn.functions.atanh(dfn.col("a")).alias("atanh")) - >>> result.collect_column("atanh")[0].as_py() - 0.0 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0.0]}) + >>> result = df.select(dfn.functions.atanh(dfn.col("a")).alias("atanh")) + >>> result.collect_column("atanh")[0].as_py() + 0.0 """ return Expr(f.atanh(arg.expr)) @@ -585,13 +579,12 @@ def atan2(y: Expr, x: Expr) -> Expr: """Returns inverse tangent of a division given in the argument. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"y": [0.0], "x": [1.0]}) - >>> result = df.select( - ... dfn.functions.atan2(dfn.col("y"), dfn.col("x")).alias("atan2")) - >>> result.collect_column("atan2")[0].as_py() - 0.0 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"y": [0.0], "x": [1.0]}) + >>> result = df.select( + ... dfn.functions.atan2(dfn.col("y"), dfn.col("x")).alias("atan2")) + >>> result.collect_column("atan2")[0].as_py() + 0.0 """ return Expr(f.atan2(y.expr, x.expr)) @@ -637,7 +630,16 @@ def chr(arg: Expr) -> Expr: def coalesce(*args: Expr) -> Expr: - """Returns the value of the first expr in ``args`` which is not NULL.""" + """Returns the value of the first expr in ``args`` which is not NULL. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [None, 1], "b": [2, 3]}) + >>> result = df.select( + ... dfn.functions.coalesce(dfn.col("a"), dfn.col("b")).alias("c")) + >>> result.collect_column("c")[0].as_py() + 2 + """ args = [arg.expr for arg in args] return Expr(f.coalesce(*args)) @@ -646,12 +648,11 @@ def cos(arg: Expr) -> Expr: """Returns the cosine of the argument. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [0,-1,1]}) - >>> cos_df = df.select(dfn.functions.cos(dfn.col("a")).alias("cos")) - >>> cos_df.collect_column("cos")[0].as_py() - 1.0 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0,-1,1]}) + >>> cos_df = df.select(dfn.functions.cos(dfn.col("a")).alias("cos")) + >>> cos_df.collect_column("cos")[0].as_py() + 1.0 """ return Expr(f.cos(arg.expr)) @@ -660,12 +661,11 @@ def cosh(arg: Expr) -> Expr: """Returns the hyperbolic cosine of the argument. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [0,-1,1]}) - >>> cosh_df = df.select(dfn.functions.cosh(dfn.col("a")).alias("cosh")) - >>> cosh_df.collect_column("cosh")[0].as_py() - 1.0 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0,-1,1]}) + >>> cosh_df = df.select(dfn.functions.cosh(dfn.col("a")).alias("cosh")) + >>> cosh_df.collect_column("cosh")[0].as_py() + 1.0 """ return Expr(f.cosh(arg.expr)) @@ -674,18 +674,17 @@ def cot(arg: Expr) -> Expr: """Returns the cotangent of the argument. Examples: - --------- - >>> from math import pi - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [pi / 4]}) - >>> import builtins - >>> result = df.select( - ... dfn.functions.cot(dfn.col("a")).alias("cot") - ... ) - >>> builtins.round( - ... result.collect_column("cot")[0].as_py(), 1 - ... ) - 1.0 + >>> from math import pi + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [pi / 4]}) + >>> import builtins + >>> result = df.select( + ... dfn.functions.cot(dfn.col("a")).alias("cot") + ... ) + >>> builtins.round( + ... result.collect_column("cot")[0].as_py(), 1 + ... ) + 1.0 """ return Expr(f.cot(arg.expr)) @@ -694,13 +693,12 @@ def degrees(arg: Expr) -> Expr: """Converts the argument from radians to degrees. Examples: - --------- - >>> from math import pi - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [0,pi,2*pi]}) - >>> deg_df = df.select(dfn.functions.degrees(dfn.col("a")).alias("deg")) - >>> deg_df.collect_column("deg")[2].as_py() - 360.0 + >>> from math import pi + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0,pi,2*pi]}) + >>> deg_df = df.select(dfn.functions.degrees(dfn.col("a")).alias("deg")) + >>> deg_df.collect_column("deg")[2].as_py() + 360.0 """ return Expr(f.degrees(arg.expr)) @@ -820,7 +818,15 @@ def ltrim(arg: Expr) -> Expr: def md5(arg: Expr) -> Expr: - """Computes an MD5 128-bit checksum for a string expression.""" + """Computes an MD5 128-bit checksum for a string expression. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"]}) + >>> result = df.select(dfn.functions.md5(dfn.col("a")).alias("md5")) + >>> result.collect_column("md5")[0].as_py() + '5d41402abc4b2a76b9719d911017c592' + """ return Expr(f.md5(arg.expr)) @@ -830,7 +836,19 @@ def nanvl(x: Expr, y: Expr) -> Expr: def nvl(x: Expr, y: Expr) -> Expr: - """Returns ``x`` if ``x`` is not ``NULL``. Otherwise returns ``y``.""" + """Returns ``x`` if ``x`` is not ``NULL``. Otherwise returns ``y``. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [None, 1], "b": [0, 0]}) + >>> nvl_df = df.select( + ... dfn.functions.nvl(dfn.col("a"), dfn.col("b")).alias("nvl") + ... ) + >>> nvl_df.collect_column("nvl")[0].as_py() + 0 + >>> nvl_df.collect_column("nvl")[1].as_py() + 1 + """ return Expr(f.nvl(x.expr, y.expr)) @@ -882,27 +900,37 @@ def radians(arg: Expr) -> Expr: """Converts the argument from degrees to radians. Examples: - --------- - >>> from math import pi - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [180.0]}) - >>> import builtins - >>> result = df.select( - ... dfn.functions.radians(dfn.col("a")).alias("rad") - ... ) - >>> builtins.round( - ... result.collect_column("rad")[0].as_py(), 6 - ... ) - 3.141593 + >>> from math import pi + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [180.0]}) + >>> import builtins + >>> result = df.select( + ... dfn.functions.radians(dfn.col("a")).alias("rad") + ... ) + >>> builtins.round( + ... result.collect_column("rad")[0].as_py(), 6 + ... ) + 3.141593 """ return Expr(f.radians(arg.expr)) def regexp_like(string: Expr, regex: Expr, flags: Expr | None = None) -> Expr: - """Find if any regular expression (regex) matches exist. + r"""Find if any regular expression (regex) matches exist. Tests a string using a regular expression returning true if at least one match, false otherwise. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello123"]}) + >>> result = df.select( + ... dfn.functions.regexp_like( + ... dfn.col("a"), dfn.lit("\\d+") + ... ).alias("m") + ... ) + >>> result.collect_column("m")[0].as_py() + True """ if flags is not None: flags = flags.expr @@ -910,10 +938,21 @@ def regexp_like(string: Expr, regex: Expr, flags: Expr | None = None) -> Expr: def regexp_match(string: Expr, regex: Expr, flags: Expr | None = None) -> Expr: - """Perform regular expression (regex) matching. + r"""Perform regular expression (regex) matching. Returns an array with each element containing the leftmost-first match of the corresponding index in ``regex`` to string in ``string``. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello 42 world"]}) + >>> result = df.select( + ... dfn.functions.regexp_match( + ... dfn.col("a"), dfn.lit("(\\d+)") + ... ).alias("m") + ... ) + >>> result.collect_column("m")[0].as_py() + ['42'] """ if flags is not None: flags = flags.expr @@ -923,13 +962,25 @@ def regexp_match(string: Expr, regex: Expr, flags: Expr | None = None) -> Expr: def regexp_replace( string: Expr, pattern: Expr, replacement: Expr, flags: Expr | None = None ) -> Expr: - """Replaces substring(s) matching a PCRE-like regular expression. + r"""Replaces substring(s) matching a PCRE-like regular expression. The full list of supported features and syntax can be found at Supported flags with the addition of 'g' can be found at + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello 42"]}) + >>> result = df.select( + ... dfn.functions.regexp_replace( + ... dfn.col("a"), dfn.lit("\\d+"), + ... dfn.lit("XX") + ... ).alias("r") + ... ) + >>> result.collect_column("r")[0].as_py() + 'hello XX' """ if flags is not None: flags = flags.expr @@ -943,6 +994,14 @@ def regexp_count( Optional start position (the first position is 1) to search for the regular expression. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["abcabc"]}) + >>> result = df.select( + ... dfn.functions.regexp_count(dfn.col("a"), dfn.lit("abc")).alias("c")) + >>> result.collect_column("c")[0].as_py() + 2 """ if flags is not None: flags = flags.expr @@ -958,12 +1017,26 @@ def regexp_instr( flags: Expr | None = None, sub_expr: Expr | None = None, ) -> Expr: - """Returns the position of a regular expression match in a string. + r"""Returns the position of a regular expression match in a string. + + Args: + values: Data to search for the regular expression match. + regex: Regular expression to search for. + start: Optional position to start the search (the first position is 1). + n: Optional occurrence of the match to find (the first occurrence is 1). + flags: Optional regular expression flags to control regex behavior. + sub_expr: Optionally capture group position instead of the entire match. - Searches ``values`` for the ``n``-th occurrence of ``regex``, starting at position - ``start`` (the first position is 1). Returns the starting or ending position based - on ``end_position``. Use ``flags`` to control regex behavior and ``sub_expr`` to - return the position of a specific capture group instead of the entire match. + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello 42 world"]}) + >>> result = df.select( + ... dfn.functions.regexp_instr( + ... dfn.col("a"), dfn.lit("\\d+") + ... ).alias("pos") + ... ) + >>> result.collect_column("pos")[0].as_py() + 7 """ start = start.expr if start is not None else None n = n.expr if n is not None else None @@ -1030,22 +1103,62 @@ def rtrim(arg: Expr) -> Expr: def sha224(arg: Expr) -> Expr: - """Computes the SHA-224 hash of a binary string.""" + """Computes the SHA-224 hash of a binary string. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"]}) + >>> result = df.select( + ... dfn.functions.sha224(dfn.col("a")).alias("h") + ... ) + >>> result.collect_column("h")[0].as_py().hex() + 'ea09ae9cc6768c50fcee903ed054556e5bfc8347907f12598aa24193' + """ return Expr(f.sha224(arg.expr)) def sha256(arg: Expr) -> Expr: - """Computes the SHA-256 hash of a binary string.""" + """Computes the SHA-256 hash of a binary string. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"]}) + >>> result = df.select( + ... dfn.functions.sha256(dfn.col("a")).alias("h") + ... ) + >>> result.collect_column("h")[0].as_py().hex() + '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824' + """ return Expr(f.sha256(arg.expr)) def sha384(arg: Expr) -> Expr: - """Computes the SHA-384 hash of a binary string.""" + """Computes the SHA-384 hash of a binary string. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"]}) + >>> result = df.select( + ... dfn.functions.sha384(dfn.col("a")).alias("h") + ... ) + >>> result.collect_column("h")[0].as_py().hex() + '59e1748777448c69de6b800d7a33bbfb9ff1b... + """ return Expr(f.sha384(arg.expr)) def sha512(arg: Expr) -> Expr: - """Computes the SHA-512 hash of a binary string.""" + """Computes the SHA-512 hash of a binary string. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"]}) + >>> result = df.select( + ... dfn.functions.sha512(dfn.col("a")).alias("h") + ... ) + >>> result.collect_column("h")[0].as_py().hex() + '9b71d224bd62f3785d96d46ad3ea3d73319bfb... + """ return Expr(f.sha512(arg.expr)) @@ -1058,12 +1171,11 @@ def sin(arg: Expr) -> Expr: """Returns the sine of the argument. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [0.0]}) - >>> result = df.select(dfn.functions.sin(dfn.col("a")).alias("sin")) - >>> result.collect_column("sin")[0].as_py() - 0.0 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0.0]}) + >>> result = df.select(dfn.functions.sin(dfn.col("a")).alias("sin")) + >>> result.collect_column("sin")[0].as_py() + 0.0 """ return Expr(f.sin(arg.expr)) @@ -1072,12 +1184,11 @@ def sinh(arg: Expr) -> Expr: """Returns the hyperbolic sine of the argument. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [0.0]}) - >>> result = df.select(dfn.functions.sinh(dfn.col("a")).alias("sinh")) - >>> result.collect_column("sinh")[0].as_py() - 0.0 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0.0]}) + >>> result = df.select(dfn.functions.sinh(dfn.col("a")).alias("sinh")) + >>> result.collect_column("sinh")[0].as_py() + 0.0 """ return Expr(f.sinh(arg.expr)) @@ -1129,12 +1240,11 @@ def tan(arg: Expr) -> Expr: """Returns the tangent of the argument. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [0.0]}) - >>> result = df.select(dfn.functions.tan(dfn.col("a")).alias("tan")) - >>> result.collect_column("tan")[0].as_py() - 0.0 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0.0]}) + >>> result = df.select(dfn.functions.tan(dfn.col("a")).alias("tan")) + >>> result.collect_column("tan")[0].as_py() + 0.0 """ return Expr(f.tan(arg.expr)) @@ -1143,12 +1253,11 @@ def tanh(arg: Expr) -> Expr: """Returns the hyperbolic tangent of the argument. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [0.0]}) - >>> result = df.select(dfn.functions.tanh(dfn.col("a")).alias("tanh")) - >>> result.collect_column("tanh")[0].as_py() - 0.0 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0.0]}) + >>> result = df.select(dfn.functions.tanh(dfn.col("a")).alias("tanh")) + >>> result.collect_column("tanh")[0].as_py() + 0.0 """ return Expr(f.tanh(arg.expr)) @@ -1370,18 +1479,56 @@ def range(start: Expr, stop: Expr, step: Expr) -> Expr: def uuid() -> Expr: - """Returns uuid v4 as a string value.""" + """Returns uuid v4 as a string value. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1]}) + >>> result = df.select( + ... dfn.functions.uuid().alias("u") + ... ) + >>> len(result.collect_column("u")[0].as_py()) == 36 + True + """ return Expr(f.uuid()) def struct(*args: Expr) -> Expr: - """Returns a struct with the given arguments.""" + """Returns a struct with the given arguments. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1], "b": [2]}) + >>> result = df.select( + ... dfn.functions.struct( + ... dfn.col("a"), dfn.col("b") + ... ).alias("s") + ... ) + + Children in the new struct will always be `c0`, ..., `cN-1` + for `N` children. + + >>> result.collect_column("s")[0].as_py() == {"c0": 1, "c1": 2} + True + """ args = [arg.expr for arg in args] return Expr(f.struct(*args)) def named_struct(name_pairs: list[tuple[str, Expr]]) -> Expr: - """Returns a struct with the given names and arguments pairs.""" + """Returns a struct with the given names and arguments pairs. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1]}) + >>> result = df.select( + ... dfn.functions.named_struct( + ... [("x", dfn.lit(10)), ("y", dfn.lit(20))] + ... ).alias("s") + ... ) + >>> result.collect_column("s")[0].as_py() == {"x": 10, "y": 20} + True + """ name_pair_exprs = [ [Expr.literal(pa.scalar(pair[0], type=pa.string())), pair[1]] for pair in name_pairs @@ -1398,12 +1545,31 @@ def from_unixtime(arg: Expr) -> Expr: def arrow_typeof(arg: Expr) -> Expr: - """Returns the Arrow type of the expression.""" + """Returns the Arrow type of the expression. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1]}) + >>> result = df.select(dfn.functions.arrow_typeof(dfn.col("a")).alias("t")) + >>> result.collect_column("t")[0].as_py() + 'Int64' + """ return Expr(f.arrow_typeof(arg.expr)) def arrow_cast(expr: Expr, data_type: Expr) -> Expr: - """Casts an expression to a specified data type.""" + """Casts an expression to a specified data type. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1]}) + >>> data_type = dfn.string_literal("Float64") + >>> result = df.select( + ... dfn.functions.arrow_cast(dfn.col("a"), data_type).alias("c") + ... ) + >>> result.collect_column("c")[0].as_py() + 1.0 + """ return Expr(f.arrow_cast(expr.expr, data_type.expr)) From b9a958e3893a9a208d67aac314a9ede97b370679 Mon Sep 17 00:00:00 2001 From: Nick <24689722+ntjohnson1@users.noreply.github.com> Date: Tue, 17 Mar 2026 02:13:18 -0400 Subject: [PATCH 13/56] Add docstring examples for Scalar math functions (#1421) * Add docstring examples for Scalar math functions Add example usage to docstrings for Scalar math functions to improve documentation. Co-Authored-By: Claude Opus 4.6 * Fix copy past error on name * Remove example from alias * Examples google docstyle --------- Co-authored-by: Claude Opus 4.6 --- python/datafusion/functions.py | 228 +++++++++++++++++++++++++++++---- 1 file changed, 205 insertions(+), 23 deletions(-) diff --git a/python/datafusion/functions.py b/python/datafusion/functions.py index 73df56643..3de2f130d 100644 --- a/python/datafusion/functions.py +++ b/python/datafusion/functions.py @@ -484,10 +484,12 @@ def window( def abs(arg: Expr) -> Expr: """Return the absolute value of a given number. - Returns: - -------- - Expr - A new expression representing the absolute value of the input expression. + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [-1, 0, 1]}) + >>> result = df.select(dfn.functions.abs(dfn.col("a")).alias("abs")) + >>> result.collect_column("abs")[0].as_py() + 1 """ return Expr(f.abs(arg.expr)) @@ -600,12 +602,28 @@ def btrim(arg: Expr) -> Expr: def cbrt(arg: Expr) -> Expr: - """Returns the cube root of a number.""" + """Returns the cube root of a number. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [27]}) + >>> cbrt_df = df.select(dfn.functions.cbrt(dfn.col("a")).alias("cbrt")) + >>> cbrt_df.collect_column("cbrt")[0].as_py() + 3.0 + """ return Expr(f.cbrt(arg.expr)) def ceil(arg: Expr) -> Expr: - """Returns the nearest integer greater than or equal to argument.""" + """Returns the nearest integer greater than or equal to argument. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.9]}) + >>> ceil_df = df.select(dfn.functions.ceil(dfn.col("a")).alias("ceil")) + >>> ceil_df.collect_column("ceil")[0].as_py() + 2.0 + """ return Expr(f.ceil(arg.expr)) @@ -709,12 +727,30 @@ def ends_with(arg: Expr, suffix: Expr) -> Expr: def exp(arg: Expr) -> Expr: - """Returns the exponential of the argument.""" + """Returns the exponential of the argument. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0.0]}) + >>> result = df.select(dfn.functions.exp(dfn.col("a")).alias("exp")) + >>> result.collect_column("exp")[0].as_py() + 1.0 + """ return Expr(f.exp(arg.expr)) def factorial(arg: Expr) -> Expr: - """Returns the factorial of the argument.""" + """Returns the factorial of the argument. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [3]}) + >>> result = df.select( + ... dfn.functions.factorial(dfn.col("a")).alias("factorial") + ... ) + >>> result.collect_column("factorial")[0].as_py() + 6 + """ return Expr(f.factorial(arg.expr)) @@ -730,12 +766,30 @@ def find_in_set(string: Expr, string_list: Expr) -> Expr: def floor(arg: Expr) -> Expr: - """Returns the nearest integer less than or equal to the argument.""" + """Returns the nearest integer less than or equal to the argument. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.9]}) + >>> floor_df = df.select(dfn.functions.floor(dfn.col("a")).alias("floor")) + >>> floor_df.collect_column("floor")[0].as_py() + 1.0 + """ return Expr(f.floor(arg.expr)) def gcd(x: Expr, y: Expr) -> Expr: - """Returns the greatest common divisor.""" + """Returns the greatest common divisor. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [12], "b": [8]}) + >>> result = df.select( + ... dfn.functions.gcd(dfn.col("a"), dfn.col("b")).alias("gcd") + ... ) + >>> result.collect_column("gcd")[0].as_py() + 4 + """ return Expr(f.gcd(x.expr, y.expr)) @@ -757,12 +811,30 @@ def instr(string: Expr, substring: Expr) -> Expr: def iszero(arg: Expr) -> Expr: - """Returns true if a given number is +0.0 or -0.0 otherwise returns false.""" + """Returns true if a given number is +0.0 or -0.0 otherwise returns false. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0.0, 1.0]}) + >>> result = df.select(dfn.functions.iszero(dfn.col("a")).alias("iz")) + >>> result.collect_column("iz")[0].as_py() + True + """ return Expr(f.iszero(arg.expr)) def lcm(x: Expr, y: Expr) -> Expr: - """Returns the least common multiple.""" + """Returns the least common multiple. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [4], "b": [6]}) + >>> result = df.select( + ... dfn.functions.lcm(dfn.col("a"), dfn.col("b")).alias("lcm") + ... ) + >>> result.collect_column("lcm")[0].as_py() + 12 + """ return Expr(f.lcm(x.expr, y.expr)) @@ -777,22 +849,56 @@ def levenshtein(string1: Expr, string2: Expr) -> Expr: def ln(arg: Expr) -> Expr: - """Returns the natural logarithm (base e) of the argument.""" + """Returns the natural logarithm (base e) of the argument. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0]}) + >>> result = df.select(dfn.functions.ln(dfn.col("a")).alias("ln")) + >>> result.collect_column("ln")[0].as_py() + 0.0 + """ return Expr(f.ln(arg.expr)) def log(base: Expr, num: Expr) -> Expr: - """Returns the logarithm of a number for a particular ``base``.""" + """Returns the logarithm of a number for a particular ``base``. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [100.0]}) + >>> result = df.select( + ... dfn.functions.log(dfn.lit(10.0), dfn.col("a")).alias("log") + ... ) + >>> result.collect_column("log")[0].as_py() + 2.0 + """ return Expr(f.log(base.expr, num.expr)) def log10(arg: Expr) -> Expr: - """Base 10 logarithm of the argument.""" + """Base 10 logarithm of the argument. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [100.0]}) + >>> result = df.select(dfn.functions.log10(dfn.col("a")).alias("log10")) + >>> result.collect_column("log10")[0].as_py() + 2.0 + """ return Expr(f.log10(arg.expr)) def log2(arg: Expr) -> Expr: - """Base 2 logarithm of the argument.""" + """Base 2 logarithm of the argument. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [8.0]}) + >>> result = df.select(dfn.functions.log2(dfn.col("a")).alias("log2")) + >>> result.collect_column("log2")[0].as_py() + 3.0 + """ return Expr(f.log2(arg.expr)) @@ -831,7 +937,18 @@ def md5(arg: Expr) -> Expr: def nanvl(x: Expr, y: Expr) -> Expr: - """Returns ``x`` if ``x`` is not ``NaN``. Otherwise returns ``y``.""" + """Returns ``x`` if ``x`` is not ``NaN``. Otherwise returns ``y``. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [np.nan, 1.0], "b": [0.0, 0.0]}) + >>> nanvl_df = df.select( + ... dfn.functions.nanvl(dfn.col("a"), dfn.col("b")).alias("nanvl")) + >>> nanvl_df.collect_column("nanvl")[0].as_py() + 0.0 + >>> nanvl_df.collect_column("nanvl")[1].as_py() + 1.0 + """ return Expr(f.nanvl(x.expr, y.expr)) @@ -871,7 +988,20 @@ def overlay( def pi() -> Expr: - """Returns an approximate value of π.""" + """Returns an approximate value of π. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1]}) + >>> import builtins + >>> result = df.select( + ... dfn.functions.pi().alias("pi") + ... ) + >>> builtins.round( + ... result.collect_column("pi")[0].as_py(), 5 + ... ) + 3.14159 + """ return Expr(f.pi()) @@ -884,7 +1014,17 @@ def position(string: Expr, substring: Expr) -> Expr: def power(base: Expr, exponent: Expr) -> Expr: - """Returns ``base`` raised to the power of ``exponent``.""" + """Returns ``base`` raised to the power of ``exponent``. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [2.0]}) + >>> result = df.select( + ... dfn.functions.power(dfn.col("a"), dfn.lit(3.0)).alias("pow") + ... ) + >>> result.collect_column("pow")[0].as_py() + 8.0 + """ return Expr(f.power(base.expr, exponent.expr)) @@ -1081,6 +1221,13 @@ def round(value: Expr, decimal_places: Expr | None = None) -> Expr: If the optional ``decimal_places`` is specified, round to the nearest number of decimal places. You can specify a negative number of decimal places. For example ``round(lit(125.2345), lit(-2))`` would yield a value of ``100.0``. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.567]}) + >>> result = df.select(dfn.functions.round(dfn.col("a"), dfn.lit(2)).alias("r")) + >>> result.collect_column("r")[0].as_py() + 1.57 """ if decimal_places is None: decimal_places = Expr.literal(0) @@ -1163,7 +1310,15 @@ def sha512(arg: Expr) -> Expr: def signum(arg: Expr) -> Expr: - """Returns the sign of the argument (-1, 0, +1).""" + """Returns the sign of the argument (-1, 0, +1). + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [-5.0, 0.0, 5.0]}) + >>> result = df.select(dfn.functions.signum(dfn.col("a")).alias("s")) + >>> result.collect_column("s").to_pylist() + [-1.0, 0.0, 1.0] + """ return Expr(f.signum(arg.expr)) @@ -1203,7 +1358,15 @@ def split_part(string: Expr, delimiter: Expr, index: Expr) -> Expr: def sqrt(arg: Expr) -> Expr: - """Returns the square root of the argument.""" + """Returns the square root of the argument. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [9.0]}) + >>> result = df.select(dfn.functions.sqrt(dfn.col("a")).alias("sqrt")) + >>> result.collect_column("sqrt")[0].as_py() + 3.0 + """ return Expr(f.sqrt(arg.expr)) @@ -1440,7 +1603,15 @@ def trim(arg: Expr) -> Expr: def trunc(num: Expr, precision: Expr | None = None) -> Expr: - """Truncate the number toward zero with optional precision.""" + """Truncate the number toward zero with optional precision. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.567]}) + >>> result = df.select(dfn.functions.trunc(dfn.col("a")).alias("t")) + >>> result.collect_column("t")[0].as_py() + 1.0 + """ if precision is not None: return Expr(f.trunc(num.expr, precision.expr)) return Expr(f.trunc(num.expr)) @@ -1574,7 +1745,18 @@ def arrow_cast(expr: Expr, data_type: Expr) -> Expr: def random() -> Expr: - """Returns a random value in the range ``0.0 <= x < 1.0``.""" + """Returns a random value in the range ``0.0 <= x < 1.0``. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1]}) + >>> result = df.select( + ... dfn.functions.random().alias("r") + ... ) + >>> val = result.collect_column("r")[0].as_py() + >>> 0.0 <= val < 1.0 + True + """ return Expr(f.random()) From e524121c8a68171d1031db0487ec13a547871c42 Mon Sep 17 00:00:00 2001 From: Nick <24689722+ntjohnson1@users.noreply.github.com> Date: Tue, 17 Mar 2026 02:13:40 -0400 Subject: [PATCH 14/56] Add docstring examples for Common utility functions (#1419) * Add docstring examples for Common utility functions Add example usage to docstrings for Common utility functions to improve documentation. Co-Authored-By: Claude Opus 4.6 * Don't add examples for aliases * Parameters back to args * Examples to google doc style --------- Co-authored-by: Claude Opus 4.6 --- python/datafusion/functions.py | 150 ++++++++++++++++++++++++++++++--- 1 file changed, 139 insertions(+), 11 deletions(-) diff --git a/python/datafusion/functions.py b/python/datafusion/functions.py index 3de2f130d..4738061c0 100644 --- a/python/datafusion/functions.py +++ b/python/datafusion/functions.py @@ -295,7 +295,15 @@ def isnan(expr: Expr) -> Expr: - """Returns true if a given number is +NaN or -NaN otherwise returns false.""" + """Returns true if a given number is +NaN or -NaN otherwise returns false. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0, np.nan]}) + >>> result = df.select(dfn.functions.isnan(dfn.col("a")).alias("isnan")) + >>> result.collect_column("isnan")[1].as_py() + True + """ return Expr(f.isnan(expr.expr)) @@ -303,29 +311,65 @@ def nullif(expr1: Expr, expr2: Expr) -> Expr: """Returns NULL if expr1 equals expr2; otherwise it returns expr1. This can be used to perform the inverse operation of the COALESCE expression. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2], "b": [1, 3]}) + >>> result = df.select( + ... dfn.functions.nullif(dfn.col("a"), dfn.col("b")).alias("nullif")) + >>> result.collect_column("nullif").to_pylist() + [None, 2] """ return Expr(f.nullif(expr1.expr, expr2.expr)) def encode(expr: Expr, encoding: Expr) -> Expr: - """Encode the ``input``, using the ``encoding``. encoding can be base64 or hex.""" + """Encode the ``input``, using the ``encoding``. encoding can be base64 or hex. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"]}) + >>> result = df.select( + ... dfn.functions.encode(dfn.col("a"), dfn.lit("base64")).alias("enc")) + >>> result.collect_column("enc")[0].as_py() + 'aGVsbG8' + """ return Expr(f.encode(expr.expr, encoding.expr)) def decode(expr: Expr, encoding: Expr) -> Expr: - """Decode the ``input``, using the ``encoding``. encoding can be base64 or hex.""" + """Decode the ``input``, using the ``encoding``. encoding can be base64 or hex. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["aGVsbG8="]}) + >>> result = df.select( + ... dfn.functions.decode(dfn.col("a"), dfn.lit("base64")).alias("dec")) + >>> result.collect_column("dec")[0].as_py() + b'hello' + """ return Expr(f.decode(expr.expr, encoding.expr)) def array_to_string(expr: Expr, delimiter: Expr) -> Expr: - """Converts each element to its text representation.""" + """Converts each element to its text representation. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) + >>> result = df.select( + ... dfn.functions.array_to_string(dfn.col("a"), dfn.lit(",")).alias("s")) + >>> result.collect_column("s")[0].as_py() + '1,2,3' + """ return Expr(f.array_to_string(expr.expr, delimiter.expr.cast(pa.string()))) def array_join(expr: Expr, delimiter: Expr) -> Expr: """Converts each element to its text representation. - This is an alias for :py:func:`array_to_string`. + See Also: + This is an alias for :py:func:`array_to_string`. """ return array_to_string(expr, delimiter) @@ -333,7 +377,8 @@ def array_join(expr: Expr, delimiter: Expr) -> Expr: def list_to_string(expr: Expr, delimiter: Expr) -> Expr: """Converts each element to its text representation. - This is an alias for :py:func:`array_to_string`. + See Also: + This is an alias for :py:func:`array_to_string`. """ return array_to_string(expr, delimiter) @@ -342,12 +387,27 @@ def list_join(expr: Expr, delimiter: Expr) -> Expr: """Converts each element to its text representation. This is an alias for :py:func:`array_to_string`. + + See Also: + This is an alias for :py:func:`array_to_string`. """ return array_to_string(expr, delimiter) def in_list(arg: Expr, values: list[Expr], negated: bool = False) -> Expr: - """Returns whether the argument is contained within the list ``values``.""" + """Returns whether the argument is contained within the list ``values``. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2, 3]}) + >>> result = df.select( + ... dfn.functions.in_list( + ... dfn.col("a"), [dfn.lit(1), dfn.lit(3)] + ... ).alias("in") + ... ) + >>> result.collect_column("in").to_pylist() + [True, False, True] + """ values = [v.expr for v in values] return Expr(f.in_list(arg.expr, values, negated)) @@ -357,6 +417,14 @@ def digest(value: Expr, method: Expr) -> Expr: Standard algorithms are md5, sha224, sha256, sha384, sha512, blake2s, blake2b, and blake3. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"]}) + >>> result = df.select( + ... dfn.functions.digest(dfn.col("a"), dfn.lit("md5")).alias("d")) + >>> len(result.collect_column("d")[0].as_py()) > 0 + True """ return Expr(f.digest(value.expr, method.expr)) @@ -365,6 +433,15 @@ def concat(*args: Expr) -> Expr: """Concatenates the text representations of all the arguments. NULL arguments are ignored. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"], "b": [" world"]}) + >>> result = df.select( + ... dfn.functions.concat(dfn.col("a"), dfn.col("b")).alias("c") + ... ) + >>> result.collect_column("c")[0].as_py() + 'hello world' """ args = [arg.expr for arg in args] return Expr(f.concat(args)) @@ -374,13 +451,27 @@ def concat_ws(separator: str, *args: Expr) -> Expr: """Concatenates the list ``args`` with the separator. ``NULL`` arguments are ignored. ``separator`` should not be ``NULL``. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"], "b": ["world"]}) + >>> result = df.select( + ... dfn.functions.concat_ws("-", dfn.col("a"), dfn.col("b")).alias("c")) + >>> result.collect_column("c")[0].as_py() + 'hello-world' """ args = [arg.expr for arg in args] return Expr(f.concat_ws(separator, args)) def order_by(expr: Expr, ascending: bool = True, nulls_first: bool = True) -> SortExpr: - """Creates a new sort expression.""" + """Creates a new sort expression. + + Examples: + >>> sort_expr = dfn.functions.order_by(dfn.col("a"), ascending=False) + >>> sort_expr.ascending() + False + """ return SortExpr(expr, ascending=ascending, nulls_first=nulls_first) @@ -392,14 +483,26 @@ def alias(expr: Expr, name: str, metadata: dict[str, str] | None = None) -> Expr name: The alias name metadata: Optional metadata to attach to the column - Returns: - An expression with the given alias + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2]}) + >>> df.select( + ... dfn.functions.alias(dfn.col("a"), "b") + ... ).collect_column("b")[0].as_py() + 1 """ return Expr(f.alias(expr.expr, name, metadata)) def col(name: str) -> Expr: - """Creates a column reference expression.""" + """Creates a column reference expression. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2, 3]}) + >>> df.select(dfn.functions.col("a")).collect_column("a")[0].as_py() + 1 + """ return Expr(f.col(name)) @@ -413,6 +516,13 @@ def count_star(filter: Expr | None = None) -> Expr: Args: filter: If provided, only count rows for which the filter is True + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2, 3]}) + >>> result = df.aggregate([], [dfn.functions.count_star().alias("cnt")]) + >>> result.collect_column("cnt")[0].as_py() + 3 """ return count(Expr.literal(1), filter=filter) @@ -423,6 +533,15 @@ def case(expr: Expr) -> CaseBuilder: Create a :py:class:`~datafusion.expr.CaseBuilder` to match cases for the expression ``expr``. See :py:class:`~datafusion.expr.CaseBuilder` for detailed usage. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2, 3]}) + >>> result = df.select( + ... dfn.functions.case(dfn.col("a")).when(dfn.lit(1), + ... dfn.lit("one")).otherwise(dfn.lit("other")).alias("c")) + >>> result.collect_column("c")[0].as_py() + 'one' """ return CaseBuilder(f.case(expr.expr)) @@ -433,6 +552,15 @@ def when(when: Expr, then: Expr) -> CaseBuilder: Create a :py:class:`~datafusion.expr.CaseBuilder` to match cases for the expression ``expr``. See :py:class:`~datafusion.expr.CaseBuilder` for detailed usage. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2, 3]}) + >>> result = df.select( + ... dfn.functions.when(dfn.col("a") > dfn.lit(2), + ... dfn.lit("big")).otherwise(dfn.lit("small")).alias("c")) + >>> result.collect_column("c")[2].as_py() + 'big' """ return CaseBuilder(f.when(when.expr, then.expr)) From 93f4c34bf5a4afae2547d5ccb677143d1833ebf0 Mon Sep 17 00:00:00 2001 From: Nick <24689722+ntjohnson1@users.noreply.github.com> Date: Tue, 17 Mar 2026 02:14:42 -0400 Subject: [PATCH 15/56] Add docstring examples for Aggregate basic and bitwise/boolean functions (#1416) * Add docstring examples for Aggregate basic and bitwise/boolean functions Add example usage to docstrings for Aggregate basic and bitwise/boolean functions to improve documentation. Co-Authored-By: Claude Opus 4.6 * Add tighter bound on approx_distinct for small sizes --------- Co-authored-by: Claude Opus 4.6 --- python/datafusion/functions.py | 141 +++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/python/datafusion/functions.py b/python/datafusion/functions.py index 4738061c0..765d13653 100644 --- a/python/datafusion/functions.py +++ b/python/datafusion/functions.py @@ -2370,6 +2370,15 @@ def approx_distinct( Args: expression: Values to check for distinct entries filter: If provided, only compute against rows for which the filter is True + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 1, 2, 3]}) + >>> result = df.aggregate( + ... [], [dfn.functions.approx_distinct(dfn.col("a")).alias("v")]) + >>> result.collect_column("v")[0].as_py() == 3 + True """ filter_raw = filter.expr if filter is not None else None @@ -2388,6 +2397,15 @@ def approx_median(expression: Expr, filter: Expr | None = None) -> Expr: Args: expression: Values to find the median for filter: If provided, only compute against rows for which the filter is True + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.approx_median(dfn.col("a")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 2.0 """ filter_raw = filter.expr if filter is not None else None return Expr(f.approx_median(expression.expr, filter=filter_raw)) @@ -2419,6 +2437,15 @@ def approx_percentile_cont( percentile: This must be between 0.0 and 1.0, inclusive num_centroids: Max bin size for the t-digest algorithm filter: If provided, only compute against rows for which the filter is True + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0, 4.0, 5.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.approx_percentile_cont(dfn.col("a"), 0.5).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 3.0 """ sort_expr_raw = sort_or_default(sort_expression) filter_raw = filter.expr if filter is not None else None @@ -2451,6 +2478,15 @@ def approx_percentile_cont_with_weight( num_centroids: Max bin size for the t-digest algorithm filter: If provided, only compute against rows for which the filter is True + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0], "w": [1.0, 1.0, 1.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.approx_percentile_cont_with_weight(dfn.col("a"), + ... dfn.col("w"), 0.5).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 2.0 """ sort_expr_raw = sort_or_default(sort_expression) filter_raw = filter.expr if filter is not None else None @@ -2514,6 +2550,14 @@ def avg( Args: expression: Values to combine into an array filter: If provided, only compute against rows for which the filter is True + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0]}) + >>> result = df.aggregate([], [dfn.functions.avg(dfn.col("a")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 2.0 """ filter_raw = filter.expr if filter is not None else None return Expr(f.avg(expression.expr, filter=filter_raw)) @@ -2552,6 +2596,14 @@ def count( expressions: Argument to perform bitwise calculation on distinct: If True, a single entry for each distinct value will be in the result filter: If provided, only compute against rows for which the filter is True + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2, 3]}) + >>> result = df.aggregate([], [dfn.functions.count(dfn.col("a")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 3 """ filter_raw = filter.expr if filter is not None else None @@ -2616,6 +2668,14 @@ def max(expression: Expr, filter: Expr | None = None) -> Expr: Args: expression: The value to find the maximum of filter: If provided, only compute against rows for which the filter is True + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2, 3]}) + >>> result = df.aggregate([], [dfn.functions.max(dfn.col("a")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 3 """ filter_raw = filter.expr if filter is not None else None return Expr(f.max(expression.expr, filter=filter_raw)) @@ -2625,6 +2685,14 @@ def mean(expression: Expr, filter: Expr | None = None) -> Expr: """Returns the average (mean) value of the argument. This is an alias for :py:func:`avg`. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0]}) + >>> result = df.aggregate([], [dfn.functions.mean(dfn.col("a")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 2.0 """ return avg(expression, filter) @@ -2644,6 +2712,14 @@ def median( expression: The value to compute the median of distinct: If True, a single entry for each distinct value will be in the result filter: If provided, only compute against rows for which the filter is True + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0]}) + >>> result = df.aggregate([], [dfn.functions.median(dfn.col("a")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 2.0 """ filter_raw = filter.expr if filter is not None else None return Expr(f.median(expression.expr, distinct=distinct, filter=filter_raw)) @@ -2658,6 +2734,14 @@ def min(expression: Expr, filter: Expr | None = None) -> Expr: Args: expression: The value to find the minimum of filter: If provided, only compute against rows for which the filter is True + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2, 3]}) + >>> result = df.aggregate([], [dfn.functions.min(dfn.col("a")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 1 """ filter_raw = filter.expr if filter is not None else None return Expr(f.min(expression.expr, filter=filter_raw)) @@ -2677,6 +2761,14 @@ def sum( Args: expression: Values to combine into an array filter: If provided, only compute against rows for which the filter is True + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2, 3]}) + >>> result = df.aggregate([], [dfn.functions.sum(dfn.col("a")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 6 """ filter_raw = filter.expr if filter is not None else None return Expr(f.sum(expression.expr, filter=filter_raw)) @@ -3094,6 +3186,14 @@ def bit_and(expression: Expr, filter: Expr | None = None) -> Expr: Args: expression: Argument to perform bitwise calculation on filter: If provided, only compute against rows for which the filter is True + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [7, 3]}) + >>> result = df.aggregate([], [dfn.functions.bit_and(dfn.col("a")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 3 """ filter_raw = filter.expr if filter is not None else None return Expr(f.bit_and(expression.expr, filter=filter_raw)) @@ -3110,6 +3210,14 @@ def bit_or(expression: Expr, filter: Expr | None = None) -> Expr: Args: expression: Argument to perform bitwise calculation on filter: If provided, only compute against rows for which the filter is True + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2]}) + >>> result = df.aggregate([], [dfn.functions.bit_or(dfn.col("a")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 3 """ filter_raw = filter.expr if filter is not None else None return Expr(f.bit_or(expression.expr, filter=filter_raw)) @@ -3129,6 +3237,14 @@ def bit_xor( expression: Argument to perform bitwise calculation on distinct: If True, evaluate each unique value of expression only once filter: If provided, only compute against rows for which the filter is True + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [5, 3]}) + >>> result = df.aggregate([], [dfn.functions.bit_xor(dfn.col("a")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 6 """ filter_raw = filter.expr if filter is not None else None return Expr(f.bit_xor(expression.expr, distinct=distinct, filter=filter_raw)) @@ -3146,6 +3262,14 @@ def bool_and(expression: Expr, filter: Expr | None = None) -> Expr: Args: expression: Argument to perform calculation on filter: If provided, only compute against rows for which the filter is True + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [True, True, False]}) + >>> result = df.aggregate([], [dfn.functions.bool_and(dfn.col("a")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + False """ filter_raw = filter.expr if filter is not None else None return Expr(f.bool_and(expression.expr, filter=filter_raw)) @@ -3163,6 +3287,14 @@ def bool_or(expression: Expr, filter: Expr | None = None) -> Expr: Args: expression: Argument to perform calculation on filter: If provided, only compute against rows for which the filter is True + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [False, False, True]}) + >>> result = df.aggregate([], [dfn.functions.bool_or(dfn.col("a")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + True """ filter_raw = filter.expr if filter is not None else None return Expr(f.bool_or(expression.expr, filter=filter_raw)) @@ -3553,6 +3685,15 @@ def string_agg( For example:: df.aggregate([], string_agg(col("a"), ",", order_by="b")) + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["x", "y", "z"]}) + >>> result = df.aggregate( + ... [], [dfn.functions.string_agg(dfn.col("a"), ",", order_by="a").alias("s")]) + >>> result.collect_column("s")[0].as_py() + 'x,y,z' """ order_by_raw = sort_list_to_raw_sort_list(order_by) filter_raw = filter.expr if filter is not None else None From 3dfd6ee5d9ba7de0896f195cef5bc16b4d5f0dd0 Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Tue, 17 Mar 2026 15:58:34 -0400 Subject: [PATCH 16/56] Fix CI errors on main (#1432) * Do not run check for patches on main, just release candidates * It is not necessary to pull submodules. It's only slowing down CI --- .github/workflows/build.yml | 2 +- Cargo.toml | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 455a0dc1a..4c07b08bb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -99,7 +99,7 @@ jobs: run: taplo format --check check-crates-patch: - if: inputs.build_mode == 'release' + if: inputs.build_mode == 'release' && startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 diff --git a/Cargo.toml b/Cargo.toml index aa8aa6b38..19b79daf8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,12 +70,12 @@ codegen-units = 1 # We cannot publish to crates.io with any patches in the below section. Developers # must remove any entries in this section before creating a release candidate. [patch.crates-io] -datafusion = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } -datafusion-substrait = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } -datafusion-proto = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } -datafusion-ffi = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } -datafusion-catalog = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } -datafusion-common = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } -datafusion-functions-aggregate = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } -datafusion-functions-window = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } -datafusion-expr = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f" } +datafusion = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f", submodules = false } +datafusion-substrait = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f", submodules = false } +datafusion-proto = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f", submodules = false } +datafusion-ffi = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f", submodules = false } +datafusion-catalog = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f", submodules = false } +datafusion-common = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f", submodules = false } +datafusion-functions-aggregate = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f", submodules = false } +datafusion-functions-window = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f", submodules = false } +datafusion-expr = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f", submodules = false } From f01f30c6332e40208e9f943a163a66e3d2781d08 Mon Sep 17 00:00:00 2001 From: Nick <24689722+ntjohnson1@users.noreply.github.com> Date: Wed, 18 Mar 2026 01:51:06 -0400 Subject: [PATCH 17/56] Add docstring examples for Scalar temporal functions (#1424) * Add docstring examples for Scalar temporal functions Add example usage to docstrings for Scalar temporal functions to improve documentation. Co-Authored-By: Claude Opus 4.6 * Remove examples for aliases * Fix claude's attempt to cheat with sql * Make examples follow google docstyle --------- Co-authored-by: Claude Opus 4.6 --- python/datafusion/functions.py | 178 +++++++++++++++++++++++++++++++-- 1 file changed, 170 insertions(+), 8 deletions(-) diff --git a/python/datafusion/functions.py b/python/datafusion/functions.py index 765d13653..f457e2c94 100644 --- a/python/datafusion/functions.py +++ b/python/datafusion/functions.py @@ -1562,6 +1562,19 @@ def now() -> Expr: """Returns the current timestamp in nanoseconds. This will use the same value for all instances of now() in same statement. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1]}) + >>> result = df.select( + ... dfn.functions.now().alias("now") + ... ) + + Use .value instead of .as_py() because nanosecond timestamps + require pandas to convert to Python datetime objects. + + >>> result.collect_column("now")[0].value > 0 + True """ return Expr(f.now()) @@ -1622,6 +1635,17 @@ def to_timestamp(arg: Expr, *formatters: Expr) -> Expr: For usage of ``formatters`` see the rust chrono package ``strftime`` package. [Documentation here.](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["2021-01-01T00:00:00"]}) + >>> result = df.select( + ... dfn.functions.to_timestamp( + ... dfn.col("a") + ... ).alias("ts") + ... ) + >>> str(result.collect_column("ts")[0].as_py()) + '2021-01-01 00:00:00' """ return Expr(f.to_timestamp(arg.expr, *_unwrap_exprs(formatters))) @@ -1630,6 +1654,17 @@ def to_timestamp_millis(arg: Expr, *formatters: Expr) -> Expr: """Converts a string and optional formats to a ``Timestamp`` in milliseconds. See :py:func:`to_timestamp` for a description on how to use formatters. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["2021-01-01T00:00:00"]}) + >>> result = df.select( + ... dfn.functions.to_timestamp_millis( + ... dfn.col("a") + ... ).alias("ts") + ... ) + >>> str(result.collect_column("ts")[0].as_py()) + '2021-01-01 00:00:00' """ return Expr(f.to_timestamp_millis(arg.expr, *_unwrap_exprs(formatters))) @@ -1638,6 +1673,17 @@ def to_timestamp_micros(arg: Expr, *formatters: Expr) -> Expr: """Converts a string and optional formats to a ``Timestamp`` in microseconds. See :py:func:`to_timestamp` for a description on how to use formatters. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["2021-01-01T00:00:00"]}) + >>> result = df.select( + ... dfn.functions.to_timestamp_micros( + ... dfn.col("a") + ... ).alias("ts") + ... ) + >>> str(result.collect_column("ts")[0].as_py()) + '2021-01-01 00:00:00' """ return Expr(f.to_timestamp_micros(arg.expr, *_unwrap_exprs(formatters))) @@ -1646,6 +1692,17 @@ def to_timestamp_nanos(arg: Expr, *formatters: Expr) -> Expr: """Converts a string and optional formats to a ``Timestamp`` in nanoseconds. See :py:func:`to_timestamp` for a description on how to use formatters. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["2021-01-01T00:00:00"]}) + >>> result = df.select( + ... dfn.functions.to_timestamp_nanos( + ... dfn.col("a") + ... ).alias("ts") + ... ) + >>> str(result.collect_column("ts")[0].as_py()) + '2021-01-01 00:00:00' """ return Expr(f.to_timestamp_nanos(arg.expr, *_unwrap_exprs(formatters))) @@ -1654,17 +1711,46 @@ def to_timestamp_seconds(arg: Expr, *formatters: Expr) -> Expr: """Converts a string and optional formats to a ``Timestamp`` in seconds. See :py:func:`to_timestamp` for a description on how to use formatters. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["2021-01-01T00:00:00"]}) + >>> result = df.select( + ... dfn.functions.to_timestamp_seconds( + ... dfn.col("a") + ... ).alias("ts") + ... ) + >>> str(result.collect_column("ts")[0].as_py()) + '2021-01-01 00:00:00' """ return Expr(f.to_timestamp_seconds(arg.expr, *_unwrap_exprs(formatters))) def to_unixtime(string: Expr, *format_arguments: Expr) -> Expr: - """Converts a string and optional formats to a Unixtime.""" + """Converts a string and optional formats to a Unixtime. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["1970-01-01T00:00:00"]}) + >>> result = df.select(dfn.functions.to_unixtime(dfn.col("a")).alias("u")) + >>> result.collect_column("u")[0].as_py() + 0 + """ return Expr(f.to_unixtime(string.expr, *_unwrap_exprs(format_arguments))) def current_date() -> Expr: - """Returns current UTC date as a Date32 value.""" + """Returns current UTC date as a Date32 value. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1]}) + >>> result = df.select( + ... dfn.functions.current_date().alias("d") + ... ) + >>> result.collect_column("d")[0].as_py() is not None + True + """ return Expr(f.current_date()) @@ -1672,7 +1758,21 @@ def current_date() -> Expr: def current_time() -> Expr: - """Returns current UTC time as a Time64 value.""" + """Returns current UTC time as a Time64 value. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1]}) + >>> result = df.select( + ... dfn.functions.current_time().alias("t") + ... ) + + Use .value instead of .as_py() because nanosecond timestamps + require pandas to convert to Python datetime objects. + + >>> result.collect_column("t")[0].value > 0 + True + """ return Expr(f.current_time()) @@ -1685,7 +1785,17 @@ def datepart(part: Expr, date: Expr) -> Expr: def date_part(part: Expr, date: Expr) -> Expr: - """Extracts a subfield from the date.""" + """Extracts a subfield from the date. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["2021-07-15T00:00:00"]}) + >>> df = df.select(dfn.functions.to_timestamp(dfn.col("a")).alias("a")) + >>> result = df.select( + ... dfn.functions.date_part(dfn.lit("year"), dfn.col("a")).alias("y")) + >>> result.collect_column("y")[0].as_py() + 2021 + """ return Expr(f.date_part(part.expr, date.expr)) @@ -1698,7 +1808,20 @@ def extract(part: Expr, date: Expr) -> Expr: def date_trunc(part: Expr, date: Expr) -> Expr: - """Truncates the date to a specified level of precision.""" + """Truncates the date to a specified level of precision. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["2021-07-15T12:34:56"]}) + >>> df = df.select(dfn.functions.to_timestamp(dfn.col("a")).alias("a")) + >>> result = df.select( + ... dfn.functions.date_trunc( + ... dfn.lit("month"), dfn.col("a") + ... ).alias("t") + ... ) + >>> str(result.collect_column("t")[0].as_py()) + '2021-07-01 00:00:00' + """ return Expr(f.date_trunc(part.expr, date.expr)) @@ -1711,12 +1834,39 @@ def datetrunc(part: Expr, date: Expr) -> Expr: def date_bin(stride: Expr, source: Expr, origin: Expr) -> Expr: - """Coerces an arbitrary timestamp to the start of the nearest specified interval.""" + """Coerces an arbitrary timestamp to the start of the nearest specified interval. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"timestamp": ['2021-07-15 12:34:56', '2021-01-01']}) + >>> result = df.select( + ... dfn.functions.date_bin( + ... dfn.string_literal("15 minutes"), + ... dfn.col("timestamp"), + ... dfn.string_literal("2001-01-01 00:00:00") + ... ).alias("b") + ... ) + >>> str(result.collect_column("b")[0].as_py()) + '2021-07-15 12:30:00' + >>> str(result.collect_column("b")[1].as_py()) + '2021-01-01 00:00:00' + """ return Expr(f.date_bin(stride.expr, source.expr, origin.expr)) def make_date(year: Expr, month: Expr, day: Expr) -> Expr: - """Make a date from year, month and day component parts.""" + """Make a date from year, month and day component parts. + + Examples: + >>> from datetime import date + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"y": [2024], "m": [1], "d": [15]}) + >>> result = df.select( + ... dfn.functions.make_date(dfn.col("y"), dfn.col("m"), + ... dfn.col("d")).alias("dt")) + >>> result.collect_column("dt")[0].as_py() + datetime.date(2024, 1, 15) + """ return Expr(f.make_date(year.expr, month.expr, day.expr)) @@ -1839,7 +1989,19 @@ def named_struct(name_pairs: list[tuple[str, Expr]]) -> Expr: def from_unixtime(arg: Expr) -> Expr: - """Converts an integer to RFC3339 timestamp format string.""" + """Converts an integer to RFC3339 timestamp format string. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0]}) + >>> result = df.select( + ... dfn.functions.from_unixtime( + ... dfn.col("a") + ... ).alias("ts") + ... ) + >>> str(result.collect_column("ts")[0].as_py()) + '1970-01-01 00:00:00' + """ return Expr(f.from_unixtime(arg.expr)) From 74b32214fb2c9a06f72cd0495b19fee5d5a3047b Mon Sep 17 00:00:00 2001 From: Nick <24689722+ntjohnson1@users.noreply.github.com> Date: Wed, 18 Mar 2026 01:52:23 -0400 Subject: [PATCH 18/56] Add docstring examples for Aggregate statistical and regression functions (#1417) * Add docstring examples for Aggregate statistical and regression functions Add example usage to docstrings for Aggregate statistical and regression functions to improve documentation. Co-Authored-By: Claude Opus 4.6 * Simplify covar * Make sure everything is google doc style --------- Co-authored-by: Claude Opus 4.6 --- python/datafusion/functions.py | 173 ++++++++++++++++++++++++++++++--- 1 file changed, 162 insertions(+), 11 deletions(-) diff --git a/python/datafusion/functions.py b/python/datafusion/functions.py index f457e2c94..026a6d04b 100644 --- a/python/datafusion/functions.py +++ b/python/datafusion/functions.py @@ -823,14 +823,11 @@ def cot(arg: Expr) -> Expr: >>> from math import pi >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"a": [pi / 4]}) - >>> import builtins >>> result = df.select( ... dfn.functions.cot(dfn.col("a")).alias("cot") ... ) - >>> builtins.round( - ... result.collect_column("cot")[0].as_py(), 1 - ... ) - 1.0 + >>> result.collect_column("cot")[0].as_py() + 1.0... """ return Expr(f.cot(arg.expr)) @@ -1171,14 +1168,11 @@ def radians(arg: Expr) -> Expr: >>> from math import pi >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"a": [180.0]}) - >>> import builtins >>> result = df.select( ... dfn.functions.radians(dfn.col("a")).alias("rad") ... ) - >>> builtins.round( - ... result.collect_column("rad")[0].as_py(), 6 - ... ) - 3.141593 + >>> result.collect_column("rad")[0].as_py() == pi + True """ return Expr(f.radians(arg.expr)) @@ -2737,6 +2731,14 @@ def corr(value_y: Expr, value_x: Expr, filter: Expr | None = None) -> Expr: value_y: The dependent variable for correlation value_x: The independent variable for correlation filter: If provided, only compute against rows for which the filter is True + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0], "b": [1.0, 2.0, 3.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.corr(dfn.col("a"), dfn.col("b")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 1.0 """ filter_raw = filter.expr if filter is not None else None return Expr(f.corr(value_y.expr, value_x.expr, filter=filter_raw)) @@ -2791,6 +2793,18 @@ def covar_pop(value_y: Expr, value_x: Expr, filter: Expr | None = None) -> Expr: value_y: The dependent variable for covariance value_x: The independent variable for covariance filter: If provided, only compute against rows for which the filter is True + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0, 5.0, 10.0], "b": [1.0, 2.0, 3.0]}) + >>> result = df.aggregate( + ... [], + ... [dfn.functions.covar_pop( + ... dfn.col("a"), dfn.col("b") + ... ).alias("v")] + ... ) + >>> result.collect_column("v")[0].as_py() + 3.0 """ filter_raw = filter.expr if filter is not None else None return Expr(f.covar_pop(value_y.expr, value_x.expr, filter=filter_raw)) @@ -2808,6 +2822,14 @@ def covar_samp(value_y: Expr, value_x: Expr, filter: Expr | None = None) -> Expr value_y: The dependent variable for covariance value_x: The independent variable for covariance filter: If provided, only compute against rows for which the filter is True + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0], "b": [4.0, 5.0, 6.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.covar_samp(dfn.col("a"), dfn.col("b")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 1.0 """ filter_raw = filter.expr if filter is not None else None return Expr(f.covar_samp(value_y.expr, value_x.expr, filter=filter_raw)) @@ -2816,7 +2838,8 @@ def covar_samp(value_y: Expr, value_x: Expr, filter: Expr | None = None) -> Expr def covar(value_y: Expr, value_x: Expr, filter: Expr | None = None) -> Expr: """Computes the sample covariance. - This is an alias for :py:func:`covar_samp`. + See Also: + This is an alias for :py:func:`covar_samp`. """ return covar_samp(value_y, value_x, filter) @@ -2945,6 +2968,13 @@ def stddev(expression: Expr, filter: Expr | None = None) -> Expr: Args: expression: The value to find the minimum of filter: If provided, only compute against rows for which the filter is True + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [2.0, 4.0, 6.0]}) + >>> result = df.aggregate([], [dfn.functions.stddev(dfn.col("a")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 2.0 """ filter_raw = filter.expr if filter is not None else None return Expr(f.stddev(expression.expr, filter=filter_raw)) @@ -2959,6 +2989,15 @@ def stddev_pop(expression: Expr, filter: Expr | None = None) -> Expr: Args: expression: The value to find the minimum of filter: If provided, only compute against rows for which the filter is True + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0, 3.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.stddev_pop(dfn.col("a")).alias("v")] + ... ) + >>> result.collect_column("v")[0].as_py() + 1.0 """ filter_raw = filter.expr if filter is not None else None return Expr(f.stddev_pop(expression.expr, filter=filter_raw)) @@ -2968,6 +3007,15 @@ def stddev_samp(arg: Expr, filter: Expr | None = None) -> Expr: """Computes the sample standard deviation of the argument. This is an alias for :py:func:`stddev`. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [2.0, 4.0, 6.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.stddev_samp(dfn.col("a")).alias("v")] + ... ) + >>> result.collect_column("v")[0].as_py() + 2.0 """ return stddev(arg, filter=filter) @@ -2976,6 +3024,13 @@ def var(expression: Expr, filter: Expr | None = None) -> Expr: """Computes the sample variance of the argument. This is an alias for :py:func:`var_samp`. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0]}) + >>> result = df.aggregate([], [dfn.functions.var(dfn.col("a")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 1.0 """ return var_samp(expression, filter) @@ -2989,6 +3044,13 @@ def var_pop(expression: Expr, filter: Expr | None = None) -> Expr: Args: expression: The variable to compute the variance for filter: If provided, only compute against rows for which the filter is True + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0.0, 2.0]}) + >>> result = df.aggregate([], [dfn.functions.var_pop(dfn.col("a")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 1.0 """ filter_raw = filter.expr if filter is not None else None return Expr(f.var_pop(expression.expr, filter=filter_raw)) @@ -3003,6 +3065,13 @@ def var_samp(expression: Expr, filter: Expr | None = None) -> Expr: Args: expression: The variable to compute the variance for filter: If provided, only compute against rows for which the filter is True + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0]}) + >>> result = df.aggregate([], [dfn.functions.var_samp(dfn.col("a")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 1.0 """ filter_raw = filter.expr if filter is not None else None return Expr(f.var_sample(expression.expr, filter=filter_raw)) @@ -3012,6 +3081,15 @@ def var_sample(expression: Expr, filter: Expr | None = None) -> Expr: """Computes the sample variance of the argument. This is an alias for :py:func:`var_samp`. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.var_sample(dfn.col("a")).alias("v")] + ... ) + >>> result.collect_column("v")[0].as_py() + 1.0 """ return var_samp(expression, filter) @@ -3033,6 +3111,14 @@ def regr_avgx( y: The linear regression dependent variable x: The linear regression independent variable filter: If provided, only compute against rows for which the filter is True + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"y": [1.0, 2.0, 3.0], "x": [4.0, 5.0, 6.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.regr_avgx(dfn.col("y"), dfn.col("x")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 5.0 """ filter_raw = filter.expr if filter is not None else None @@ -3056,6 +3142,14 @@ def regr_avgy( y: The linear regression dependent variable x: The linear regression independent variable filter: If provided, only compute against rows for which the filter is True + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"y": [1.0, 2.0, 3.0], "x": [4.0, 5.0, 6.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.regr_avgy(dfn.col("y"), dfn.col("x")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 2.0 """ filter_raw = filter.expr if filter is not None else None @@ -3079,6 +3173,14 @@ def regr_count( y: The linear regression dependent variable x: The linear regression independent variable filter: If provided, only compute against rows for which the filter is True + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"y": [1.0, 2.0, 3.0], "x": [4.0, 5.0, 6.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.regr_count(dfn.col("y"), dfn.col("x")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 3 """ filter_raw = filter.expr if filter is not None else None @@ -3102,6 +3204,15 @@ def regr_intercept( y: The linear regression dependent variable x: The linear regression independent variable filter: If provided, only compute against rows for which the filter is True + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"y": [2.0, 4.0, 6.0], "x": [1.0, 2.0, 3.0]}) + >>> result = df.aggregate( + ... [], + ... [dfn.functions.regr_intercept(dfn.col("y"), dfn.col("x")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 0.0 """ filter_raw = filter.expr if filter is not None else None @@ -3125,6 +3236,14 @@ def regr_r2( y: The linear regression dependent variable x: The linear regression independent variable filter: If provided, only compute against rows for which the filter is True + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"y": [2.0, 4.0, 6.0], "x": [1.0, 2.0, 3.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.regr_r2(dfn.col("y"), dfn.col("x")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 1.0 """ filter_raw = filter.expr if filter is not None else None @@ -3148,6 +3267,14 @@ def regr_slope( y: The linear regression dependent variable x: The linear regression independent variable filter: If provided, only compute against rows for which the filter is True + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"y": [2.0, 4.0, 6.0], "x": [1.0, 2.0, 3.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.regr_slope(dfn.col("y"), dfn.col("x")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 2.0 """ filter_raw = filter.expr if filter is not None else None @@ -3171,6 +3298,14 @@ def regr_sxx( y: The linear regression dependent variable x: The linear regression independent variable filter: If provided, only compute against rows for which the filter is True + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"y": [1.0, 2.0, 3.0], "x": [1.0, 2.0, 3.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.regr_sxx(dfn.col("y"), dfn.col("x")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 2.0 """ filter_raw = filter.expr if filter is not None else None @@ -3194,6 +3329,14 @@ def regr_sxy( y: The linear regression dependent variable x: The linear regression independent variable filter: If provided, only compute against rows for which the filter is True + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"y": [1.0, 2.0, 3.0], "x": [1.0, 2.0, 3.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.regr_sxy(dfn.col("y"), dfn.col("x")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 2.0 """ filter_raw = filter.expr if filter is not None else None @@ -3217,6 +3360,14 @@ def regr_syy( y: The linear regression dependent variable x: The linear regression independent variable filter: If provided, only compute against rows for which the filter is True + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"y": [1.0, 2.0, 3.0], "x": [1.0, 2.0, 3.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.regr_syy(dfn.col("y"), dfn.col("x")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 2.0 """ filter_raw = filter.expr if filter is not None else None From 3c5013dd57369c55aaf5a463797b73f1d65f3d8a Mon Sep 17 00:00:00 2001 From: Nick <24689722+ntjohnson1@users.noreply.github.com> Date: Wed, 18 Mar 2026 01:58:30 -0400 Subject: [PATCH 19/56] Add docstring examples for Scalar array/list functions (#1420) * Add docstring examples for Scalar array/list functions Add example usage to docstrings for Scalar array/list functions to improve documentation. Co-Authored-By: Claude Opus 4.6 * Remove examples from all aliases, maybe we should just remove the aliases for simple api surface --------- Co-authored-by: Claude Opus 4.6 --- python/datafusion/functions.py | 398 ++++++++++++++++++++++++++++++--- 1 file changed, 371 insertions(+), 27 deletions(-) diff --git a/python/datafusion/functions.py b/python/datafusion/functions.py index 026a6d04b..fbca979ca 100644 --- a/python/datafusion/functions.py +++ b/python/datafusion/functions.py @@ -1895,7 +1895,17 @@ def upper(arg: Expr) -> Expr: def make_array(*args: Expr) -> Expr: - """Returns an array using the specified input expressions.""" + """Returns an array using the specified input expressions. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1]}) + >>> result = df.select( + ... dfn.functions.make_array(dfn.lit(1), dfn.lit(2), dfn.lit(3)).alias("arr")) + >>> result.collect_column("arr")[0].as_py() + [1, 2, 3] + """ args = [arg.expr for arg in args] return Expr(f.make_array(args)) @@ -1917,7 +1927,17 @@ def array(*args: Expr) -> Expr: def range(start: Expr, stop: Expr, step: Expr) -> Expr: - """Create a list of values in the range between start and stop.""" + """Create a list of values in the range between start and stop. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1]}) + >>> result = df.select( + ... dfn.functions.range(dfn.lit(0), dfn.lit(5), dfn.lit(2)).alias("r")) + >>> result.collect_column("r")[0].as_py() + [0, 2, 4] + """ return Expr(f.range(start.expr, stop.expr, step.expr)) @@ -2045,7 +2065,17 @@ def random() -> Expr: def array_append(array: Expr, element: Expr) -> Expr: - """Appends an element to the end of an array.""" + """Appends an element to the end of an array. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) + >>> result = df.select( + ... dfn.functions.array_append(dfn.col("a"), dfn.lit(4)).alias("result")) + >>> result.collect_column("result")[0].as_py() + [1, 2, 3, 4] + """ return Expr(f.array_append(array.expr, element.expr)) @@ -2074,7 +2104,17 @@ def list_push_back(array: Expr, element: Expr) -> Expr: def array_concat(*args: Expr) -> Expr: - """Concatenates the input arrays.""" + """Concatenates the input arrays. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2]], "b": [[3, 4]]}) + >>> result = df.select( + ... dfn.functions.array_concat(dfn.col("a"), dfn.col("b")).alias("result")) + >>> result.collect_column("result")[0].as_py() + [1, 2, 3, 4] + """ args = [arg.expr for arg in args] return Expr(f.array_concat(args)) @@ -2088,12 +2128,36 @@ def array_cat(*args: Expr) -> Expr: def array_dims(array: Expr) -> Expr: - """Returns an array of the array's dimensions.""" + """Returns an array of the array's dimensions. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) + >>> result = df.select(dfn.functions.array_dims(dfn.col("a")).alias("result")) + >>> result.collect_column("result")[0].as_py() + [3] + """ return Expr(f.array_dims(array.expr)) def array_distinct(array: Expr) -> Expr: - """Returns distinct values from the array after removing duplicates.""" + """Returns distinct values from the array after removing duplicates. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 1, 2, 3]]}) + >>> result = df.select( + ... dfn.functions.array_distinct( + ... dfn.col("a") + ... ).alias("result") + ... ) + >>> sorted( + ... result.collect_column("result")[0].as_py() + ... ) + [1, 2, 3] + """ return Expr(f.array_distinct(array.expr)) @@ -2130,12 +2194,31 @@ def list_dims(array: Expr) -> Expr: def array_element(array: Expr, n: Expr) -> Expr: - """Extracts the element with the index n from the array.""" + """Extracts the element with the index n from the array. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[10, 20, 30]]}) + >>> result = df.select( + ... dfn.functions.array_element(dfn.col("a"), dfn.lit(2)).alias("result")) + >>> result.collect_column("result")[0].as_py() + 20 + """ return Expr(f.array_element(array.expr, n.expr)) def array_empty(array: Expr) -> Expr: - """Returns a boolean indicating whether the array is empty.""" + """Returns a boolean indicating whether the array is empty. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2]]}) + >>> result = df.select(dfn.functions.array_empty(dfn.col("a")).alias("result")) + >>> result.collect_column("result")[0].as_py() + False + """ return Expr(f.array_empty(array.expr)) @@ -2164,7 +2247,16 @@ def list_extract(array: Expr, n: Expr) -> Expr: def array_length(array: Expr) -> Expr: - """Returns the length of the array.""" + """Returns the length of the array. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) + >>> result = df.select(dfn.functions.array_length(dfn.col("a")).alias("result")) + >>> result.collect_column("result")[0].as_py() + 3 + """ return Expr(f.array_length(array.expr)) @@ -2177,7 +2269,17 @@ def list_length(array: Expr) -> Expr: def array_has(first_array: Expr, second_array: Expr) -> Expr: - """Returns true if the element appears in the first array, otherwise false.""" + """Returns true if the element appears in the first array, otherwise false. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) + >>> result = df.select( + ... dfn.functions.array_has(dfn.col("a"), dfn.lit(2)).alias("result")) + >>> result.collect_column("result")[0].as_py() + True + """ return Expr(f.array_has(first_array.expr, second_array.expr)) @@ -2186,6 +2288,15 @@ def array_has_all(first_array: Expr, second_array: Expr) -> Expr: Returns true if each element of the second array appears in the first array. Otherwise, it returns false. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]], "b": [[1, 2]]}) + >>> result = df.select( + ... dfn.functions.array_has_all(dfn.col("a"), dfn.col("b")).alias("result")) + >>> result.collect_column("result")[0].as_py() + True """ return Expr(f.array_has_all(first_array.expr, second_array.expr)) @@ -2195,12 +2306,31 @@ def array_has_any(first_array: Expr, second_array: Expr) -> Expr: Returns true if at least one element of the second array appears in the first array. Otherwise, it returns false. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]], "b": [[2, 5]]}) + >>> result = df.select( + ... dfn.functions.array_has_any(dfn.col("a"), dfn.col("b")).alias("result")) + >>> result.collect_column("result")[0].as_py() + True """ return Expr(f.array_has_any(first_array.expr, second_array.expr)) def array_position(array: Expr, element: Expr, index: int | None = 1) -> Expr: - """Return the position of the first occurrence of ``element`` in ``array``.""" + """Return the position of the first occurrence of ``element`` in ``array``. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[10, 20, 30]]}) + >>> result = df.select( + ... dfn.functions.array_position(dfn.col("a"), dfn.lit(20)).alias("result")) + >>> result.collect_column("result")[0].as_py() + 2 + """ return Expr(f.array_position(array.expr, element.expr, index)) @@ -2229,7 +2359,17 @@ def list_indexof(array: Expr, element: Expr, index: int | None = 1) -> Expr: def array_positions(array: Expr, element: Expr) -> Expr: - """Searches for an element in the array and returns all occurrences.""" + """Searches for an element in the array and returns all occurrences. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 1]]}) + >>> result = df.select( + ... dfn.functions.array_positions(dfn.col("a"), dfn.lit(1)).alias("result")) + >>> result.collect_column("result")[0].as_py() + [1, 3] + """ return Expr(f.array_positions(array.expr, element.expr)) @@ -2242,7 +2382,16 @@ def list_positions(array: Expr, element: Expr) -> Expr: def array_ndims(array: Expr) -> Expr: - """Returns the number of dimensions of the array.""" + """Returns the number of dimensions of the array. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) + >>> result = df.select(dfn.functions.array_ndims(dfn.col("a")).alias("result")) + >>> result.collect_column("result")[0].as_py() + 1 + """ return Expr(f.array_ndims(array.expr)) @@ -2255,7 +2404,17 @@ def list_ndims(array: Expr) -> Expr: def array_prepend(element: Expr, array: Expr) -> Expr: - """Prepends an element to the beginning of an array.""" + """Prepends an element to the beginning of an array. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2]]}) + >>> result = df.select( + ... dfn.functions.array_prepend(dfn.lit(0), dfn.col("a")).alias("result")) + >>> result.collect_column("result")[0].as_py() + [0, 1, 2] + """ return Expr(f.array_prepend(element.expr, array.expr)) @@ -2284,17 +2443,45 @@ def list_push_front(element: Expr, array: Expr) -> Expr: def array_pop_back(array: Expr) -> Expr: - """Returns the array without the last element.""" + """Returns the array without the last element. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) + >>> result = df.select(dfn.functions.array_pop_back(dfn.col("a")).alias("result")) + >>> result.collect_column("result")[0].as_py() + [1, 2] + """ return Expr(f.array_pop_back(array.expr)) def array_pop_front(array: Expr) -> Expr: - """Returns the array without the first element.""" + """Returns the array without the first element. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) + >>> result = df.select(dfn.functions.array_pop_front(dfn.col("a")).alias("result")) + >>> result.collect_column("result")[0].as_py() + [2, 3] + """ return Expr(f.array_pop_front(array.expr)) def array_remove(array: Expr, element: Expr) -> Expr: - """Removes the first element from the array equal to the given value.""" + """Removes the first element from the array equal to the given value. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 1]]}) + >>> result = df.select( + ... dfn.functions.array_remove(dfn.col("a"), dfn.lit(1)).alias("result")) + >>> result.collect_column("result")[0].as_py() + [2, 1] + """ return Expr(f.array_remove(array.expr, element.expr)) @@ -2307,7 +2494,18 @@ def list_remove(array: Expr, element: Expr) -> Expr: def array_remove_n(array: Expr, element: Expr, max: Expr) -> Expr: - """Removes the first ``max`` elements from the array equal to the given value.""" + """Removes the first ``max`` elements from the array equal to the given value. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 1, 1]]}) + >>> result = df.select( + ... dfn.functions.array_remove_n(dfn.col("a"), dfn.lit(1), + ... dfn.lit(2)).alias("result")) + >>> result.collect_column("result")[0].as_py() + [2, 1] + """ return Expr(f.array_remove_n(array.expr, element.expr, max.expr)) @@ -2320,7 +2518,17 @@ def list_remove_n(array: Expr, element: Expr, max: Expr) -> Expr: def array_remove_all(array: Expr, element: Expr) -> Expr: - """Removes all elements from the array equal to the given value.""" + """Removes all elements from the array equal to the given value. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 1]]}) + >>> result = df.select( + ... dfn.functions.array_remove_all(dfn.col("a"), dfn.lit(1)).alias("result")) + >>> result.collect_column("result")[0].as_py() + [2] + """ return Expr(f.array_remove_all(array.expr, element.expr)) @@ -2333,7 +2541,17 @@ def list_remove_all(array: Expr, element: Expr) -> Expr: def array_repeat(element: Expr, count: Expr) -> Expr: - """Returns an array containing ``element`` ``count`` times.""" + """Returns an array containing ``element`` ``count`` times. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1]}) + >>> result = df.select( + ... dfn.functions.array_repeat(dfn.lit(3), dfn.lit(3)).alias("result")) + >>> result.collect_column("result")[0].as_py() + [3, 3, 3] + """ return Expr(f.array_repeat(element.expr, count.expr)) @@ -2346,7 +2564,18 @@ def list_repeat(element: Expr, count: Expr) -> Expr: def array_replace(array: Expr, from_val: Expr, to_val: Expr) -> Expr: - """Replaces the first occurrence of ``from_val`` with ``to_val``.""" + """Replaces the first occurrence of ``from_val`` with ``to_val``. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 1]]}) + >>> result = df.select( + ... dfn.functions.array_replace(dfn.col("a"), dfn.lit(1), + ... dfn.lit(9)).alias("result")) + >>> result.collect_column("result")[0].as_py() + [9, 2, 1] + """ return Expr(f.array_replace(array.expr, from_val.expr, to_val.expr)) @@ -2363,6 +2592,16 @@ def array_replace_n(array: Expr, from_val: Expr, to_val: Expr, max: Expr) -> Exp Replaces the first ``max`` occurrences of the specified element with another specified element. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 1, 1]]}) + >>> result = df.select( + ... dfn.functions.array_replace_n(dfn.col("a"), dfn.lit(1), dfn.lit(9), + ... dfn.lit(2)).alias("result")) + >>> result.collect_column("result")[0].as_py() + [9, 2, 9, 1] """ return Expr(f.array_replace_n(array.expr, from_val.expr, to_val.expr, max.expr)) @@ -2379,7 +2618,18 @@ def list_replace_n(array: Expr, from_val: Expr, to_val: Expr, max: Expr) -> Expr def array_replace_all(array: Expr, from_val: Expr, to_val: Expr) -> Expr: - """Replaces all occurrences of ``from_val`` with ``to_val``.""" + """Replaces all occurrences of ``from_val`` with ``to_val``. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 1]]}) + >>> result = df.select( + ... dfn.functions.array_replace_all(dfn.col("a"), dfn.lit(1), + ... dfn.lit(9)).alias("result")) + >>> result.collect_column("result")[0].as_py() + [9, 2, 9] + """ return Expr(f.array_replace_all(array.expr, from_val.expr, to_val.expr)) @@ -2398,6 +2648,14 @@ def array_sort(array: Expr, descending: bool = False, null_first: bool = False) array: The input array to sort. descending: If True, sorts in descending order. null_first: If True, nulls will be returned at the beginning of the array. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[3, 1, 2]]}) + >>> result = df.select(dfn.functions.array_sort(dfn.col("a")).alias("result")) + >>> result.collect_column("result")[0].as_py() + [1, 2, 3] """ desc = "DESC" if descending else "ASC" nulls_first = "NULLS FIRST" if null_first else "NULLS LAST" @@ -2418,7 +2676,18 @@ def list_sort(array: Expr, descending: bool = False, null_first: bool = False) - def array_slice( array: Expr, begin: Expr, end: Expr, stride: Expr | None = None ) -> Expr: - """Returns a slice of the array.""" + """Returns a slice of the array. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3, 4]]}) + >>> result = df.select( + ... dfn.functions.array_slice(dfn.col("a"), dfn.lit(2), + ... dfn.lit(3)).alias("result")) + >>> result.collect_column("result")[0].as_py() + [2, 3] + """ if stride is not None: stride = stride.expr return Expr(f.array_slice(array.expr, begin.expr, end.expr, stride)) @@ -2433,7 +2702,22 @@ def list_slice(array: Expr, begin: Expr, end: Expr, stride: Expr | None = None) def array_intersect(array1: Expr, array2: Expr) -> Expr: - """Returns the intersection of ``array1`` and ``array2``.""" + """Returns the intersection of ``array1`` and ``array2``. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]], "b": [[2, 3, 4]]}) + >>> result = df.select( + ... dfn.functions.array_intersect( + ... dfn.col("a"), dfn.col("b") + ... ).alias("result") + ... ) + >>> sorted( + ... result.collect_column("result")[0].as_py() + ... ) + [2, 3] + """ return Expr(f.array_intersect(array1.expr, array2.expr)) @@ -2449,6 +2733,20 @@ def array_union(array1: Expr, array2: Expr) -> Expr: """Returns an array of the elements in the union of array1 and array2. Duplicate rows will not be returned. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]], "b": [[2, 3, 4]]}) + >>> result = df.select( + ... dfn.functions.array_union( + ... dfn.col("a"), dfn.col("b") + ... ).alias("result") + ... ) + >>> sorted( + ... result.collect_column("result")[0].as_py() + ... ) + [1, 2, 3, 4] """ return Expr(f.array_union(array1.expr, array2.expr)) @@ -2464,7 +2762,17 @@ def list_union(array1: Expr, array2: Expr) -> Expr: def array_except(array1: Expr, array2: Expr) -> Expr: - """Returns the elements that appear in ``array1`` but not in ``array2``.""" + """Returns the elements that appear in ``array1`` but not in ``array2``. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]], "b": [[2, 3, 4]]}) + >>> result = df.select( + ... dfn.functions.array_except(dfn.col("a"), dfn.col("b")).alias("result")) + >>> result.collect_column("result")[0].as_py() + [1] + """ return Expr(f.array_except(array1.expr, array2.expr)) @@ -2481,6 +2789,16 @@ def array_resize(array: Expr, size: Expr, value: Expr) -> Expr: If ``size`` is greater than the ``array`` length, the additional entries will be filled with the given ``value``. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2]]}) + >>> result = df.select( + ... dfn.functions.array_resize(dfn.col("a"), dfn.lit(4), + ... dfn.lit(0)).alias("result")) + >>> result.collect_column("result")[0].as_py() + [1, 2, 0, 0] """ return Expr(f.array_resize(array.expr, size.expr, value.expr)) @@ -2495,12 +2813,30 @@ def list_resize(array: Expr, size: Expr, value: Expr) -> Expr: def flatten(array: Expr) -> Expr: - """Flattens an array of arrays into a single array.""" + """Flattens an array of arrays into a single array. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[[1, 2], [3, 4]]]}) + >>> result = df.select(dfn.functions.flatten(dfn.col("a")).alias("result")) + >>> result.collect_column("result")[0].as_py() + [1, 2, 3, 4] + """ return Expr(f.flatten(array.expr)) def cardinality(array: Expr) -> Expr: - """Returns the total number of elements in the array.""" + """Returns the total number of elements in the array. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) + >>> result = df.select(dfn.functions.cardinality(dfn.col("a")).alias("result")) + >>> result.collect_column("result")[0].as_py() + 3 + """ return Expr(f.cardinality(array.expr)) @@ -2681,6 +3017,14 @@ def array_agg( For example:: df.aggregate([], array_agg(col("a"), order_by="b")) + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2, 3]}) + >>> result = df.aggregate([], [dfn.functions.array_agg(dfn.col("a")).alias("v")]) + >>> result.collect_column("v")[0].as_py() + [1, 2, 3] """ order_by_raw = sort_list_to_raw_sort_list(order_by) filter_raw = filter.expr if filter is not None else None From 4e51fa8935799343c973e9cd306f42d278620d42 Mon Sep 17 00:00:00 2001 From: Nick <24689722+ntjohnson1@users.noreply.github.com> Date: Wed, 18 Mar 2026 02:01:44 -0400 Subject: [PATCH 20/56] Add docstring examples for Scalar string functions (#1423) * Add docstring examples for Scalar string functions Add example usage to docstrings for Scalar string functions to improve documentation. Co-Authored-By: Claude Opus 4.6 * Remove examples for aliases --------- Co-authored-by: Claude Opus 4.6 --- python/datafusion/functions.py | 361 ++++++++++++++++++++++++++++++--- 1 file changed, 335 insertions(+), 26 deletions(-) diff --git a/python/datafusion/functions.py b/python/datafusion/functions.py index fbca979ca..a4933a747 100644 --- a/python/datafusion/functions.py +++ b/python/datafusion/functions.py @@ -649,7 +649,16 @@ def acosh(arg: Expr) -> Expr: def ascii(arg: Expr) -> Expr: - """Returns the numeric code of the first character of the argument.""" + """Returns the numeric code of the first character of the argument. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["a","b","c"]}) + >>> ascii_df = df.select(dfn.functions.ascii(dfn.col("a")).alias("ascii")) + >>> ascii_df.collect_column("ascii")[0].as_py() + 97 + """ return Expr(f.ascii(arg.expr)) @@ -720,12 +729,30 @@ def atan2(y: Expr, x: Expr) -> Expr: def bit_length(arg: Expr) -> Expr: - """Returns the number of bits in the string argument.""" + """Returns the number of bits in the string argument. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["a","b","c"]}) + >>> bit_df = df.select(dfn.functions.bit_length(dfn.col("a")).alias("bit_len")) + >>> bit_df.collect_column("bit_len")[0].as_py() + 8 + """ return Expr(f.bit_length(arg.expr)) def btrim(arg: Expr) -> Expr: - """Removes all characters, spaces by default, from both sides of a string.""" + """Removes all characters, spaces by default, from both sides of a string. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [" a "]}) + >>> trim_df = df.select(dfn.functions.btrim(dfn.col("a")).alias("trimmed")) + >>> trim_df.collect_column("trimmed")[0].as_py() + 'a' + """ return Expr(f.btrim(arg.expr)) @@ -756,22 +783,59 @@ def ceil(arg: Expr) -> Expr: def character_length(arg: Expr) -> Expr: - """Returns the number of characters in the argument.""" + """Returns the number of characters in the argument. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["abc","b","c"]}) + >>> char_len_df = df.select( + ... dfn.functions.character_length(dfn.col("a")).alias("char_len")) + >>> char_len_df.collect_column("char_len")[0].as_py() + 3 + """ return Expr(f.character_length(arg.expr)) def length(string: Expr) -> Expr: - """The number of characters in the ``string``.""" + """The number of characters in the ``string``. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"]}) + >>> result = df.select(dfn.functions.length(dfn.col("a")).alias("len")) + >>> result.collect_column("len")[0].as_py() + 5 + """ return Expr(f.length(string.expr)) def char_length(string: Expr) -> Expr: - """The number of characters in the ``string``.""" + """The number of characters in the ``string``. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"]}) + >>> result = df.select(dfn.functions.char_length(dfn.col("a")).alias("len")) + >>> result.collect_column("len")[0].as_py() + 5 + """ return Expr(f.char_length(string.expr)) def chr(arg: Expr) -> Expr: - """Converts the Unicode code point to a UTF8 character.""" + """Converts the Unicode code point to a UTF8 character. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [65]}) + >>> result = df.select(dfn.functions.chr(dfn.col("a")).alias("chr")) + >>> result.collect_column("chr")[0].as_py() + 'A' + """ return Expr(f.chr(arg.expr)) @@ -847,7 +911,17 @@ def degrees(arg: Expr) -> Expr: def ends_with(arg: Expr, suffix: Expr) -> Expr: - """Returns true if the ``string`` ends with the ``suffix``, false otherwise.""" + """Returns true if the ``string`` ends with the ``suffix``, false otherwise. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["abc","b","c"]}) + >>> ends_with_df = df.select( + ... dfn.functions.ends_with(dfn.col("a"), dfn.lit("c")).alias("ends_with")) + >>> ends_with_df.collect_column("ends_with")[0].as_py() + True + """ return Expr(f.ends_with(arg.expr, suffix.expr)) @@ -886,6 +960,15 @@ def find_in_set(string: Expr, string_list: Expr) -> Expr: ``string_list`` consisting of N substrings. The string list is a string composed of substrings separated by ``,`` characters. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["b"]}) + >>> result = df.select( + ... dfn.functions.find_in_set(dfn.col("a"), dfn.lit("a,b,c")).alias("pos")) + >>> result.collect_column("pos")[0].as_py() + 2 """ return Expr(f.find_in_set(string.expr, string_list.expr)) @@ -923,6 +1006,14 @@ def initcap(string: Expr) -> Expr: Converts the first letter of each word in ``string`` to uppercase and the remaining characters to lowercase. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["the cat"]}) + >>> cap_df = df.select(dfn.functions.initcap(dfn.col("a")).alias("cap")) + >>> cap_df.collect_column("cap")[0].as_py() + 'The Cat' """ return Expr(f.initcap(string.expr)) @@ -964,12 +1055,31 @@ def lcm(x: Expr, y: Expr) -> Expr: def left(string: Expr, n: Expr) -> Expr: - """Returns the first ``n`` characters in the ``string``.""" + """Returns the first ``n`` characters in the ``string``. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["the cat"]}) + >>> left_df = df.select(dfn.functions.left(dfn.col("a"), dfn.lit(3)).alias("left")) + >>> left_df.collect_column("left")[0].as_py() + 'the' + """ return Expr(f.left(string.expr, n.expr)) def levenshtein(string1: Expr, string2: Expr) -> Expr: - """Returns the Levenshtein distance between the two given strings.""" + """Returns the Levenshtein distance between the two given strings. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["kitten"]}) + >>> result = df.select( + ... dfn.functions.levenshtein(dfn.col("a"), dfn.lit("sitting")).alias("d")) + >>> result.collect_column("d")[0].as_py() + 3 + """ return Expr(f.levenshtein(string1.expr, string2.expr)) @@ -1028,7 +1138,16 @@ def log2(arg: Expr) -> Expr: def lower(arg: Expr) -> Expr: - """Converts a string to lowercase.""" + """Converts a string to lowercase. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["THE CaT"]}) + >>> lower_df = df.select(dfn.functions.lower(dfn.col("a")).alias("lower")) + >>> lower_df.collect_column("lower")[0].as_py() + 'the cat' + """ return Expr(f.lower(arg.expr)) @@ -1038,13 +1157,32 @@ def lpad(string: Expr, count: Expr, characters: Expr | None = None) -> Expr: Extends the string to length length by prepending the characters fill (a space by default). If the string is already longer than length then it is truncated (on the right). + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["the cat", "a hat"]}) + >>> lpad_df = df.select(dfn.functions.lpad(dfn.col("a"), dfn.lit(6)).alias("lpad")) + >>> lpad_df.collect_column("lpad")[0].as_py() + 'the ca' + >>> lpad_df.collect_column("lpad")[1].as_py() + ' a hat' """ characters = characters if characters is not None else Expr.literal(" ") return Expr(f.lpad(string.expr, count.expr, characters.expr)) def ltrim(arg: Expr) -> Expr: - """Removes all characters, spaces by default, from the beginning of a string.""" + """Removes all characters, spaces by default, from the beginning of a string. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [" a "]}) + >>> trim_df = df.select(dfn.functions.ltrim(dfn.col("a")).alias("trimmed")) + >>> trim_df.collect_column("trimmed")[0].as_py() + 'a ' + """ return Expr(f.ltrim(arg.expr)) @@ -1095,7 +1233,16 @@ def nvl(x: Expr, y: Expr) -> Expr: def octet_length(arg: Expr) -> Expr: - """Returns the number of bytes of a string.""" + """Returns the number of bytes of a string. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"]}) + >>> result = df.select(dfn.functions.octet_length(dfn.col("a")).alias("len")) + >>> result.collect_column("len")[0].as_py() + 5 + """ return Expr(f.octet_length(arg.expr)) @@ -1106,6 +1253,16 @@ def overlay( Replace the substring of string that starts at the ``start``'th character and extends for ``length`` characters with new substring. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["abcdef"]}) + >>> result = df.select( + ... dfn.functions.overlay(dfn.col("a"), dfn.lit("XY"), dfn.lit(3), + ... dfn.lit(2)).alias("o")) + >>> result.collect_column("o")[0].as_py() + 'abXYef' """ if length is None: return Expr(f.overlay(string.expr, substring.expr, start.expr)) @@ -1318,22 +1475,60 @@ def regexp_instr( def repeat(string: Expr, n: Expr) -> Expr: - """Repeats the ``string`` to ``n`` times.""" + """Repeats the ``string`` to ``n`` times. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["ha"]}) + >>> result = df.select(dfn.functions.repeat(dfn.col("a"), dfn.lit(3)).alias("r")) + >>> result.collect_column("r")[0].as_py() + 'hahaha' + """ return Expr(f.repeat(string.expr, n.expr)) def replace(string: Expr, from_val: Expr, to_val: Expr) -> Expr: - """Replaces all occurrences of ``from_val`` with ``to_val`` in the ``string``.""" + """Replaces all occurrences of ``from_val`` with ``to_val`` in the ``string``. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello world"]}) + >>> result = df.select( + ... dfn.functions.replace(dfn.col("a"), dfn.lit("world"), + ... dfn.lit("there")).alias("r")) + >>> result.collect_column("r")[0].as_py() + 'hello there' + """ return Expr(f.replace(string.expr, from_val.expr, to_val.expr)) def reverse(arg: Expr) -> Expr: - """Reverse the string argument.""" + """Reverse the string argument. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"]}) + >>> result = df.select(dfn.functions.reverse(dfn.col("a")).alias("r")) + >>> result.collect_column("r")[0].as_py() + 'olleh' + """ return Expr(f.reverse(arg.expr)) def right(string: Expr, n: Expr) -> Expr: - """Returns the last ``n`` characters in the ``string``.""" + """Returns the last ``n`` characters in the ``string``. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"]}) + >>> result = df.select(dfn.functions.right(dfn.col("a"), dfn.lit(3)).alias("r")) + >>> result.collect_column("r")[0].as_py() + 'llo' + """ return Expr(f.right(string.expr, n.expr)) @@ -1361,13 +1556,31 @@ def rpad(string: Expr, count: Expr, characters: Expr | None = None) -> Expr: Extends the string to length length by appending the characters fill (a space by default). If the string is already longer than length then it is truncated. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hi"]}) + >>> result = df.select( + ... dfn.functions.rpad(dfn.col("a"), dfn.lit(5), dfn.lit("!")).alias("r")) + >>> result.collect_column("r")[0].as_py() + 'hi!!!' """ characters = characters if characters is not None else Expr.literal(" ") return Expr(f.rpad(string.expr, count.expr, characters.expr)) def rtrim(arg: Expr) -> Expr: - """Removes all characters, spaces by default, from the end of a string.""" + """Removes all characters, spaces by default, from the end of a string. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [" a "]}) + >>> trim_df = df.select(dfn.functions.rtrim(dfn.col("a")).alias("trimmed")) + >>> trim_df.collect_column("trimmed")[0].as_py() + ' a' + """ return Expr(f.rtrim(arg.expr)) @@ -1475,6 +1688,15 @@ def split_part(string: Expr, delimiter: Expr, index: Expr) -> Expr: Splits a string based on a delimiter and picks out the desired field based on the index. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["a,b,c"]}) + >>> result = df.select( + ... dfn.functions.split_part(dfn.col("a"), dfn.lit(","), dfn.lit(2)).alias("s")) + >>> result.collect_column("s")[0].as_py() + 'b' """ return Expr(f.split_part(string.expr, delimiter.expr, index.expr)) @@ -1493,17 +1715,46 @@ def sqrt(arg: Expr) -> Expr: def starts_with(string: Expr, prefix: Expr) -> Expr: - """Returns true if string starts with prefix.""" + """Returns true if string starts with prefix. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello_from_datafusion"]}) + >>> result = df.select( + ... dfn.functions.starts_with(dfn.col("a"), dfn.lit("hello")).alias("sw")) + >>> result.collect_column("sw")[0].as_py() + True + """ return Expr(f.starts_with(string.expr, prefix.expr)) def strpos(string: Expr, substring: Expr) -> Expr: - """Finds the position from where the ``substring`` matches the ``string``.""" + """Finds the position from where the ``substring`` matches the ``string``. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"]}) + >>> result = df.select( + ... dfn.functions.strpos(dfn.col("a"), dfn.lit("llo")).alias("pos")) + >>> result.collect_column("pos")[0].as_py() + 3 + """ return Expr(f.strpos(string.expr, substring.expr)) def substr(string: Expr, position: Expr) -> Expr: - """Substring from the ``position`` to the end.""" + """Substring from the ``position`` to the end. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"]}) + >>> result = df.select(dfn.functions.substr(dfn.col("a"), dfn.lit(3)).alias("s")) + >>> result.collect_column("s")[0].as_py() + 'llo' + """ return Expr(f.substr(string.expr, position.expr)) @@ -1512,12 +1763,32 @@ def substr_index(string: Expr, delimiter: Expr, count: Expr) -> Expr: The return will be the ``string`` from before ``count`` occurrences of ``delimiter``. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["a.b.c"]}) + >>> result = df.select( + ... dfn.functions.substr_index(dfn.col("a"), dfn.lit("."), + ... dfn.lit(2)).alias("s")) + >>> result.collect_column("s")[0].as_py() + 'a.b' """ return Expr(f.substr_index(string.expr, delimiter.expr, count.expr)) def substring(string: Expr, position: Expr, length: Expr) -> Expr: - """Substring from the ``position`` with ``length`` characters.""" + """Substring from the ``position`` with ``length`` characters. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello world"]}) + >>> result = df.select( + ... dfn.functions.substring(dfn.col("a"), dfn.lit(1), dfn.lit(5)).alias("s")) + >>> result.collect_column("s")[0].as_py() + 'hello' + """ return Expr(f.substring(string.expr, position.expr, length.expr)) @@ -1548,7 +1819,16 @@ def tanh(arg: Expr) -> Expr: def to_hex(arg: Expr) -> Expr: - """Converts an integer to a hexadecimal string.""" + """Converts an integer to a hexadecimal string. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [255]}) + >>> result = df.select(dfn.functions.to_hex(dfn.col("a")).alias("hex")) + >>> result.collect_column("hex")[0].as_py() + 'ff' + """ return Expr(f.to_hex(arg.expr)) @@ -1865,12 +2145,32 @@ def make_date(year: Expr, month: Expr, day: Expr) -> Expr: def translate(string: Expr, from_val: Expr, to_val: Expr) -> Expr: - """Replaces the characters in ``from_val`` with the counterpart in ``to_val``.""" + """Replaces the characters in ``from_val`` with the counterpart in ``to_val``. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"]}) + >>> result = df.select( + ... dfn.functions.translate(dfn.col("a"), dfn.lit("helo"), + ... dfn.lit("HELO")).alias("t")) + >>> result.collect_column("t")[0].as_py() + 'HELLO' + """ return Expr(f.translate(string.expr, from_val.expr, to_val.expr)) def trim(arg: Expr) -> Expr: - """Removes all characters, spaces by default, from both sides of a string.""" + """Removes all characters, spaces by default, from both sides of a string. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [" hello "]}) + >>> result = df.select(dfn.functions.trim(dfn.col("a")).alias("t")) + >>> result.collect_column("t")[0].as_py() + 'hello' + """ return Expr(f.trim(arg.expr)) @@ -1890,7 +2190,16 @@ def trunc(num: Expr, precision: Expr | None = None) -> Expr: def upper(arg: Expr) -> Expr: - """Converts a string to uppercase.""" + """Converts a string to uppercase. + + Examples: + --------- + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"]}) + >>> result = df.select(dfn.functions.upper(dfn.col("a")).alias("u")) + >>> result.collect_column("u")[0].as_py() + 'HELLO' + """ return Expr(f.upper(arg.expr)) From 85a3595444e7946dc4eaa166cb4843bee2bf2f07 Mon Sep 17 00:00:00 2001 From: Nick <24689722+ntjohnson1@users.noreply.github.com> Date: Wed, 18 Mar 2026 22:06:31 -0400 Subject: [PATCH 21/56] Add docstring examples for Aggregate window functions (#1418) * Add docstring examples for Aggregate window functions Add example usage to docstrings for Aggregate window functions to improve documentation. Co-Authored-By: Claude Opus 4.6 * Remove for example for example docstring * Actually remove all for example calls in favor of docstrings * Remove builtins * Make google docstyle * Fix bad merge leading to duplicate xample --------- Co-authored-by: Claude Opus 4.6 --- python/datafusion/functions.py | 127 ++++++++++++++++++++++----------- 1 file changed, 87 insertions(+), 40 deletions(-) diff --git a/python/datafusion/functions.py b/python/datafusion/functions.py index a4933a747..e85d710e7 100644 --- a/python/datafusion/functions.py +++ b/python/datafusion/functions.py @@ -3323,10 +3323,6 @@ def array_agg( filter: If provided, only compute against rows for which the filter is True order_by: Order the resultant array values. Accepts column names or expressions. - For example:: - - df.aggregate([], array_agg(col("a"), order_by="b")) - Examples: --------- >>> ctx = dfn.SessionContext() @@ -4047,9 +4043,14 @@ def first_value( column names or expressions. null_treatment: Assign whether to respect or ignore null values. - For example:: - - df.aggregate([], first_value(col("a"), order_by="ts")) + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [10, 20, 30]}) + >>> result = df.aggregate( + ... [], [dfn.functions.first_value(dfn.col("a")).alias("v")] + ... ) + >>> result.collect_column("v")[0].as_py() + 10 """ order_by_raw = sort_list_to_raw_sort_list(order_by) filter_raw = filter.expr if filter is not None else None @@ -4084,9 +4085,14 @@ def last_value( column names or expressions. null_treatment: Assign whether to respect or ignore null values. - For example:: - - df.aggregate([], last_value(col("a"), order_by="ts")) + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [10, 20, 30]}) + >>> result = df.aggregate( + ... [], [dfn.functions.last_value(dfn.col("a")).alias("v")] + ... ) + >>> result.collect_column("v")[0].as_py() + 30 """ order_by_raw = sort_list_to_raw_sort_list(order_by) filter_raw = filter.expr if filter is not None else None @@ -4123,9 +4129,14 @@ def nth_value( column names or expressions. null_treatment: Assign whether to respect or ignore null values. - For example:: - - df.aggregate([], nth_value(col("a"), 2, order_by="ts")) + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [10, 20, 30]}) + >>> result = df.aggregate( + ... [], [dfn.functions.nth_value(dfn.col("a"), 2).alias("v")] + ... ) + >>> result.collect_column("v")[0].as_py() + 20 """ order_by_raw = sort_list_to_raw_sort_list(order_by) filter_raw = filter.expr if filter is not None else None @@ -4303,9 +4314,14 @@ def lead( order_by: Set ordering within the window frame. Accepts column names or expressions. - For example:: - - lead(col("b"), order_by="ts") + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2, 3]}) + >>> result = df.select( + ... dfn.col("a"), dfn.functions.lead(dfn.col("a"), shift_offset=1, + ... default_value=0, order_by="a").alias("lead")) + >>> result.sort(dfn.col("a")).collect_column("lead").to_pylist() + [2, 3, 0] """ if not isinstance(default_value, pa.Scalar) and default_value is not None: default_value = pa.scalar(default_value) @@ -4358,9 +4374,14 @@ def lag( order_by: Set ordering within the window frame. Accepts column names or expressions. - For example:: - - lag(col("b"), order_by="ts") + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2, 3]}) + >>> result = df.select( + ... dfn.col("a"), dfn.functions.lag(dfn.col("a"), shift_offset=1, + ... default_value=0, order_by="a").alias("lag")) + >>> result.sort(dfn.col("a")).collect_column("lag").to_pylist() + [0, 1, 2] """ if not isinstance(default_value, pa.Scalar): default_value = pa.scalar(default_value) @@ -4403,9 +4424,13 @@ def row_number( order_by: Set ordering within the window frame. Accepts column names or expressions. - For example:: - - row_number(order_by="points") + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [10, 20, 30]}) + >>> result = df.select( + ... dfn.col("a"), dfn.functions.row_number(order_by="a").alias("rn")) + >>> result.sort(dfn.col("a")).collect_column("rn").to_pylist() + [1, 2, 3] """ partition_by_raw = expr_list_to_raw_expr_list(partition_by) order_by_raw = sort_list_to_raw_sort_list(order_by) @@ -4447,9 +4472,14 @@ def rank( order_by: Set ordering within the window frame. Accepts column names or expressions. - For example:: - - rank(order_by="points") + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [10, 10, 20]}) + >>> result = df.select( + ... dfn.col("a"), dfn.functions.rank(order_by="a").alias("rnk") + ... ) + >>> result.sort(dfn.col("a")).collect_column("rnk").to_pylist() + [1, 1, 3] """ partition_by_raw = expr_list_to_raw_expr_list(partition_by) order_by_raw = sort_list_to_raw_sort_list(order_by) @@ -4486,9 +4516,13 @@ def dense_rank( order_by: Set ordering within the window frame. Accepts column names or expressions. - For example:: - - dense_rank(order_by="points") + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [10, 10, 20]}) + >>> result = df.select( + ... dfn.col("a"), dfn.functions.dense_rank(order_by="a").alias("dr")) + >>> result.sort(dfn.col("a")).collect_column("dr").to_pylist() + [1, 1, 2] """ partition_by_raw = expr_list_to_raw_expr_list(partition_by) order_by_raw = sort_list_to_raw_sort_list(order_by) @@ -4526,9 +4560,14 @@ def percent_rank( order_by: Set ordering within the window frame. Accepts column names or expressions. - For example:: - percent_rank(order_by="points") + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [10, 20, 30]}) + >>> result = df.select( + ... dfn.col("a"), dfn.functions.percent_rank(order_by="a").alias("pr")) + >>> result.sort(dfn.col("a")).collect_column("pr").to_pylist() + [0.0, 0.5, 1.0] """ partition_by_raw = expr_list_to_raw_expr_list(partition_by) order_by_raw = sort_list_to_raw_sort_list(order_by) @@ -4566,9 +4605,17 @@ def cume_dist( order_by: Set ordering within the window frame. Accepts column names or expressions. - For example:: - - cume_dist(order_by="points") + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1., 2., 2., 3.]}) + >>> result = df.select( + ... dfn.col("a"), + ... dfn.functions.cume_dist( + ... order_by="a" + ... ).alias("cd") + ... ) + >>> result.collect_column("cd").to_pylist() + [0.25..., 0.75..., 0.75..., 1.0...] """ partition_by_raw = expr_list_to_raw_expr_list(partition_by) order_by_raw = sort_list_to_raw_sort_list(order_by) @@ -4610,9 +4657,13 @@ def ntile( order_by: Set ordering within the window frame. Accepts column names or expressions. - For example:: - - ntile(3, order_by="points") + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [10, 20, 30, 40]}) + >>> result = df.select( + ... dfn.col("a"), dfn.functions.ntile(2, order_by="a").alias("nt")) + >>> result.sort(dfn.col("a")).collect_column("nt").to_pylist() + [1, 1, 2, 2] """ partition_by_raw = expr_list_to_raw_expr_list(partition_by) order_by_raw = sort_list_to_raw_sort_list(order_by) @@ -4648,10 +4699,6 @@ def string_agg( order_by: Set the ordering of the expression to evaluate. Accepts column names or expressions. - For example:: - - df.aggregate([], string_agg(col("a"), ",", order_by="b")) - Examples: --------- >>> ctx = dfn.SessionContext() From 0c33524dc05091cf0bd5b510417e5b3e2ee48922 Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Tue, 24 Mar 2026 05:04:39 -0700 Subject: [PATCH 22/56] pin setup-uv (#1438) --- .github/workflows/build.yml | 14 +++++++------- .github/workflows/test.yml | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4c07b08bb..5a921a427 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,7 +68,7 @@ jobs: with: python-version: "3.12" - - uses: astral-sh/setup-uv@v6 + - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 with: enable-cache: true @@ -112,7 +112,7 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: astral-sh/setup-uv@v6 + - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 with: enable-cache: true @@ -154,7 +154,7 @@ jobs: with: key: ${{ inputs.build_mode }} - - uses: astral-sh/setup-uv@v6 + - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 with: enable-cache: true @@ -222,7 +222,7 @@ jobs: with: key: ${{ inputs.build_mode }} - - uses: astral-sh/setup-uv@v6 + - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 with: enable-cache: true @@ -280,7 +280,7 @@ jobs: with: key: ${{ inputs.build_mode }} - - uses: astral-sh/setup-uv@v7 + - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 with: enable-cache: true @@ -354,7 +354,7 @@ jobs: with: key: ${{ inputs.build_mode }} - - uses: astral-sh/setup-uv@v7 + - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 with: enable-cache: true @@ -478,7 +478,7 @@ jobs: python-version: "3.10" - name: Install dependencies - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 with: enable-cache: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a2f304aa5..02274a363 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,7 +53,7 @@ jobs: key: cargo-cache-${{ matrix.toolchain }}-${{ hashFiles('Cargo.lock') }} - name: Install dependencies - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 with: enable-cache: true From 1397c5d6444e370a0feee69231fb8bc92c778d5f Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Tue, 24 Mar 2026 08:05:42 -0400 Subject: [PATCH 23/56] bump datafusion to release version (#1441) --- Cargo.lock | 108 +++++++++++++++++++++++++++++++++++------------------ Cargo.toml | 9 ----- 2 files changed, 72 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e44c84b97..eb4b76d3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -901,7 +901,8 @@ dependencies = [ [[package]] name = "datafusion" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de9f8117889ba9503440f1dd79ebab32ba52ccf1720bb83cd718a29d4edc0d16" dependencies = [ "arrow", "arrow-schema", @@ -956,7 +957,8 @@ dependencies = [ [[package]] name = "datafusion-catalog" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be893b73a13671f310ffcc8da2c546b81efcc54c22e0382c0a28aa3537017137" dependencies = [ "arrow", "async-trait", @@ -980,7 +982,8 @@ dependencies = [ [[package]] name = "datafusion-catalog-listing" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830487b51ed83807d6b32d6325f349c3144ae0c9bf772cf2a712db180c31d5e6" dependencies = [ "arrow", "async-trait", @@ -1002,7 +1005,8 @@ dependencies = [ [[package]] name = "datafusion-common" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d7663f3af955292f8004e74bcaf8f7ea3d66cc38438749615bb84815b61a293" dependencies = [ "ahash", "apache-avro", @@ -1027,7 +1031,8 @@ dependencies = [ [[package]] name = "datafusion-common-runtime" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f590205c7e32fe1fea48dd53ffb406e56ae0e7a062213a3ac848db8771641bd" dependencies = [ "futures", "log", @@ -1037,7 +1042,8 @@ dependencies = [ [[package]] name = "datafusion-datasource" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde1e030a9dc87b743c806fbd631f5ecfa2ccaa4ffb61fa19144a07fea406b79" dependencies = [ "arrow", "async-compression", @@ -1071,7 +1077,8 @@ dependencies = [ [[package]] name = "datafusion-datasource-arrow" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331ebae7055dc108f9b54994b93dff91f3a17445539efe5b74e89264f7b36e15" dependencies = [ "arrow", "arrow-ipc", @@ -1094,7 +1101,8 @@ dependencies = [ [[package]] name = "datafusion-datasource-avro" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49dda81c79b6ba57b1853a9158abc66eb85a3aa1cede0c517dabec6d8a4ed3aa" dependencies = [ "apache-avro", "arrow", @@ -1113,7 +1121,8 @@ dependencies = [ [[package]] name = "datafusion-datasource-csv" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e0d475088325e2986876aa27bb30d0574f72a22955a527d202f454681d55c5c" dependencies = [ "arrow", "async-trait", @@ -1135,7 +1144,8 @@ dependencies = [ [[package]] name = "datafusion-datasource-json" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea1520d81f31770f3ad6ee98b391e75e87a68a5bb90de70064ace5e0a7182fe8" dependencies = [ "arrow", "async-trait", @@ -1158,7 +1168,8 @@ dependencies = [ [[package]] name = "datafusion-datasource-parquet" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95be805d0742ab129720f4c51ad9242cd872599cdb076098b03f061fcdc7f946" dependencies = [ "arrow", "async-trait", @@ -1187,12 +1198,14 @@ dependencies = [ [[package]] name = "datafusion-doc" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c93ad9e37730d2c7196e68616f3f2dd3b04c892e03acd3a8eeca6e177f3c06a" [[package]] name = "datafusion-execution" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9437d3cd5d363f9319f8122182d4d233427de79c7eb748f23054c9aaa0fdd8df" dependencies = [ "arrow", "arrow-buffer", @@ -1214,7 +1227,8 @@ dependencies = [ [[package]] name = "datafusion-expr" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67164333342b86521d6d93fa54081ee39839894fb10f7a700c099af96d7552cf" dependencies = [ "arrow", "async-trait", @@ -1236,7 +1250,8 @@ dependencies = [ [[package]] name = "datafusion-expr-common" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab05fdd00e05d5a6ee362882546d29d6d3df43a6c55355164a7fbee12d163bc9" dependencies = [ "arrow", "datafusion-common", @@ -1248,7 +1263,8 @@ dependencies = [ [[package]] name = "datafusion-ffi" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8250f7cdf463a0ad145f41d7508bcfa54c9b9f027317e599f0331097e3cc38" dependencies = [ "abi_stable", "arrow", @@ -1297,7 +1313,8 @@ dependencies = [ [[package]] name = "datafusion-functions" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04fb863482d987cf938db2079e07ab0d3bb64595f28907a6c2f8671ad71cca7e" dependencies = [ "arrow", "arrow-buffer", @@ -1328,7 +1345,8 @@ dependencies = [ [[package]] name = "datafusion-functions-aggregate" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829856f4e14275fb376c104f27cbf3c3b57a9cfe24885d98677525f5e43ce8d6" dependencies = [ "ahash", "arrow", @@ -1349,7 +1367,8 @@ dependencies = [ [[package]] name = "datafusion-functions-aggregate-common" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08af79cc3d2aa874a362fb97decfcbd73d687190cb096f16a6c85a7780cce311" dependencies = [ "ahash", "arrow", @@ -1361,7 +1380,8 @@ dependencies = [ [[package]] name = "datafusion-functions-nested" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465ae3368146d49c2eda3e2c0ef114424c87e8a6b509ab34c1026ace6497e790" dependencies = [ "arrow", "arrow-ord", @@ -1385,7 +1405,8 @@ dependencies = [ [[package]] name = "datafusion-functions-table" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6156e6b22fcf1784112fc0173f3ae6e78c8fdb4d3ed0eace9543873b437e2af6" dependencies = [ "arrow", "async-trait", @@ -1400,7 +1421,8 @@ dependencies = [ [[package]] name = "datafusion-functions-window" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca7baec14f866729012efb89011a6973f3a346dc8090c567bfcd328deff551c1" dependencies = [ "arrow", "datafusion-common", @@ -1417,7 +1439,8 @@ dependencies = [ [[package]] name = "datafusion-functions-window-common" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "159228c3280d342658466bb556dc24de30047fe1d7e559dc5d16ccc5324166f9" dependencies = [ "datafusion-common", "datafusion-physical-expr-common", @@ -1426,7 +1449,8 @@ dependencies = [ [[package]] name = "datafusion-macros" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5427e5da5edca4d21ea1c7f50e1c9421775fe33d7d5726e5641a833566e7578" dependencies = [ "datafusion-doc", "quote", @@ -1436,7 +1460,8 @@ dependencies = [ [[package]] name = "datafusion-optimizer" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89099eefcd5b223ec685c36a41d35c69239236310d71d339f2af0fa4383f3f46" dependencies = [ "arrow", "chrono", @@ -1455,7 +1480,8 @@ dependencies = [ [[package]] name = "datafusion-physical-expr" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f222df5195d605d79098ef37bdd5323bff0131c9d877a24da6ec98dfca9fe36" dependencies = [ "ahash", "arrow", @@ -1478,7 +1504,8 @@ dependencies = [ [[package]] name = "datafusion-physical-expr-adapter" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40838625d63d9c12549d81979db3dd675d159055eb9135009ba272ab0e8d0f64" dependencies = [ "arrow", "datafusion-common", @@ -1492,7 +1519,8 @@ dependencies = [ [[package]] name = "datafusion-physical-expr-common" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eacbcc4cfd502558184ed58fa3c72e775ec65bf077eef5fd2b3453db676f893c" dependencies = [ "ahash", "arrow", @@ -1508,7 +1536,8 @@ dependencies = [ [[package]] name = "datafusion-physical-optimizer" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d501d0e1d0910f015677121601ac177ec59272ef5c9324d1147b394988f40941" dependencies = [ "arrow", "datafusion-common", @@ -1526,7 +1555,8 @@ dependencies = [ [[package]] name = "datafusion-physical-plan" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "463c88ad6f1ecab1810f4c9f046898bee035b370137eb79b2b2db925e270631d" dependencies = [ "ahash", "arrow", @@ -1557,7 +1587,8 @@ dependencies = [ [[package]] name = "datafusion-proto" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677ee4448a010ed5faeff8d73ff78972c2ace59eff3cd7bd15833a1dafa00492" dependencies = [ "arrow", "chrono", @@ -1584,7 +1615,8 @@ dependencies = [ [[package]] name = "datafusion-proto-common" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965eca01edc8259edbbd95883a00b6d81e329fd44a019cfac3a03b026a83eade" dependencies = [ "arrow", "datafusion-common", @@ -1594,7 +1626,8 @@ dependencies = [ [[package]] name = "datafusion-pruning" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857618a0ecbd8cd0cf29826889edd3a25774ec26b2995fc3862095c95d88fc6" dependencies = [ "arrow", "datafusion-common", @@ -1652,7 +1685,8 @@ dependencies = [ [[package]] name = "datafusion-session" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8637e35022c5c775003b3ab1debc6b4a8f0eb41b069bdd5475dd3aa93f6eba" dependencies = [ "async-trait", "datafusion-common", @@ -1665,7 +1699,8 @@ dependencies = [ [[package]] name = "datafusion-sql" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12d9e9f16a1692a11c94bcc418191fa15fd2b4d72a0c1a0c607db93c0b84dd81" dependencies = [ "arrow", "bigdecimal", @@ -1683,7 +1718,8 @@ dependencies = [ [[package]] name = "datafusion-substrait" version = "53.0.0" -source = "git+https://github.com/apache/datafusion.git?rev=35749607f585b3bf25b66b7d2289c56c18d03e4f#35749607f585b3bf25b66b7d2289c56c18d03e4f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5e5656a7e63d51dd3e5af3dbd347ea83bbe993a77c66b854b74961570d16490" dependencies = [ "async-recursion", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 19b79daf8..fd8b7c7c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,12 +70,3 @@ codegen-units = 1 # We cannot publish to crates.io with any patches in the below section. Developers # must remove any entries in this section before creating a release candidate. [patch.crates-io] -datafusion = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f", submodules = false } -datafusion-substrait = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f", submodules = false } -datafusion-proto = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f", submodules = false } -datafusion-ffi = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f", submodules = false } -datafusion-catalog = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f", submodules = false } -datafusion-common = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f", submodules = false } -datafusion-functions-aggregate = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f", submodules = false } -datafusion-functions-window = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f", submodules = false } -datafusion-expr = { git = "https://github.com/apache/datafusion.git", rev = "35749607f585b3bf25b66b7d2289c56c18d03e4f", submodules = false } From e09c93bbe5c7d78c3752adc9158f3ff012d0c4cd Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Thu, 26 Mar 2026 15:45:05 -0400 Subject: [PATCH 24/56] ci: add swap during build, use tpchgen-cli (#1443) * restrict number of rustc jobs during build stage * temporarily run release build on PR * Change optimization setting for substrait * Add swap during release build * Remove temporary checks to build in PR * Try using tpchgen-cli for test files. commit answers * taplo fmt * do not run rat on data files * ci needs ./ in path * add no-project to uv run * Temporary debug lines to figure out what is happening in CI * filter null value during aggregation instead now that https://github.com/apache/datafusion/issues/21011 is closed --- .github/workflows/build.yml | 13 + .github/workflows/test.yml | 17 +- dev/release/rat_exclude_files.txt | 3 +- examples/tpch/_tests.py | 2 +- examples/tpch/answers_sf1/q1.tbl | 5 + examples/tpch/answers_sf1/q10.tbl | 21 + examples/tpch/answers_sf1/q11.tbl | 1049 + examples/tpch/answers_sf1/q12.tbl | 3 + examples/tpch/answers_sf1/q13.tbl | 43 + examples/tpch/answers_sf1/q14.tbl | 2 + examples/tpch/answers_sf1/q15.tbl | 2 + examples/tpch/answers_sf1/q16.tbl | 18315 ++++++++++++++++ examples/tpch/answers_sf1/q17.tbl | 2 + examples/tpch/answers_sf1/q18.tbl | 58 + examples/tpch/answers_sf1/q19.tbl | 2 + examples/tpch/answers_sf1/q2.tbl | 101 + examples/tpch/answers_sf1/q20.tbl | 187 + examples/tpch/answers_sf1/q21.tbl | 101 + examples/tpch/answers_sf1/q22.tbl | 8 + examples/tpch/answers_sf1/q3.tbl | 11 + examples/tpch/answers_sf1/q4.tbl | 6 + examples/tpch/answers_sf1/q5.tbl | 6 + examples/tpch/answers_sf1/q6.tbl | 2 + examples/tpch/answers_sf1/q7.tbl | 5 + examples/tpch/answers_sf1/q8.tbl | 3 + examples/tpch/answers_sf1/q9.tbl | 176 + .../tpch/q21_suppliers_kept_orders_waiting.py | 10 +- examples/tpch/util.py | 2 +- pyproject.toml | 7 +- 29 files changed, 20139 insertions(+), 23 deletions(-) create mode 100644 examples/tpch/answers_sf1/q1.tbl create mode 100644 examples/tpch/answers_sf1/q10.tbl create mode 100644 examples/tpch/answers_sf1/q11.tbl create mode 100644 examples/tpch/answers_sf1/q12.tbl create mode 100644 examples/tpch/answers_sf1/q13.tbl create mode 100644 examples/tpch/answers_sf1/q14.tbl create mode 100644 examples/tpch/answers_sf1/q15.tbl create mode 100644 examples/tpch/answers_sf1/q16.tbl create mode 100644 examples/tpch/answers_sf1/q17.tbl create mode 100644 examples/tpch/answers_sf1/q18.tbl create mode 100644 examples/tpch/answers_sf1/q19.tbl create mode 100644 examples/tpch/answers_sf1/q2.tbl create mode 100644 examples/tpch/answers_sf1/q20.tbl create mode 100644 examples/tpch/answers_sf1/q21.tbl create mode 100644 examples/tpch/answers_sf1/q22.tbl create mode 100644 examples/tpch/answers_sf1/q3.tbl create mode 100644 examples/tpch/answers_sf1/q4.tbl create mode 100644 examples/tpch/answers_sf1/q5.tbl create mode 100644 examples/tpch/answers_sf1/q6.tbl create mode 100644 examples/tpch/answers_sf1/q7.tbl create mode 100644 examples/tpch/answers_sf1/q8.tbl create mode 100644 examples/tpch/answers_sf1/q9.tbl diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5a921a427..97ab2b2f0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -226,6 +226,19 @@ jobs: with: enable-cache: true + - name: Add extra swap for release build + if: inputs.build_mode == 'release' + run: | + set -euxo pipefail + sudo swapoff -a || true + sudo rm -f /swapfile + sudo fallocate -l 16G /swapfile || sudo dd if=/dev/zero of=/swapfile bs=1M count=16384 + sudo chmod 600 /swapfile + sudo mkswap /swapfile + sudo swapon /swapfile + free -h + swapon --show + - name: Build (release mode) uses: PyO3/maturin-action@v1 if: inputs.build_mode == 'release' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 02274a363..881a1aca2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -101,21 +101,14 @@ jobs: cd examples/datafusion-ffi-example uv run --no-project pytest python/tests/_test*.py - - name: Cache the generated dataset - id: cache-tpch-dataset - uses: actions/cache@v5 - with: - path: benchmarks/tpch/data - key: tpch-data-2.18.0 - - - name: Run dbgen to create 1 Gb dataset - if: ${{ steps.cache-tpch-dataset.outputs.cache-hit != 'true' }} + - name: Run tpchgen-cli to create 1 Gb dataset run: | - cd benchmarks/tpch - RUN_IN_CI=TRUE ./tpch-gen.sh 1 + mkdir examples/tpch/data + cd examples/tpch/data + uv pip install tpchgen-cli + uv run --no-project tpchgen-cli -s 1 --format=parquet - name: Run TPC-H examples run: | cd examples/tpch - uv run --no-project python convert_data_to_parquet.py uv run --no-project pytest _tests.py diff --git a/dev/release/rat_exclude_files.txt b/dev/release/rat_exclude_files.txt index dcd5d9aac..b2db144e8 100644 --- a/dev/release/rat_exclude_files.txt +++ b/dev/release/rat_exclude_files.txt @@ -47,4 +47,5 @@ benchmarks/tpch/queries/q*.sql benchmarks/tpch/create_tables.sql .cargo/config.toml **/.cargo/config.toml -uv.lock \ No newline at end of file +uv.lock +examples/tpch/answers_sf1/*.tbl \ No newline at end of file diff --git a/examples/tpch/_tests.py b/examples/tpch/_tests.py index 780fcf5e5..82dab2974 100644 --- a/examples/tpch/_tests.py +++ b/examples/tpch/_tests.py @@ -115,7 +115,7 @@ def test_tpch_query_vs_answer_file(query_code: str, answer_file: str) -> None: get_answer_file(answer_file), schema=read_schema, delimiter="|", - file_extension=".out", + file_extension=".tbl", ) df_expected = df_expected.select(*expected_selections) diff --git a/examples/tpch/answers_sf1/q1.tbl b/examples/tpch/answers_sf1/q1.tbl new file mode 100644 index 000000000..03ca993e7 --- /dev/null +++ b/examples/tpch/answers_sf1/q1.tbl @@ -0,0 +1,5 @@ +l|l|sum_qty |sum_base_price |sum_disc_price |sum_charge |avg_qty |avg_price |avg_disc |count_order +A|F|37734107.00|56586554400.73|53758257134.87|55909065222.83|25.52|38273.13|0.05| 1478493 +N|F|991417.00|1487504710.38|1413082168.05|1469649223.19|25.52|38284.47|0.05| 38854 +N|O|74476040.00|111701729697.74|106118230307.61|110367043872.50|25.50|38249.12|0.05| 2920374 +R|F|37719753.00|56568041380.90|53741292684.60|55889619119.83|25.51|38250.85|0.05| 1478870 diff --git a/examples/tpch/answers_sf1/q10.tbl b/examples/tpch/answers_sf1/q10.tbl new file mode 100644 index 000000000..95f303907 --- /dev/null +++ b/examples/tpch/answers_sf1/q10.tbl @@ -0,0 +1,21 @@ +c_custkey |c_name |revenue |c_acctbal |n_name |c_address |c_phone |c_comment + 57040|Customer#000057040 |734235.25|632.87|JAPAN |Eioyzjf4pp |22-895-641-3466|sits. slyly regular requests sleep alongside of the regular inst + 143347|Customer#000143347 |721002.69|2557.47|EGYPT |1aReFYv,Kw4 |14-742-935-3718|ggle carefully enticing requests. final deposits use bold, bold pinto beans. ironic, idle re + 60838|Customer#000060838 |679127.31|2454.77|BRAZIL |64EaJ5vMAHWJlBOxJklpNc2RJiWE |12-913-494-9813| need to boost against the slyly regular account + 101998|Customer#000101998 |637029.57|3790.89|UNITED KINGDOM |01c9CILnNtfOQYmZj |33-593-865-6378|ress foxes wake slyly after the bold excuses. ironic platelets are furiously carefully bold theodolites + 125341|Customer#000125341 |633508.09|4983.51|GERMANY |S29ODD6bceU8QSuuEJznkNaK |17-582-695-5962|arefully even depths. blithely even excuses sleep furiously. foxes use except the dependencies. ca + 25501|Customer#000025501 |620269.78|7725.04|ETHIOPIA | W556MXuoiaYCCZamJI,Rn0B4ACUGdkQ8DZ |15-874-808-6793|he pending instructions wake carefully at the pinto beans. regular, final instructions along the slyly fina + 115831|Customer#000115831 |596423.87|5098.10|FRANCE |rFeBbEEyk dl ne7zV5fDrmiq1oK09wV7pxqCgIc|16-715-386-3788|l somas sleep. furiously final deposits wake blithely regular pinto b + 84223|Customer#000084223 |594998.02|528.65|UNITED KINGDOM |nAVZCs6BaWap rrM27N 2qBnzc5WBauxbA |33-442-824-8191| slyly final deposits haggle regular, pending dependencies. pending escapades wake + 54289|Customer#000054289 |585603.39|5583.02|IRAN |vXCxoCsU0Bad5JQI ,oobkZ |20-834-292-4707|ely special foxes are quickly finally ironic p + 39922|Customer#000039922 |584878.11|7321.11|GERMANY |Zgy4s50l2GKN4pLDPBU8m342gIw6R |17-147-757-8036|y final requests. furiously final foxes cajole blithely special platelets. f + 6226|Customer#000006226 |576783.76|2230.09|UNITED KINGDOM |8gPu8,NPGkfyQQ0hcIYUGPIBWc,ybP5g, |33-657-701-3391|ending platelets along the express deposits cajole carefully final + 922|Customer#000000922 |576767.53|3869.25|GERMANY |Az9RFaut7NkPnc5zSD2PwHgVwr4jRzq |17-945-916-9648|luffily fluffy deposits. packages c + 147946|Customer#000147946 |576455.13|2030.13|ALGERIA |iANyZHjqhyy7Ajah0pTrYyhJ |10-886-956-3143|ithely ironic deposits haggle blithely ironic requests. quickly regu + 115640|Customer#000115640 |569341.19|6436.10|ARGENTINA |Vtgfia9qI 7EpHgecU1X |11-411-543-4901|ost slyly along the patterns; pinto be + 73606|Customer#000073606 |568656.86|1785.67|JAPAN |xuR0Tro5yChDfOCrjkd2ol |22-437-653-6966|he furiously regular ideas. slowly + 110246|Customer#000110246 |566842.98|7763.35|VIETNAM |7KzflgX MDOq7sOkI |31-943-426-9837|egular deposits serve blithely above the fl + 142549|Customer#000142549 |563537.24|5085.99|INDONESIA |ChqEoK43OysjdHbtKCp6dKqjNyvvi9 |19-955-562-2398|sleep pending courts. ironic deposits against the carefully unusual platelets cajole carefully express accounts. + 146149|Customer#000146149 |557254.99|1791.55|ROMANIA |s87fvzFQpU |29-744-164-6487| of the slyly silent accounts. quickly final accounts across the + 52528|Customer#000052528 |556397.35|551.79|ARGENTINA |NFztyTOR10UOJ |11-208-192-3205| deposits hinder. blithely pending asymptotes breach slyly regular re + 23431|Customer#000023431 |554269.54|3381.86|ROMANIA |HgiV0phqhaIa9aydNoIlb |29-915-458-2654|nusual, even instructions: furiously stealthy n diff --git a/examples/tpch/answers_sf1/q11.tbl b/examples/tpch/answers_sf1/q11.tbl new file mode 100644 index 000000000..f48a8989d --- /dev/null +++ b/examples/tpch/answers_sf1/q11.tbl @@ -0,0 +1,1049 @@ +ps_partkey |value + 129760|17538456.86 + 166726|16503353.92 + 191287|16474801.97 + 161758|16101755.54 + 34452|15983844.72 + 139035|15907078.34 + 9403|15451755.62 + 154358|15212937.88 + 38823|15064802.86 + 85606|15053957.15 + 33354|14408297.40 + 154747|14407580.68 + 82865|14235489.78 + 76094|14094247.04 + 222|13937777.74 + 121271|13908336.00 + 55221|13716120.47 + 22819|13666434.28 + 76281|13646853.68 + 85298|13581154.93 + 85158|13554904.00 + 139684|13535538.72 + 31034|13498025.25 + 87305|13482847.04 + 10181|13445148.75 + 62323|13411824.30 + 26489|13377256.38 + 96493|13339057.83 + 56548|13329014.97 + 55576|13306843.35 + 159751|13306614.48 + 92406|13287414.50 + 182636|13223726.74 + 199969|13135288.21 + 62865|13001926.94 + 7284|12945298.19 + 197867|12944510.52 + 11562|12931575.51 + 75165|12916918.12 + 97175|12911283.50 + 140840|12896562.23 + 65241|12890600.46 + 166120|12876927.22 + 9035|12863828.70 + 144616|12853549.30 + 176723|12832309.74 + 170884|12792136.58 + 29790|12723300.33 + 95213|12555483.73 + 183873|12550533.05 + 171235|12476538.30 + 21533|12437821.32 + 17290|12432159.50 + 156397|12260623.50 + 122611|12222812.98 + 139155|12220319.25 + 146316|12215800.61 + 171381|12199734.52 + 198633|12078226.95 + 167417|12046637.62 + 59512|12043468.76 + 31688|12034893.64 + 159586|12001505.84 + 8993|11963814.30 + 120302|11857707.55 + 43536|11779340.52 + 9552|11776909.16 + 86223|11772205.08 + 53776|11758669.65 + 131285|11616953.74 + 91628|11611114.83 + 169644|11567959.72 + 182299|11567462.05 + 33107|11453818.76 + 104184|11436657.44 + 67027|11419127.14 + 176869|11371451.71 + 30885|11369674.79 + 54420|11345076.88 + 72240|11313951.05 + 178708|11294635.17 + 81298|11273686.13 + 158324|11243442.72 + 117095|11242535.24 + 176793|11237733.38 + 86091|11177793.79 + 116033|11145434.36 + 129058|11119112.20 + 193714|11104706.39 + 117195|11077217.96 + 49851|11043701.78 + 19791|11030662.62 + 75800|11012401.62 + 161562|10996371.69 + 10119|10980015.75 + 39185|10970042.56 + 47223|10950022.13 + 175594|10942923.05 + 111295|10893675.61 + 155446|10852764.57 + 156391|10839810.38 + 40884|10837234.19 + 141288|10837130.21 + 152388|10830977.82 + 33449|10830858.72 + 149035|10826130.02 + 162620|10814275.68 + 118324|10791788.10 + 38932|10777541.75 + 121294|10764225.22 + 48721|10762582.49 + 63342|10740132.60 + 5614|10724668.80 + 62266|10711143.10 + 100202|10696675.55 + 197741|10688560.72 + 169178|10648522.80 + 5271|10639392.65 + 34499|10584177.10 + 71108|10569117.56 + 137132|10539880.47 + 78451|10524873.24 + 150827|10503810.48 + 107237|10488030.84 + 101727|10473558.10 + 58708|10466280.44 + 89768|10465477.22 + 146493|10444291.58 + 55424|10444006.48 + 16560|10425574.74 + 133114|10415097.90 + 195810|10413625.20 + 76673|10391977.18 + 97305|10390890.57 + 134210|10387210.02 + 188536|10386529.92 + 122255|10335760.32 + 2682|10312966.10 + 43814|10303086.61 + 34767|10290405.18 + 165584|10273705.89 + 2231|10270415.55 + 111259|10263256.56 + 195578|10239795.82 + 21093|10217531.30 + 29856|10216932.54 + 133686|10213345.76 + 87745|10185509.40 + 135153|10179379.70 + 11773|10167410.84 + 76316|10165151.70 + 123076|10161225.78 + 91894|10130462.19 + 39741|10128387.52 + 111753|10119780.98 + 142729|10104748.89 + 116775|10097750.42 + 102589|10034784.36 + 186268|10012181.57 + 44545|10000286.48 + 23307|9966577.50 + 124281|9930018.90 + 69604|9925730.64 + 21971|9908982.03 + 58148|9895894.40 + 16532|9886529.90 + 159180|9883744.43 + 74733|9877582.88 + 35173|9858275.92 + 7116|9856881.02 + 124620|9838589.14 + 122108|9829949.35 + 67200|9828690.69 + 164775|9821424.44 + 9039|9816447.72 + 14912|9803102.20 + 190906|9791315.70 + 130398|9781674.27 + 119310|9776927.21 + 10132|9770930.78 + 107211|9757586.25 + 113958|9757065.50 + 37009|9748362.69 + 66746|9743528.76 + 134486|9731922.00 + 15945|9731096.45 + 55307|9717745.80 + 56362|9714922.83 + 57726|9711792.10 + 57256|9708621.00 + 112292|9701653.08 + 87514|9699492.53 + 174206|9680562.02 + 72865|9679043.34 + 114357|9671017.44 + 112807|9665019.21 + 115203|9661018.73 + 177454|9658906.35 + 161275|9634313.71 + 61893|9617095.44 + 122219|9604888.20 + 183427|9601362.58 + 59158|9599705.96 + 61931|9584918.98 + 5532|9579964.14 + 20158|9576714.38 + 167199|9557413.08 + 38869|9550279.53 + 86949|9541943.70 + 198544|9538613.92 + 193762|9538238.94 + 108807|9536247.16 + 168324|9535647.99 + 115588|9532195.04 + 141372|9529702.14 + 175120|9526068.66 + 163851|9522808.83 + 160954|9520359.45 + 117757|9517882.80 + 52594|9508325.76 + 60960|9498843.06 + 70272|9495775.62 + 44050|9495515.36 + 152213|9494756.96 + 121203|9492601.30 + 70114|9491012.30 + 167588|9484741.11 + 136455|9476241.78 + 4357|9464355.64 + 6786|9463632.57 + 61345|9455336.70 + 160826|9446754.84 + 71275|9440138.40 + 77746|9439118.35 + 91289|9437472.00 + 56723|9435102.16 + 86647|9434604.18 + 131234|9432120.00 + 198129|9427651.36 + 165530|9426193.68 + 69233|9425053.92 + 6243|9423304.66 + 90110|9420422.70 + 191980|9419368.36 + 38461|9419316.07 + 167873|9419024.49 + 159373|9416950.15 + 128707|9413428.50 + 45267|9410863.78 + 48460|9409793.93 + 197672|9406887.68 + 60884|9403442.40 + 15209|9403245.31 + 138049|9401262.10 + 199286|9391770.70 + 19629|9391236.40 + 134019|9390615.15 + 169475|9387639.58 + 165918|9379510.44 + 135602|9374251.54 + 162323|9367566.51 + 96277|9360850.68 + 98336|9359671.29 + 119781|9356395.73 + 34440|9355365.00 + 57362|9355180.10 + 167236|9352973.84 + 38463|9347530.94 + 86749|9346826.44 + 170007|9345699.90 + 193087|9343744.00 + 150383|9332576.75 + 60932|9329582.02 + 128420|9328206.35 + 162145|9327722.88 + 55686|9320304.40 + 163080|9304916.96 + 160583|9303515.92 + 118153|9298606.56 + 152634|9282184.57 + 84731|9276586.92 + 119989|9273814.20 + 114584|9269698.65 + 131817|9268570.08 + 29068|9256583.88 + 44116|9255922.00 + 115818|9253311.91 + 103388|9239218.08 + 186118|9236209.12 + 155809|9235410.84 + 147003|9234847.99 + 27769|9232511.64 + 112779|9231927.36 + 124851|9228982.68 + 158488|9227216.40 + 83328|9224792.20 + 136797|9222927.09 + 141730|9216370.68 + 87304|9215695.50 + 156004|9215557.90 + 140740|9215329.20 + 100648|9212185.08 + 174774|9211718.00 + 37644|9211578.60 + 48807|9209496.24 + 95940|9207948.40 + 141586|9206699.22 + 147248|9205654.95 + 61372|9205228.76 + 52970|9204415.95 + 26430|9203710.51 + 28504|9201669.20 + 25810|9198878.50 + 125329|9198688.50 + 167867|9194022.72 + 134767|9191444.72 + 127745|9191271.56 + 69208|9187110.00 + 155222|9186469.16 + 196916|9182995.82 + 195590|9176353.12 + 169155|9175176.09 + 81558|9171946.50 + 185136|9171293.04 + 114790|9168509.10 + 194142|9165836.61 + 167639|9161165.00 + 11241|9160789.46 + 82628|9160155.54 + 41399|9148338.00 + 30755|9146196.84 + 6944|9143574.58 + 6326|9138803.16 + 101296|9135657.62 + 181479|9121093.30 + 76898|9120983.10 + 64274|9118745.25 + 175826|9117387.99 + 142215|9116876.88 + 103415|9113128.62 + 119765|9110768.79 + 107624|9108837.45 + 84215|9105257.36 + 73774|9102651.92 + 173972|9102069.00 + 69817|9095513.88 + 86943|9092253.00 + 138859|9087719.30 + 162273|9085296.48 + 175945|9080401.21 + 16836|9075715.44 + 70224|9075265.95 + 139765|9074755.89 + 30319|9073233.10 + 3851|9072657.24 + 181271|9070631.52 + 162184|9068835.78 + 81683|9067258.47 + 153028|9067010.51 + 123324|9061870.95 + 186481|9058608.30 + 167680|9052908.76 + 165293|9050545.70 + 122148|9046298.17 + 138604|9045840.80 + 78851|9044822.60 + 137280|9042355.34 + 8823|9040855.10 + 163900|9040848.48 + 75600|9035392.45 + 81676|9031999.40 + 46033|9031460.58 + 194917|9028500.00 + 133936|9026949.02 + 33182|9024971.10 + 34220|9021485.39 + 20118|9019942.60 + 178258|9019881.66 + 15560|9017687.28 + 111425|9016198.56 + 95942|9015585.12 + 132709|9015240.15 + 39731|9014746.95 + 154307|9012571.20 + 23769|9008157.60 + 93328|9007211.20 + 142826|8998297.44 + 188792|8996014.00 + 68703|8994982.22 + 145280|8990941.05 + 150725|8985686.16 + 172046|8982469.52 + 70476|8967629.50 + 124988|8966805.22 + 17937|8963319.76 + 177372|8954873.64 + 137994|8950916.79 + 84019|8950039.98 + 40389|8946158.20 + 69187|8941054.14 + 4863|8939044.92 + 50465|8930503.14 + 43686|8915543.84 + 131352|8909053.59 + 198916|8906940.03 + 135932|8905282.95 + 104673|8903682.00 + 152308|8903244.08 + 135298|8900323.20 + 156873|8899429.10 + 157454|8897339.20 + 75415|8897068.09 + 46325|8895569.09 + 1966|8895117.06 + 24576|8895034.75 + 19425|8890156.60 + 169735|8890085.56 + 32225|8889829.28 + 124537|8889770.71 + 146327|8887836.23 + 121562|8887740.40 + 44731|8882444.95 + 93141|8881850.88 + 187871|8873506.18 + 71709|8873057.28 + 151913|8869321.17 + 33786|8868955.39 + 35902|8868126.06 + 23588|8867769.90 + 24508|8867616.00 + 161282|8866661.43 + 188061|8862304.00 + 132847|8862082.00 + 166843|8861200.80 + 30609|8860214.73 + 56191|8856546.96 + 160740|8852685.43 + 71229|8846106.99 + 91208|8845541.28 + 10995|8845306.56 + 78094|8839938.29 + 36489|8838538.10 + 198437|8836494.84 + 151693|8833807.64 + 185367|8829791.37 + 65682|8820622.89 + 65421|8819329.24 + 122225|8816821.86 + 85330|8811013.16 + 64555|8810643.12 + 104188|8808211.02 + 54411|8805703.40 + 39438|8805282.56 + 70795|8800060.92 + 20383|8799073.28 + 21952|8798624.19 + 63584|8796590.00 + 158768|8796422.95 + 166588|8796214.38 + 120600|8793558.06 + 157202|8788287.88 + 55358|8786820.75 + 168322|8786670.73 + 25143|8786324.80 + 5368|8786274.14 + 114025|8786201.12 + 97744|8785315.94 + 164327|8784503.86 + 76542|8782613.28 + 4731|8772846.70 + 157590|8772006.45 + 154276|8771733.91 + 28705|8771576.64 + 100226|8769455.00 + 179195|8769185.16 + 184355|8768118.05 + 120408|8768011.12 + 63145|8761991.96 + 53135|8753491.80 + 173071|8750508.80 + 41087|8749436.79 + 194830|8747438.40 + 43496|8743359.30 + 30235|8741611.00 + 26391|8741399.64 + 191816|8740258.72 + 47616|8737229.68 + 152101|8734432.76 + 163784|8730514.34 + 5134|8728424.64 + 155241|8725429.86 + 188814|8724182.40 + 140782|8720378.75 + 153141|8719407.51 + 169373|8718609.06 + 41335|8714773.80 + 197450|8714617.32 + 87004|8714017.79 + 181804|8712257.76 + 122814|8711119.14 + 109939|8709193.16 + 98094|8708780.04 + 74630|8708040.75 + 197291|8706519.09 + 184173|8705467.45 + 192175|8705411.12 + 19471|8702536.12 + 18052|8702155.70 + 135560|8698137.72 + 152791|8697325.80 + 170953|8696909.19 + 116137|8696687.17 + 7722|8696589.40 + 49788|8694846.71 + 13252|8694822.42 + 12633|8694559.36 + 193438|8690426.72 + 17326|8689329.16 + 96124|8679794.58 + 143802|8676626.48 + 30389|8675826.60 + 75250|8675257.14 + 72613|8673524.94 + 123520|8672456.25 + 325|8667741.28 + 167291|8667556.18 + 150119|8663403.54 + 88420|8663355.40 + 179784|8653021.34 + 130884|8651970.00 + 172611|8648217.00 + 85373|8647796.22 + 122717|8646758.54 + 113431|8646348.34 + 66015|8643349.40 + 33141|8643243.18 + 69786|8637396.92 + 181857|8637393.28 + 122939|8636378.00 + 196223|8635391.02 + 50532|8632648.24 + 58102|8632614.54 + 93581|8632372.36 + 52804|8632109.25 + 755|8627091.68 + 16597|8623357.05 + 119041|8622397.00 + 89050|8621185.98 + 98696|8620784.82 + 94399|8620524.00 + 151295|8616671.02 + 56417|8613450.35 + 121322|8612948.23 + 126883|8611373.42 + 29155|8610163.64 + 114530|8608471.74 + 131007|8607394.82 + 128715|8606833.62 + 72522|8601479.98 + 144061|8595718.74 + 83503|8595034.20 + 112199|8590717.44 + 9227|8587350.42 + 116318|8585910.66 + 41248|8585559.64 + 159398|8584821.00 + 105966|8582308.79 + 137876|8580641.30 + 122272|8580400.77 + 195717|8577278.10 + 165295|8571121.92 + 5840|8570728.74 + 120860|8570610.44 + 66692|8567540.52 + 135596|8563276.31 + 150576|8562794.10 + 7500|8562393.84 + 107716|8561541.56 + 100611|8559995.85 + 171192|8557390.08 + 107660|8556696.60 + 13461|8556545.12 + 90310|8555131.51 + 141493|8553782.93 + 71286|8552682.00 + 136423|8551300.76 + 54241|8550785.25 + 120325|8549976.60 + 424|8547527.10 + 196543|8545907.09 + 13042|8542717.18 + 58332|8536074.69 + 9191|8535663.92 + 134357|8535429.90 + 96207|8534900.60 + 92292|8530618.78 + 181093|8528303.52 + 105064|8527491.60 + 59635|8526854.08 + 136974|8524351.56 + 126694|8522783.37 + 6247|8522606.90 + 139447|8522521.92 + 96313|8520949.92 + 108454|8520916.25 + 181254|8519496.10 + 71117|8519223.00 + 131703|8517215.28 + 59312|8510568.36 + 2903|8509960.35 + 102838|8509527.69 + 162806|8508906.05 + 41527|8508222.36 + 118416|8505858.36 + 180203|8505024.16 + 14773|8500598.28 + 140446|8499514.24 + 199641|8497362.59 + 109240|8494617.12 + 150268|8494188.38 + 45310|8492380.65 + 36552|8490733.60 + 199690|8490145.80 + 185353|8488726.68 + 163615|8484985.01 + 196520|8483545.04 + 133438|8483482.35 + 77285|8481442.32 + 55824|8476893.90 + 76753|8475522.12 + 46129|8472717.96 + 28358|8472515.50 + 9317|8472145.32 + 33823|8469721.44 + 39055|8469145.07 + 91471|8468874.56 + 142299|8466039.55 + 97672|8464119.80 + 134712|8461781.79 + 157988|8460123.20 + 102284|8458652.44 + 73533|8458453.32 + 90599|8457874.86 + 112160|8457863.36 + 124792|8457633.70 + 66097|8457573.15 + 165271|8456969.01 + 146925|8454887.91 + 164277|8454838.50 + 131290|8454811.20 + 179386|8450909.90 + 90486|8447873.86 + 175924|8444421.66 + 185922|8442394.88 + 38492|8436438.32 + 172511|8436287.34 + 139539|8434180.29 + 11926|8433199.52 + 55889|8431449.88 + 163068|8431116.40 + 138772|8428406.36 + 126821|8425180.68 + 22091|8420687.88 + 55981|8419434.38 + 100960|8419403.46 + 172568|8417955.21 + 63135|8415945.53 + 137651|8413170.35 + 191353|8413039.84 + 62988|8411571.48 + 103417|8411541.12 + 12052|8411519.28 + 104260|8408516.55 + 157129|8405730.08 + 77254|8405537.22 + 112966|8403512.89 + 168114|8402764.56 + 49940|8402328.20 + 52017|8398753.60 + 176179|8398087.00 + 100215|8395906.61 + 61256|8392811.20 + 15366|8388907.80 + 109479|8388027.20 + 66202|8386522.83 + 81707|8385761.19 + 51727|8385426.40 + 9980|8382754.62 + 174403|8378575.73 + 54558|8378041.92 + 3141|8377378.22 + 134829|8377105.52 + 145056|8376920.76 + 194020|8375157.64 + 7117|8373982.27 + 120146|8373796.20 + 126843|8370761.28 + 62117|8369493.44 + 111221|8367525.81 + 159337|8366092.26 + 173903|8365428.48 + 136438|8364065.45 + 56684|8363198.00 + 137597|8363185.94 + 20039|8361138.24 + 121326|8359635.52 + 48435|8352863.10 + 1712|8349107.00 + 167190|8347238.70 + 32113|8346452.04 + 40580|8342983.32 + 74785|8342519.13 + 14799|8342236.75 + 177291|8341736.83 + 198956|8340370.65 + 69179|8338465.99 + 118764|8337616.56 + 128814|8336435.56 + 82729|8331766.88 + 152048|8330638.99 + 171085|8326259.50 + 126730|8325974.40 + 77525|8323282.50 + 170653|8322840.50 + 5257|8320350.78 + 67350|8318987.56 + 109008|8317836.54 + 199043|8316603.54 + 139969|8316551.54 + 22634|8316531.24 + 173309|8315750.25 + 10887|8315019.36 + 42392|8312895.96 + 126040|8312623.20 + 101590|8304555.42 + 46891|8302192.12 + 138721|8301745.62 + 113715|8301533.20 + 78778|8299685.64 + 142908|8299447.77 + 64419|8297631.80 + 21396|8296272.27 + 4180|8295646.92 + 63534|8295383.67 + 135957|8294389.86 + 30126|8291920.32 + 158427|8288938.00 + 14545|8288395.92 + 75548|8288287.20 + 64473|8286137.44 + 149553|8285714.88 + 151284|8283526.65 + 171091|8282934.36 + 194256|8278985.34 + 952|8276136.00 + 121541|8275390.26 + 177664|8275315.20 + 51117|8274504.30 + 66770|8273407.80 + 37238|8272728.06 + 46679|8270486.55 + 165852|8268312.60 + 99458|8266564.47 + 114519|8265493.54 + 7231|8264881.50 + 19033|8264826.56 + 125123|8262732.65 + 18642|8261578.99 + 50386|8261380.05 + 193770|8259578.82 + 7276|8258101.60 + 178045|8253904.15 + 49033|8253696.23 + 187195|8251334.58 + 10590|8249227.40 + 143779|8247057.70 + 35205|8245675.17 + 19729|8245081.60 + 144946|8240479.80 + 123786|8239581.24 + 70843|8237973.20 + 112437|8236907.52 + 5436|8236039.57 + 163754|8235471.16 + 115945|8234811.36 + 27918|8233957.88 + 105712|8233571.86 + 41007|8229431.79 + 40476|8226640.41 + 145620|8221371.60 + 7771|8220413.33 + 86424|8215572.61 + 129137|8215478.40 + 76020|8210495.36 + 140213|8209831.80 + 32379|8208338.88 + 130616|8207715.75 + 195469|8206609.80 + 191805|8205147.75 + 90906|8200951.20 + 170910|8195558.01 + 105399|8193122.63 + 123798|8192385.97 + 90218|8191689.16 + 114766|8189339.54 + 11289|8187354.72 + 178308|8185750.50 + 71271|8185519.24 + 1115|8184903.38 + 152636|8184530.72 + 151619|8182909.05 + 116943|8181072.69 + 28891|8181051.54 + 47049|8180955.00 + 158827|8180470.90 + 92620|8179671.55 + 20814|8176953.54 + 179323|8176795.55 + 193453|8174343.94 + 56888|8173342.00 + 28087|8169876.30 + 164254|8169632.35 + 57661|8168848.16 + 7363|8167538.05 + 164499|8167512.08 + 197557|8165940.45 + 5495|8164805.22 + 966|8163824.79 + 98435|8161771.45 + 127227|8161344.92 + 194100|8160978.78 + 40134|8160358.08 + 107341|8159952.05 + 6790|8158792.66 + 43851|8157101.40 + 51295|8156419.20 + 69512|8151537.00 + 164274|8149869.93 + 130854|8145338.85 + 186865|8143586.82 + 176629|8141411.20 + 193739|8141377.77 + 6810|8139822.60 + 27732|8136724.96 + 50616|8134089.82 + 123908|8128920.54 + 140994|8128470.82 + 99039|8128290.78 + 62735|8124940.50 + 47829|8122796.50 + 192635|8122687.57 + 192429|8119268.00 + 145812|8119165.63 + 42896|8118529.80 + 146877|8118266.16 + 60882|8116095.04 + 18254|8114783.04 + 165464|8114571.80 + 57936|8111927.25 + 52226|8110723.32 + 128571|8106788.80 + 100308|8105837.04 + 8872|8102395.62 + 58867|8102033.19 + 145153|8100222.84 + 172088|8098138.20 + 59398|8095845.45 + 89395|8093576.10 + 171961|8093538.00 + 88736|8090762.16 + 174053|8090350.11 + 102237|8089103.22 + 43041|8086537.90 + 110219|8085296.90 + 126738|8084199.20 + 44787|8083628.40 + 31277|8083580.76 + 93595|8082188.80 + 189040|8080257.21 + 59851|8079024.24 + 175100|8077904.01 + 43429|8076729.96 + 154199|8074940.76 + 60963|8073894.40 + 8768|8072760.96 + 66095|8071421.70 + 111552|8068184.48 + 24563|8067500.40 + 16167|8067495.24 + 12662|8067248.85 + 94540|8063727.16 + 23308|8063463.18 + 27390|8062823.25 + 130660|8062787.48 + 8608|8062411.16 + 181552|8062008.30 + 199319|8060248.56 + 55475|8058850.92 + 142711|8057926.58 + 103499|8056978.00 + 105943|8056698.75 + 8432|8053052.16 + 149392|8049675.69 + 101248|8048855.49 + 140962|8047260.70 + 87101|8046651.83 + 133107|8046476.73 + 45126|8045924.40 + 87508|8042966.39 + 124711|8042722.72 + 173169|8042224.41 + 175161|8041331.98 + 167787|8040075.78 + 3242|8038855.53 + 114789|8038628.35 + 43833|8038545.83 + 141198|8035110.72 + 137248|8034109.35 + 96673|8033491.20 + 32180|8032380.72 + 166493|8031902.40 + 66959|8031839.40 + 85628|8029693.44 + 110971|8029469.70 + 130395|8027463.92 + 7757|8026840.37 + 178446|8025379.09 + 41295|8024785.53 + 100956|8024179.30 + 131917|8021604.78 + 24224|8020463.52 + 2073|8020009.64 + 121622|8018462.17 + 14357|8016906.30 + 135601|8016209.44 + 58458|8016192.52 + 73036|8015799.00 + 184722|8015680.31 + 151664|8014821.96 + 195090|8012680.20 + 162609|8011241.00 + 83532|8009753.85 + 50166|8007137.89 + 181562|8006805.96 + 175165|8005319.76 + 62500|8005316.28 + 36342|8004333.40 + 128435|8004242.88 + 92516|8003836.80 + 30802|8003710.88 + 107418|8000430.30 + 46620|7999778.35 + 191803|7994734.15 + 106343|7993087.76 + 59362|7990397.46 + 8329|7990052.90 + 75133|7988244.00 + 179023|7986829.62 + 135899|7985726.64 + 5824|7985340.02 + 148579|7984889.56 + 95888|7984735.72 + 9791|7982699.79 + 170437|7982370.72 + 39782|7977858.24 + 20605|7977556.00 + 28682|7976960.00 + 42172|7973399.00 + 56137|7971405.40 + 64729|7970769.72 + 98643|7968603.73 + 153787|7967535.58 + 8932|7967222.19 + 20134|7965713.28 + 197635|7963507.58 + 80408|7963312.17 + 37728|7961875.68 + 26624|7961772.31 + 44736|7961144.10 + 29763|7960605.03 + 36147|7959463.68 + 146040|7957587.66 + 115469|7957485.14 + 142276|7956790.63 + 181280|7954037.35 + 115096|7953047.55 + 109650|7952258.73 + 93862|7951992.24 + 158325|7950728.30 + 55952|7950387.06 + 122397|7947106.27 + 28114|7946945.72 + 11966|7945197.48 + 47814|7944083.00 + 85096|7943691.06 + 51657|7943593.77 + 196680|7943578.89 + 13141|7942730.34 + 193327|7941036.25 + 152612|7940663.71 + 139680|7939242.36 + 31134|7938318.30 + 45636|7937240.85 + 56694|7936015.95 + 8114|7933921.88 + 71518|7932261.69 + 72922|7930400.64 + 146699|7929167.40 + 92387|7928972.67 + 186289|7928786.19 + 95952|7927972.78 + 196514|7927180.70 + 4403|7925729.04 + 2267|7925649.37 + 45924|7925047.68 + 11493|7916722.23 + 104478|7916253.60 + 166794|7913842.00 + 161995|7910874.27 + 23538|7909752.06 + 41093|7909579.92 + 112073|7908617.57 + 92814|7908262.50 + 88919|7907992.50 + 79753|7907933.88 + 108765|7905338.98 + 146530|7905336.60 + 71475|7903367.58 + 36289|7901946.50 + 61739|7900794.00 + 52338|7898638.08 + 194299|7898421.24 + 105235|7897829.94 + 77207|7897752.72 + 96712|7897575.27 + 10157|7897046.25 + 171154|7896814.50 + 79373|7896186.00 + 113808|7893353.88 + 27901|7892952.00 + 128820|7892882.72 + 25891|7890511.20 + 122819|7888881.02 + 154731|7888301.33 + 101674|7879324.60 + 51968|7879102.21 + 72073|7877736.11 + 5182|7874521.73 diff --git a/examples/tpch/answers_sf1/q12.tbl b/examples/tpch/answers_sf1/q12.tbl new file mode 100644 index 000000000..752fbae05 --- /dev/null +++ b/examples/tpch/answers_sf1/q12.tbl @@ -0,0 +1,3 @@ +l_shipmode|high_line_count |low_line_count +MAIL | 6202| 9324 +SHIP | 6200| 9262 diff --git a/examples/tpch/answers_sf1/q13.tbl b/examples/tpch/answers_sf1/q13.tbl new file mode 100644 index 000000000..deecc0d83 --- /dev/null +++ b/examples/tpch/answers_sf1/q13.tbl @@ -0,0 +1,43 @@ +c_count |custdist + 0| 50005 + 9| 6641 + 10| 6532 + 11| 6014 + 8| 5937 + 12| 5639 + 13| 5024 + 19| 4793 + 7| 4687 + 17| 4587 + 18| 4529 + 20| 4516 + 15| 4505 + 14| 4446 + 16| 4273 + 21| 4190 + 22| 3623 + 6| 3265 + 23| 3225 + 24| 2742 + 25| 2086 + 5| 1948 + 26| 1612 + 27| 1179 + 4| 1007 + 28| 893 + 29| 593 + 3| 415 + 30| 376 + 31| 226 + 32| 148 + 2| 134 + 33| 75 + 34| 50 + 35| 37 + 1| 17 + 36| 14 + 38| 5 + 37| 5 + 40| 4 + 41| 2 + 39| 1 diff --git a/examples/tpch/answers_sf1/q14.tbl b/examples/tpch/answers_sf1/q14.tbl new file mode 100644 index 000000000..0e6571a1d --- /dev/null +++ b/examples/tpch/answers_sf1/q14.tbl @@ -0,0 +1,2 @@ +promo_revenue +16.38 diff --git a/examples/tpch/answers_sf1/q15.tbl b/examples/tpch/answers_sf1/q15.tbl new file mode 100644 index 000000000..ece5d5900 --- /dev/null +++ b/examples/tpch/answers_sf1/q15.tbl @@ -0,0 +1,2 @@ +s_suppkey |s_name |s_address |s_phone |total_revenue + 8449|Supplier#000008449 |Wp34zim9qYFbVctdW |20-469-856-8873|1772627.21 diff --git a/examples/tpch/answers_sf1/q16.tbl b/examples/tpch/answers_sf1/q16.tbl new file mode 100644 index 000000000..854a168b7 --- /dev/null +++ b/examples/tpch/answers_sf1/q16.tbl @@ -0,0 +1,18315 @@ +p_brand |p_type |p_size |supplier_cnt +Brand#41 |MEDIUM BRUSHED TIN | 3| 28 +Brand#54 |STANDARD BRUSHED COPPER | 14| 27 +Brand#11 |STANDARD BRUSHED TIN | 23| 24 +Brand#11 |STANDARD BURNISHED BRASS | 36| 24 +Brand#15 |MEDIUM ANODIZED NICKEL | 3| 24 +Brand#15 |SMALL ANODIZED BRASS | 45| 24 +Brand#15 |SMALL BURNISHED NICKEL | 19| 24 +Brand#21 |MEDIUM ANODIZED COPPER | 3| 24 +Brand#22 |SMALL BRUSHED NICKEL | 3| 24 +Brand#22 |SMALL BURNISHED BRASS | 19| 24 +Brand#25 |MEDIUM BURNISHED COPPER | 36| 24 +Brand#31 |PROMO POLISHED COPPER | 36| 24 +Brand#33 |LARGE POLISHED TIN | 23| 24 +Brand#33 |PROMO POLISHED STEEL | 14| 24 +Brand#35 |PROMO BRUSHED NICKEL | 14| 24 +Brand#41 |ECONOMY BRUSHED STEEL | 9| 24 +Brand#41 |ECONOMY POLISHED TIN | 19| 24 +Brand#41 |LARGE PLATED COPPER | 36| 24 +Brand#42 |ECONOMY PLATED BRASS | 3| 24 +Brand#42 |STANDARD POLISHED TIN | 49| 24 +Brand#43 |PROMO BRUSHED TIN | 3| 24 +Brand#43 |SMALL ANODIZED COPPER | 36| 24 +Brand#44 |STANDARD POLISHED NICKEL | 3| 24 +Brand#52 |ECONOMY PLATED TIN | 14| 24 +Brand#52 |STANDARD BURNISHED NICKEL| 3| 24 +Brand#53 |MEDIUM ANODIZED STEEL | 14| 24 +Brand#14 |PROMO ANODIZED NICKEL | 45| 23 +Brand#32 |ECONOMY PLATED BRASS | 9| 23 +Brand#52 |SMALL ANODIZED COPPER | 3| 23 +Brand#11 |ECONOMY BRUSHED COPPER | 45| 20 +Brand#11 |ECONOMY PLATED BRASS | 23| 20 +Brand#11 |LARGE BRUSHED COPPER | 49| 20 +Brand#11 |LARGE POLISHED COPPER | 49| 20 +Brand#12 |STANDARD ANODIZED TIN | 49| 20 +Brand#12 |STANDARD PLATED BRASS | 19| 20 +Brand#13 |ECONOMY BRUSHED BRASS | 9| 20 +Brand#13 |ECONOMY BURNISHED STEEL | 14| 20 +Brand#13 |LARGE BURNISHED NICKEL | 19| 20 +Brand#13 |MEDIUM BURNISHED COPPER | 36| 20 +Brand#13 |SMALL BRUSHED TIN | 45| 20 +Brand#13 |STANDARD ANODIZED COPPER | 3| 20 +Brand#13 |STANDARD PLATED NICKEL | 23| 20 +Brand#14 |ECONOMY ANODIZED COPPER | 14| 20 +Brand#14 |ECONOMY PLATED TIN | 36| 20 +Brand#14 |ECONOMY POLISHED NICKEL | 3| 20 +Brand#14 |MEDIUM ANODIZED NICKEL | 3| 20 +Brand#14 |SMALL POLISHED TIN | 14| 20 +Brand#15 |MEDIUM ANODIZED COPPER | 9| 20 +Brand#15 |MEDIUM PLATED TIN | 23| 20 +Brand#15 |PROMO PLATED BRASS | 14| 20 +Brand#15 |SMALL ANODIZED COPPER | 45| 20 +Brand#15 |SMALL PLATED COPPER | 49| 20 +Brand#15 |STANDARD PLATED TIN | 3| 20 +Brand#21 |LARGE ANODIZED COPPER | 36| 20 +Brand#21 |LARGE BRUSHED TIN | 3| 20 +Brand#21 |MEDIUM ANODIZED COPPER | 14| 20 +Brand#21 |PROMO BRUSHED TIN | 36| 20 +Brand#21 |PROMO POLISHED NICKEL | 45| 20 +Brand#21 |SMALL ANODIZED COPPER | 9| 20 +Brand#21 |SMALL POLISHED NICKEL | 23| 20 +Brand#22 |LARGE ANODIZED COPPER | 36| 20 +Brand#22 |LARGE BRUSHED COPPER | 49| 20 +Brand#22 |PROMO ANODIZED TIN | 49| 20 +Brand#22 |PROMO POLISHED BRASS | 45| 20 +Brand#22 |SMALL BURNISHED STEEL | 45| 20 +Brand#23 |MEDIUM ANODIZED STEEL | 45| 20 +Brand#23 |PROMO POLISHED STEEL | 23| 20 +Brand#23 |STANDARD BRUSHED TIN | 14| 20 +Brand#23 |STANDARD PLATED NICKEL | 36| 20 +Brand#24 |PROMO PLATED COPPER | 49| 20 +Brand#24 |PROMO PLATED STEEL | 49| 20 +Brand#24 |PROMO POLISHED STEEL | 9| 20 +Brand#24 |STANDARD BRUSHED TIN | 36| 20 +Brand#25 |LARGE ANODIZED BRASS | 3| 20 +Brand#25 |PROMO BURNISHED TIN | 3| 20 +Brand#31 |ECONOMY POLISHED NICKEL | 3| 20 +Brand#31 |MEDIUM PLATED TIN | 45| 20 +Brand#31 |SMALL ANODIZED STEEL | 14| 20 +Brand#32 |ECONOMY ANODIZED COPPER | 36| 20 +Brand#32 |ECONOMY BRUSHED NICKEL | 49| 20 +Brand#32 |LARGE ANODIZED TIN | 19| 20 +Brand#32 |MEDIUM BURNISHED COPPER | 19| 20 +Brand#32 |SMALL ANODIZED STEEL | 45| 20 +Brand#33 |ECONOMY POLISHED COPPER | 19| 20 +Brand#33 |PROMO PLATED NICKEL | 14| 20 +Brand#33 |SMALL POLISHED TIN | 9| 20 +Brand#33 |STANDARD ANODIZED BRASS | 49| 20 +Brand#33 |STANDARD BURNISHED BRASS | 45| 20 +Brand#34 |ECONOMY BRUSHED NICKEL | 49| 20 +Brand#34 |LARGE BRUSHED BRASS | 19| 20 +Brand#34 |SMALL BRUSHED TIN | 3| 20 +Brand#34 |STANDARD PLATED COPPER | 9| 20 +Brand#35 |LARGE ANODIZED NICKEL | 3| 20 +Brand#35 |MEDIUM ANODIZED BRASS | 45| 20 +Brand#35 |MEDIUM ANODIZED STEEL | 23| 20 +Brand#35 |PROMO ANODIZED COPPER | 49| 20 +Brand#35 |SMALL POLISHED COPPER | 14| 20 +Brand#41 |LARGE ANODIZED STEEL | 3| 20 +Brand#41 |LARGE BRUSHED NICKEL | 23| 20 +Brand#41 |LARGE BURNISHED COPPER | 3| 20 +Brand#41 |MEDIUM PLATED STEEL | 19| 20 +Brand#41 |SMALL BURNISHED COPPER | 23| 20 +Brand#42 |MEDIUM BURNISHED BRASS | 14| 20 +Brand#42 |SMALL BURNISHED COPPER | 3| 20 +Brand#43 |ECONOMY POLISHED COPPER | 9| 20 +Brand#43 |SMALL PLATED STEEL | 3| 20 +Brand#43 |STANDARD BURNISHED TIN | 23| 20 +Brand#44 |LARGE ANODIZED STEEL | 23| 20 +Brand#44 |PROMO ANODIZED TIN | 23| 20 +Brand#51 |ECONOMY BRUSHED BRASS | 49| 20 +Brand#51 |ECONOMY POLISHED NICKEL | 9| 20 +Brand#51 |MEDIUM BRUSHED TIN | 9| 20 +Brand#51 |MEDIUM PLATED BRASS | 9| 20 +Brand#51 |PROMO BURNISHED BRASS | 9| 20 +Brand#51 |SMALL PLATED NICKEL | 49| 20 +Brand#51 |STANDARD ANODIZED NICKEL | 49| 20 +Brand#51 |STANDARD BRUSHED COPPER | 3| 20 +Brand#52 |ECONOMY ANODIZED BRASS | 3| 20 +Brand#52 |ECONOMY BRUSHED COPPER | 49| 20 +Brand#52 |LARGE ANODIZED NICKEL | 45| 20 +Brand#52 |MEDIUM ANODIZED TIN | 23| 20 +Brand#52 |MEDIUM BURNISHED TIN | 45| 20 +Brand#52 |SMALL PLATED COPPER | 36| 20 +Brand#52 |STANDARD ANODIZED BRASS | 45| 20 +Brand#53 |ECONOMY PLATED COPPER | 45| 20 +Brand#53 |PROMO ANODIZED COPPER | 49| 20 +Brand#53 |PROMO BRUSHED COPPER | 23| 20 +Brand#53 |PROMO PLATED TIN | 19| 20 +Brand#53 |PROMO POLISHED NICKEL | 3| 20 +Brand#53 |SMALL ANODIZED STEEL | 9| 20 +Brand#53 |SMALL BRUSHED COPPER | 3| 20 +Brand#53 |SMALL BRUSHED NICKEL | 3| 20 +Brand#54 |ECONOMY PLATED STEEL | 9| 20 +Brand#54 |ECONOMY POLISHED TIN | 3| 20 +Brand#54 |SMALL BRUSHED BRASS | 19| 20 +Brand#55 |MEDIUM ANODIZED COPPER | 3| 20 +Brand#55 |PROMO BURNISHED STEEL | 14| 20 +Brand#55 |PROMO POLISHED NICKEL | 49| 20 +Brand#55 |STANDARD ANODIZED BRASS | 19| 20 +Brand#55 |STANDARD BURNISHED COPPER| 45| 20 +Brand#43 |ECONOMY ANODIZED TIN | 3| 19 +Brand#11 |ECONOMY ANODIZED BRASS | 14| 16 +Brand#11 |ECONOMY ANODIZED BRASS | 23| 16 +Brand#11 |ECONOMY ANODIZED COPPER | 14| 16 +Brand#11 |ECONOMY BRUSHED BRASS | 49| 16 +Brand#11 |ECONOMY BRUSHED STEEL | 19| 16 +Brand#11 |ECONOMY BURNISHED NICKEL | 23| 16 +Brand#11 |LARGE ANODIZED COPPER | 14| 16 +Brand#11 |LARGE BRUSHED TIN | 45| 16 +Brand#11 |LARGE BURNISHED COPPER | 23| 16 +Brand#11 |LARGE BURNISHED NICKEL | 36| 16 +Brand#11 |LARGE PLATED STEEL | 14| 16 +Brand#11 |MEDIUM BRUSHED NICKEL | 14| 16 +Brand#11 |MEDIUM BRUSHED STEEL | 49| 16 +Brand#11 |MEDIUM BURNISHED NICKEL | 49| 16 +Brand#11 |MEDIUM BURNISHED TIN | 3| 16 +Brand#11 |MEDIUM PLATED COPPER | 9| 16 +Brand#11 |PROMO ANODIZED BRASS | 19| 16 +Brand#11 |PROMO ANODIZED BRASS | 49| 16 +Brand#11 |PROMO ANODIZED STEEL | 45| 16 +Brand#11 |PROMO PLATED BRASS | 45| 16 +Brand#11 |SMALL ANODIZED TIN | 45| 16 +Brand#11 |SMALL BRUSHED STEEL | 49| 16 +Brand#11 |SMALL BURNISHED COPPER | 19| 16 +Brand#11 |SMALL BURNISHED COPPER | 45| 16 +Brand#11 |SMALL BURNISHED NICKEL | 14| 16 +Brand#11 |SMALL POLISHED NICKEL | 36| 16 +Brand#11 |STANDARD ANODIZED BRASS | 19| 16 +Brand#11 |STANDARD ANODIZED COPPER | 14| 16 +Brand#11 |STANDARD BRUSHED STEEL | 45| 16 +Brand#11 |STANDARD POLISHED NICKEL | 23| 16 +Brand#12 |ECONOMY ANODIZED TIN | 14| 16 +Brand#12 |ECONOMY BRUSHED COPPER | 9| 16 +Brand#12 |ECONOMY BRUSHED COPPER | 36| 16 +Brand#12 |ECONOMY BURNISHED BRASS | 9| 16 +Brand#12 |ECONOMY BURNISHED NICKEL | 36| 16 +Brand#12 |LARGE ANODIZED BRASS | 14| 16 +Brand#12 |LARGE ANODIZED COPPER | 9| 16 +Brand#12 |LARGE ANODIZED STEEL | 23| 16 +Brand#12 |LARGE BURNISHED TIN | 36| 16 +Brand#12 |LARGE PLATED COPPER | 49| 16 +Brand#12 |LARGE POLISHED COPPER | 49| 16 +Brand#12 |MEDIUM PLATED COPPER | 19| 16 +Brand#12 |MEDIUM PLATED NICKEL | 23| 16 +Brand#12 |PROMO ANODIZED BRASS | 45| 16 +Brand#12 |PROMO ANODIZED STEEL | 49| 16 +Brand#12 |PROMO BURNISHED STEEL | 9| 16 +Brand#12 |SMALL BRUSHED NICKEL | 36| 16 +Brand#12 |SMALL BRUSHED TIN | 45| 16 +Brand#12 |STANDARD ANODIZED BRASS | 3| 16 +Brand#12 |STANDARD ANODIZED NICKEL | 14| 16 +Brand#12 |STANDARD BRUSHED BRASS | 3| 16 +Brand#12 |STANDARD BRUSHED TIN | 9| 16 +Brand#12 |STANDARD BRUSHED TIN | 36| 16 +Brand#12 |STANDARD POLISHED COPPER | 9| 16 +Brand#13 |ECONOMY ANODIZED STEEL | 45| 16 +Brand#13 |ECONOMY POLISHED BRASS | 3| 16 +Brand#13 |LARGE BRUSHED NICKEL | 23| 16 +Brand#13 |LARGE BURNISHED NICKEL | 9| 16 +Brand#13 |MEDIUM BRUSHED STEEL | 49| 16 +Brand#13 |MEDIUM BURNISHED NICKEL | 49| 16 +Brand#13 |MEDIUM PLATED BRASS | 49| 16 +Brand#13 |PROMO ANODIZED BRASS | 14| 16 +Brand#13 |PROMO ANODIZED COPPER | 3| 16 +Brand#13 |SMALL ANODIZED STEEL | 45| 16 +Brand#13 |SMALL BURNISHED STEEL | 19| 16 +Brand#13 |SMALL PLATED BRASS | 36| 16 +Brand#13 |STANDARD ANODIZED BRASS | 23| 16 +Brand#13 |STANDARD ANODIZED STEEL | 23| 16 +Brand#13 |STANDARD BURNISHED BRASS | 9| 16 +Brand#13 |STANDARD PLATED NICKEL | 9| 16 +Brand#13 |STANDARD PLATED TIN | 23| 16 +Brand#14 |ECONOMY BRUSHED STEEL | 3| 16 +Brand#14 |ECONOMY PLATED NICKEL | 9| 16 +Brand#14 |ECONOMY PLATED STEEL | 9| 16 +Brand#14 |ECONOMY POLISHED NICKEL | 19| 16 +Brand#14 |LARGE ANODIZED COPPER | 14| 16 +Brand#14 |LARGE BRUSHED NICKEL | 19| 16 +Brand#14 |LARGE POLISHED STEEL | 3| 16 +Brand#14 |LARGE POLISHED TIN | 23| 16 +Brand#14 |MEDIUM BURNISHED COPPER | 3| 16 +Brand#14 |PROMO ANODIZED STEEL | 36| 16 +Brand#14 |PROMO PLATED BRASS | 9| 16 +Brand#14 |PROMO PLATED NICKEL | 49| 16 +Brand#14 |PROMO POLISHED BRASS | 19| 16 +Brand#14 |PROMO POLISHED STEEL | 19| 16 +Brand#14 |PROMO POLISHED TIN | 45| 16 +Brand#14 |SMALL BRUSHED BRASS | 14| 16 +Brand#14 |SMALL BURNISHED COPPER | 45| 16 +Brand#14 |STANDARD BRUSHED TIN | 19| 16 +Brand#14 |STANDARD PLATED COPPER | 45| 16 +Brand#14 |STANDARD PLATED TIN | 9| 16 +Brand#14 |STANDARD POLISHED TIN | 49| 16 +Brand#15 |ECONOMY BRUSHED STEEL | 19| 16 +Brand#15 |LARGE BRUSHED BRASS | 14| 16 +Brand#15 |LARGE BRUSHED STEEL | 14| 16 +Brand#15 |LARGE BURNISHED NICKEL | 3| 16 +Brand#15 |LARGE PLATED COPPER | 49| 16 +Brand#15 |PROMO ANODIZED NICKEL | 3| 16 +Brand#15 |PROMO BURNISHED TIN | 49| 16 +Brand#15 |PROMO PLATED STEEL | 3| 16 +Brand#15 |PROMO POLISHED STEEL | 49| 16 +Brand#15 |SMALL BRUSHED COPPER | 9| 16 +Brand#15 |SMALL BRUSHED NICKEL | 23| 16 +Brand#15 |SMALL PLATED BRASS | 49| 16 +Brand#15 |STANDARD ANODIZED COPPER | 45| 16 +Brand#15 |STANDARD BRUSHED COPPER | 14| 16 +Brand#15 |STANDARD PLATED TIN | 36| 16 +Brand#21 |ECONOMY ANODIZED STEEL | 45| 16 +Brand#21 |ECONOMY BRUSHED COPPER | 9| 16 +Brand#21 |ECONOMY POLISHED STEEL | 19| 16 +Brand#21 |LARGE ANODIZED STEEL | 14| 16 +Brand#21 |MEDIUM ANODIZED STEEL | 36| 16 +Brand#21 |PROMO POLISHED BRASS | 14| 16 +Brand#21 |PROMO POLISHED TIN | 49| 16 +Brand#21 |SMALL BRUSHED COPPER | 3| 16 +Brand#21 |SMALL PLATED STEEL | 45| 16 +Brand#21 |SMALL PLATED TIN | 45| 16 +Brand#21 |STANDARD POLISHED STEEL | 36| 16 +Brand#22 |ECONOMY BRUSHED BRASS | 9| 16 +Brand#22 |ECONOMY BRUSHED NICKEL | 36| 16 +Brand#22 |ECONOMY POLISHED TIN | 36| 16 +Brand#22 |LARGE BRUSHED COPPER | 19| 16 +Brand#22 |LARGE BRUSHED TIN | 36| 16 +Brand#22 |LARGE POLISHED COPPER | 19| 16 +Brand#22 |MEDIUM ANODIZED BRASS | 23| 16 +Brand#22 |MEDIUM ANODIZED NICKEL | 9| 16 +Brand#22 |MEDIUM BRUSHED NICKEL | 14| 16 +Brand#22 |MEDIUM PLATED NICKEL | 23| 16 +Brand#22 |PROMO ANODIZED TIN | 45| 16 +Brand#22 |PROMO POLISHED STEEL | 49| 16 +Brand#22 |SMALL BRUSHED NICKEL | 45| 16 +Brand#22 |SMALL POLISHED BRASS | 36| 16 +Brand#22 |SMALL POLISHED STEEL | 9| 16 +Brand#22 |STANDARD BURNISHED BRASS | 45| 16 +Brand#22 |STANDARD BURNISHED NICKEL| 3| 16 +Brand#22 |STANDARD PLATED BRASS | 9| 16 +Brand#23 |ECONOMY BRUSHED TIN | 49| 16 +Brand#23 |ECONOMY BURNISHED COPPER | 45| 16 +Brand#23 |ECONOMY BURNISHED NICKEL | 19| 16 +Brand#23 |ECONOMY BURNISHED TIN | 9| 16 +Brand#23 |ECONOMY PLATED BRASS | 9| 16 +Brand#23 |ECONOMY PLATED COPPER | 14| 16 +Brand#23 |LARGE ANODIZED STEEL | 23| 16 +Brand#23 |LARGE ANODIZED STEEL | 49| 16 +Brand#23 |LARGE BURNISHED COPPER | 23| 16 +Brand#23 |LARGE POLISHED NICKEL | 9| 16 +Brand#23 |MEDIUM BRUSHED STEEL | 3| 16 +Brand#23 |PROMO ANODIZED COPPER | 19| 16 +Brand#23 |PROMO ANODIZED TIN | 3| 16 +Brand#23 |PROMO BURNISHED COPPER | 14| 16 +Brand#23 |PROMO PLATED BRASS | 3| 16 +Brand#23 |SMALL ANODIZED BRASS | 23| 16 +Brand#23 |SMALL BRUSHED BRASS | 45| 16 +Brand#23 |SMALL POLISHED TIN | 3| 16 +Brand#23 |STANDARD BURNISHED COPPER| 19| 16 +Brand#23 |STANDARD BURNISHED NICKEL| 49| 16 +Brand#23 |STANDARD PLATED BRASS | 9| 16 +Brand#23 |STANDARD PLATED COPPER | 45| 16 +Brand#23 |STANDARD POLISHED BRASS | 9| 16 +Brand#24 |ECONOMY ANODIZED BRASS | 3| 16 +Brand#24 |ECONOMY BRUSHED COPPER | 36| 16 +Brand#24 |ECONOMY BRUSHED STEEL | 14| 16 +Brand#24 |ECONOMY POLISHED COPPER | 36| 16 +Brand#24 |ECONOMY POLISHED NICKEL | 3| 16 +Brand#24 |LARGE ANODIZED BRASS | 23| 16 +Brand#24 |LARGE BURNISHED BRASS | 45| 16 +Brand#24 |LARGE BURNISHED STEEL | 14| 16 +Brand#24 |LARGE PLATED TIN | 9| 16 +Brand#24 |MEDIUM BRUSHED NICKEL | 49| 16 +Brand#24 |MEDIUM BURNISHED STEEL | 3| 16 +Brand#24 |PROMO BURNISHED COPPER | 49| 16 +Brand#24 |PROMO BURNISHED STEEL | 49| 16 +Brand#24 |PROMO POLISHED STEEL | 23| 16 +Brand#24 |SMALL ANODIZED NICKEL | 19| 16 +Brand#24 |STANDARD BURNISHED COPPER| 19| 16 +Brand#24 |STANDARD BURNISHED STEEL | 36| 16 +Brand#24 |STANDARD PLATED NICKEL | 23| 16 +Brand#24 |STANDARD PLATED TIN | 49| 16 +Brand#25 |ECONOMY ANODIZED COPPER | 14| 16 +Brand#25 |ECONOMY BURNISHED NICKEL | 9| 16 +Brand#25 |ECONOMY PLATED TIN | 14| 16 +Brand#25 |ECONOMY POLISHED TIN | 45| 16 +Brand#25 |LARGE ANODIZED STEEL | 9| 16 +Brand#25 |LARGE ANODIZED TIN | 45| 16 +Brand#25 |LARGE BRUSHED NICKEL | 36| 16 +Brand#25 |LARGE BURNISHED NICKEL | 14| 16 +Brand#25 |LARGE POLISHED STEEL | 19| 16 +Brand#25 |MEDIUM BRUSHED COPPER | 9| 16 +Brand#25 |MEDIUM BURNISHED COPPER | 49| 16 +Brand#25 |MEDIUM BURNISHED TIN | 3| 16 +Brand#25 |MEDIUM PLATED STEEL | 9| 16 +Brand#25 |PROMO ANODIZED BRASS | 49| 16 +Brand#25 |PROMO ANODIZED STEEL | 19| 16 +Brand#25 |PROMO ANODIZED TIN | 23| 16 +Brand#25 |PROMO BURNISHED COPPER | 49| 16 +Brand#25 |PROMO POLISHED COPPER | 14| 16 +Brand#25 |SMALL ANODIZED COPPER | 23| 16 +Brand#25 |SMALL BRUSHED STEEL | 23| 16 +Brand#25 |SMALL POLISHED COPPER | 23| 16 +Brand#25 |STANDARD BURNISHED STEEL | 23| 16 +Brand#25 |STANDARD BURNISHED TIN | 3| 16 +Brand#25 |STANDARD BURNISHED TIN | 36| 16 +Brand#25 |STANDARD PLATED BRASS | 45| 16 +Brand#25 |STANDARD PLATED COPPER | 49| 16 +Brand#31 |ECONOMY ANODIZED BRASS | 45| 16 +Brand#31 |ECONOMY BRUSHED COPPER | 14| 16 +Brand#31 |ECONOMY BRUSHED COPPER | 36| 16 +Brand#31 |LARGE ANODIZED STEEL | 45| 16 +Brand#31 |LARGE BURNISHED NICKEL | 45| 16 +Brand#31 |LARGE PLATED TIN | 14| 16 +Brand#31 |LARGE POLISHED COPPER | 49| 16 +Brand#31 |MEDIUM ANODIZED NICKEL | 49| 16 +Brand#31 |MEDIUM BURNISHED BRASS | 19| 16 +Brand#31 |PROMO ANODIZED NICKEL | 14| 16 +Brand#31 |PROMO BRUSHED TIN | 45| 16 +Brand#31 |PROMO BURNISHED STEEL | 36| 16 +Brand#31 |SMALL ANODIZED NICKEL | 23| 16 +Brand#31 |SMALL BRUSHED NICKEL | 14| 16 +Brand#31 |SMALL BRUSHED TIN | 19| 16 +Brand#31 |SMALL PLATED NICKEL | 23| 16 +Brand#31 |SMALL POLISHED BRASS | 23| 16 +Brand#31 |SMALL POLISHED TIN | 14| 16 +Brand#31 |SMALL POLISHED TIN | 45| 16 +Brand#31 |STANDARD BRUSHED COPPER | 45| 16 +Brand#31 |STANDARD POLISHED STEEL | 36| 16 +Brand#32 |ECONOMY BRUSHED STEEL | 9| 16 +Brand#32 |ECONOMY PLATED STEEL | 14| 16 +Brand#32 |LARGE ANODIZED BRASS | 36| 16 +Brand#32 |LARGE BURNISHED NICKEL | 36| 16 +Brand#32 |LARGE PLATED BRASS | 36| 16 +Brand#32 |LARGE PLATED STEEL | 23| 16 +Brand#32 |MEDIUM BRUSHED BRASS | 49| 16 +Brand#32 |MEDIUM BRUSHED TIN | 9| 16 +Brand#32 |MEDIUM PLATED COPPER | 36| 16 +Brand#32 |PROMO ANODIZED TIN | 36| 16 +Brand#32 |PROMO BRUSHED BRASS | 9| 16 +Brand#32 |PROMO BURNISHED STEEL | 36| 16 +Brand#32 |PROMO PLATED STEEL | 3| 16 +Brand#32 |PROMO PLATED TIN | 45| 16 +Brand#32 |SMALL BURNISHED TIN | 49| 16 +Brand#32 |SMALL PLATED NICKEL | 36| 16 +Brand#32 |SMALL POLISHED NICKEL | 36| 16 +Brand#32 |SMALL POLISHED STEEL | 9| 16 +Brand#32 |SMALL POLISHED TIN | 36| 16 +Brand#32 |STANDARD ANODIZED COPPER | 14| 16 +Brand#32 |STANDARD ANODIZED TIN | 9| 16 +Brand#32 |STANDARD BURNISHED COPPER| 45| 16 +Brand#32 |STANDARD BURNISHED COPPER| 49| 16 +Brand#32 |STANDARD POLISHED BRASS | 14| 16 +Brand#32 |STANDARD POLISHED STEEL | 14| 16 +Brand#33 |ECONOMY ANODIZED STEEL | 49| 16 +Brand#33 |ECONOMY PLATED BRASS | 36| 16 +Brand#33 |ECONOMY PLATED COPPER | 19| 16 +Brand#33 |ECONOMY POLISHED NICKEL | 19| 16 +Brand#33 |LARGE ANODIZED STEEL | 45| 16 +Brand#33 |LARGE ANODIZED TIN | 45| 16 +Brand#33 |LARGE BURNISHED COPPER | 45| 16 +Brand#33 |LARGE POLISHED STEEL | 3| 16 +Brand#33 |MEDIUM ANODIZED BRASS | 23| 16 +Brand#33 |MEDIUM ANODIZED NICKEL | 3| 16 +Brand#33 |MEDIUM ANODIZED TIN | 14| 16 +Brand#33 |MEDIUM BRUSHED COPPER | 49| 16 +Brand#33 |MEDIUM BURNISHED COPPER | 9| 16 +Brand#33 |PROMO BURNISHED BRASS | 9| 16 +Brand#33 |PROMO BURNISHED BRASS | 19| 16 +Brand#33 |PROMO PLATED STEEL | 49| 16 +Brand#33 |SMALL ANODIZED BRASS | 36| 16 +Brand#33 |SMALL BRUSHED BRASS | 3| 16 +Brand#33 |SMALL BRUSHED STEEL | 9| 16 +Brand#33 |SMALL POLISHED BRASS | 14| 16 +Brand#33 |SMALL POLISHED COPPER | 36| 16 +Brand#33 |SMALL POLISHED NICKEL | 19| 16 +Brand#33 |STANDARD ANODIZED BRASS | 9| 16 +Brand#33 |STANDARD ANODIZED TIN | 3| 16 +Brand#33 |STANDARD BURNISHED NICKEL| 49| 16 +Brand#33 |STANDARD PLATED NICKEL | 49| 16 +Brand#33 |STANDARD POLISHED BRASS | 9| 16 +Brand#33 |STANDARD POLISHED BRASS | 14| 16 +Brand#33 |STANDARD POLISHED COPPER | 49| 16 +Brand#33 |STANDARD POLISHED STEEL | 3| 16 +Brand#34 |ECONOMY BURNISHED BRASS | 14| 16 +Brand#34 |ECONOMY POLISHED STEEL | 36| 16 +Brand#34 |LARGE BRUSHED BRASS | 23| 16 +Brand#34 |LARGE PLATED BRASS | 36| 16 +Brand#34 |LARGE PLATED TIN | 3| 16 +Brand#34 |LARGE POLISHED COPPER | 14| 16 +Brand#34 |MEDIUM ANODIZED COPPER | 36| 16 +Brand#34 |MEDIUM BRUSHED STEEL | 23| 16 +Brand#34 |MEDIUM PLATED NICKEL | 23| 16 +Brand#34 |PROMO BRUSHED NICKEL | 45| 16 +Brand#34 |PROMO POLISHED TIN | 3| 16 +Brand#34 |SMALL ANODIZED NICKEL | 14| 16 +Brand#34 |SMALL BURNISHED TIN | 3| 16 +Brand#34 |SMALL POLISHED NICKEL | 36| 16 +Brand#34 |STANDARD ANODIZED STEEL | 9| 16 +Brand#34 |STANDARD BURNISHED NICKEL| 19| 16 +Brand#34 |STANDARD BURNISHED NICKEL| 23| 16 +Brand#34 |STANDARD POLISHED COPPER | 23| 16 +Brand#35 |ECONOMY ANODIZED COPPER | 36| 16 +Brand#35 |ECONOMY BURNISHED NICKEL | 19| 16 +Brand#35 |ECONOMY BURNISHED TIN | 9| 16 +Brand#35 |ECONOMY PLATED STEEL | 14| 16 +Brand#35 |LARGE ANODIZED BRASS | 9| 16 +Brand#35 |LARGE ANODIZED COPPER | 49| 16 +Brand#35 |LARGE ANODIZED NICKEL | 9| 16 +Brand#35 |LARGE BRUSHED TIN | 49| 16 +Brand#35 |LARGE BURNISHED COPPER | 23| 16 +Brand#35 |LARGE BURNISHED NICKEL | 9| 16 +Brand#35 |LARGE BURNISHED STEEL | 3| 16 +Brand#35 |LARGE PLATED COPPER | 19| 16 +Brand#35 |MEDIUM BRUSHED STEEL | 23| 16 +Brand#35 |MEDIUM PLATED NICKEL | 23| 16 +Brand#35 |PROMO BRUSHED NICKEL | 19| 16 +Brand#35 |SMALL ANODIZED BRASS | 45| 16 +Brand#35 |SMALL BRUSHED TIN | 49| 16 +Brand#41 |ECONOMY ANODIZED STEEL | 49| 16 +Brand#41 |ECONOMY PLATED STEEL | 3| 16 +Brand#41 |ECONOMY PLATED TIN | 3| 16 +Brand#41 |ECONOMY POLISHED STEEL | 19| 16 +Brand#41 |ECONOMY POLISHED STEEL | 45| 16 +Brand#41 |LARGE ANODIZED BRASS | 36| 16 +Brand#41 |LARGE BURNISHED BRASS | 23| 16 +Brand#41 |LARGE POLISHED BRASS | 36| 16 +Brand#41 |LARGE POLISHED NICKEL | 3| 16 +Brand#41 |MEDIUM BURNISHED TIN | 3| 16 +Brand#41 |MEDIUM PLATED STEEL | 3| 16 +Brand#41 |PROMO PLATED BRASS | 9| 16 +Brand#41 |PROMO PLATED STEEL | 36| 16 +Brand#41 |PROMO POLISHED STEEL | 36| 16 +Brand#41 |PROMO POLISHED TIN | 19| 16 +Brand#41 |SMALL ANODIZED COPPER | 23| 16 +Brand#41 |SMALL ANODIZED STEEL | 45| 16 +Brand#41 |SMALL BRUSHED NICKEL | 45| 16 +Brand#41 |SMALL BURNISHED NICKEL | 36| 16 +Brand#41 |SMALL POLISHED NICKEL | 9| 16 +Brand#41 |SMALL POLISHED STEEL | 45| 16 +Brand#41 |SMALL POLISHED TIN | 14| 16 +Brand#41 |STANDARD BRUSHED NICKEL | 45| 16 +Brand#42 |ECONOMY BRUSHED STEEL | 14| 16 +Brand#42 |ECONOMY BURNISHED STEEL | 9| 16 +Brand#42 |ECONOMY BURNISHED STEEL | 45| 16 +Brand#42 |LARGE ANODIZED TIN | 23| 16 +Brand#42 |LARGE BRUSHED STEEL | 14| 16 +Brand#42 |LARGE BURNISHED NICKEL | 19| 16 +Brand#42 |LARGE PLATED STEEL | 45| 16 +Brand#42 |LARGE POLISHED STEEL | 14| 16 +Brand#42 |MEDIUM ANODIZED STEEL | 14| 16 +Brand#42 |MEDIUM ANODIZED TIN | 19| 16 +Brand#42 |MEDIUM BRUSHED COPPER | 9| 16 +Brand#42 |MEDIUM BRUSHED STEEL | 14| 16 +Brand#42 |MEDIUM BURNISHED COPPER | 49| 16 +Brand#42 |MEDIUM BURNISHED NICKEL | 23| 16 +Brand#42 |MEDIUM BURNISHED TIN | 49| 16 +Brand#42 |PROMO ANODIZED NICKEL | 49| 16 +Brand#42 |PROMO ANODIZED STEEL | 49| 16 +Brand#42 |PROMO BURNISHED TIN | 49| 16 +Brand#42 |SMALL ANODIZED BRASS | 23| 16 +Brand#42 |SMALL ANODIZED NICKEL | 19| 16 +Brand#42 |SMALL ANODIZED TIN | 49| 16 +Brand#42 |SMALL PLATED COPPER | 23| 16 +Brand#42 |STANDARD ANODIZED BRASS | 9| 16 +Brand#42 |STANDARD ANODIZED NICKEL | 9| 16 +Brand#42 |STANDARD BRUSHED STEEL | 49| 16 +Brand#42 |STANDARD BRUSHED TIN | 45| 16 +Brand#42 |STANDARD PLATED TIN | 23| 16 +Brand#43 |ECONOMY BRUSHED STEEL | 23| 16 +Brand#43 |ECONOMY PLATED TIN | 49| 16 +Brand#43 |ECONOMY POLISHED TIN | 14| 16 +Brand#43 |LARGE BRUSHED COPPER | 9| 16 +Brand#43 |LARGE BURNISHED STEEL | 9| 16 +Brand#43 |LARGE PLATED BRASS | 14| 16 +Brand#43 |LARGE PLATED BRASS | 19| 16 +Brand#43 |LARGE PLATED NICKEL | 45| 16 +Brand#43 |MEDIUM ANODIZED COPPER | 49| 16 +Brand#43 |PROMO BRUSHED BRASS | 36| 16 +Brand#43 |PROMO BRUSHED STEEL | 49| 16 +Brand#43 |PROMO PLATED BRASS | 45| 16 +Brand#43 |SMALL BURNISHED COPPER | 19| 16 +Brand#43 |SMALL BURNISHED TIN | 23| 16 +Brand#43 |SMALL BURNISHED TIN | 45| 16 +Brand#43 |SMALL PLATED COPPER | 23| 16 +Brand#43 |SMALL POLISHED STEEL | 19| 16 +Brand#43 |STANDARD ANODIZED TIN | 45| 16 +Brand#43 |STANDARD PLATED BRASS | 3| 16 +Brand#44 |ECONOMY ANODIZED BRASS | 45| 16 +Brand#44 |ECONOMY BRUSHED TIN | 45| 16 +Brand#44 |ECONOMY PLATED COPPER | 23| 16 +Brand#44 |ECONOMY PLATED STEEL | 3| 16 +Brand#44 |LARGE BRUSHED BRASS | 9| 16 +Brand#44 |LARGE PLATED BRASS | 49| 16 +Brand#44 |LARGE PLATED STEEL | 14| 16 +Brand#44 |LARGE POLISHED TIN | 19| 16 +Brand#44 |MEDIUM ANODIZED NICKEL | 9| 16 +Brand#44 |MEDIUM ANODIZED TIN | 49| 16 +Brand#44 |MEDIUM BRUSHED NICKEL | 36| 16 +Brand#44 |MEDIUM BURNISHED NICKEL | 23| 16 +Brand#44 |MEDIUM BURNISHED NICKEL | 45| 16 +Brand#44 |MEDIUM PLATED BRASS | 9| 16 +Brand#44 |MEDIUM PLATED STEEL | 49| 16 +Brand#44 |PROMO BURNISHED TIN | 3| 16 +Brand#44 |SMALL ANODIZED COPPER | 9| 16 +Brand#44 |SMALL ANODIZED STEEL | 14| 16 +Brand#44 |SMALL BRUSHED STEEL | 19| 16 +Brand#44 |SMALL BRUSHED TIN | 14| 16 +Brand#44 |SMALL BURNISHED STEEL | 23| 16 +Brand#44 |SMALL PLATED STEEL | 19| 16 +Brand#44 |STANDARD ANODIZED NICKEL | 45| 16 +Brand#44 |STANDARD ANODIZED STEEL | 19| 16 +Brand#44 |STANDARD BRUSHED COPPER | 36| 16 +Brand#44 |STANDARD PLATED BRASS | 49| 16 +Brand#44 |STANDARD PLATED NICKEL | 45| 16 +Brand#44 |STANDARD PLATED STEEL | 36| 16 +Brand#51 |ECONOMY ANODIZED STEEL | 9| 16 +Brand#51 |ECONOMY BRUSHED STEEL | 23| 16 +Brand#51 |ECONOMY PLATED STEEL | 9| 16 +Brand#51 |LARGE BURNISHED COPPER | 14| 16 +Brand#51 |LARGE PLATED BRASS | 3| 16 +Brand#51 |LARGE PLATED BRASS | 36| 16 +Brand#51 |LARGE PLATED BRASS | 49| 16 +Brand#51 |LARGE POLISHED BRASS | 3| 16 +Brand#51 |LARGE POLISHED NICKEL | 19| 16 +Brand#51 |MEDIUM ANODIZED BRASS | 9| 16 +Brand#51 |MEDIUM ANODIZED TIN | 9| 16 +Brand#51 |MEDIUM PLATED BRASS | 14| 16 +Brand#51 |PROMO BURNISHED NICKEL | 14| 16 +Brand#51 |PROMO BURNISHED TIN | 9| 16 +Brand#51 |PROMO PLATED NICKEL | 14| 16 +Brand#51 |SMALL ANODIZED COPPER | 45| 16 +Brand#51 |SMALL BURNISHED COPPER | 36| 16 +Brand#51 |SMALL BURNISHED TIN | 9| 16 +Brand#51 |STANDARD BURNISHED STEEL | 45| 16 +Brand#51 |STANDARD BURNISHED TIN | 9| 16 +Brand#51 |STANDARD PLATED BRASS | 36| 16 +Brand#51 |STANDARD PLATED STEEL | 45| 16 +Brand#52 |ECONOMY BRUSHED NICKEL | 3| 16 +Brand#52 |ECONOMY BURNISHED COPPER | 9| 16 +Brand#52 |ECONOMY BURNISHED STEEL | 14| 16 +Brand#52 |LARGE ANODIZED BRASS | 23| 16 +Brand#52 |LARGE BRUSHED BRASS | 14| 16 +Brand#52 |LARGE BURNISHED TIN | 23| 16 +Brand#52 |MEDIUM ANODIZED COPPER | 23| 16 +Brand#52 |PROMO BRUSHED STEEL | 36| 16 +Brand#52 |PROMO PLATED COPPER | 14| 16 +Brand#52 |SMALL PLATED COPPER | 3| 16 +Brand#52 |STANDARD BRUSHED COPPER | 14| 16 +Brand#52 |STANDARD BURNISHED BRASS | 14| 16 +Brand#52 |STANDARD BURNISHED BRASS | 19| 16 +Brand#52 |STANDARD POLISHED NICKEL | 36| 16 +Brand#53 |ECONOMY ANODIZED BRASS | 19| 16 +Brand#53 |LARGE BRUSHED COPPER | 14| 16 +Brand#53 |LARGE BRUSHED NICKEL | 45| 16 +Brand#53 |LARGE BURNISHED COPPER | 36| 16 +Brand#53 |LARGE PLATED COPPER | 36| 16 +Brand#53 |LARGE PLATED STEEL | 36| 16 +Brand#53 |LARGE PLATED TIN | 14| 16 +Brand#53 |LARGE POLISHED BRASS | 14| 16 +Brand#53 |LARGE POLISHED STEEL | 49| 16 +Brand#53 |MEDIUM BRUSHED NICKEL | 49| 16 +Brand#53 |MEDIUM BURNISHED BRASS | 3| 16 +Brand#53 |MEDIUM BURNISHED COPPER | 49| 16 +Brand#53 |PROMO ANODIZED COPPER | 36| 16 +Brand#53 |PROMO ANODIZED NICKEL | 3| 16 +Brand#53 |PROMO BURNISHED STEEL | 9| 16 +Brand#53 |PROMO PLATED COPPER | 3| 16 +Brand#53 |SMALL ANODIZED TIN | 9| 16 +Brand#53 |STANDARD PLATED BRASS | 23| 16 +Brand#54 |ECONOMY BRUSHED BRASS | 45| 16 +Brand#54 |ECONOMY BRUSHED COPPER | 14| 16 +Brand#54 |LARGE ANODIZED NICKEL | 49| 16 +Brand#54 |LARGE BURNISHED BRASS | 49| 16 +Brand#54 |LARGE BURNISHED COPPER | 19| 16 +Brand#54 |LARGE POLISHED NICKEL | 36| 16 +Brand#54 |PROMO BURNISHED TIN | 19| 16 +Brand#54 |PROMO PLATED BRASS | 49| 16 +Brand#54 |PROMO POLISHED TIN | 23| 16 +Brand#54 |SMALL ANODIZED COPPER | 14| 16 +Brand#54 |SMALL BRUSHED COPPER | 9| 16 +Brand#54 |SMALL PLATED NICKEL | 9| 16 +Brand#54 |STANDARD ANODIZED COPPER | 49| 16 +Brand#54 |STANDARD ANODIZED TIN | 14| 16 +Brand#54 |STANDARD BRUSHED COPPER | 45| 16 +Brand#54 |STANDARD PLATED COPPER | 23| 16 +Brand#54 |STANDARD PLATED COPPER | 45| 16 +Brand#54 |STANDARD POLISHED BRASS | 19| 16 +Brand#54 |STANDARD POLISHED STEEL | 14| 16 +Brand#55 |ECONOMY BRUSHED TIN | 36| 16 +Brand#55 |ECONOMY POLISHED TIN | 14| 16 +Brand#55 |LARGE PLATED BRASS | 9| 16 +Brand#55 |LARGE POLISHED STEEL | 9| 16 +Brand#55 |MEDIUM BURNISHED TIN | 36| 16 +Brand#55 |PROMO ANODIZED BRASS | 14| 16 +Brand#55 |PROMO ANODIZED COPPER | 14| 16 +Brand#55 |SMALL BURNISHED STEEL | 9| 16 +Brand#55 |STANDARD POLISHED COPPER | 19| 16 +Brand#23 |PROMO POLISHED COPPER | 36| 15 +Brand#33 |PROMO POLISHED STEEL | 9| 15 +Brand#34 |LARGE BURNISHED BRASS | 23| 15 +Brand#41 |PROMO ANODIZED BRASS | 49| 15 +Brand#11 |ECONOMY ANODIZED NICKEL | 14| 12 +Brand#11 |ECONOMY ANODIZED NICKEL | 23| 12 +Brand#11 |ECONOMY ANODIZED STEEL | 36| 12 +Brand#11 |ECONOMY ANODIZED TIN | 14| 12 +Brand#11 |ECONOMY BRUSHED COPPER | 14| 12 +Brand#11 |ECONOMY BURNISHED BRASS | 36| 12 +Brand#11 |ECONOMY BURNISHED COPPER | 3| 12 +Brand#11 |ECONOMY BURNISHED COPPER | 49| 12 +Brand#11 |ECONOMY PLATED COPPER | 3| 12 +Brand#11 |ECONOMY PLATED COPPER | 19| 12 +Brand#11 |ECONOMY PLATED NICKEL | 14| 12 +Brand#11 |ECONOMY POLISHED COPPER | 14| 12 +Brand#11 |ECONOMY POLISHED TIN | 23| 12 +Brand#11 |LARGE ANODIZED NICKEL | 9| 12 +Brand#11 |LARGE ANODIZED STEEL | 23| 12 +Brand#11 |LARGE ANODIZED TIN | 36| 12 +Brand#11 |LARGE BRUSHED BRASS | 19| 12 +Brand#11 |LARGE BRUSHED STEEL | 19| 12 +Brand#11 |LARGE BRUSHED STEEL | 36| 12 +Brand#11 |LARGE BURNISHED BRASS | 3| 12 +Brand#11 |LARGE PLATED TIN | 19| 12 +Brand#11 |MEDIUM ANODIZED BRASS | 45| 12 +Brand#11 |MEDIUM BRUSHED BRASS | 3| 12 +Brand#11 |MEDIUM BRUSHED BRASS | 23| 12 +Brand#11 |MEDIUM BRUSHED BRASS | 45| 12 +Brand#11 |MEDIUM BRUSHED NICKEL | 36| 12 +Brand#11 |MEDIUM BRUSHED STEEL | 19| 12 +Brand#11 |MEDIUM BRUSHED STEEL | 23| 12 +Brand#11 |MEDIUM BURNISHED NICKEL | 23| 12 +Brand#11 |MEDIUM BURNISHED STEEL | 9| 12 +Brand#11 |MEDIUM PLATED BRASS | 14| 12 +Brand#11 |MEDIUM PLATED COPPER | 3| 12 +Brand#11 |MEDIUM PLATED STEEL | 14| 12 +Brand#11 |PROMO ANODIZED BRASS | 45| 12 +Brand#11 |PROMO BRUSHED NICKEL | 9| 12 +Brand#11 |PROMO BRUSHED STEEL | 45| 12 +Brand#11 |PROMO BURNISHED BRASS | 23| 12 +Brand#11 |PROMO BURNISHED COPPER | 23| 12 +Brand#11 |PROMO BURNISHED NICKEL | 36| 12 +Brand#11 |PROMO PLATED BRASS | 14| 12 +Brand#11 |PROMO PLATED COPPER | 14| 12 +Brand#11 |PROMO PLATED STEEL | 49| 12 +Brand#11 |PROMO PLATED TIN | 3| 12 +Brand#11 |PROMO POLISHED COPPER | 14| 12 +Brand#11 |PROMO POLISHED NICKEL | 3| 12 +Brand#11 |PROMO POLISHED STEEL | 3| 12 +Brand#11 |PROMO POLISHED STEEL | 23| 12 +Brand#11 |PROMO POLISHED TIN | 14| 12 +Brand#11 |SMALL ANODIZED BRASS | 49| 12 +Brand#11 |SMALL ANODIZED COPPER | 49| 12 +Brand#11 |SMALL ANODIZED NICKEL | 9| 12 +Brand#11 |SMALL ANODIZED STEEL | 45| 12 +Brand#11 |SMALL BURNISHED BRASS | 19| 12 +Brand#11 |SMALL BURNISHED BRASS | 49| 12 +Brand#11 |SMALL BURNISHED NICKEL | 9| 12 +Brand#11 |SMALL BURNISHED NICKEL | 49| 12 +Brand#11 |SMALL PLATED COPPER | 45| 12 +Brand#11 |SMALL PLATED NICKEL | 45| 12 +Brand#11 |SMALL PLATED TIN | 36| 12 +Brand#11 |SMALL POLISHED BRASS | 14| 12 +Brand#11 |SMALL POLISHED BRASS | 19| 12 +Brand#11 |SMALL POLISHED STEEL | 3| 12 +Brand#11 |SMALL POLISHED STEEL | 36| 12 +Brand#11 |STANDARD ANODIZED COPPER | 49| 12 +Brand#11 |STANDARD BRUSHED COPPER | 23| 12 +Brand#11 |STANDARD BRUSHED NICKEL | 9| 12 +Brand#11 |STANDARD BURNISHED BRASS | 19| 12 +Brand#11 |STANDARD BURNISHED COPPER| 9| 12 +Brand#11 |STANDARD PLATED STEEL | 19| 12 +Brand#11 |STANDARD PLATED TIN | 45| 12 +Brand#11 |STANDARD POLISHED STEEL | 9| 12 +Brand#11 |STANDARD POLISHED STEEL | 19| 12 +Brand#11 |STANDARD POLISHED TIN | 14| 12 +Brand#12 |ECONOMY ANODIZED BRASS | 49| 12 +Brand#12 |ECONOMY ANODIZED COPPER | 14| 12 +Brand#12 |ECONOMY ANODIZED NICKEL | 19| 12 +Brand#12 |ECONOMY ANODIZED NICKEL | 45| 12 +Brand#12 |ECONOMY BRUSHED BRASS | 23| 12 +Brand#12 |ECONOMY BRUSHED STEEL | 9| 12 +Brand#12 |ECONOMY BRUSHED TIN | 3| 12 +Brand#12 |ECONOMY BRUSHED TIN | 19| 12 +Brand#12 |ECONOMY BURNISHED BRASS | 19| 12 +Brand#12 |ECONOMY BURNISHED COPPER | 49| 12 +Brand#12 |ECONOMY BURNISHED STEEL | 9| 12 +Brand#12 |ECONOMY BURNISHED STEEL | 36| 12 +Brand#12 |ECONOMY PLATED BRASS | 3| 12 +Brand#12 |ECONOMY PLATED NICKEL | 9| 12 +Brand#12 |ECONOMY PLATED TIN | 45| 12 +Brand#12 |ECONOMY POLISHED NICKEL | 45| 12 +Brand#12 |ECONOMY POLISHED STEEL | 9| 12 +Brand#12 |ECONOMY POLISHED STEEL | 19| 12 +Brand#12 |ECONOMY POLISHED TIN | 14| 12 +Brand#12 |LARGE ANODIZED COPPER | 19| 12 +Brand#12 |LARGE ANODIZED NICKEL | 49| 12 +Brand#12 |LARGE ANODIZED TIN | 49| 12 +Brand#12 |LARGE BRUSHED BRASS | 9| 12 +Brand#12 |LARGE BRUSHED BRASS | 23| 12 +Brand#12 |LARGE BRUSHED BRASS | 49| 12 +Brand#12 |LARGE BURNISHED NICKEL | 45| 12 +Brand#12 |LARGE PLATED BRASS | 3| 12 +Brand#12 |LARGE POLISHED BRASS | 23| 12 +Brand#12 |LARGE POLISHED COPPER | 19| 12 +Brand#12 |MEDIUM ANODIZED BRASS | 3| 12 +Brand#12 |MEDIUM ANODIZED COPPER | 9| 12 +Brand#12 |MEDIUM BRUSHED BRASS | 14| 12 +Brand#12 |MEDIUM BRUSHED BRASS | 23| 12 +Brand#12 |MEDIUM BRUSHED BRASS | 45| 12 +Brand#12 |MEDIUM BRUSHED COPPER | 23| 12 +Brand#12 |MEDIUM BRUSHED NICKEL | 14| 12 +Brand#12 |MEDIUM BRUSHED TIN | 14| 12 +Brand#12 |MEDIUM BRUSHED TIN | 36| 12 +Brand#12 |MEDIUM BURNISHED BRASS | 19| 12 +Brand#12 |MEDIUM PLATED BRASS | 23| 12 +Brand#12 |MEDIUM PLATED NICKEL | 45| 12 +Brand#12 |MEDIUM PLATED STEEL | 19| 12 +Brand#12 |MEDIUM PLATED TIN | 23| 12 +Brand#12 |PROMO BRUSHED COPPER | 36| 12 +Brand#12 |PROMO BRUSHED STEEL | 19| 12 +Brand#12 |PROMO BRUSHED STEEL | 45| 12 +Brand#12 |PROMO PLATED COPPER | 14| 12 +Brand#12 |PROMO PLATED STEEL | 19| 12 +Brand#12 |PROMO POLISHED COPPER | 45| 12 +Brand#12 |PROMO POLISHED STEEL | 45| 12 +Brand#12 |PROMO POLISHED TIN | 3| 12 +Brand#12 |PROMO POLISHED TIN | 14| 12 +Brand#12 |SMALL ANODIZED BRASS | 9| 12 +Brand#12 |SMALL ANODIZED STEEL | 14| 12 +Brand#12 |SMALL BRUSHED BRASS | 36| 12 +Brand#12 |SMALL BRUSHED NICKEL | 3| 12 +Brand#12 |SMALL BRUSHED NICKEL | 9| 12 +Brand#12 |SMALL BURNISHED BRASS | 14| 12 +Brand#12 |SMALL BURNISHED BRASS | 23| 12 +Brand#12 |SMALL BURNISHED TIN | 14| 12 +Brand#12 |SMALL POLISHED NICKEL | 23| 12 +Brand#12 |STANDARD ANODIZED COPPER | 45| 12 +Brand#12 |STANDARD BRUSHED COPPER | 3| 12 +Brand#12 |STANDARD BRUSHED NICKEL | 23| 12 +Brand#12 |STANDARD BRUSHED STEEL | 3| 12 +Brand#12 |STANDARD BRUSHED TIN | 45| 12 +Brand#12 |STANDARD BURNISHED BRASS | 14| 12 +Brand#12 |STANDARD BURNISHED COPPER| 3| 12 +Brand#12 |STANDARD BURNISHED COPPER| 45| 12 +Brand#12 |STANDARD BURNISHED STEEL | 9| 12 +Brand#12 |STANDARD BURNISHED TIN | 3| 12 +Brand#12 |STANDARD PLATED COPPER | 49| 12 +Brand#12 |STANDARD PLATED NICKEL | 19| 12 +Brand#12 |STANDARD PLATED NICKEL | 45| 12 +Brand#12 |STANDARD PLATED STEEL | 19| 12 +Brand#12 |STANDARD PLATED STEEL | 36| 12 +Brand#12 |STANDARD POLISHED BRASS | 45| 12 +Brand#13 |ECONOMY ANODIZED BRASS | 36| 12 +Brand#13 |ECONOMY ANODIZED BRASS | 45| 12 +Brand#13 |ECONOMY ANODIZED COPPER | 14| 12 +Brand#13 |ECONOMY ANODIZED NICKEL | 14| 12 +Brand#13 |ECONOMY ANODIZED NICKEL | 19| 12 +Brand#13 |ECONOMY ANODIZED TIN | 23| 12 +Brand#13 |ECONOMY BRUSHED BRASS | 45| 12 +Brand#13 |ECONOMY BRUSHED NICKEL | 45| 12 +Brand#13 |ECONOMY BURNISHED BRASS | 3| 12 +Brand#13 |ECONOMY BURNISHED COPPER | 19| 12 +Brand#13 |ECONOMY BURNISHED NICKEL | 36| 12 +Brand#13 |ECONOMY PLATED COPPER | 49| 12 +Brand#13 |ECONOMY PLATED NICKEL | 3| 12 +Brand#13 |ECONOMY PLATED NICKEL | 19| 12 +Brand#13 |ECONOMY PLATED STEEL | 23| 12 +Brand#13 |ECONOMY POLISHED STEEL | 19| 12 +Brand#13 |ECONOMY POLISHED STEEL | 36| 12 +Brand#13 |LARGE ANODIZED BRASS | 49| 12 +Brand#13 |LARGE ANODIZED TIN | 9| 12 +Brand#13 |LARGE ANODIZED TIN | 19| 12 +Brand#13 |LARGE BRUSHED BRASS | 3| 12 +Brand#13 |LARGE BRUSHED COPPER | 9| 12 +Brand#13 |LARGE BRUSHED NICKEL | 3| 12 +Brand#13 |LARGE BURNISHED COPPER | 45| 12 +Brand#13 |LARGE PLATED COPPER | 23| 12 +Brand#13 |LARGE PLATED COPPER | 36| 12 +Brand#13 |LARGE PLATED NICKEL | 23| 12 +Brand#13 |LARGE PLATED NICKEL | 49| 12 +Brand#13 |LARGE PLATED STEEL | 14| 12 +Brand#13 |LARGE PLATED TIN | 9| 12 +Brand#13 |LARGE POLISHED BRASS | 49| 12 +Brand#13 |LARGE POLISHED STEEL | 9| 12 +Brand#13 |MEDIUM ANODIZED NICKEL | 3| 12 +Brand#13 |MEDIUM ANODIZED NICKEL | 36| 12 +Brand#13 |MEDIUM ANODIZED NICKEL | 45| 12 +Brand#13 |MEDIUM ANODIZED STEEL | 9| 12 +Brand#13 |MEDIUM ANODIZED STEEL | 14| 12 +Brand#13 |MEDIUM BRUSHED BRASS | 9| 12 +Brand#13 |MEDIUM BRUSHED COPPER | 3| 12 +Brand#13 |MEDIUM BRUSHED COPPER | 14| 12 +Brand#13 |MEDIUM BRUSHED STEEL | 19| 12 +Brand#13 |MEDIUM BRUSHED TIN | 19| 12 +Brand#13 |MEDIUM BURNISHED NICKEL | 36| 12 +Brand#13 |MEDIUM PLATED BRASS | 9| 12 +Brand#13 |PROMO ANODIZED COPPER | 45| 12 +Brand#13 |PROMO BRUSHED NICKEL | 23| 12 +Brand#13 |PROMO BRUSHED STEEL | 45| 12 +Brand#13 |PROMO BRUSHED TIN | 3| 12 +Brand#13 |PROMO BURNISHED BRASS | 19| 12 +Brand#13 |PROMO BURNISHED COPPER | 19| 12 +Brand#13 |PROMO BURNISHED NICKEL | 3| 12 +Brand#13 |PROMO BURNISHED NICKEL | 49| 12 +Brand#13 |PROMO PLATED COPPER | 3| 12 +Brand#13 |PROMO PLATED NICKEL | 3| 12 +Brand#13 |PROMO PLATED STEEL | 45| 12 +Brand#13 |PROMO POLISHED NICKEL | 3| 12 +Brand#13 |PROMO POLISHED STEEL | 14| 12 +Brand#13 |SMALL ANODIZED BRASS | 49| 12 +Brand#13 |SMALL ANODIZED COPPER | 36| 12 +Brand#13 |SMALL ANODIZED TIN | 9| 12 +Brand#13 |SMALL ANODIZED TIN | 23| 12 +Brand#13 |SMALL BRUSHED COPPER | 14| 12 +Brand#13 |SMALL BRUSHED COPPER | 45| 12 +Brand#13 |SMALL BURNISHED NICKEL | 3| 12 +Brand#13 |SMALL PLATED BRASS | 45| 12 +Brand#13 |SMALL PLATED NICKEL | 45| 12 +Brand#13 |SMALL PLATED TIN | 14| 12 +Brand#13 |SMALL POLISHED BRASS | 49| 12 +Brand#13 |SMALL POLISHED NICKEL | 19| 12 +Brand#13 |STANDARD BRUSHED BRASS | 14| 12 +Brand#13 |STANDARD BRUSHED COPPER | 23| 12 +Brand#13 |STANDARD BURNISHED COPPER| 3| 12 +Brand#13 |STANDARD BURNISHED COPPER| 23| 12 +Brand#13 |STANDARD BURNISHED COPPER| 45| 12 +Brand#13 |STANDARD BURNISHED STEEL | 3| 12 +Brand#13 |STANDARD BURNISHED STEEL | 19| 12 +Brand#13 |STANDARD BURNISHED TIN | 23| 12 +Brand#13 |STANDARD PLATED BRASS | 14| 12 +Brand#13 |STANDARD PLATED COPPER | 45| 12 +Brand#13 |STANDARD PLATED NICKEL | 45| 12 +Brand#13 |STANDARD PLATED STEEL | 9| 12 +Brand#13 |STANDARD POLISHED BRASS | 19| 12 +Brand#13 |STANDARD POLISHED NICKEL | 19| 12 +Brand#14 |ECONOMY ANODIZED COPPER | 9| 12 +Brand#14 |ECONOMY ANODIZED NICKEL | 49| 12 +Brand#14 |ECONOMY ANODIZED STEEL | 45| 12 +Brand#14 |ECONOMY BRUSHED BRASS | 23| 12 +Brand#14 |ECONOMY BRUSHED COPPER | 19| 12 +Brand#14 |ECONOMY BRUSHED COPPER | 45| 12 +Brand#14 |ECONOMY BRUSHED NICKEL | 36| 12 +Brand#14 |ECONOMY BRUSHED TIN | 14| 12 +Brand#14 |ECONOMY BURNISHED COPPER | 9| 12 +Brand#14 |ECONOMY BURNISHED COPPER | 23| 12 +Brand#14 |ECONOMY BURNISHED STEEL | 9| 12 +Brand#14 |ECONOMY BURNISHED STEEL | 14| 12 +Brand#14 |ECONOMY PLATED BRASS | 9| 12 +Brand#14 |ECONOMY POLISHED BRASS | 19| 12 +Brand#14 |ECONOMY POLISHED COPPER | 23| 12 +Brand#14 |ECONOMY POLISHED STEEL | 45| 12 +Brand#14 |LARGE ANODIZED COPPER | 49| 12 +Brand#14 |LARGE ANODIZED NICKEL | 23| 12 +Brand#14 |LARGE ANODIZED NICKEL | 45| 12 +Brand#14 |LARGE ANODIZED STEEL | 9| 12 +Brand#14 |LARGE BRUSHED COPPER | 14| 12 +Brand#14 |LARGE BRUSHED TIN | 3| 12 +Brand#14 |LARGE BRUSHED TIN | 45| 12 +Brand#14 |LARGE BURNISHED COPPER | 49| 12 +Brand#14 |LARGE PLATED BRASS | 19| 12 +Brand#14 |LARGE PLATED COPPER | 3| 12 +Brand#14 |LARGE PLATED NICKEL | 36| 12 +Brand#14 |MEDIUM ANODIZED STEEL | 36| 12 +Brand#14 |MEDIUM BRUSHED BRASS | 9| 12 +Brand#14 |MEDIUM BRUSHED TIN | 19| 12 +Brand#14 |MEDIUM BURNISHED BRASS | 49| 12 +Brand#14 |MEDIUM BURNISHED COPPER | 14| 12 +Brand#14 |MEDIUM BURNISHED NICKEL | 36| 12 +Brand#14 |MEDIUM BURNISHED STEEL | 3| 12 +Brand#14 |MEDIUM BURNISHED STEEL | 19| 12 +Brand#14 |MEDIUM PLATED COPPER | 36| 12 +Brand#14 |MEDIUM PLATED TIN | 49| 12 +Brand#14 |PROMO ANODIZED NICKEL | 36| 12 +Brand#14 |PROMO BRUSHED COPPER | 14| 12 +Brand#14 |PROMO BURNISHED NICKEL | 14| 12 +Brand#14 |PROMO PLATED COPPER | 45| 12 +Brand#14 |PROMO PLATED NICKEL | 36| 12 +Brand#14 |PROMO PLATED STEEL | 9| 12 +Brand#14 |PROMO PLATED TIN | 19| 12 +Brand#14 |PROMO PLATED TIN | 45| 12 +Brand#14 |PROMO PLATED TIN | 49| 12 +Brand#14 |PROMO POLISHED BRASS | 9| 12 +Brand#14 |PROMO POLISHED COPPER | 14| 12 +Brand#14 |PROMO POLISHED NICKEL | 9| 12 +Brand#14 |SMALL ANODIZED NICKEL | 45| 12 +Brand#14 |SMALL ANODIZED TIN | 45| 12 +Brand#14 |SMALL BRUSHED NICKEL | 19| 12 +Brand#14 |SMALL BRUSHED TIN | 19| 12 +Brand#14 |SMALL BURNISHED STEEL | 9| 12 +Brand#14 |SMALL BURNISHED STEEL | 36| 12 +Brand#14 |SMALL PLATED BRASS | 23| 12 +Brand#14 |SMALL PLATED COPPER | 9| 12 +Brand#14 |SMALL PLATED STEEL | 23| 12 +Brand#14 |SMALL POLISHED BRASS | 3| 12 +Brand#14 |SMALL POLISHED BRASS | 9| 12 +Brand#14 |SMALL POLISHED COPPER | 36| 12 +Brand#14 |SMALL POLISHED NICKEL | 49| 12 +Brand#14 |SMALL POLISHED STEEL | 14| 12 +Brand#14 |SMALL POLISHED TIN | 49| 12 +Brand#14 |STANDARD ANODIZED STEEL | 49| 12 +Brand#14 |STANDARD BRUSHED BRASS | 3| 12 +Brand#14 |STANDARD BRUSHED STEEL | 49| 12 +Brand#14 |STANDARD BURNISHED BRASS | 23| 12 +Brand#14 |STANDARD PLATED NICKEL | 49| 12 +Brand#14 |STANDARD POLISHED COPPER | 36| 12 +Brand#14 |STANDARD POLISHED COPPER | 45| 12 +Brand#15 |ECONOMY ANODIZED TIN | 19| 12 +Brand#15 |ECONOMY BRUSHED NICKEL | 14| 12 +Brand#15 |ECONOMY BURNISHED STEEL | 19| 12 +Brand#15 |ECONOMY PLATED NICKEL | 9| 12 +Brand#15 |ECONOMY PLATED STEEL | 3| 12 +Brand#15 |ECONOMY PLATED STEEL | 19| 12 +Brand#15 |ECONOMY PLATED TIN | 9| 12 +Brand#15 |ECONOMY POLISHED COPPER | 36| 12 +Brand#15 |ECONOMY POLISHED NICKEL | 45| 12 +Brand#15 |LARGE ANODIZED BRASS | 19| 12 +Brand#15 |LARGE ANODIZED STEEL | 14| 12 +Brand#15 |LARGE ANODIZED TIN | 23| 12 +Brand#15 |LARGE BRUSHED BRASS | 19| 12 +Brand#15 |LARGE BRUSHED BRASS | 49| 12 +Brand#15 |LARGE BURNISHED BRASS | 3| 12 +Brand#15 |LARGE BURNISHED BRASS | 23| 12 +Brand#15 |LARGE BURNISHED COPPER | 9| 12 +Brand#15 |LARGE BURNISHED COPPER | 49| 12 +Brand#15 |LARGE BURNISHED STEEL | 9| 12 +Brand#15 |LARGE PLATED BRASS | 9| 12 +Brand#15 |MEDIUM BRUSHED BRASS | 14| 12 +Brand#15 |MEDIUM BRUSHED NICKEL | 14| 12 +Brand#15 |MEDIUM BRUSHED NICKEL | 19| 12 +Brand#15 |MEDIUM BRUSHED STEEL | 36| 12 +Brand#15 |MEDIUM BRUSHED TIN | 14| 12 +Brand#15 |MEDIUM BURNISHED STEEL | 3| 12 +Brand#15 |MEDIUM PLATED TIN | 9| 12 +Brand#15 |MEDIUM PLATED TIN | 45| 12 +Brand#15 |PROMO BRUSHED BRASS | 36| 12 +Brand#15 |PROMO BRUSHED STEEL | 9| 12 +Brand#15 |PROMO BURNISHED NICKEL | 9| 12 +Brand#15 |PROMO PLATED COPPER | 36| 12 +Brand#15 |PROMO POLISHED BRASS | 14| 12 +Brand#15 |PROMO POLISHED COPPER | 9| 12 +Brand#15 |PROMO POLISHED NICKEL | 36| 12 +Brand#15 |PROMO POLISHED TIN | 49| 12 +Brand#15 |SMALL ANODIZED STEEL | 45| 12 +Brand#15 |SMALL BRUSHED BRASS | 45| 12 +Brand#15 |SMALL BRUSHED COPPER | 14| 12 +Brand#15 |SMALL BRUSHED COPPER | 19| 12 +Brand#15 |SMALL BRUSHED NICKEL | 36| 12 +Brand#15 |SMALL BURNISHED BRASS | 3| 12 +Brand#15 |SMALL PLATED COPPER | 19| 12 +Brand#15 |SMALL PLATED COPPER | 23| 12 +Brand#15 |SMALL PLATED NICKEL | 19| 12 +Brand#15 |SMALL POLISHED BRASS | 45| 12 +Brand#15 |SMALL POLISHED NICKEL | 19| 12 +Brand#15 |SMALL POLISHED NICKEL | 23| 12 +Brand#15 |SMALL POLISHED TIN | 3| 12 +Brand#15 |SMALL POLISHED TIN | 49| 12 +Brand#15 |STANDARD ANODIZED NICKEL | 3| 12 +Brand#15 |STANDARD ANODIZED STEEL | 19| 12 +Brand#15 |STANDARD ANODIZED TIN | 36| 12 +Brand#15 |STANDARD BRUSHED BRASS | 49| 12 +Brand#15 |STANDARD BRUSHED COPPER | 49| 12 +Brand#15 |STANDARD BRUSHED NICKEL | 3| 12 +Brand#15 |STANDARD BRUSHED STEEL | 19| 12 +Brand#15 |STANDARD BURNISHED BRASS | 19| 12 +Brand#15 |STANDARD BURNISHED COPPER| 14| 12 +Brand#15 |STANDARD BURNISHED COPPER| 36| 12 +Brand#15 |STANDARD BURNISHED TIN | 49| 12 +Brand#15 |STANDARD PLATED COPPER | 14| 12 +Brand#15 |STANDARD PLATED STEEL | 3| 12 +Brand#15 |STANDARD PLATED TIN | 9| 12 +Brand#15 |STANDARD PLATED TIN | 45| 12 +Brand#15 |STANDARD POLISHED TIN | 14| 12 +Brand#21 |ECONOMY ANODIZED STEEL | 19| 12 +Brand#21 |ECONOMY BRUSHED COPPER | 14| 12 +Brand#21 |ECONOMY BRUSHED NICKEL | 23| 12 +Brand#21 |ECONOMY BRUSHED STEEL | 45| 12 +Brand#21 |ECONOMY BRUSHED TIN | 19| 12 +Brand#21 |ECONOMY BURNISHED BRASS | 19| 12 +Brand#21 |ECONOMY BURNISHED COPPER | 45| 12 +Brand#21 |ECONOMY BURNISHED STEEL | 9| 12 +Brand#21 |ECONOMY BURNISHED STEEL | 14| 12 +Brand#21 |ECONOMY BURNISHED TIN | 49| 12 +Brand#21 |ECONOMY PLATED BRASS | 49| 12 +Brand#21 |ECONOMY PLATED COPPER | 14| 12 +Brand#21 |ECONOMY PLATED NICKEL | 3| 12 +Brand#21 |ECONOMY PLATED STEEL | 9| 12 +Brand#21 |ECONOMY PLATED TIN | 19| 12 +Brand#21 |ECONOMY PLATED TIN | 23| 12 +Brand#21 |ECONOMY POLISHED BRASS | 9| 12 +Brand#21 |ECONOMY POLISHED STEEL | 14| 12 +Brand#21 |LARGE ANODIZED COPPER | 3| 12 +Brand#21 |LARGE ANODIZED TIN | 3| 12 +Brand#21 |LARGE ANODIZED TIN | 14| 12 +Brand#21 |LARGE ANODIZED TIN | 45| 12 +Brand#21 |LARGE BRUSHED COPPER | 23| 12 +Brand#21 |LARGE BRUSHED NICKEL | 36| 12 +Brand#21 |LARGE BRUSHED STEEL | 23| 12 +Brand#21 |LARGE BRUSHED TIN | 45| 12 +Brand#21 |LARGE BRUSHED TIN | 49| 12 +Brand#21 |LARGE BURNISHED BRASS | 14| 12 +Brand#21 |LARGE BURNISHED NICKEL | 14| 12 +Brand#21 |LARGE BURNISHED STEEL | 19| 12 +Brand#21 |LARGE PLATED BRASS | 14| 12 +Brand#21 |LARGE PLATED COPPER | 19| 12 +Brand#21 |LARGE PLATED COPPER | 49| 12 +Brand#21 |LARGE POLISHED COPPER | 14| 12 +Brand#21 |LARGE POLISHED STEEL | 45| 12 +Brand#21 |MEDIUM ANODIZED NICKEL | 3| 12 +Brand#21 |MEDIUM ANODIZED STEEL | 14| 12 +Brand#21 |MEDIUM BRUSHED BRASS | 23| 12 +Brand#21 |MEDIUM BURNISHED COPPER | 49| 12 +Brand#21 |MEDIUM BURNISHED NICKEL | 9| 12 +Brand#21 |MEDIUM BURNISHED TIN | 9| 12 +Brand#21 |MEDIUM PLATED BRASS | 36| 12 +Brand#21 |MEDIUM PLATED NICKEL | 36| 12 +Brand#21 |MEDIUM PLATED STEEL | 36| 12 +Brand#21 |MEDIUM PLATED TIN | 9| 12 +Brand#21 |PROMO ANODIZED BRASS | 9| 12 +Brand#21 |PROMO ANODIZED COPPER | 9| 12 +Brand#21 |PROMO ANODIZED NICKEL | 19| 12 +Brand#21 |PROMO ANODIZED STEEL | 36| 12 +Brand#21 |PROMO ANODIZED TIN | 45| 12 +Brand#21 |PROMO BRUSHED NICKEL | 9| 12 +Brand#21 |PROMO BRUSHED STEEL | 14| 12 +Brand#21 |PROMO BRUSHED STEEL | 19| 12 +Brand#21 |PROMO BRUSHED STEEL | 45| 12 +Brand#21 |PROMO BRUSHED TIN | 14| 12 +Brand#21 |PROMO BURNISHED COPPER | 3| 12 +Brand#21 |PROMO BURNISHED STEEL | 14| 12 +Brand#21 |PROMO PLATED BRASS | 36| 12 +Brand#21 |PROMO PLATED COPPER | 49| 12 +Brand#21 |PROMO PLATED TIN | 45| 12 +Brand#21 |PROMO POLISHED COPPER | 9| 12 +Brand#21 |PROMO POLISHED COPPER | 19| 12 +Brand#21 |PROMO POLISHED NICKEL | 23| 12 +Brand#21 |PROMO POLISHED STEEL | 3| 12 +Brand#21 |PROMO POLISHED STEEL | 9| 12 +Brand#21 |PROMO POLISHED TIN | 9| 12 +Brand#21 |PROMO POLISHED TIN | 14| 12 +Brand#21 |PROMO POLISHED TIN | 19| 12 +Brand#21 |SMALL BRUSHED NICKEL | 9| 12 +Brand#21 |SMALL BRUSHED NICKEL | 45| 12 +Brand#21 |SMALL BRUSHED STEEL | 3| 12 +Brand#21 |SMALL BRUSHED STEEL | 9| 12 +Brand#21 |SMALL BRUSHED TIN | 14| 12 +Brand#21 |SMALL PLATED BRASS | 36| 12 +Brand#21 |SMALL PLATED COPPER | 14| 12 +Brand#21 |SMALL PLATED COPPER | 23| 12 +Brand#21 |SMALL POLISHED NICKEL | 9| 12 +Brand#21 |SMALL POLISHED STEEL | 3| 12 +Brand#21 |STANDARD ANODIZED NICKEL | 3| 12 +Brand#21 |STANDARD ANODIZED NICKEL | 19| 12 +Brand#21 |STANDARD BRUSHED BRASS | 9| 12 +Brand#21 |STANDARD BRUSHED NICKEL | 23| 12 +Brand#21 |STANDARD BRUSHED NICKEL | 45| 12 +Brand#21 |STANDARD BURNISHED BRASS | 49| 12 +Brand#21 |STANDARD PLATED COPPER | 45| 12 +Brand#21 |STANDARD PLATED NICKEL | 49| 12 +Brand#21 |STANDARD PLATED STEEL | 36| 12 +Brand#21 |STANDARD PLATED TIN | 9| 12 +Brand#21 |STANDARD POLISHED COPPER | 49| 12 +Brand#22 |ECONOMY ANODIZED COPPER | 36| 12 +Brand#22 |ECONOMY ANODIZED COPPER | 45| 12 +Brand#22 |ECONOMY ANODIZED NICKEL | 45| 12 +Brand#22 |ECONOMY ANODIZED STEEL | 45| 12 +Brand#22 |ECONOMY ANODIZED TIN | 49| 12 +Brand#22 |ECONOMY BRUSHED STEEL | 45| 12 +Brand#22 |ECONOMY BRUSHED TIN | 49| 12 +Brand#22 |ECONOMY BURNISHED BRASS | 19| 12 +Brand#22 |ECONOMY BURNISHED BRASS | 23| 12 +Brand#22 |ECONOMY BURNISHED BRASS | 45| 12 +Brand#22 |ECONOMY BURNISHED COPPER | 3| 12 +Brand#22 |ECONOMY BURNISHED COPPER | 9| 12 +Brand#22 |ECONOMY BURNISHED COPPER | 49| 12 +Brand#22 |ECONOMY BURNISHED NICKEL | 14| 12 +Brand#22 |ECONOMY BURNISHED NICKEL | 23| 12 +Brand#22 |ECONOMY BURNISHED STEEL | 23| 12 +Brand#22 |ECONOMY BURNISHED STEEL | 45| 12 +Brand#22 |ECONOMY BURNISHED STEEL | 49| 12 +Brand#22 |ECONOMY BURNISHED TIN | 9| 12 +Brand#22 |ECONOMY BURNISHED TIN | 19| 12 +Brand#22 |ECONOMY PLATED BRASS | 36| 12 +Brand#22 |ECONOMY PLATED COPPER | 3| 12 +Brand#22 |ECONOMY PLATED STEEL | 23| 12 +Brand#22 |ECONOMY POLISHED COPPER | 14| 12 +Brand#22 |ECONOMY POLISHED TIN | 49| 12 +Brand#22 |LARGE ANODIZED NICKEL | 14| 12 +Brand#22 |LARGE ANODIZED TIN | 14| 12 +Brand#22 |LARGE BRUSHED BRASS | 9| 12 +Brand#22 |LARGE BRUSHED BRASS | 49| 12 +Brand#22 |LARGE BRUSHED COPPER | 14| 12 +Brand#22 |LARGE BRUSHED STEEL | 19| 12 +Brand#22 |LARGE BRUSHED TIN | 23| 12 +Brand#22 |LARGE BURNISHED BRASS | 14| 12 +Brand#22 |LARGE BURNISHED TIN | 36| 12 +Brand#22 |LARGE PLATED STEEL | 9| 12 +Brand#22 |LARGE PLATED TIN | 49| 12 +Brand#22 |LARGE POLISHED COPPER | 23| 12 +Brand#22 |LARGE POLISHED NICKEL | 19| 12 +Brand#22 |LARGE POLISHED NICKEL | 23| 12 +Brand#22 |LARGE POLISHED STEEL | 3| 12 +Brand#22 |MEDIUM ANODIZED COPPER | 19| 12 +Brand#22 |MEDIUM ANODIZED NICKEL | 45| 12 +Brand#22 |MEDIUM BRUSHED NICKEL | 9| 12 +Brand#22 |MEDIUM BRUSHED STEEL | 3| 12 +Brand#22 |MEDIUM PLATED BRASS | 36| 12 +Brand#22 |MEDIUM PLATED NICKEL | 14| 12 +Brand#22 |PROMO ANODIZED COPPER | 45| 12 +Brand#22 |PROMO ANODIZED STEEL | 36| 12 +Brand#22 |PROMO BURNISHED BRASS | 3| 12 +Brand#22 |PROMO BURNISHED BRASS | 23| 12 +Brand#22 |PROMO BURNISHED STEEL | 3| 12 +Brand#22 |PROMO PLATED BRASS | 14| 12 +Brand#22 |PROMO POLISHED BRASS | 14| 12 +Brand#22 |PROMO POLISHED COPPER | 3| 12 +Brand#22 |PROMO POLISHED COPPER | 23| 12 +Brand#22 |PROMO POLISHED NICKEL | 19| 12 +Brand#22 |PROMO POLISHED NICKEL | 36| 12 +Brand#22 |PROMO POLISHED STEEL | 36| 12 +Brand#22 |SMALL ANODIZED COPPER | 9| 12 +Brand#22 |SMALL ANODIZED STEEL | 19| 12 +Brand#22 |SMALL ANODIZED TIN | 19| 12 +Brand#22 |SMALL ANODIZED TIN | 49| 12 +Brand#22 |SMALL BRUSHED COPPER | 36| 12 +Brand#22 |SMALL BRUSHED TIN | 45| 12 +Brand#22 |SMALL BURNISHED COPPER | 49| 12 +Brand#22 |SMALL BURNISHED NICKEL | 9| 12 +Brand#22 |SMALL PLATED BRASS | 9| 12 +Brand#22 |SMALL PLATED COPPER | 3| 12 +Brand#22 |SMALL POLISHED NICKEL | 9| 12 +Brand#22 |SMALL POLISHED NICKEL | 49| 12 +Brand#22 |SMALL POLISHED STEEL | 49| 12 +Brand#22 |STANDARD ANODIZED BRASS | 23| 12 +Brand#22 |STANDARD ANODIZED STEEL | 49| 12 +Brand#22 |STANDARD BRUSHED BRASS | 36| 12 +Brand#22 |STANDARD BRUSHED TIN | 19| 12 +Brand#22 |STANDARD BRUSHED TIN | 49| 12 +Brand#22 |STANDARD BURNISHED TIN | 14| 12 +Brand#22 |STANDARD PLATED BRASS | 45| 12 +Brand#22 |STANDARD PLATED COPPER | 36| 12 +Brand#22 |STANDARD PLATED NICKEL | 9| 12 +Brand#22 |STANDARD PLATED STEEL | 36| 12 +Brand#22 |STANDARD PLATED STEEL | 49| 12 +Brand#22 |STANDARD PLATED TIN | 3| 12 +Brand#22 |STANDARD PLATED TIN | 36| 12 +Brand#22 |STANDARD PLATED TIN | 49| 12 +Brand#22 |STANDARD POLISHED BRASS | 19| 12 +Brand#22 |STANDARD POLISHED COPPER | 9| 12 +Brand#22 |STANDARD POLISHED NICKEL | 19| 12 +Brand#22 |STANDARD POLISHED STEEL | 9| 12 +Brand#22 |STANDARD POLISHED TIN | 45| 12 +Brand#23 |ECONOMY ANODIZED BRASS | 36| 12 +Brand#23 |ECONOMY ANODIZED NICKEL | 9| 12 +Brand#23 |ECONOMY ANODIZED STEEL | 49| 12 +Brand#23 |ECONOMY BRUSHED COPPER | 3| 12 +Brand#23 |ECONOMY BRUSHED COPPER | 49| 12 +Brand#23 |ECONOMY BRUSHED NICKEL | 23| 12 +Brand#23 |ECONOMY BURNISHED STEEL | 49| 12 +Brand#23 |ECONOMY BURNISHED TIN | 3| 12 +Brand#23 |ECONOMY PLATED STEEL | 14| 12 +Brand#23 |ECONOMY PLATED TIN | 49| 12 +Brand#23 |ECONOMY POLISHED COPPER | 23| 12 +Brand#23 |ECONOMY POLISHED NICKEL | 36| 12 +Brand#23 |ECONOMY POLISHED TIN | 3| 12 +Brand#23 |LARGE ANODIZED TIN | 14| 12 +Brand#23 |LARGE BURNISHED STEEL | 23| 12 +Brand#23 |LARGE BURNISHED TIN | 19| 12 +Brand#23 |LARGE PLATED COPPER | 14| 12 +Brand#23 |LARGE PLATED STEEL | 9| 12 +Brand#23 |LARGE POLISHED BRASS | 19| 12 +Brand#23 |LARGE POLISHED COPPER | 45| 12 +Brand#23 |LARGE POLISHED COPPER | 49| 12 +Brand#23 |LARGE POLISHED TIN | 3| 12 +Brand#23 |MEDIUM BRUSHED BRASS | 9| 12 +Brand#23 |MEDIUM BRUSHED COPPER | 3| 12 +Brand#23 |MEDIUM BRUSHED NICKEL | 23| 12 +Brand#23 |MEDIUM BRUSHED NICKEL | 36| 12 +Brand#23 |MEDIUM BURNISHED COPPER | 9| 12 +Brand#23 |MEDIUM BURNISHED COPPER | 19| 12 +Brand#23 |MEDIUM PLATED COPPER | 19| 12 +Brand#23 |MEDIUM PLATED STEEL | 14| 12 +Brand#23 |PROMO ANODIZED BRASS | 9| 12 +Brand#23 |PROMO ANODIZED BRASS | 19| 12 +Brand#23 |PROMO ANODIZED NICKEL | 3| 12 +Brand#23 |PROMO ANODIZED STEEL | 36| 12 +Brand#23 |PROMO BRUSHED COPPER | 36| 12 +Brand#23 |PROMO BURNISHED BRASS | 9| 12 +Brand#23 |PROMO BURNISHED STEEL | 9| 12 +Brand#23 |PROMO BURNISHED TIN | 3| 12 +Brand#23 |PROMO BURNISHED TIN | 45| 12 +Brand#23 |PROMO PLATED BRASS | 19| 12 +Brand#23 |PROMO PLATED BRASS | 23| 12 +Brand#23 |PROMO PLATED BRASS | 49| 12 +Brand#23 |PROMO PLATED NICKEL | 3| 12 +Brand#23 |PROMO PLATED TIN | 14| 12 +Brand#23 |PROMO POLISHED TIN | 45| 12 +Brand#23 |SMALL ANODIZED STEEL | 3| 12 +Brand#23 |SMALL ANODIZED TIN | 45| 12 +Brand#23 |SMALL BRUSHED BRASS | 19| 12 +Brand#23 |SMALL BRUSHED STEEL | 3| 12 +Brand#23 |SMALL BURNISHED BRASS | 14| 12 +Brand#23 |SMALL BURNISHED COPPER | 36| 12 +Brand#23 |SMALL BURNISHED STEEL | 45| 12 +Brand#23 |SMALL PLATED BRASS | 49| 12 +Brand#23 |SMALL PLATED STEEL | 23| 12 +Brand#23 |SMALL PLATED TIN | 14| 12 +Brand#23 |SMALL POLISHED COPPER | 49| 12 +Brand#23 |SMALL POLISHED TIN | 23| 12 +Brand#23 |STANDARD ANODIZED BRASS | 23| 12 +Brand#23 |STANDARD ANODIZED TIN | 3| 12 +Brand#23 |STANDARD ANODIZED TIN | 45| 12 +Brand#23 |STANDARD BRUSHED BRASS | 3| 12 +Brand#23 |STANDARD BRUSHED STEEL | 9| 12 +Brand#23 |STANDARD BRUSHED TIN | 19| 12 +Brand#23 |STANDARD PLATED BRASS | 3| 12 +Brand#23 |STANDARD PLATED NICKEL | 49| 12 +Brand#23 |STANDARD PLATED TIN | 9| 12 +Brand#23 |STANDARD PLATED TIN | 19| 12 +Brand#23 |STANDARD POLISHED STEEL | 23| 12 +Brand#23 |STANDARD POLISHED TIN | 23| 12 +Brand#24 |ECONOMY ANODIZED BRASS | 19| 12 +Brand#24 |ECONOMY ANODIZED COPPER | 36| 12 +Brand#24 |ECONOMY ANODIZED COPPER | 49| 12 +Brand#24 |ECONOMY ANODIZED NICKEL | 3| 12 +Brand#24 |ECONOMY ANODIZED STEEL | 23| 12 +Brand#24 |ECONOMY ANODIZED STEEL | 45| 12 +Brand#24 |ECONOMY BRUSHED STEEL | 9| 12 +Brand#24 |ECONOMY BRUSHED TIN | 49| 12 +Brand#24 |ECONOMY BURNISHED BRASS | 14| 12 +Brand#24 |ECONOMY BURNISHED COPPER | 3| 12 +Brand#24 |ECONOMY BURNISHED COPPER | 19| 12 +Brand#24 |ECONOMY BURNISHED STEEL | 45| 12 +Brand#24 |ECONOMY PLATED COPPER | 49| 12 +Brand#24 |ECONOMY PLATED STEEL | 45| 12 +Brand#24 |ECONOMY POLISHED BRASS | 23| 12 +Brand#24 |ECONOMY POLISHED STEEL | 14| 12 +Brand#24 |ECONOMY POLISHED TIN | 14| 12 +Brand#24 |ECONOMY POLISHED TIN | 45| 12 +Brand#24 |ECONOMY POLISHED TIN | 49| 12 +Brand#24 |LARGE ANODIZED BRASS | 3| 12 +Brand#24 |LARGE ANODIZED BRASS | 45| 12 +Brand#24 |LARGE BRUSHED BRASS | 14| 12 +Brand#24 |LARGE BRUSHED BRASS | 45| 12 +Brand#24 |LARGE BRUSHED STEEL | 23| 12 +Brand#24 |LARGE BRUSHED STEEL | 45| 12 +Brand#24 |LARGE BURNISHED STEEL | 3| 12 +Brand#24 |LARGE BURNISHED TIN | 23| 12 +Brand#24 |LARGE PLATED COPPER | 23| 12 +Brand#24 |LARGE PLATED STEEL | 3| 12 +Brand#24 |LARGE POLISHED COPPER | 9| 12 +Brand#24 |LARGE POLISHED TIN | 14| 12 +Brand#24 |MEDIUM ANODIZED BRASS | 14| 12 +Brand#24 |MEDIUM BRUSHED NICKEL | 9| 12 +Brand#24 |MEDIUM BRUSHED NICKEL | 36| 12 +Brand#24 |MEDIUM BRUSHED STEEL | 23| 12 +Brand#24 |MEDIUM BRUSHED STEEL | 49| 12 +Brand#24 |MEDIUM BURNISHED BRASS | 36| 12 +Brand#24 |MEDIUM BURNISHED STEEL | 49| 12 +Brand#24 |MEDIUM BURNISHED TIN | 23| 12 +Brand#24 |MEDIUM PLATED BRASS | 3| 12 +Brand#24 |MEDIUM PLATED NICKEL | 36| 12 +Brand#24 |PROMO ANODIZED NICKEL | 19| 12 +Brand#24 |PROMO ANODIZED NICKEL | 45| 12 +Brand#24 |PROMO ANODIZED TIN | 14| 12 +Brand#24 |PROMO BRUSHED COPPER | 23| 12 +Brand#24 |PROMO BRUSHED COPPER | 49| 12 +Brand#24 |PROMO BRUSHED NICKEL | 3| 12 +Brand#24 |PROMO BURNISHED BRASS | 36| 12 +Brand#24 |PROMO BURNISHED STEEL | 14| 12 +Brand#24 |PROMO BURNISHED TIN | 14| 12 +Brand#24 |PROMO PLATED STEEL | 3| 12 +Brand#24 |PROMO POLISHED BRASS | 3| 12 +Brand#24 |PROMO POLISHED BRASS | 14| 12 +Brand#24 |PROMO POLISHED COPPER | 45| 12 +Brand#24 |SMALL ANODIZED COPPER | 3| 12 +Brand#24 |SMALL ANODIZED NICKEL | 23| 12 +Brand#24 |SMALL BRUSHED BRASS | 45| 12 +Brand#24 |SMALL BRUSHED COPPER | 9| 12 +Brand#24 |SMALL BRUSHED NICKEL | 49| 12 +Brand#24 |SMALL BURNISHED BRASS | 3| 12 +Brand#24 |SMALL BURNISHED BRASS | 14| 12 +Brand#24 |SMALL BURNISHED COPPER | 19| 12 +Brand#24 |SMALL BURNISHED NICKEL | 9| 12 +Brand#24 |SMALL PLATED BRASS | 3| 12 +Brand#24 |SMALL PLATED BRASS | 14| 12 +Brand#24 |SMALL PLATED NICKEL | 14| 12 +Brand#24 |SMALL POLISHED BRASS | 3| 12 +Brand#24 |SMALL POLISHED NICKEL | 19| 12 +Brand#24 |SMALL POLISHED TIN | 9| 12 +Brand#24 |STANDARD ANODIZED TIN | 49| 12 +Brand#24 |STANDARD BRUSHED BRASS | 14| 12 +Brand#24 |STANDARD BRUSHED BRASS | 23| 12 +Brand#24 |STANDARD BRUSHED NICKEL | 19| 12 +Brand#24 |STANDARD BRUSHED STEEL | 23| 12 +Brand#24 |STANDARD PLATED BRASS | 36| 12 +Brand#24 |STANDARD PLATED COPPER | 49| 12 +Brand#24 |STANDARD PLATED NICKEL | 36| 12 +Brand#24 |STANDARD POLISHED BRASS | 9| 12 +Brand#24 |STANDARD POLISHED COPPER | 9| 12 +Brand#25 |ECONOMY ANODIZED STEEL | 14| 12 +Brand#25 |ECONOMY ANODIZED STEEL | 45| 12 +Brand#25 |ECONOMY BRUSHED NICKEL | 9| 12 +Brand#25 |ECONOMY BRUSHED STEEL | 3| 12 +Brand#25 |ECONOMY BRUSHED TIN | 14| 12 +Brand#25 |ECONOMY PLATED COPPER | 3| 12 +Brand#25 |ECONOMY PLATED NICKEL | 19| 12 +Brand#25 |ECONOMY PLATED STEEL | 9| 12 +Brand#25 |ECONOMY POLISHED BRASS | 3| 12 +Brand#25 |ECONOMY POLISHED BRASS | 9| 12 +Brand#25 |ECONOMY POLISHED NICKEL | 3| 12 +Brand#25 |LARGE ANODIZED BRASS | 14| 12 +Brand#25 |LARGE ANODIZED BRASS | 23| 12 +Brand#25 |LARGE ANODIZED COPPER | 19| 12 +Brand#25 |LARGE ANODIZED COPPER | 36| 12 +Brand#25 |LARGE BRUSHED BRASS | 19| 12 +Brand#25 |LARGE BRUSHED NICKEL | 49| 12 +Brand#25 |LARGE BRUSHED STEEL | 36| 12 +Brand#25 |LARGE BRUSHED TIN | 3| 12 +Brand#25 |LARGE BRUSHED TIN | 9| 12 +Brand#25 |LARGE BURNISHED BRASS | 23| 12 +Brand#25 |LARGE BURNISHED STEEL | 36| 12 +Brand#25 |LARGE BURNISHED TIN | 14| 12 +Brand#25 |LARGE BURNISHED TIN | 36| 12 +Brand#25 |LARGE PLATED NICKEL | 45| 12 +Brand#25 |LARGE PLATED TIN | 23| 12 +Brand#25 |MEDIUM ANODIZED BRASS | 3| 12 +Brand#25 |MEDIUM ANODIZED BRASS | 9| 12 +Brand#25 |MEDIUM ANODIZED BRASS | 14| 12 +Brand#25 |MEDIUM ANODIZED BRASS | 19| 12 +Brand#25 |MEDIUM ANODIZED STEEL | 36| 12 +Brand#25 |MEDIUM ANODIZED TIN | 3| 12 +Brand#25 |MEDIUM BRUSHED BRASS | 14| 12 +Brand#25 |MEDIUM BRUSHED BRASS | 49| 12 +Brand#25 |MEDIUM BRUSHED TIN | 9| 12 +Brand#25 |MEDIUM BRUSHED TIN | 49| 12 +Brand#25 |MEDIUM BURNISHED STEEL | 36| 12 +Brand#25 |MEDIUM PLATED COPPER | 14| 12 +Brand#25 |MEDIUM PLATED COPPER | 23| 12 +Brand#25 |MEDIUM PLATED STEEL | 36| 12 +Brand#25 |MEDIUM PLATED TIN | 14| 12 +Brand#25 |PROMO ANODIZED COPPER | 3| 12 +Brand#25 |PROMO ANODIZED NICKEL | 23| 12 +Brand#25 |PROMO ANODIZED TIN | 36| 12 +Brand#25 |PROMO BURNISHED COPPER | 19| 12 +Brand#25 |PROMO BURNISHED COPPER | 36| 12 +Brand#25 |PROMO BURNISHED COPPER | 45| 12 +Brand#25 |PROMO BURNISHED STEEL | 9| 12 +Brand#25 |PROMO PLATED BRASS | 9| 12 +Brand#25 |PROMO POLISHED BRASS | 3| 12 +Brand#25 |PROMO POLISHED BRASS | 49| 12 +Brand#25 |PROMO POLISHED NICKEL | 36| 12 +Brand#25 |PROMO POLISHED STEEL | 45| 12 +Brand#25 |SMALL ANODIZED COPPER | 45| 12 +Brand#25 |SMALL ANODIZED TIN | 14| 12 +Brand#25 |SMALL BRUSHED COPPER | 14| 12 +Brand#25 |SMALL BURNISHED BRASS | 3| 12 +Brand#25 |SMALL BURNISHED NICKEL | 45| 12 +Brand#25 |SMALL BURNISHED STEEL | 14| 12 +Brand#25 |SMALL PLATED BRASS | 19| 12 +Brand#25 |SMALL PLATED BRASS | 49| 12 +Brand#25 |SMALL PLATED COPPER | 23| 12 +Brand#25 |SMALL PLATED TIN | 3| 12 +Brand#25 |SMALL POLISHED COPPER | 9| 12 +Brand#25 |STANDARD BRUSHED TIN | 45| 12 +Brand#25 |STANDARD BURNISHED BRASS | 3| 12 +Brand#25 |STANDARD BURNISHED BRASS | 14| 12 +Brand#25 |STANDARD BURNISHED NICKEL| 36| 12 +Brand#25 |STANDARD PLATED COPPER | 9| 12 +Brand#25 |STANDARD PLATED COPPER | 23| 12 +Brand#25 |STANDARD PLATED NICKEL | 36| 12 +Brand#25 |STANDARD PLATED NICKEL | 49| 12 +Brand#25 |STANDARD PLATED TIN | 36| 12 +Brand#25 |STANDARD POLISHED COPPER | 23| 12 +Brand#25 |STANDARD POLISHED NICKEL | 45| 12 +Brand#25 |STANDARD POLISHED TIN | 3| 12 +Brand#31 |ECONOMY ANODIZED BRASS | 19| 12 +Brand#31 |ECONOMY ANODIZED TIN | 36| 12 +Brand#31 |ECONOMY BRUSHED NICKEL | 14| 12 +Brand#31 |ECONOMY BURNISHED COPPER | 14| 12 +Brand#31 |ECONOMY BURNISHED NICKEL | 19| 12 +Brand#31 |ECONOMY PLATED NICKEL | 9| 12 +Brand#31 |ECONOMY POLISHED COPPER | 3| 12 +Brand#31 |ECONOMY POLISHED TIN | 36| 12 +Brand#31 |LARGE ANODIZED COPPER | 3| 12 +Brand#31 |LARGE ANODIZED COPPER | 14| 12 +Brand#31 |LARGE ANODIZED STEEL | 36| 12 +Brand#31 |LARGE ANODIZED TIN | 3| 12 +Brand#31 |LARGE BRUSHED BRASS | 36| 12 +Brand#31 |LARGE BRUSHED NICKEL | 19| 12 +Brand#31 |LARGE BRUSHED STEEL | 36| 12 +Brand#31 |LARGE BRUSHED TIN | 14| 12 +Brand#31 |LARGE BURNISHED BRASS | 36| 12 +Brand#31 |LARGE BURNISHED NICKEL | 14| 12 +Brand#31 |LARGE PLATED STEEL | 23| 12 +Brand#31 |LARGE POLISHED BRASS | 9| 12 +Brand#31 |LARGE POLISHED STEEL | 45| 12 +Brand#31 |MEDIUM ANODIZED STEEL | 14| 12 +Brand#31 |MEDIUM ANODIZED TIN | 9| 12 +Brand#31 |MEDIUM ANODIZED TIN | 23| 12 +Brand#31 |MEDIUM BRUSHED BRASS | 23| 12 +Brand#31 |MEDIUM BRUSHED STEEL | 3| 12 +Brand#31 |MEDIUM BURNISHED BRASS | 14| 12 +Brand#31 |MEDIUM BURNISHED STEEL | 9| 12 +Brand#31 |PROMO ANODIZED COPPER | 14| 12 +Brand#31 |PROMO ANODIZED TIN | 36| 12 +Brand#31 |PROMO BRUSHED BRASS | 3| 12 +Brand#31 |PROMO BRUSHED COPPER | 23| 12 +Brand#31 |PROMO BRUSHED STEEL | 23| 12 +Brand#31 |PROMO BURNISHED BRASS | 49| 12 +Brand#31 |PROMO BURNISHED STEEL | 3| 12 +Brand#31 |PROMO PLATED BRASS | 36| 12 +Brand#31 |PROMO POLISHED NICKEL | 49| 12 +Brand#31 |SMALL ANODIZED COPPER | 3| 12 +Brand#31 |SMALL ANODIZED NICKEL | 9| 12 +Brand#31 |SMALL ANODIZED TIN | 3| 12 +Brand#31 |SMALL BRUSHED COPPER | 14| 12 +Brand#31 |SMALL BRUSHED COPPER | 19| 12 +Brand#31 |SMALL BRUSHED NICKEL | 3| 12 +Brand#31 |SMALL BRUSHED NICKEL | 23| 12 +Brand#31 |SMALL BRUSHED NICKEL | 36| 12 +Brand#31 |SMALL BURNISHED BRASS | 3| 12 +Brand#31 |SMALL BURNISHED NICKEL | 9| 12 +Brand#31 |SMALL BURNISHED TIN | 23| 12 +Brand#31 |SMALL PLATED STEEL | 19| 12 +Brand#31 |SMALL PLATED STEEL | 23| 12 +Brand#31 |SMALL POLISHED STEEL | 3| 12 +Brand#31 |STANDARD ANODIZED BRASS | 45| 12 +Brand#31 |STANDARD ANODIZED NICKEL | 3| 12 +Brand#31 |STANDARD BRUSHED COPPER | 3| 12 +Brand#31 |STANDARD BURNISHED STEEL | 45| 12 +Brand#31 |STANDARD PLATED BRASS | 3| 12 +Brand#31 |STANDARD PLATED BRASS | 19| 12 +Brand#31 |STANDARD PLATED STEEL | 19| 12 +Brand#31 |STANDARD POLISHED BRASS | 23| 12 +Brand#31 |STANDARD POLISHED COPPER | 45| 12 +Brand#32 |ECONOMY ANODIZED BRASS | 14| 12 +Brand#32 |ECONOMY ANODIZED STEEL | 23| 12 +Brand#32 |ECONOMY ANODIZED STEEL | 49| 12 +Brand#32 |ECONOMY ANODIZED TIN | 23| 12 +Brand#32 |ECONOMY BRUSHED NICKEL | 3| 12 +Brand#32 |ECONOMY BRUSHED STEEL | 36| 12 +Brand#32 |ECONOMY BRUSHED TIN | 19| 12 +Brand#32 |ECONOMY BURNISHED TIN | 19| 12 +Brand#32 |ECONOMY PLATED BRASS | 19| 12 +Brand#32 |ECONOMY PLATED NICKEL | 23| 12 +Brand#32 |ECONOMY PLATED TIN | 45| 12 +Brand#32 |LARGE ANODIZED NICKEL | 3| 12 +Brand#32 |LARGE ANODIZED STEEL | 14| 12 +Brand#32 |LARGE BRUSHED BRASS | 45| 12 +Brand#32 |LARGE BRUSHED NICKEL | 3| 12 +Brand#32 |LARGE BRUSHED STEEL | 45| 12 +Brand#32 |LARGE BRUSHED TIN | 19| 12 +Brand#32 |LARGE PLATED BRASS | 3| 12 +Brand#32 |LARGE PLATED BRASS | 9| 12 +Brand#32 |LARGE POLISHED COPPER | 19| 12 +Brand#32 |LARGE POLISHED NICKEL | 3| 12 +Brand#32 |MEDIUM ANODIZED COPPER | 45| 12 +Brand#32 |MEDIUM ANODIZED STEEL | 19| 12 +Brand#32 |MEDIUM ANODIZED STEEL | 49| 12 +Brand#32 |MEDIUM ANODIZED TIN | 45| 12 +Brand#32 |MEDIUM ANODIZED TIN | 49| 12 +Brand#32 |MEDIUM BURNISHED BRASS | 23| 12 +Brand#32 |MEDIUM BURNISHED NICKEL | 23| 12 +Brand#32 |MEDIUM PLATED BRASS | 49| 12 +Brand#32 |MEDIUM PLATED TIN | 3| 12 +Brand#32 |PROMO ANODIZED NICKEL | 49| 12 +Brand#32 |PROMO BRUSHED COPPER | 45| 12 +Brand#32 |PROMO BRUSHED STEEL | 23| 12 +Brand#32 |PROMO BRUSHED STEEL | 49| 12 +Brand#32 |PROMO BRUSHED TIN | 14| 12 +Brand#32 |PROMO BRUSHED TIN | 36| 12 +Brand#32 |PROMO BURNISHED NICKEL | 45| 12 +Brand#32 |PROMO BURNISHED TIN | 49| 12 +Brand#32 |PROMO PLATED COPPER | 49| 12 +Brand#32 |PROMO PLATED STEEL | 49| 12 +Brand#32 |PROMO POLISHED STEEL | 49| 12 +Brand#32 |PROMO POLISHED TIN | 19| 12 +Brand#32 |PROMO POLISHED TIN | 23| 12 +Brand#32 |PROMO POLISHED TIN | 45| 12 +Brand#32 |SMALL ANODIZED NICKEL | 9| 12 +Brand#32 |SMALL BRUSHED TIN | 3| 12 +Brand#32 |SMALL BRUSHED TIN | 9| 12 +Brand#32 |SMALL BURNISHED TIN | 23| 12 +Brand#32 |SMALL BURNISHED TIN | 36| 12 +Brand#32 |SMALL PLATED BRASS | 36| 12 +Brand#32 |SMALL PLATED COPPER | 14| 12 +Brand#32 |SMALL PLATED COPPER | 45| 12 +Brand#32 |SMALL PLATED STEEL | 36| 12 +Brand#32 |SMALL PLATED TIN | 14| 12 +Brand#32 |SMALL POLISHED NICKEL | 45| 12 +Brand#32 |SMALL POLISHED STEEL | 23| 12 +Brand#32 |SMALL POLISHED STEEL | 36| 12 +Brand#32 |STANDARD ANODIZED NICKEL | 9| 12 +Brand#32 |STANDARD ANODIZED STEEL | 3| 12 +Brand#32 |STANDARD ANODIZED TIN | 14| 12 +Brand#32 |STANDARD ANODIZED TIN | 19| 12 +Brand#32 |STANDARD BRUSHED BRASS | 14| 12 +Brand#32 |STANDARD BRUSHED STEEL | 14| 12 +Brand#32 |STANDARD BRUSHED TIN | 9| 12 +Brand#32 |STANDARD BURNISHED BRASS | 45| 12 +Brand#32 |STANDARD BURNISHED COPPER| 3| 12 +Brand#32 |STANDARD BURNISHED NICKEL| 3| 12 +Brand#32 |STANDARD PLATED STEEL | 9| 12 +Brand#32 |STANDARD PLATED STEEL | 49| 12 +Brand#32 |STANDARD POLISHED COPPER | 36| 12 +Brand#33 |ECONOMY ANODIZED NICKEL | 36| 12 +Brand#33 |ECONOMY ANODIZED STEEL | 23| 12 +Brand#33 |ECONOMY ANODIZED STEEL | 45| 12 +Brand#33 |ECONOMY BURNISHED NICKEL | 14| 12 +Brand#33 |ECONOMY BURNISHED TIN | 45| 12 +Brand#33 |ECONOMY PLATED STEEL | 3| 12 +Brand#33 |ECONOMY PLATED TIN | 3| 12 +Brand#33 |ECONOMY PLATED TIN | 9| 12 +Brand#33 |ECONOMY POLISHED BRASS | 3| 12 +Brand#33 |ECONOMY POLISHED BRASS | 14| 12 +Brand#33 |LARGE ANODIZED BRASS | 3| 12 +Brand#33 |LARGE ANODIZED BRASS | 36| 12 +Brand#33 |LARGE ANODIZED NICKEL | 23| 12 +Brand#33 |LARGE ANODIZED STEEL | 3| 12 +Brand#33 |LARGE ANODIZED TIN | 36| 12 +Brand#33 |LARGE BRUSHED BRASS | 23| 12 +Brand#33 |LARGE BRUSHED STEEL | 3| 12 +Brand#33 |LARGE BRUSHED TIN | 36| 12 +Brand#33 |LARGE BURNISHED BRASS | 19| 12 +Brand#33 |LARGE BURNISHED BRASS | 49| 12 +Brand#33 |LARGE PLATED NICKEL | 9| 12 +Brand#33 |LARGE PLATED NICKEL | 19| 12 +Brand#33 |LARGE POLISHED BRASS | 9| 12 +Brand#33 |LARGE POLISHED NICKEL | 45| 12 +Brand#33 |MEDIUM ANODIZED NICKEL | 19| 12 +Brand#33 |MEDIUM ANODIZED TIN | 49| 12 +Brand#33 |MEDIUM BRUSHED BRASS | 45| 12 +Brand#33 |MEDIUM BRUSHED NICKEL | 14| 12 +Brand#33 |MEDIUM BRUSHED STEEL | 14| 12 +Brand#33 |MEDIUM BRUSHED STEEL | 36| 12 +Brand#33 |MEDIUM BURNISHED BRASS | 49| 12 +Brand#33 |MEDIUM BURNISHED TIN | 3| 12 +Brand#33 |MEDIUM BURNISHED TIN | 49| 12 +Brand#33 |MEDIUM PLATED STEEL | 3| 12 +Brand#33 |MEDIUM PLATED TIN | 23| 12 +Brand#33 |PROMO ANODIZED STEEL | 23| 12 +Brand#33 |PROMO ANODIZED TIN | 9| 12 +Brand#33 |PROMO ANODIZED TIN | 49| 12 +Brand#33 |PROMO BRUSHED BRASS | 3| 12 +Brand#33 |PROMO BRUSHED BRASS | 19| 12 +Brand#33 |PROMO BRUSHED TIN | 49| 12 +Brand#33 |PROMO BURNISHED NICKEL | 23| 12 +Brand#33 |PROMO BURNISHED TIN | 3| 12 +Brand#33 |PROMO BURNISHED TIN | 19| 12 +Brand#33 |PROMO BURNISHED TIN | 23| 12 +Brand#33 |PROMO BURNISHED TIN | 36| 12 +Brand#33 |PROMO BURNISHED TIN | 49| 12 +Brand#33 |PROMO PLATED BRASS | 23| 12 +Brand#33 |PROMO PLATED BRASS | 36| 12 +Brand#33 |PROMO POLISHED COPPER | 3| 12 +Brand#33 |PROMO POLISHED NICKEL | 3| 12 +Brand#33 |PROMO POLISHED STEEL | 23| 12 +Brand#33 |SMALL ANODIZED STEEL | 14| 12 +Brand#33 |SMALL ANODIZED STEEL | 49| 12 +Brand#33 |SMALL ANODIZED TIN | 19| 12 +Brand#33 |SMALL BRUSHED BRASS | 36| 12 +Brand#33 |SMALL BRUSHED NICKEL | 19| 12 +Brand#33 |SMALL BRUSHED NICKEL | 45| 12 +Brand#33 |SMALL BURNISHED BRASS | 36| 12 +Brand#33 |SMALL BURNISHED TIN | 9| 12 +Brand#33 |SMALL PLATED BRASS | 14| 12 +Brand#33 |SMALL PLATED NICKEL | 49| 12 +Brand#33 |SMALL PLATED STEEL | 3| 12 +Brand#33 |SMALL POLISHED NICKEL | 9| 12 +Brand#33 |STANDARD ANODIZED STEEL | 14| 12 +Brand#33 |STANDARD ANODIZED STEEL | 45| 12 +Brand#33 |STANDARD ANODIZED TIN | 9| 12 +Brand#33 |STANDARD BRUSHED BRASS | 19| 12 +Brand#33 |STANDARD BRUSHED NICKEL | 14| 12 +Brand#33 |STANDARD BURNISHED BRASS | 9| 12 +Brand#33 |STANDARD BURNISHED TIN | 23| 12 +Brand#33 |STANDARD POLISHED STEEL | 45| 12 +Brand#34 |ECONOMY ANODIZED NICKEL | 9| 12 +Brand#34 |ECONOMY ANODIZED NICKEL | 49| 12 +Brand#34 |ECONOMY ANODIZED STEEL | 45| 12 +Brand#34 |ECONOMY BURNISHED COPPER | 9| 12 +Brand#34 |ECONOMY BURNISHED COPPER | 23| 12 +Brand#34 |ECONOMY BURNISHED COPPER | 36| 12 +Brand#34 |ECONOMY BURNISHED NICKEL | 19| 12 +Brand#34 |ECONOMY BURNISHED NICKEL | 49| 12 +Brand#34 |ECONOMY BURNISHED STEEL | 9| 12 +Brand#34 |ECONOMY BURNISHED TIN | 14| 12 +Brand#34 |ECONOMY PLATED BRASS | 3| 12 +Brand#34 |ECONOMY PLATED COPPER | 3| 12 +Brand#34 |ECONOMY PLATED TIN | 3| 12 +Brand#34 |ECONOMY PLATED TIN | 14| 12 +Brand#34 |ECONOMY POLISHED TIN | 36| 12 +Brand#34 |LARGE ANODIZED COPPER | 3| 12 +Brand#34 |LARGE ANODIZED NICKEL | 3| 12 +Brand#34 |LARGE ANODIZED NICKEL | 49| 12 +Brand#34 |LARGE BRUSHED COPPER | 36| 12 +Brand#34 |LARGE BRUSHED NICKEL | 19| 12 +Brand#34 |LARGE BRUSHED NICKEL | 49| 12 +Brand#34 |LARGE BURNISHED COPPER | 23| 12 +Brand#34 |LARGE BURNISHED NICKEL | 23| 12 +Brand#34 |LARGE BURNISHED TIN | 14| 12 +Brand#34 |LARGE BURNISHED TIN | 23| 12 +Brand#34 |LARGE BURNISHED TIN | 49| 12 +Brand#34 |LARGE PLATED COPPER | 9| 12 +Brand#34 |LARGE PLATED TIN | 14| 12 +Brand#34 |LARGE POLISHED BRASS | 3| 12 +Brand#34 |LARGE POLISHED BRASS | 45| 12 +Brand#34 |LARGE POLISHED COPPER | 3| 12 +Brand#34 |LARGE POLISHED NICKEL | 3| 12 +Brand#34 |LARGE POLISHED NICKEL | 49| 12 +Brand#34 |MEDIUM ANODIZED BRASS | 45| 12 +Brand#34 |MEDIUM BRUSHED BRASS | 49| 12 +Brand#34 |MEDIUM BRUSHED COPPER | 9| 12 +Brand#34 |MEDIUM BRUSHED COPPER | 23| 12 +Brand#34 |MEDIUM BRUSHED NICKEL | 9| 12 +Brand#34 |MEDIUM BRUSHED STEEL | 45| 12 +Brand#34 |MEDIUM BRUSHED TIN | 36| 12 +Brand#34 |MEDIUM BURNISHED BRASS | 14| 12 +Brand#34 |MEDIUM BURNISHED NICKEL | 3| 12 +Brand#34 |MEDIUM PLATED BRASS | 23| 12 +Brand#34 |PROMO ANODIZED NICKEL | 3| 12 +Brand#34 |PROMO BRUSHED COPPER | 49| 12 +Brand#34 |PROMO BRUSHED NICKEL | 49| 12 +Brand#34 |PROMO BURNISHED STEEL | 14| 12 +Brand#34 |PROMO PLATED BRASS | 3| 12 +Brand#34 |PROMO PLATED BRASS | 36| 12 +Brand#34 |PROMO PLATED TIN | 49| 12 +Brand#34 |PROMO POLISHED BRASS | 14| 12 +Brand#34 |PROMO POLISHED COPPER | 23| 12 +Brand#34 |PROMO POLISHED NICKEL | 49| 12 +Brand#34 |SMALL ANODIZED BRASS | 19| 12 +Brand#34 |SMALL ANODIZED COPPER | 14| 12 +Brand#34 |SMALL ANODIZED STEEL | 19| 12 +Brand#34 |SMALL ANODIZED TIN | 9| 12 +Brand#34 |SMALL BRUSHED COPPER | 14| 12 +Brand#34 |SMALL BURNISHED BRASS | 9| 12 +Brand#34 |SMALL BURNISHED BRASS | 23| 12 +Brand#34 |SMALL BURNISHED COPPER | 9| 12 +Brand#34 |SMALL BURNISHED COPPER | 36| 12 +Brand#34 |SMALL BURNISHED NICKEL | 9| 12 +Brand#34 |SMALL BURNISHED NICKEL | 14| 12 +Brand#34 |SMALL BURNISHED NICKEL | 36| 12 +Brand#34 |SMALL BURNISHED STEEL | 14| 12 +Brand#34 |SMALL PLATED BRASS | 14| 12 +Brand#34 |SMALL PLATED TIN | 45| 12 +Brand#34 |SMALL POLISHED STEEL | 19| 12 +Brand#34 |STANDARD ANODIZED BRASS | 36| 12 +Brand#34 |STANDARD ANODIZED TIN | 3| 12 +Brand#34 |STANDARD ANODIZED TIN | 14| 12 +Brand#34 |STANDARD BRUSHED BRASS | 36| 12 +Brand#34 |STANDARD BRUSHED COPPER | 3| 12 +Brand#34 |STANDARD BRUSHED STEEL | 23| 12 +Brand#34 |STANDARD BRUSHED TIN | 45| 12 +Brand#34 |STANDARD BURNISHED STEEL | 14| 12 +Brand#34 |STANDARD BURNISHED TIN | 45| 12 +Brand#34 |STANDARD POLISHED COPPER | 14| 12 +Brand#35 |ECONOMY ANODIZED BRASS | 14| 12 +Brand#35 |ECONOMY ANODIZED COPPER | 19| 12 +Brand#35 |ECONOMY ANODIZED NICKEL | 14| 12 +Brand#35 |ECONOMY ANODIZED STEEL | 14| 12 +Brand#35 |ECONOMY ANODIZED STEEL | 45| 12 +Brand#35 |ECONOMY BRUSHED BRASS | 36| 12 +Brand#35 |ECONOMY BRUSHED NICKEL | 49| 12 +Brand#35 |ECONOMY BURNISHED BRASS | 19| 12 +Brand#35 |ECONOMY BURNISHED BRASS | 36| 12 +Brand#35 |ECONOMY BURNISHED STEEL | 36| 12 +Brand#35 |ECONOMY PLATED TIN | 45| 12 +Brand#35 |ECONOMY PLATED TIN | 49| 12 +Brand#35 |ECONOMY POLISHED COPPER | 9| 12 +Brand#35 |ECONOMY POLISHED NICKEL | 23| 12 +Brand#35 |ECONOMY POLISHED STEEL | 9| 12 +Brand#35 |ECONOMY POLISHED TIN | 23| 12 +Brand#35 |LARGE ANODIZED BRASS | 3| 12 +Brand#35 |LARGE ANODIZED BRASS | 45| 12 +Brand#35 |LARGE ANODIZED COPPER | 19| 12 +Brand#35 |LARGE ANODIZED COPPER | 36| 12 +Brand#35 |LARGE ANODIZED STEEL | 45| 12 +Brand#35 |LARGE ANODIZED TIN | 45| 12 +Brand#35 |LARGE BRUSHED COPPER | 23| 12 +Brand#35 |LARGE BRUSHED NICKEL | 36| 12 +Brand#35 |LARGE BRUSHED STEEL | 3| 12 +Brand#35 |LARGE BRUSHED TIN | 36| 12 +Brand#35 |LARGE BURNISHED BRASS | 45| 12 +Brand#35 |LARGE BURNISHED STEEL | 9| 12 +Brand#35 |LARGE BURNISHED STEEL | 45| 12 +Brand#35 |LARGE BURNISHED TIN | 49| 12 +Brand#35 |LARGE PLATED BRASS | 3| 12 +Brand#35 |LARGE PLATED BRASS | 23| 12 +Brand#35 |LARGE PLATED STEEL | 19| 12 +Brand#35 |LARGE PLATED STEEL | 49| 12 +Brand#35 |MEDIUM ANODIZED TIN | 3| 12 +Brand#35 |MEDIUM BRUSHED BRASS | 49| 12 +Brand#35 |MEDIUM BRUSHED COPPER | 14| 12 +Brand#35 |MEDIUM BRUSHED NICKEL | 3| 12 +Brand#35 |MEDIUM BRUSHED STEEL | 45| 12 +Brand#35 |MEDIUM BURNISHED STEEL | 19| 12 +Brand#35 |MEDIUM PLATED NICKEL | 45| 12 +Brand#35 |MEDIUM PLATED STEEL | 3| 12 +Brand#35 |MEDIUM PLATED TIN | 36| 12 +Brand#35 |PROMO ANODIZED BRASS | 14| 12 +Brand#35 |PROMO ANODIZED STEEL | 3| 12 +Brand#35 |PROMO ANODIZED STEEL | 23| 12 +Brand#35 |PROMO ANODIZED TIN | 49| 12 +Brand#35 |PROMO BRUSHED COPPER | 9| 12 +Brand#35 |PROMO BRUSHED COPPER | 23| 12 +Brand#35 |PROMO BRUSHED STEEL | 36| 12 +Brand#35 |PROMO BURNISHED NICKEL | 19| 12 +Brand#35 |PROMO BURNISHED STEEL | 3| 12 +Brand#35 |PROMO BURNISHED STEEL | 14| 12 +Brand#35 |PROMO BURNISHED STEEL | 49| 12 +Brand#35 |PROMO BURNISHED TIN | 9| 12 +Brand#35 |PROMO BURNISHED TIN | 14| 12 +Brand#35 |PROMO POLISHED BRASS | 19| 12 +Brand#35 |PROMO POLISHED COPPER | 49| 12 +Brand#35 |PROMO POLISHED NICKEL | 49| 12 +Brand#35 |PROMO POLISHED STEEL | 9| 12 +Brand#35 |PROMO POLISHED TIN | 36| 12 +Brand#35 |SMALL ANODIZED BRASS | 9| 12 +Brand#35 |SMALL ANODIZED BRASS | 19| 12 +Brand#35 |SMALL BRUSHED NICKEL | 19| 12 +Brand#35 |SMALL BRUSHED STEEL | 45| 12 +Brand#35 |SMALL BRUSHED TIN | 45| 12 +Brand#35 |SMALL BURNISHED BRASS | 9| 12 +Brand#35 |SMALL BURNISHED BRASS | 23| 12 +Brand#35 |SMALL BURNISHED BRASS | 36| 12 +Brand#35 |SMALL BURNISHED BRASS | 49| 12 +Brand#35 |SMALL BURNISHED COPPER | 45| 12 +Brand#35 |SMALL PLATED BRASS | 9| 12 +Brand#35 |SMALL PLATED BRASS | 36| 12 +Brand#35 |SMALL PLATED TIN | 36| 12 +Brand#35 |STANDARD ANODIZED TIN | 3| 12 +Brand#35 |STANDARD ANODIZED TIN | 9| 12 +Brand#35 |STANDARD BURNISHED BRASS | 36| 12 +Brand#35 |STANDARD BURNISHED STEEL | 49| 12 +Brand#35 |STANDARD PLATED BRASS | 49| 12 +Brand#35 |STANDARD PLATED COPPER | 9| 12 +Brand#35 |STANDARD PLATED NICKEL | 23| 12 +Brand#35 |STANDARD PLATED NICKEL | 49| 12 +Brand#35 |STANDARD PLATED STEEL | 23| 12 +Brand#35 |STANDARD PLATED TIN | 45| 12 +Brand#35 |STANDARD POLISHED STEEL | 23| 12 +Brand#35 |STANDARD POLISHED TIN | 3| 12 +Brand#41 |ECONOMY ANODIZED BRASS | 45| 12 +Brand#41 |ECONOMY ANODIZED TIN | 14| 12 +Brand#41 |ECONOMY BRUSHED BRASS | 23| 12 +Brand#41 |ECONOMY BRUSHED NICKEL | 49| 12 +Brand#41 |ECONOMY BRUSHED STEEL | 36| 12 +Brand#41 |ECONOMY BRUSHED TIN | 45| 12 +Brand#41 |ECONOMY BURNISHED COPPER | 3| 12 +Brand#41 |ECONOMY BURNISHED COPPER | 45| 12 +Brand#41 |ECONOMY PLATED NICKEL | 23| 12 +Brand#41 |ECONOMY PLATED STEEL | 36| 12 +Brand#41 |ECONOMY PLATED TIN | 23| 12 +Brand#41 |ECONOMY POLISHED BRASS | 36| 12 +Brand#41 |ECONOMY POLISHED COPPER | 49| 12 +Brand#41 |ECONOMY POLISHED NICKEL | 9| 12 +Brand#41 |ECONOMY POLISHED NICKEL | 19| 12 +Brand#41 |ECONOMY POLISHED NICKEL | 23| 12 +Brand#41 |ECONOMY POLISHED STEEL | 49| 12 +Brand#41 |LARGE ANODIZED BRASS | 14| 12 +Brand#41 |LARGE ANODIZED BRASS | 23| 12 +Brand#41 |LARGE ANODIZED COPPER | 36| 12 +Brand#41 |LARGE ANODIZED STEEL | 23| 12 +Brand#41 |LARGE BRUSHED BRASS | 9| 12 +Brand#41 |LARGE BRUSHED COPPER | 23| 12 +Brand#41 |LARGE BURNISHED BRASS | 36| 12 +Brand#41 |LARGE BURNISHED STEEL | 23| 12 +Brand#41 |LARGE PLATED NICKEL | 14| 12 +Brand#41 |LARGE POLISHED BRASS | 45| 12 +Brand#41 |LARGE POLISHED COPPER | 23| 12 +Brand#41 |LARGE POLISHED COPPER | 36| 12 +Brand#41 |LARGE POLISHED STEEL | 3| 12 +Brand#41 |LARGE POLISHED STEEL | 9| 12 +Brand#41 |MEDIUM ANODIZED NICKEL | 3| 12 +Brand#41 |MEDIUM ANODIZED TIN | 3| 12 +Brand#41 |MEDIUM BURNISHED COPPER | 23| 12 +Brand#41 |MEDIUM BURNISHED TIN | 14| 12 +Brand#41 |MEDIUM BURNISHED TIN | 45| 12 +Brand#41 |MEDIUM PLATED BRASS | 19| 12 +Brand#41 |MEDIUM PLATED COPPER | 19| 12 +Brand#41 |MEDIUM PLATED COPPER | 45| 12 +Brand#41 |PROMO ANODIZED BRASS | 14| 12 +Brand#41 |PROMO ANODIZED NICKEL | 49| 12 +Brand#41 |PROMO ANODIZED TIN | 9| 12 +Brand#41 |PROMO BURNISHED COPPER | 49| 12 +Brand#41 |PROMO BURNISHED TIN | 14| 12 +Brand#41 |PROMO PLATED NICKEL | 14| 12 +Brand#41 |PROMO PLATED STEEL | 45| 12 +Brand#41 |PROMO PLATED TIN | 3| 12 +Brand#41 |PROMO PLATED TIN | 36| 12 +Brand#41 |PROMO POLISHED COPPER | 23| 12 +Brand#41 |PROMO POLISHED NICKEL | 19| 12 +Brand#41 |SMALL ANODIZED BRASS | 3| 12 +Brand#41 |SMALL ANODIZED COPPER | 14| 12 +Brand#41 |SMALL ANODIZED NICKEL | 36| 12 +Brand#41 |SMALL BRUSHED STEEL | 36| 12 +Brand#41 |SMALL BRUSHED TIN | 14| 12 +Brand#41 |SMALL BURNISHED TIN | 3| 12 +Brand#41 |SMALL PLATED BRASS | 14| 12 +Brand#41 |SMALL PLATED STEEL | 14| 12 +Brand#41 |SMALL POLISHED COPPER | 36| 12 +Brand#41 |SMALL POLISHED TIN | 36| 12 +Brand#41 |STANDARD ANODIZED BRASS | 3| 12 +Brand#41 |STANDARD ANODIZED BRASS | 36| 12 +Brand#41 |STANDARD ANODIZED COPPER | 14| 12 +Brand#41 |STANDARD ANODIZED NICKEL | 36| 12 +Brand#41 |STANDARD BURNISHED STEEL | 9| 12 +Brand#41 |STANDARD BURNISHED TIN | 3| 12 +Brand#41 |STANDARD PLATED BRASS | 45| 12 +Brand#41 |STANDARD PLATED COPPER | 49| 12 +Brand#41 |STANDARD POLISHED COPPER | 23| 12 +Brand#41 |STANDARD POLISHED NICKEL | 3| 12 +Brand#42 |ECONOMY ANODIZED BRASS | 36| 12 +Brand#42 |ECONOMY ANODIZED STEEL | 9| 12 +Brand#42 |ECONOMY BRUSHED NICKEL | 45| 12 +Brand#42 |ECONOMY BRUSHED TIN | 14| 12 +Brand#42 |ECONOMY BURNISHED NICKEL | 49| 12 +Brand#42 |ECONOMY BURNISHED STEEL | 49| 12 +Brand#42 |ECONOMY BURNISHED TIN | 19| 12 +Brand#42 |ECONOMY PLATED COPPER | 14| 12 +Brand#42 |ECONOMY PLATED NICKEL | 9| 12 +Brand#42 |ECONOMY POLISHED COPPER | 9| 12 +Brand#42 |LARGE ANODIZED BRASS | 49| 12 +Brand#42 |LARGE ANODIZED COPPER | 36| 12 +Brand#42 |LARGE BURNISHED COPPER | 9| 12 +Brand#42 |LARGE BURNISHED COPPER | 19| 12 +Brand#42 |LARGE BURNISHED TIN | 9| 12 +Brand#42 |LARGE PLATED BRASS | 23| 12 +Brand#42 |LARGE PLATED BRASS | 36| 12 +Brand#42 |LARGE PLATED NICKEL | 23| 12 +Brand#42 |LARGE PLATED TIN | 9| 12 +Brand#42 |LARGE PLATED TIN | 19| 12 +Brand#42 |LARGE POLISHED BRASS | 36| 12 +Brand#42 |LARGE POLISHED STEEL | 9| 12 +Brand#42 |LARGE POLISHED STEEL | 45| 12 +Brand#42 |LARGE POLISHED TIN | 14| 12 +Brand#42 |MEDIUM ANODIZED NICKEL | 19| 12 +Brand#42 |MEDIUM ANODIZED STEEL | 23| 12 +Brand#42 |MEDIUM ANODIZED TIN | 49| 12 +Brand#42 |MEDIUM BRUSHED NICKEL | 9| 12 +Brand#42 |MEDIUM BRUSHED STEEL | 19| 12 +Brand#42 |MEDIUM BRUSHED TIN | 14| 12 +Brand#42 |MEDIUM BURNISHED BRASS | 36| 12 +Brand#42 |MEDIUM BURNISHED NICKEL | 36| 12 +Brand#42 |MEDIUM BURNISHED STEEL | 49| 12 +Brand#42 |MEDIUM PLATED BRASS | 36| 12 +Brand#42 |MEDIUM PLATED COPPER | 36| 12 +Brand#42 |MEDIUM PLATED COPPER | 45| 12 +Brand#42 |MEDIUM PLATED STEEL | 3| 12 +Brand#42 |MEDIUM PLATED TIN | 45| 12 +Brand#42 |PROMO ANODIZED TIN | 23| 12 +Brand#42 |PROMO BRUSHED BRASS | 19| 12 +Brand#42 |PROMO BRUSHED NICKEL | 3| 12 +Brand#42 |PROMO BRUSHED TIN | 45| 12 +Brand#42 |PROMO BURNISHED BRASS | 19| 12 +Brand#42 |PROMO BURNISHED NICKEL | 3| 12 +Brand#42 |PROMO BURNISHED TIN | 9| 12 +Brand#42 |PROMO PLATED BRASS | 14| 12 +Brand#42 |PROMO PLATED BRASS | 23| 12 +Brand#42 |PROMO PLATED STEEL | 19| 12 +Brand#42 |PROMO POLISHED STEEL | 45| 12 +Brand#42 |SMALL ANODIZED BRASS | 36| 12 +Brand#42 |SMALL BRUSHED BRASS | 36| 12 +Brand#42 |SMALL BURNISHED BRASS | 3| 12 +Brand#42 |SMALL BURNISHED BRASS | 36| 12 +Brand#42 |SMALL BURNISHED STEEL | 23| 12 +Brand#42 |SMALL BURNISHED TIN | 9| 12 +Brand#42 |SMALL BURNISHED TIN | 49| 12 +Brand#42 |SMALL PLATED COPPER | 9| 12 +Brand#42 |SMALL PLATED COPPER | 19| 12 +Brand#42 |SMALL POLISHED BRASS | 3| 12 +Brand#42 |SMALL POLISHED COPPER | 36| 12 +Brand#42 |SMALL POLISHED NICKEL | 23| 12 +Brand#42 |STANDARD ANODIZED BRASS | 23| 12 +Brand#42 |STANDARD ANODIZED COPPER | 45| 12 +Brand#42 |STANDARD ANODIZED STEEL | 23| 12 +Brand#42 |STANDARD ANODIZED TIN | 23| 12 +Brand#42 |STANDARD BRUSHED TIN | 3| 12 +Brand#42 |STANDARD BURNISHED COPPER| 36| 12 +Brand#42 |STANDARD BURNISHED TIN | 23| 12 +Brand#42 |STANDARD PLATED COPPER | 9| 12 +Brand#42 |STANDARD PLATED TIN | 3| 12 +Brand#42 |STANDARD POLISHED NICKEL | 9| 12 +Brand#42 |STANDARD POLISHED STEEL | 14| 12 +Brand#43 |ECONOMY ANODIZED BRASS | 14| 12 +Brand#43 |ECONOMY ANODIZED COPPER | 9| 12 +Brand#43 |ECONOMY ANODIZED COPPER | 19| 12 +Brand#43 |ECONOMY ANODIZED COPPER | 45| 12 +Brand#43 |ECONOMY BRUSHED STEEL | 9| 12 +Brand#43 |ECONOMY BRUSHED STEEL | 14| 12 +Brand#43 |ECONOMY BRUSHED STEEL | 36| 12 +Brand#43 |ECONOMY BRUSHED STEEL | 45| 12 +Brand#43 |ECONOMY BRUSHED TIN | 49| 12 +Brand#43 |ECONOMY BURNISHED BRASS | 3| 12 +Brand#43 |ECONOMY BURNISHED BRASS | 49| 12 +Brand#43 |ECONOMY BURNISHED NICKEL | 3| 12 +Brand#43 |ECONOMY BURNISHED NICKEL | 36| 12 +Brand#43 |ECONOMY BURNISHED STEEL | 9| 12 +Brand#43 |ECONOMY BURNISHED TIN | 19| 12 +Brand#43 |ECONOMY PLATED COPPER | 3| 12 +Brand#43 |ECONOMY PLATED STEEL | 3| 12 +Brand#43 |ECONOMY POLISHED BRASS | 45| 12 +Brand#43 |ECONOMY POLISHED NICKEL | 45| 12 +Brand#43 |ECONOMY POLISHED TIN | 49| 12 +Brand#43 |LARGE ANODIZED TIN | 14| 12 +Brand#43 |LARGE BRUSHED NICKEL | 23| 12 +Brand#43 |LARGE BRUSHED STEEL | 45| 12 +Brand#43 |LARGE BURNISHED COPPER | 14| 12 +Brand#43 |LARGE BURNISHED NICKEL | 3| 12 +Brand#43 |LARGE BURNISHED STEEL | 3| 12 +Brand#43 |LARGE BURNISHED TIN | 45| 12 +Brand#43 |LARGE PLATED TIN | 9| 12 +Brand#43 |LARGE POLISHED BRASS | 9| 12 +Brand#43 |LARGE POLISHED COPPER | 23| 12 +Brand#43 |LARGE POLISHED NICKEL | 9| 12 +Brand#43 |LARGE POLISHED TIN | 45| 12 +Brand#43 |MEDIUM ANODIZED BRASS | 14| 12 +Brand#43 |MEDIUM ANODIZED BRASS | 19| 12 +Brand#43 |MEDIUM ANODIZED BRASS | 36| 12 +Brand#43 |MEDIUM ANODIZED COPPER | 45| 12 +Brand#43 |MEDIUM ANODIZED NICKEL | 36| 12 +Brand#43 |MEDIUM BRUSHED BRASS | 45| 12 +Brand#43 |MEDIUM BURNISHED BRASS | 36| 12 +Brand#43 |MEDIUM BURNISHED BRASS | 45| 12 +Brand#43 |MEDIUM BURNISHED BRASS | 49| 12 +Brand#43 |MEDIUM BURNISHED COPPER | 3| 12 +Brand#43 |MEDIUM BURNISHED COPPER | 14| 12 +Brand#43 |MEDIUM PLATED BRASS | 3| 12 +Brand#43 |MEDIUM PLATED BRASS | 49| 12 +Brand#43 |MEDIUM PLATED COPPER | 19| 12 +Brand#43 |PROMO ANODIZED NICKEL | 19| 12 +Brand#43 |PROMO ANODIZED STEEL | 9| 12 +Brand#43 |PROMO ANODIZED TIN | 9| 12 +Brand#43 |PROMO BRUSHED NICKEL | 23| 12 +Brand#43 |PROMO BRUSHED TIN | 49| 12 +Brand#43 |PROMO BURNISHED STEEL | 36| 12 +Brand#43 |PROMO BURNISHED STEEL | 45| 12 +Brand#43 |PROMO BURNISHED TIN | 14| 12 +Brand#43 |PROMO PLATED NICKEL | 9| 12 +Brand#43 |PROMO PLATED NICKEL | 14| 12 +Brand#43 |PROMO PLATED STEEL | 9| 12 +Brand#43 |PROMO POLISHED COPPER | 23| 12 +Brand#43 |PROMO POLISHED NICKEL | 3| 12 +Brand#43 |PROMO POLISHED STEEL | 3| 12 +Brand#43 |PROMO POLISHED STEEL | 36| 12 +Brand#43 |SMALL ANODIZED NICKEL | 3| 12 +Brand#43 |SMALL ANODIZED NICKEL | 23| 12 +Brand#43 |SMALL BRUSHED BRASS | 49| 12 +Brand#43 |SMALL BRUSHED COPPER | 36| 12 +Brand#43 |SMALL BRUSHED NICKEL | 36| 12 +Brand#43 |SMALL BRUSHED STEEL | 9| 12 +Brand#43 |SMALL BURNISHED COPPER | 49| 12 +Brand#43 |SMALL BURNISHED NICKEL | 45| 12 +Brand#43 |SMALL PLATED BRASS | 36| 12 +Brand#43 |SMALL PLATED COPPER | 9| 12 +Brand#43 |SMALL PLATED COPPER | 49| 12 +Brand#43 |SMALL POLISHED NICKEL | 14| 12 +Brand#43 |SMALL POLISHED TIN | 49| 12 +Brand#43 |STANDARD ANODIZED BRASS | 36| 12 +Brand#43 |STANDARD ANODIZED NICKEL | 14| 12 +Brand#43 |STANDARD ANODIZED TIN | 9| 12 +Brand#43 |STANDARD ANODIZED TIN | 49| 12 +Brand#43 |STANDARD BRUSHED BRASS | 3| 12 +Brand#43 |STANDARD BRUSHED COPPER | 19| 12 +Brand#43 |STANDARD BURNISHED STEEL | 23| 12 +Brand#43 |STANDARD BURNISHED TIN | 14| 12 +Brand#43 |STANDARD PLATED BRASS | 19| 12 +Brand#43 |STANDARD PLATED NICKEL | 14| 12 +Brand#43 |STANDARD PLATED NICKEL | 23| 12 +Brand#43 |STANDARD PLATED NICKEL | 36| 12 +Brand#43 |STANDARD POLISHED COPPER | 3| 12 +Brand#43 |STANDARD POLISHED STEEL | 36| 12 +Brand#43 |STANDARD POLISHED TIN | 9| 12 +Brand#44 |ECONOMY ANODIZED COPPER | 9| 12 +Brand#44 |ECONOMY ANODIZED NICKEL | 36| 12 +Brand#44 |ECONOMY ANODIZED STEEL | 14| 12 +Brand#44 |ECONOMY BRUSHED COPPER | 19| 12 +Brand#44 |ECONOMY BURNISHED STEEL | 45| 12 +Brand#44 |ECONOMY POLISHED TIN | 36| 12 +Brand#44 |ECONOMY POLISHED TIN | 49| 12 +Brand#44 |LARGE ANODIZED TIN | 3| 12 +Brand#44 |LARGE BRUSHED COPPER | 36| 12 +Brand#44 |LARGE BRUSHED STEEL | 36| 12 +Brand#44 |LARGE BRUSHED TIN | 3| 12 +Brand#44 |LARGE BRUSHED TIN | 19| 12 +Brand#44 |LARGE BURNISHED BRASS | 19| 12 +Brand#44 |LARGE BURNISHED BRASS | 49| 12 +Brand#44 |LARGE BURNISHED NICKEL | 9| 12 +Brand#44 |LARGE PLATED BRASS | 9| 12 +Brand#44 |LARGE PLATED NICKEL | 3| 12 +Brand#44 |LARGE PLATED NICKEL | 14| 12 +Brand#44 |LARGE PLATED NICKEL | 36| 12 +Brand#44 |MEDIUM ANODIZED BRASS | 23| 12 +Brand#44 |MEDIUM ANODIZED COPPER | 45| 12 +Brand#44 |MEDIUM ANODIZED TIN | 9| 12 +Brand#44 |MEDIUM BRUSHED BRASS | 49| 12 +Brand#44 |MEDIUM BRUSHED COPPER | 3| 12 +Brand#44 |MEDIUM BRUSHED COPPER | 9| 12 +Brand#44 |MEDIUM BRUSHED COPPER | 36| 12 +Brand#44 |MEDIUM BURNISHED COPPER | 36| 12 +Brand#44 |MEDIUM BURNISHED NICKEL | 36| 12 +Brand#44 |MEDIUM PLATED STEEL | 19| 12 +Brand#44 |MEDIUM PLATED TIN | 23| 12 +Brand#44 |MEDIUM PLATED TIN | 36| 12 +Brand#44 |PROMO ANODIZED BRASS | 9| 12 +Brand#44 |PROMO ANODIZED COPPER | 19| 12 +Brand#44 |PROMO ANODIZED NICKEL | 19| 12 +Brand#44 |PROMO ANODIZED STEEL | 36| 12 +Brand#44 |PROMO BRUSHED NICKEL | 3| 12 +Brand#44 |PROMO BURNISHED BRASS | 19| 12 +Brand#44 |PROMO BURNISHED NICKEL | 49| 12 +Brand#44 |PROMO PLATED BRASS | 19| 12 +Brand#44 |PROMO PLATED STEEL | 14| 12 +Brand#44 |PROMO PLATED STEEL | 36| 12 +Brand#44 |PROMO POLISHED COPPER | 14| 12 +Brand#44 |PROMO POLISHED COPPER | 23| 12 +Brand#44 |PROMO POLISHED COPPER | 45| 12 +Brand#44 |PROMO POLISHED STEEL | 36| 12 +Brand#44 |SMALL ANODIZED STEEL | 36| 12 +Brand#44 |SMALL BRUSHED COPPER | 19| 12 +Brand#44 |SMALL BRUSHED COPPER | 45| 12 +Brand#44 |SMALL BRUSHED NICKEL | 3| 12 +Brand#44 |SMALL BRUSHED NICKEL | 9| 12 +Brand#44 |SMALL BURNISHED COPPER | 14| 12 +Brand#44 |SMALL BURNISHED NICKEL | 3| 12 +Brand#44 |SMALL BURNISHED TIN | 3| 12 +Brand#44 |SMALL BURNISHED TIN | 36| 12 +Brand#44 |SMALL PLATED BRASS | 23| 12 +Brand#44 |SMALL PLATED BRASS | 49| 12 +Brand#44 |SMALL PLATED STEEL | 3| 12 +Brand#44 |SMALL PLATED STEEL | 45| 12 +Brand#44 |SMALL POLISHED BRASS | 3| 12 +Brand#44 |SMALL POLISHED COPPER | 14| 12 +Brand#44 |STANDARD ANODIZED BRASS | 3| 12 +Brand#44 |STANDARD ANODIZED BRASS | 14| 12 +Brand#44 |STANDARD ANODIZED COPPER | 45| 12 +Brand#44 |STANDARD ANODIZED NICKEL | 9| 12 +Brand#44 |STANDARD ANODIZED NICKEL | 36| 12 +Brand#44 |STANDARD ANODIZED TIN | 9| 12 +Brand#44 |STANDARD BRUSHED BRASS | 9| 12 +Brand#44 |STANDARD BRUSHED COPPER | 23| 12 +Brand#44 |STANDARD BRUSHED TIN | 49| 12 +Brand#44 |STANDARD BURNISHED COPPER| 3| 12 +Brand#44 |STANDARD BURNISHED COPPER| 49| 12 +Brand#44 |STANDARD BURNISHED STEEL | 23| 12 +Brand#44 |STANDARD BURNISHED TIN | 36| 12 +Brand#44 |STANDARD PLATED COPPER | 14| 12 +Brand#44 |STANDARD PLATED COPPER | 45| 12 +Brand#44 |STANDARD PLATED TIN | 9| 12 +Brand#44 |STANDARD PLATED TIN | 23| 12 +Brand#44 |STANDARD POLISHED BRASS | 14| 12 +Brand#44 |STANDARD POLISHED NICKEL | 19| 12 +Brand#51 |ECONOMY ANODIZED BRASS | 9| 12 +Brand#51 |ECONOMY ANODIZED BRASS | 36| 12 +Brand#51 |ECONOMY ANODIZED BRASS | 45| 12 +Brand#51 |ECONOMY ANODIZED COPPER | 19| 12 +Brand#51 |ECONOMY ANODIZED NICKEL | 14| 12 +Brand#51 |ECONOMY ANODIZED TIN | 9| 12 +Brand#51 |ECONOMY BRUSHED STEEL | 36| 12 +Brand#51 |ECONOMY BRUSHED STEEL | 45| 12 +Brand#51 |ECONOMY BRUSHED TIN | 36| 12 +Brand#51 |ECONOMY BURNISHED COPPER | 45| 12 +Brand#51 |ECONOMY PLATED STEEL | 19| 12 +Brand#51 |ECONOMY PLATED STEEL | 23| 12 +Brand#51 |ECONOMY PLATED TIN | 45| 12 +Brand#51 |LARGE ANODIZED COPPER | 19| 12 +Brand#51 |LARGE BRUSHED COPPER | 36| 12 +Brand#51 |LARGE BRUSHED NICKEL | 49| 12 +Brand#51 |LARGE BURNISHED STEEL | 3| 12 +Brand#51 |LARGE PLATED COPPER | 9| 12 +Brand#51 |LARGE PLATED NICKEL | 45| 12 +Brand#51 |LARGE PLATED TIN | 19| 12 +Brand#51 |LARGE PLATED TIN | 23| 12 +Brand#51 |LARGE POLISHED COPPER | 3| 12 +Brand#51 |LARGE POLISHED COPPER | 23| 12 +Brand#51 |MEDIUM ANODIZED NICKEL | 3| 12 +Brand#51 |MEDIUM ANODIZED NICKEL | 19| 12 +Brand#51 |MEDIUM ANODIZED NICKEL | 23| 12 +Brand#51 |MEDIUM ANODIZED STEEL | 14| 12 +Brand#51 |MEDIUM ANODIZED TIN | 14| 12 +Brand#51 |MEDIUM BRUSHED COPPER | 49| 12 +Brand#51 |MEDIUM BRUSHED TIN | 49| 12 +Brand#51 |MEDIUM BURNISHED BRASS | 36| 12 +Brand#51 |MEDIUM BURNISHED NICKEL | 14| 12 +Brand#51 |MEDIUM BURNISHED NICKEL | 49| 12 +Brand#51 |MEDIUM PLATED NICKEL | 45| 12 +Brand#51 |PROMO ANODIZED BRASS | 3| 12 +Brand#51 |PROMO ANODIZED COPPER | 23| 12 +Brand#51 |PROMO ANODIZED NICKEL | 9| 12 +Brand#51 |PROMO ANODIZED NICKEL | 14| 12 +Brand#51 |PROMO ANODIZED TIN | 23| 12 +Brand#51 |PROMO ANODIZED TIN | 49| 12 +Brand#51 |PROMO BRUSHED BRASS | 23| 12 +Brand#51 |PROMO BRUSHED COPPER | 19| 12 +Brand#51 |PROMO BRUSHED STEEL | 36| 12 +Brand#51 |PROMO BRUSHED TIN | 3| 12 +Brand#51 |PROMO BURNISHED COPPER | 3| 12 +Brand#51 |PROMO BURNISHED COPPER | 19| 12 +Brand#51 |PROMO PLATED COPPER | 9| 12 +Brand#51 |PROMO PLATED STEEL | 45| 12 +Brand#51 |PROMO PLATED TIN | 14| 12 +Brand#51 |SMALL ANODIZED NICKEL | 9| 12 +Brand#51 |SMALL BRUSHED BRASS | 19| 12 +Brand#51 |SMALL BRUSHED NICKEL | 3| 12 +Brand#51 |SMALL BRUSHED TIN | 19| 12 +Brand#51 |SMALL BURNISHED NICKEL | 14| 12 +Brand#51 |SMALL BURNISHED NICKEL | 23| 12 +Brand#51 |SMALL BURNISHED STEEL | 45| 12 +Brand#51 |SMALL BURNISHED STEEL | 49| 12 +Brand#51 |SMALL BURNISHED TIN | 23| 12 +Brand#51 |SMALL PLATED COPPER | 14| 12 +Brand#51 |SMALL PLATED COPPER | 36| 12 +Brand#51 |SMALL PLATED NICKEL | 14| 12 +Brand#51 |SMALL PLATED STEEL | 9| 12 +Brand#51 |SMALL POLISHED COPPER | 23| 12 +Brand#51 |SMALL POLISHED NICKEL | 19| 12 +Brand#51 |SMALL POLISHED NICKEL | 23| 12 +Brand#51 |SMALL POLISHED STEEL | 3| 12 +Brand#51 |SMALL POLISHED TIN | 36| 12 +Brand#51 |STANDARD ANODIZED BRASS | 49| 12 +Brand#51 |STANDARD ANODIZED COPPER | 14| 12 +Brand#51 |STANDARD ANODIZED NICKEL | 23| 12 +Brand#51 |STANDARD ANODIZED NICKEL | 45| 12 +Brand#51 |STANDARD ANODIZED STEEL | 49| 12 +Brand#51 |STANDARD ANODIZED TIN | 19| 12 +Brand#51 |STANDARD BRUSHED BRASS | 19| 12 +Brand#51 |STANDARD BRUSHED STEEL | 23| 12 +Brand#51 |STANDARD BRUSHED STEEL | 36| 12 +Brand#51 |STANDARD BRUSHED TIN | 36| 12 +Brand#51 |STANDARD BURNISHED STEEL | 23| 12 +Brand#51 |STANDARD BURNISHED STEEL | 36| 12 +Brand#51 |STANDARD PLATED BRASS | 3| 12 +Brand#51 |STANDARD POLISHED COPPER | 45| 12 +Brand#51 |STANDARD POLISHED STEEL | 36| 12 +Brand#51 |STANDARD POLISHED STEEL | 45| 12 +Brand#51 |STANDARD POLISHED TIN | 3| 12 +Brand#52 |ECONOMY ANODIZED COPPER | 19| 12 +Brand#52 |ECONOMY ANODIZED STEEL | 14| 12 +Brand#52 |ECONOMY ANODIZED TIN | 9| 12 +Brand#52 |ECONOMY ANODIZED TIN | 19| 12 +Brand#52 |ECONOMY BURNISHED COPPER | 14| 12 +Brand#52 |ECONOMY BURNISHED COPPER | 19| 12 +Brand#52 |ECONOMY BURNISHED NICKEL | 19| 12 +Brand#52 |ECONOMY PLATED STEEL | 45| 12 +Brand#52 |ECONOMY POLISHED BRASS | 14| 12 +Brand#52 |ECONOMY POLISHED BRASS | 19| 12 +Brand#52 |ECONOMY POLISHED COPPER | 3| 12 +Brand#52 |ECONOMY POLISHED COPPER | 14| 12 +Brand#52 |ECONOMY POLISHED COPPER | 19| 12 +Brand#52 |LARGE ANODIZED COPPER | 14| 12 +Brand#52 |LARGE ANODIZED NICKEL | 3| 12 +Brand#52 |LARGE BRUSHED BRASS | 23| 12 +Brand#52 |LARGE BRUSHED STEEL | 23| 12 +Brand#52 |LARGE BURNISHED BRASS | 14| 12 +Brand#52 |LARGE BURNISHED NICKEL | 23| 12 +Brand#52 |LARGE PLATED BRASS | 23| 12 +Brand#52 |LARGE PLATED COPPER | 19| 12 +Brand#52 |LARGE PLATED NICKEL | 19| 12 +Brand#52 |LARGE PLATED NICKEL | 45| 12 +Brand#52 |LARGE PLATED STEEL | 49| 12 +Brand#52 |LARGE PLATED TIN | 3| 12 +Brand#52 |LARGE PLATED TIN | 19| 12 +Brand#52 |LARGE POLISHED BRASS | 3| 12 +Brand#52 |LARGE POLISHED BRASS | 9| 12 +Brand#52 |LARGE POLISHED BRASS | 23| 12 +Brand#52 |MEDIUM ANODIZED COPPER | 19| 12 +Brand#52 |MEDIUM ANODIZED STEEL | 9| 12 +Brand#52 |MEDIUM ANODIZED TIN | 3| 12 +Brand#52 |MEDIUM BRUSHED BRASS | 3| 12 +Brand#52 |MEDIUM BRUSHED BRASS | 36| 12 +Brand#52 |MEDIUM BRUSHED COPPER | 36| 12 +Brand#52 |MEDIUM BURNISHED BRASS | 49| 12 +Brand#52 |MEDIUM BURNISHED COPPER | 3| 12 +Brand#52 |MEDIUM BURNISHED COPPER | 23| 12 +Brand#52 |MEDIUM BURNISHED NICKEL | 45| 12 +Brand#52 |MEDIUM BURNISHED TIN | 23| 12 +Brand#52 |MEDIUM PLATED BRASS | 14| 12 +Brand#52 |MEDIUM PLATED TIN | 36| 12 +Brand#52 |MEDIUM PLATED TIN | 49| 12 +Brand#52 |PROMO ANODIZED BRASS | 9| 12 +Brand#52 |PROMO ANODIZED BRASS | 23| 12 +Brand#52 |PROMO ANODIZED COPPER | 14| 12 +Brand#52 |PROMO ANODIZED COPPER | 49| 12 +Brand#52 |PROMO ANODIZED STEEL | 36| 12 +Brand#52 |PROMO ANODIZED TIN | 3| 12 +Brand#52 |PROMO BRUSHED COPPER | 49| 12 +Brand#52 |PROMO BRUSHED NICKEL | 3| 12 +Brand#52 |PROMO BRUSHED TIN | 36| 12 +Brand#52 |PROMO BURNISHED NICKEL | 36| 12 +Brand#52 |PROMO BURNISHED STEEL | 19| 12 +Brand#52 |PROMO BURNISHED STEEL | 45| 12 +Brand#52 |PROMO BURNISHED TIN | 19| 12 +Brand#52 |PROMO BURNISHED TIN | 45| 12 +Brand#52 |PROMO PLATED BRASS | 14| 12 +Brand#52 |PROMO PLATED NICKEL | 14| 12 +Brand#52 |PROMO PLATED NICKEL | 49| 12 +Brand#52 |PROMO PLATED STEEL | 9| 12 +Brand#52 |PROMO PLATED TIN | 3| 12 +Brand#52 |PROMO POLISHED BRASS | 23| 12 +Brand#52 |PROMO POLISHED COPPER | 45| 12 +Brand#52 |PROMO POLISHED NICKEL | 49| 12 +Brand#52 |SMALL ANODIZED COPPER | 36| 12 +Brand#52 |SMALL ANODIZED NICKEL | 19| 12 +Brand#52 |SMALL ANODIZED NICKEL | 36| 12 +Brand#52 |SMALL BRUSHED BRASS | 14| 12 +Brand#52 |SMALL BRUSHED BRASS | 19| 12 +Brand#52 |SMALL BRUSHED COPPER | 9| 12 +Brand#52 |SMALL BRUSHED STEEL | 45| 12 +Brand#52 |SMALL BURNISHED BRASS | 14| 12 +Brand#52 |SMALL BURNISHED COPPER | 23| 12 +Brand#52 |SMALL BURNISHED NICKEL | 9| 12 +Brand#52 |SMALL BURNISHED NICKEL | 36| 12 +Brand#52 |SMALL BURNISHED NICKEL | 49| 12 +Brand#52 |SMALL BURNISHED STEEL | 23| 12 +Brand#52 |SMALL BURNISHED TIN | 3| 12 +Brand#52 |SMALL PLATED BRASS | 36| 12 +Brand#52 |SMALL PLATED NICKEL | 19| 12 +Brand#52 |SMALL PLATED NICKEL | 23| 12 +Brand#52 |SMALL POLISHED NICKEL | 9| 12 +Brand#52 |SMALL POLISHED NICKEL | 19| 12 +Brand#52 |STANDARD ANODIZED TIN | 14| 12 +Brand#52 |STANDARD BRUSHED BRASS | 19| 12 +Brand#52 |STANDARD BRUSHED COPPER | 19| 12 +Brand#52 |STANDARD BRUSHED TIN | 36| 12 +Brand#52 |STANDARD BRUSHED TIN | 49| 12 +Brand#52 |STANDARD BURNISHED STEEL | 9| 12 +Brand#52 |STANDARD BURNISHED TIN | 9| 12 +Brand#52 |STANDARD PLATED COPPER | 45| 12 +Brand#52 |STANDARD PLATED NICKEL | 3| 12 +Brand#52 |STANDARD PLATED NICKEL | 45| 12 +Brand#52 |STANDARD PLATED STEEL | 9| 12 +Brand#52 |STANDARD PLATED TIN | 23| 12 +Brand#52 |STANDARD POLISHED BRASS | 36| 12 +Brand#52 |STANDARD POLISHED NICKEL | 3| 12 +Brand#53 |ECONOMY ANODIZED COPPER | 23| 12 +Brand#53 |ECONOMY ANODIZED COPPER | 36| 12 +Brand#53 |ECONOMY ANODIZED STEEL | 9| 12 +Brand#53 |ECONOMY BRUSHED BRASS | 3| 12 +Brand#53 |ECONOMY BRUSHED BRASS | 23| 12 +Brand#53 |ECONOMY BRUSHED COPPER | 45| 12 +Brand#53 |ECONOMY BRUSHED STEEL | 19| 12 +Brand#53 |ECONOMY BURNISHED BRASS | 49| 12 +Brand#53 |ECONOMY BURNISHED COPPER | 45| 12 +Brand#53 |ECONOMY BURNISHED TIN | 14| 12 +Brand#53 |ECONOMY PLATED BRASS | 36| 12 +Brand#53 |ECONOMY PLATED BRASS | 45| 12 +Brand#53 |ECONOMY PLATED STEEL | 36| 12 +Brand#53 |ECONOMY PLATED TIN | 3| 12 +Brand#53 |ECONOMY PLATED TIN | 23| 12 +Brand#53 |ECONOMY POLISHED STEEL | 14| 12 +Brand#53 |ECONOMY POLISHED STEEL | 36| 12 +Brand#53 |ECONOMY POLISHED STEEL | 45| 12 +Brand#53 |ECONOMY POLISHED STEEL | 49| 12 +Brand#53 |ECONOMY POLISHED TIN | 19| 12 +Brand#53 |ECONOMY POLISHED TIN | 36| 12 +Brand#53 |LARGE ANODIZED COPPER | 45| 12 +Brand#53 |LARGE ANODIZED NICKEL | 9| 12 +Brand#53 |LARGE ANODIZED STEEL | 19| 12 +Brand#53 |LARGE BRUSHED BRASS | 9| 12 +Brand#53 |LARGE BRUSHED BRASS | 19| 12 +Brand#53 |LARGE BRUSHED NICKEL | 23| 12 +Brand#53 |LARGE BRUSHED STEEL | 19| 12 +Brand#53 |LARGE BURNISHED BRASS | 9| 12 +Brand#53 |LARGE BURNISHED STEEL | 14| 12 +Brand#53 |LARGE PLATED COPPER | 3| 12 +Brand#53 |LARGE PLATED NICKEL | 45| 12 +Brand#53 |LARGE POLISHED COPPER | 49| 12 +Brand#53 |LARGE POLISHED STEEL | 36| 12 +Brand#53 |MEDIUM ANODIZED COPPER | 14| 12 +Brand#53 |MEDIUM ANODIZED NICKEL | 14| 12 +Brand#53 |MEDIUM ANODIZED TIN | 23| 12 +Brand#53 |MEDIUM ANODIZED TIN | 36| 12 +Brand#53 |MEDIUM BRUSHED BRASS | 3| 12 +Brand#53 |MEDIUM BRUSHED BRASS | 23| 12 +Brand#53 |MEDIUM BURNISHED BRASS | 14| 12 +Brand#53 |MEDIUM BURNISHED BRASS | 49| 12 +Brand#53 |MEDIUM BURNISHED NICKEL | 23| 12 +Brand#53 |MEDIUM PLATED BRASS | 49| 12 +Brand#53 |MEDIUM PLATED COPPER | 14| 12 +Brand#53 |MEDIUM PLATED COPPER | 23| 12 +Brand#53 |MEDIUM PLATED STEEL | 14| 12 +Brand#53 |MEDIUM PLATED TIN | 45| 12 +Brand#53 |PROMO ANODIZED COPPER | 14| 12 +Brand#53 |PROMO BRUSHED COPPER | 3| 12 +Brand#53 |PROMO BURNISHED COPPER | 36| 12 +Brand#53 |PROMO BURNISHED NICKEL | 36| 12 +Brand#53 |PROMO BURNISHED STEEL | 36| 12 +Brand#53 |PROMO BURNISHED STEEL | 49| 12 +Brand#53 |PROMO PLATED COPPER | 14| 12 +Brand#53 |PROMO PLATED TIN | 3| 12 +Brand#53 |PROMO PLATED TIN | 23| 12 +Brand#53 |PROMO POLISHED COPPER | 49| 12 +Brand#53 |PROMO POLISHED NICKEL | 9| 12 +Brand#53 |PROMO POLISHED TIN | 14| 12 +Brand#53 |SMALL ANODIZED COPPER | 36| 12 +Brand#53 |SMALL ANODIZED NICKEL | 36| 12 +Brand#53 |SMALL ANODIZED STEEL | 19| 12 +Brand#53 |SMALL BRUSHED COPPER | 14| 12 +Brand#53 |SMALL BURNISHED BRASS | 9| 12 +Brand#53 |SMALL BURNISHED COPPER | 9| 12 +Brand#53 |SMALL BURNISHED NICKEL | 36| 12 +Brand#53 |SMALL BURNISHED STEEL | 19| 12 +Brand#53 |SMALL PLATED COPPER | 3| 12 +Brand#53 |SMALL POLISHED BRASS | 3| 12 +Brand#53 |SMALL POLISHED BRASS | 9| 12 +Brand#53 |SMALL POLISHED STEEL | 36| 12 +Brand#53 |STANDARD ANODIZED STEEL | 23| 12 +Brand#53 |STANDARD ANODIZED STEEL | 49| 12 +Brand#53 |STANDARD BRUSHED COPPER | 3| 12 +Brand#53 |STANDARD BRUSHED STEEL | 45| 12 +Brand#53 |STANDARD BRUSHED TIN | 14| 12 +Brand#53 |STANDARD BRUSHED TIN | 19| 12 +Brand#53 |STANDARD BURNISHED BRASS | 9| 12 +Brand#53 |STANDARD BURNISHED NICKEL| 23| 12 +Brand#53 |STANDARD PLATED BRASS | 3| 12 +Brand#53 |STANDARD PLATED BRASS | 36| 12 +Brand#53 |STANDARD PLATED COPPER | 36| 12 +Brand#53 |STANDARD PLATED COPPER | 45| 12 +Brand#53 |STANDARD POLISHED BRASS | 19| 12 +Brand#53 |STANDARD POLISHED COPPER | 14| 12 +Brand#53 |STANDARD POLISHED TIN | 19| 12 +Brand#54 |ECONOMY ANODIZED COPPER | 19| 12 +Brand#54 |ECONOMY BRUSHED STEEL | 19| 12 +Brand#54 |ECONOMY BRUSHED STEEL | 45| 12 +Brand#54 |ECONOMY BRUSHED TIN | 45| 12 +Brand#54 |ECONOMY BURNISHED BRASS | 19| 12 +Brand#54 |ECONOMY BURNISHED BRASS | 45| 12 +Brand#54 |ECONOMY BURNISHED COPPER | 14| 12 +Brand#54 |ECONOMY BURNISHED NICKEL | 9| 12 +Brand#54 |ECONOMY POLISHED NICKEL | 14| 12 +Brand#54 |ECONOMY POLISHED NICKEL | 45| 12 +Brand#54 |ECONOMY POLISHED TIN | 23| 12 +Brand#54 |LARGE ANODIZED TIN | 36| 12 +Brand#54 |LARGE BRUSHED COPPER | 9| 12 +Brand#54 |LARGE BRUSHED COPPER | 23| 12 +Brand#54 |LARGE BURNISHED BRASS | 45| 12 +Brand#54 |LARGE BURNISHED COPPER | 3| 12 +Brand#54 |LARGE BURNISHED COPPER | 45| 12 +Brand#54 |LARGE BURNISHED NICKEL | 14| 12 +Brand#54 |LARGE PLATED COPPER | 9| 12 +Brand#54 |LARGE PLATED COPPER | 45| 12 +Brand#54 |LARGE PLATED STEEL | 49| 12 +Brand#54 |LARGE POLISHED BRASS | 23| 12 +Brand#54 |LARGE POLISHED COPPER | 3| 12 +Brand#54 |MEDIUM ANODIZED STEEL | 19| 12 +Brand#54 |MEDIUM BRUSHED BRASS | 49| 12 +Brand#54 |MEDIUM BURNISHED COPPER | 23| 12 +Brand#54 |MEDIUM BURNISHED STEEL | 3| 12 +Brand#54 |MEDIUM BURNISHED STEEL | 49| 12 +Brand#54 |PROMO ANODIZED COPPER | 49| 12 +Brand#54 |PROMO ANODIZED STEEL | 19| 12 +Brand#54 |PROMO BRUSHED BRASS | 14| 12 +Brand#54 |PROMO BRUSHED COPPER | 14| 12 +Brand#54 |PROMO BRUSHED STEEL | 14| 12 +Brand#54 |PROMO BRUSHED STEEL | 45| 12 +Brand#54 |PROMO BRUSHED TIN | 14| 12 +Brand#54 |PROMO BURNISHED BRASS | 9| 12 +Brand#54 |PROMO BURNISHED COPPER | 49| 12 +Brand#54 |PROMO BURNISHED NICKEL | 23| 12 +Brand#54 |PROMO BURNISHED NICKEL | 36| 12 +Brand#54 |PROMO BURNISHED STEEL | 23| 12 +Brand#54 |PROMO BURNISHED TIN | 9| 12 +Brand#54 |PROMO BURNISHED TIN | 23| 12 +Brand#54 |PROMO PLATED BRASS | 23| 12 +Brand#54 |PROMO PLATED STEEL | 9| 12 +Brand#54 |PROMO PLATED TIN | 3| 12 +Brand#54 |PROMO PLATED TIN | 49| 12 +Brand#54 |PROMO POLISHED STEEL | 19| 12 +Brand#54 |PROMO POLISHED STEEL | 45| 12 +Brand#54 |PROMO POLISHED TIN | 19| 12 +Brand#54 |SMALL ANODIZED COPPER | 49| 12 +Brand#54 |SMALL BRUSHED BRASS | 23| 12 +Brand#54 |SMALL BRUSHED BRASS | 36| 12 +Brand#54 |SMALL BRUSHED COPPER | 19| 12 +Brand#54 |SMALL BRUSHED TIN | 14| 12 +Brand#54 |SMALL BURNISHED BRASS | 3| 12 +Brand#54 |SMALL BURNISHED COPPER | 49| 12 +Brand#54 |SMALL BURNISHED NICKEL | 14| 12 +Brand#54 |SMALL BURNISHED STEEL | 19| 12 +Brand#54 |SMALL BURNISHED TIN | 9| 12 +Brand#54 |SMALL PLATED BRASS | 23| 12 +Brand#54 |SMALL PLATED COPPER | 36| 12 +Brand#54 |SMALL PLATED NICKEL | 36| 12 +Brand#54 |STANDARD ANODIZED BRASS | 3| 12 +Brand#54 |STANDARD ANODIZED STEEL | 49| 12 +Brand#54 |STANDARD BRUSHED BRASS | 14| 12 +Brand#54 |STANDARD BRUSHED COPPER | 19| 12 +Brand#54 |STANDARD BURNISHED BRASS | 9| 12 +Brand#54 |STANDARD BURNISHED NICKEL| 14| 12 +Brand#54 |STANDARD PLATED BRASS | 45| 12 +Brand#54 |STANDARD PLATED COPPER | 9| 12 +Brand#54 |STANDARD PLATED COPPER | 19| 12 +Brand#54 |STANDARD PLATED NICKEL | 49| 12 +Brand#54 |STANDARD PLATED TIN | 45| 12 +Brand#54 |STANDARD POLISHED STEEL | 49| 12 +Brand#55 |ECONOMY BRUSHED BRASS | 3| 12 +Brand#55 |ECONOMY BRUSHED COPPER | 9| 12 +Brand#55 |ECONOMY BRUSHED COPPER | 14| 12 +Brand#55 |ECONOMY BRUSHED NICKEL | 19| 12 +Brand#55 |ECONOMY BRUSHED STEEL | 3| 12 +Brand#55 |ECONOMY BURNISHED COPPER | 9| 12 +Brand#55 |ECONOMY PLATED STEEL | 9| 12 +Brand#55 |ECONOMY POLISHED STEEL | 3| 12 +Brand#55 |LARGE ANODIZED NICKEL | 9| 12 +Brand#55 |LARGE BRUSHED COPPER | 14| 12 +Brand#55 |LARGE BRUSHED COPPER | 23| 12 +Brand#55 |LARGE BRUSHED COPPER | 49| 12 +Brand#55 |LARGE BURNISHED COPPER | 14| 12 +Brand#55 |LARGE BURNISHED NICKEL | 14| 12 +Brand#55 |LARGE PLATED BRASS | 45| 12 +Brand#55 |LARGE PLATED NICKEL | 14| 12 +Brand#55 |LARGE PLATED STEEL | 23| 12 +Brand#55 |LARGE POLISHED NICKEL | 3| 12 +Brand#55 |LARGE POLISHED STEEL | 45| 12 +Brand#55 |MEDIUM ANODIZED NICKEL | 36| 12 +Brand#55 |MEDIUM ANODIZED TIN | 49| 12 +Brand#55 |MEDIUM BRUSHED BRASS | 19| 12 +Brand#55 |MEDIUM BRUSHED COPPER | 49| 12 +Brand#55 |MEDIUM BRUSHED NICKEL | 23| 12 +Brand#55 |MEDIUM BRUSHED NICKEL | 45| 12 +Brand#55 |MEDIUM BRUSHED STEEL | 45| 12 +Brand#55 |MEDIUM BURNISHED COPPER | 36| 12 +Brand#55 |MEDIUM PLATED NICKEL | 23| 12 +Brand#55 |MEDIUM PLATED STEEL | 3| 12 +Brand#55 |MEDIUM PLATED TIN | 19| 12 +Brand#55 |PROMO ANODIZED TIN | 19| 12 +Brand#55 |PROMO BRUSHED BRASS | 23| 12 +Brand#55 |PROMO BRUSHED BRASS | 45| 12 +Brand#55 |PROMO BRUSHED NICKEL | 23| 12 +Brand#55 |PROMO BRUSHED TIN | 9| 12 +Brand#55 |PROMO BURNISHED STEEL | 23| 12 +Brand#55 |PROMO POLISHED BRASS | 45| 12 +Brand#55 |SMALL ANODIZED STEEL | 23| 12 +Brand#55 |SMALL ANODIZED STEEL | 45| 12 +Brand#55 |SMALL BRUSHED STEEL | 36| 12 +Brand#55 |SMALL BRUSHED TIN | 3| 12 +Brand#55 |SMALL BURNISHED BRASS | 49| 12 +Brand#55 |SMALL BURNISHED TIN | 49| 12 +Brand#55 |SMALL PLATED NICKEL | 36| 12 +Brand#55 |SMALL PLATED NICKEL | 45| 12 +Brand#55 |SMALL PLATED STEEL | 9| 12 +Brand#55 |SMALL PLATED STEEL | 19| 12 +Brand#55 |SMALL POLISHED STEEL | 14| 12 +Brand#55 |STANDARD ANODIZED BRASS | 3| 12 +Brand#55 |STANDARD ANODIZED STEEL | 19| 12 +Brand#55 |STANDARD ANODIZED TIN | 9| 12 +Brand#55 |STANDARD BRUSHED COPPER | 9| 12 +Brand#55 |STANDARD BRUSHED NICKEL | 9| 12 +Brand#55 |STANDARD BRUSHED TIN | 36| 12 +Brand#55 |STANDARD BRUSHED TIN | 45| 12 +Brand#55 |STANDARD BURNISHED BRASS | 3| 12 +Brand#55 |STANDARD BURNISHED COPPER| 49| 12 +Brand#55 |STANDARD BURNISHED TIN | 3| 12 +Brand#55 |STANDARD PLATED BRASS | 3| 12 +Brand#55 |STANDARD PLATED COPPER | 3| 12 +Brand#55 |STANDARD PLATED COPPER | 19| 12 +Brand#55 |STANDARD PLATED NICKEL | 9| 12 +Brand#55 |STANDARD PLATED TIN | 19| 12 +Brand#55 |STANDARD POLISHED NICKEL | 14| 12 +Brand#11 |ECONOMY POLISHED BRASS | 14| 11 +Brand#11 |SMALL PLATED BRASS | 14| 11 +Brand#12 |MEDIUM BURNISHED TIN | 45| 11 +Brand#12 |SMALL BURNISHED COPPER | 23| 11 +Brand#15 |SMALL PLATED NICKEL | 45| 11 +Brand#21 |ECONOMY PLATED COPPER | 3| 11 +Brand#21 |SMALL BRUSHED TIN | 19| 11 +Brand#23 |LARGE BRUSHED NICKEL | 23| 11 +Brand#24 |PROMO BRUSHED NICKEL | 9| 11 +Brand#25 |SMALL PLATED TIN | 23| 11 +Brand#31 |ECONOMY POLISHED COPPER | 14| 11 +Brand#32 |SMALL PLATED NICKEL | 45| 11 +Brand#33 |PROMO ANODIZED TIN | 19| 11 +Brand#43 |PROMO BRUSHED NICKEL | 9| 11 +Brand#44 |LARGE PLATED STEEL | 3| 11 +Brand#52 |ECONOMY ANODIZED COPPER | 36| 11 +Brand#52 |SMALL POLISHED BRASS | 49| 11 +Brand#53 |MEDIUM BRUSHED BRASS | 49| 11 +Brand#53 |PROMO BRUSHED NICKEL | 3| 11 +Brand#54 |LARGE PLATED BRASS | 19| 11 +Brand#54 |LARGE POLISHED NICKEL | 3| 11 +Brand#55 |PROMO ANODIZED STEEL | 45| 11 +Brand#55 |STANDARD POLISHED STEEL | 19| 11 +Brand#11 |ECONOMY ANODIZED BRASS | 19| 8 +Brand#11 |ECONOMY ANODIZED NICKEL | 9| 8 +Brand#11 |ECONOMY ANODIZED NICKEL | 19| 8 +Brand#11 |ECONOMY ANODIZED NICKEL | 36| 8 +Brand#11 |ECONOMY ANODIZED NICKEL | 45| 8 +Brand#11 |ECONOMY ANODIZED TIN | 36| 8 +Brand#11 |ECONOMY BRUSHED COPPER | 9| 8 +Brand#11 |ECONOMY BRUSHED COPPER | 49| 8 +Brand#11 |ECONOMY BRUSHED NICKEL | 49| 8 +Brand#11 |ECONOMY BRUSHED STEEL | 9| 8 +Brand#11 |ECONOMY BRUSHED STEEL | 14| 8 +Brand#11 |ECONOMY BRUSHED STEEL | 23| 8 +Brand#11 |ECONOMY BRUSHED TIN | 19| 8 +Brand#11 |ECONOMY BRUSHED TIN | 36| 8 +Brand#11 |ECONOMY BRUSHED TIN | 49| 8 +Brand#11 |ECONOMY BURNISHED BRASS | 23| 8 +Brand#11 |ECONOMY BURNISHED COPPER | 9| 8 +Brand#11 |ECONOMY BURNISHED NICKEL | 14| 8 +Brand#11 |ECONOMY BURNISHED NICKEL | 19| 8 +Brand#11 |ECONOMY BURNISHED TIN | 9| 8 +Brand#11 |ECONOMY BURNISHED TIN | 14| 8 +Brand#11 |ECONOMY BURNISHED TIN | 49| 8 +Brand#11 |ECONOMY PLATED COPPER | 14| 8 +Brand#11 |ECONOMY PLATED COPPER | 49| 8 +Brand#11 |ECONOMY PLATED NICKEL | 23| 8 +Brand#11 |ECONOMY PLATED NICKEL | 36| 8 +Brand#11 |ECONOMY PLATED NICKEL | 45| 8 +Brand#11 |ECONOMY PLATED STEEL | 23| 8 +Brand#11 |ECONOMY PLATED TIN | 49| 8 +Brand#11 |ECONOMY POLISHED BRASS | 3| 8 +Brand#11 |ECONOMY POLISHED COPPER | 45| 8 +Brand#11 |ECONOMY POLISHED COPPER | 49| 8 +Brand#11 |ECONOMY POLISHED NICKEL | 3| 8 +Brand#11 |ECONOMY POLISHED NICKEL | 9| 8 +Brand#11 |ECONOMY POLISHED NICKEL | 14| 8 +Brand#11 |ECONOMY POLISHED NICKEL | 23| 8 +Brand#11 |ECONOMY POLISHED STEEL | 19| 8 +Brand#11 |ECONOMY POLISHED TIN | 3| 8 +Brand#11 |ECONOMY POLISHED TIN | 14| 8 +Brand#11 |ECONOMY POLISHED TIN | 36| 8 +Brand#11 |LARGE ANODIZED BRASS | 49| 8 +Brand#11 |LARGE ANODIZED COPPER | 23| 8 +Brand#11 |LARGE ANODIZED NICKEL | 36| 8 +Brand#11 |LARGE ANODIZED NICKEL | 45| 8 +Brand#11 |LARGE ANODIZED NICKEL | 49| 8 +Brand#11 |LARGE ANODIZED STEEL | 9| 8 +Brand#11 |LARGE ANODIZED TIN | 23| 8 +Brand#11 |LARGE ANODIZED TIN | 45| 8 +Brand#11 |LARGE BRUSHED BRASS | 14| 8 +Brand#11 |LARGE BRUSHED BRASS | 23| 8 +Brand#11 |LARGE BRUSHED COPPER | 19| 8 +Brand#11 |LARGE BRUSHED COPPER | 23| 8 +Brand#11 |LARGE BRUSHED COPPER | 36| 8 +Brand#11 |LARGE BRUSHED NICKEL | 3| 8 +Brand#11 |LARGE BRUSHED NICKEL | 14| 8 +Brand#11 |LARGE BRUSHED NICKEL | 19| 8 +Brand#11 |LARGE BRUSHED STEEL | 49| 8 +Brand#11 |LARGE BRUSHED TIN | 14| 8 +Brand#11 |LARGE BRUSHED TIN | 23| 8 +Brand#11 |LARGE BURNISHED BRASS | 14| 8 +Brand#11 |LARGE BURNISHED BRASS | 23| 8 +Brand#11 |LARGE BURNISHED BRASS | 45| 8 +Brand#11 |LARGE BURNISHED BRASS | 49| 8 +Brand#11 |LARGE BURNISHED COPPER | 9| 8 +Brand#11 |LARGE BURNISHED COPPER | 36| 8 +Brand#11 |LARGE BURNISHED NICKEL | 45| 8 +Brand#11 |LARGE BURNISHED STEEL | 36| 8 +Brand#11 |LARGE BURNISHED STEEL | 49| 8 +Brand#11 |LARGE BURNISHED TIN | 14| 8 +Brand#11 |LARGE BURNISHED TIN | 23| 8 +Brand#11 |LARGE PLATED BRASS | 14| 8 +Brand#11 |LARGE PLATED BRASS | 23| 8 +Brand#11 |LARGE PLATED NICKEL | 3| 8 +Brand#11 |LARGE PLATED NICKEL | 36| 8 +Brand#11 |LARGE PLATED STEEL | 3| 8 +Brand#11 |LARGE PLATED STEEL | 23| 8 +Brand#11 |LARGE PLATED STEEL | 36| 8 +Brand#11 |LARGE PLATED TIN | 9| 8 +Brand#11 |LARGE PLATED TIN | 14| 8 +Brand#11 |LARGE POLISHED BRASS | 49| 8 +Brand#11 |LARGE POLISHED COPPER | 14| 8 +Brand#11 |LARGE POLISHED NICKEL | 14| 8 +Brand#11 |LARGE POLISHED STEEL | 36| 8 +Brand#11 |LARGE POLISHED TIN | 3| 8 +Brand#11 |MEDIUM ANODIZED BRASS | 14| 8 +Brand#11 |MEDIUM ANODIZED BRASS | 49| 8 +Brand#11 |MEDIUM ANODIZED COPPER | 23| 8 +Brand#11 |MEDIUM ANODIZED NICKEL | 9| 8 +Brand#11 |MEDIUM ANODIZED NICKEL | 14| 8 +Brand#11 |MEDIUM ANODIZED NICKEL | 36| 8 +Brand#11 |MEDIUM ANODIZED NICKEL | 45| 8 +Brand#11 |MEDIUM ANODIZED STEEL | 9| 8 +Brand#11 |MEDIUM ANODIZED TIN | 23| 8 +Brand#11 |MEDIUM ANODIZED TIN | 49| 8 +Brand#11 |MEDIUM BRUSHED COPPER | 23| 8 +Brand#11 |MEDIUM BRUSHED NICKEL | 23| 8 +Brand#11 |MEDIUM BURNISHED BRASS | 3| 8 +Brand#11 |MEDIUM BURNISHED BRASS | 19| 8 +Brand#11 |MEDIUM BURNISHED BRASS | 45| 8 +Brand#11 |MEDIUM BURNISHED COPPER | 9| 8 +Brand#11 |MEDIUM BURNISHED COPPER | 14| 8 +Brand#11 |MEDIUM BURNISHED COPPER | 49| 8 +Brand#11 |MEDIUM BURNISHED STEEL | 19| 8 +Brand#11 |MEDIUM BURNISHED TIN | 19| 8 +Brand#11 |MEDIUM BURNISHED TIN | 36| 8 +Brand#11 |MEDIUM PLATED BRASS | 3| 8 +Brand#11 |MEDIUM PLATED BRASS | 36| 8 +Brand#11 |MEDIUM PLATED NICKEL | 14| 8 +Brand#11 |MEDIUM PLATED NICKEL | 45| 8 +Brand#11 |MEDIUM PLATED STEEL | 3| 8 +Brand#11 |MEDIUM PLATED STEEL | 9| 8 +Brand#11 |MEDIUM PLATED STEEL | 23| 8 +Brand#11 |MEDIUM PLATED STEEL | 36| 8 +Brand#11 |MEDIUM PLATED TIN | 3| 8 +Brand#11 |MEDIUM PLATED TIN | 19| 8 +Brand#11 |MEDIUM PLATED TIN | 23| 8 +Brand#11 |MEDIUM PLATED TIN | 45| 8 +Brand#11 |PROMO ANODIZED COPPER | 14| 8 +Brand#11 |PROMO ANODIZED NICKEL | 3| 8 +Brand#11 |PROMO ANODIZED NICKEL | 45| 8 +Brand#11 |PROMO ANODIZED STEEL | 23| 8 +Brand#11 |PROMO ANODIZED STEEL | 49| 8 +Brand#11 |PROMO ANODIZED TIN | 36| 8 +Brand#11 |PROMO BRUSHED BRASS | 3| 8 +Brand#11 |PROMO BRUSHED BRASS | 36| 8 +Brand#11 |PROMO BRUSHED COPPER | 14| 8 +Brand#11 |PROMO BRUSHED COPPER | 19| 8 +Brand#11 |PROMO BRUSHED NICKEL | 19| 8 +Brand#11 |PROMO BRUSHED STEEL | 49| 8 +Brand#11 |PROMO BRUSHED TIN | 19| 8 +Brand#11 |PROMO BRUSHED TIN | 36| 8 +Brand#11 |PROMO BURNISHED BRASS | 3| 8 +Brand#11 |PROMO BURNISHED BRASS | 19| 8 +Brand#11 |PROMO BURNISHED BRASS | 36| 8 +Brand#11 |PROMO BURNISHED BRASS | 49| 8 +Brand#11 |PROMO BURNISHED COPPER | 14| 8 +Brand#11 |PROMO BURNISHED NICKEL | 3| 8 +Brand#11 |PROMO BURNISHED NICKEL | 14| 8 +Brand#11 |PROMO BURNISHED STEEL | 14| 8 +Brand#11 |PROMO BURNISHED STEEL | 19| 8 +Brand#11 |PROMO BURNISHED STEEL | 36| 8 +Brand#11 |PROMO BURNISHED STEEL | 49| 8 +Brand#11 |PROMO PLATED BRASS | 23| 8 +Brand#11 |PROMO PLATED NICKEL | 14| 8 +Brand#11 |PROMO PLATED NICKEL | 49| 8 +Brand#11 |PROMO PLATED STEEL | 19| 8 +Brand#11 |PROMO PLATED STEEL | 23| 8 +Brand#11 |PROMO POLISHED BRASS | 3| 8 +Brand#11 |PROMO POLISHED BRASS | 19| 8 +Brand#11 |PROMO POLISHED BRASS | 36| 8 +Brand#11 |PROMO POLISHED COPPER | 45| 8 +Brand#11 |PROMO POLISHED TIN | 3| 8 +Brand#11 |PROMO POLISHED TIN | 9| 8 +Brand#11 |PROMO POLISHED TIN | 49| 8 +Brand#11 |SMALL ANODIZED COPPER | 19| 8 +Brand#11 |SMALL ANODIZED NICKEL | 49| 8 +Brand#11 |SMALL ANODIZED STEEL | 3| 8 +Brand#11 |SMALL ANODIZED STEEL | 14| 8 +Brand#11 |SMALL ANODIZED TIN | 9| 8 +Brand#11 |SMALL ANODIZED TIN | 19| 8 +Brand#11 |SMALL BRUSHED BRASS | 45| 8 +Brand#11 |SMALL BRUSHED BRASS | 49| 8 +Brand#11 |SMALL BRUSHED COPPER | 14| 8 +Brand#11 |SMALL BRUSHED COPPER | 19| 8 +Brand#11 |SMALL BRUSHED NICKEL | 3| 8 +Brand#11 |SMALL BRUSHED NICKEL | 45| 8 +Brand#11 |SMALL BRUSHED NICKEL | 49| 8 +Brand#11 |SMALL BRUSHED TIN | 14| 8 +Brand#11 |SMALL BURNISHED COPPER | 23| 8 +Brand#11 |SMALL BURNISHED COPPER | 36| 8 +Brand#11 |SMALL BURNISHED COPPER | 49| 8 +Brand#11 |SMALL BURNISHED STEEL | 3| 8 +Brand#11 |SMALL BURNISHED STEEL | 9| 8 +Brand#11 |SMALL BURNISHED STEEL | 36| 8 +Brand#11 |SMALL BURNISHED STEEL | 45| 8 +Brand#11 |SMALL BURNISHED TIN | 3| 8 +Brand#11 |SMALL BURNISHED TIN | 19| 8 +Brand#11 |SMALL BURNISHED TIN | 45| 8 +Brand#11 |SMALL PLATED BRASS | 3| 8 +Brand#11 |SMALL PLATED BRASS | 19| 8 +Brand#11 |SMALL PLATED BRASS | 36| 8 +Brand#11 |SMALL PLATED COPPER | 49| 8 +Brand#11 |SMALL PLATED NICKEL | 9| 8 +Brand#11 |SMALL PLATED NICKEL | 49| 8 +Brand#11 |SMALL PLATED STEEL | 9| 8 +Brand#11 |SMALL PLATED TIN | 9| 8 +Brand#11 |SMALL PLATED TIN | 19| 8 +Brand#11 |SMALL PLATED TIN | 45| 8 +Brand#11 |SMALL POLISHED BRASS | 23| 8 +Brand#11 |SMALL POLISHED COPPER | 36| 8 +Brand#11 |SMALL POLISHED NICKEL | 45| 8 +Brand#11 |SMALL POLISHED STEEL | 14| 8 +Brand#11 |SMALL POLISHED STEEL | 23| 8 +Brand#11 |STANDARD ANODIZED BRASS | 23| 8 +Brand#11 |STANDARD ANODIZED COPPER | 9| 8 +Brand#11 |STANDARD ANODIZED STEEL | 19| 8 +Brand#11 |STANDARD BRUSHED COPPER | 14| 8 +Brand#11 |STANDARD BRUSHED NICKEL | 14| 8 +Brand#11 |STANDARD BRUSHED NICKEL | 45| 8 +Brand#11 |STANDARD BRUSHED TIN | 19| 8 +Brand#11 |STANDARD BURNISHED BRASS | 49| 8 +Brand#11 |STANDARD BURNISHED COPPER| 14| 8 +Brand#11 |STANDARD BURNISHED COPPER| 23| 8 +Brand#11 |STANDARD BURNISHED STEEL | 23| 8 +Brand#11 |STANDARD BURNISHED TIN | 49| 8 +Brand#11 |STANDARD PLATED BRASS | 19| 8 +Brand#11 |STANDARD PLATED BRASS | 23| 8 +Brand#11 |STANDARD PLATED BRASS | 49| 8 +Brand#11 |STANDARD PLATED NICKEL | 36| 8 +Brand#11 |STANDARD PLATED NICKEL | 45| 8 +Brand#11 |STANDARD PLATED STEEL | 23| 8 +Brand#11 |STANDARD PLATED STEEL | 45| 8 +Brand#11 |STANDARD PLATED TIN | 36| 8 +Brand#11 |STANDARD POLISHED BRASS | 9| 8 +Brand#11 |STANDARD POLISHED NICKEL | 19| 8 +Brand#11 |STANDARD POLISHED STEEL | 49| 8 +Brand#12 |ECONOMY ANODIZED STEEL | 3| 8 +Brand#12 |ECONOMY ANODIZED STEEL | 19| 8 +Brand#12 |ECONOMY ANODIZED STEEL | 23| 8 +Brand#12 |ECONOMY ANODIZED TIN | 23| 8 +Brand#12 |ECONOMY BRUSHED COPPER | 3| 8 +Brand#12 |ECONOMY BRUSHED COPPER | 14| 8 +Brand#12 |ECONOMY BRUSHED COPPER | 19| 8 +Brand#12 |ECONOMY BRUSHED COPPER | 49| 8 +Brand#12 |ECONOMY BRUSHED NICKEL | 3| 8 +Brand#12 |ECONOMY BRUSHED STEEL | 3| 8 +Brand#12 |ECONOMY BRUSHED STEEL | 49| 8 +Brand#12 |ECONOMY BRUSHED TIN | 9| 8 +Brand#12 |ECONOMY BRUSHED TIN | 49| 8 +Brand#12 |ECONOMY BURNISHED BRASS | 49| 8 +Brand#12 |ECONOMY BURNISHED COPPER | 3| 8 +Brand#12 |ECONOMY BURNISHED COPPER | 19| 8 +Brand#12 |ECONOMY BURNISHED NICKEL | 3| 8 +Brand#12 |ECONOMY BURNISHED NICKEL | 23| 8 +Brand#12 |ECONOMY BURNISHED STEEL | 3| 8 +Brand#12 |ECONOMY BURNISHED TIN | 14| 8 +Brand#12 |ECONOMY BURNISHED TIN | 19| 8 +Brand#12 |ECONOMY PLATED BRASS | 19| 8 +Brand#12 |ECONOMY PLATED BRASS | 49| 8 +Brand#12 |ECONOMY PLATED COPPER | 23| 8 +Brand#12 |ECONOMY PLATED STEEL | 23| 8 +Brand#12 |ECONOMY PLATED TIN | 36| 8 +Brand#12 |ECONOMY PLATED TIN | 49| 8 +Brand#12 |ECONOMY POLISHED BRASS | 9| 8 +Brand#12 |ECONOMY POLISHED BRASS | 14| 8 +Brand#12 |ECONOMY POLISHED COPPER | 9| 8 +Brand#12 |ECONOMY POLISHED COPPER | 49| 8 +Brand#12 |ECONOMY POLISHED TIN | 3| 8 +Brand#12 |ECONOMY POLISHED TIN | 19| 8 +Brand#12 |ECONOMY POLISHED TIN | 36| 8 +Brand#12 |LARGE ANODIZED BRASS | 23| 8 +Brand#12 |LARGE ANODIZED BRASS | 36| 8 +Brand#12 |LARGE ANODIZED COPPER | 14| 8 +Brand#12 |LARGE ANODIZED COPPER | 45| 8 +Brand#12 |LARGE ANODIZED NICKEL | 9| 8 +Brand#12 |LARGE ANODIZED STEEL | 9| 8 +Brand#12 |LARGE ANODIZED STEEL | 49| 8 +Brand#12 |LARGE ANODIZED TIN | 14| 8 +Brand#12 |LARGE BRUSHED BRASS | 14| 8 +Brand#12 |LARGE BRUSHED NICKEL | 9| 8 +Brand#12 |LARGE BRUSHED STEEL | 3| 8 +Brand#12 |LARGE BRUSHED STEEL | 23| 8 +Brand#12 |LARGE BRUSHED STEEL | 49| 8 +Brand#12 |LARGE BRUSHED TIN | 9| 8 +Brand#12 |LARGE BRUSHED TIN | 45| 8 +Brand#12 |LARGE BURNISHED BRASS | 14| 8 +Brand#12 |LARGE BURNISHED BRASS | 45| 8 +Brand#12 |LARGE BURNISHED COPPER | 19| 8 +Brand#12 |LARGE BURNISHED COPPER | 49| 8 +Brand#12 |LARGE BURNISHED NICKEL | 3| 8 +Brand#12 |LARGE BURNISHED NICKEL | 14| 8 +Brand#12 |LARGE BURNISHED STEEL | 3| 8 +Brand#12 |LARGE BURNISHED STEEL | 45| 8 +Brand#12 |LARGE BURNISHED TIN | 9| 8 +Brand#12 |LARGE BURNISHED TIN | 14| 8 +Brand#12 |LARGE BURNISHED TIN | 45| 8 +Brand#12 |LARGE BURNISHED TIN | 49| 8 +Brand#12 |LARGE PLATED BRASS | 49| 8 +Brand#12 |LARGE PLATED COPPER | 3| 8 +Brand#12 |LARGE PLATED COPPER | 36| 8 +Brand#12 |LARGE PLATED COPPER | 45| 8 +Brand#12 |LARGE PLATED NICKEL | 49| 8 +Brand#12 |LARGE PLATED STEEL | 3| 8 +Brand#12 |LARGE PLATED STEEL | 36| 8 +Brand#12 |LARGE PLATED TIN | 14| 8 +Brand#12 |LARGE POLISHED BRASS | 9| 8 +Brand#12 |LARGE POLISHED BRASS | 19| 8 +Brand#12 |LARGE POLISHED COPPER | 9| 8 +Brand#12 |LARGE POLISHED COPPER | 36| 8 +Brand#12 |LARGE POLISHED NICKEL | 23| 8 +Brand#12 |LARGE POLISHED NICKEL | 36| 8 +Brand#12 |LARGE POLISHED NICKEL | 49| 8 +Brand#12 |LARGE POLISHED STEEL | 49| 8 +Brand#12 |MEDIUM ANODIZED BRASS | 23| 8 +Brand#12 |MEDIUM ANODIZED NICKEL | 9| 8 +Brand#12 |MEDIUM ANODIZED STEEL | 19| 8 +Brand#12 |MEDIUM ANODIZED TIN | 9| 8 +Brand#12 |MEDIUM BRUSHED COPPER | 3| 8 +Brand#12 |MEDIUM BRUSHED COPPER | 9| 8 +Brand#12 |MEDIUM BRUSHED COPPER | 36| 8 +Brand#12 |MEDIUM BRUSHED NICKEL | 49| 8 +Brand#12 |MEDIUM BRUSHED STEEL | 3| 8 +Brand#12 |MEDIUM BRUSHED STEEL | 36| 8 +Brand#12 |MEDIUM BURNISHED BRASS | 23| 8 +Brand#12 |MEDIUM BURNISHED COPPER | 49| 8 +Brand#12 |MEDIUM BURNISHED NICKEL | 3| 8 +Brand#12 |MEDIUM BURNISHED NICKEL | 9| 8 +Brand#12 |MEDIUM BURNISHED NICKEL | 49| 8 +Brand#12 |MEDIUM BURNISHED STEEL | 3| 8 +Brand#12 |MEDIUM BURNISHED STEEL | 9| 8 +Brand#12 |MEDIUM BURNISHED STEEL | 14| 8 +Brand#12 |MEDIUM BURNISHED STEEL | 19| 8 +Brand#12 |MEDIUM BURNISHED TIN | 14| 8 +Brand#12 |MEDIUM PLATED BRASS | 14| 8 +Brand#12 |MEDIUM PLATED BRASS | 49| 8 +Brand#12 |MEDIUM PLATED NICKEL | 9| 8 +Brand#12 |MEDIUM PLATED NICKEL | 36| 8 +Brand#12 |MEDIUM PLATED NICKEL | 49| 8 +Brand#12 |MEDIUM PLATED STEEL | 14| 8 +Brand#12 |MEDIUM PLATED STEEL | 23| 8 +Brand#12 |MEDIUM PLATED STEEL | 45| 8 +Brand#12 |MEDIUM PLATED TIN | 14| 8 +Brand#12 |MEDIUM PLATED TIN | 19| 8 +Brand#12 |MEDIUM PLATED TIN | 45| 8 +Brand#12 |PROMO ANODIZED BRASS | 49| 8 +Brand#12 |PROMO ANODIZED COPPER | 3| 8 +Brand#12 |PROMO ANODIZED COPPER | 36| 8 +Brand#12 |PROMO ANODIZED COPPER | 45| 8 +Brand#12 |PROMO ANODIZED COPPER | 49| 8 +Brand#12 |PROMO ANODIZED NICKEL | 14| 8 +Brand#12 |PROMO ANODIZED NICKEL | 23| 8 +Brand#12 |PROMO ANODIZED TIN | 19| 8 +Brand#12 |PROMO ANODIZED TIN | 36| 8 +Brand#12 |PROMO BRUSHED BRASS | 3| 8 +Brand#12 |PROMO BRUSHED BRASS | 23| 8 +Brand#12 |PROMO BRUSHED BRASS | 49| 8 +Brand#12 |PROMO BRUSHED COPPER | 9| 8 +Brand#12 |PROMO BRUSHED COPPER | 23| 8 +Brand#12 |PROMO BRUSHED NICKEL | 23| 8 +Brand#12 |PROMO BRUSHED STEEL | 23| 8 +Brand#12 |PROMO BURNISHED BRASS | 3| 8 +Brand#12 |PROMO BURNISHED COPPER | 3| 8 +Brand#12 |PROMO BURNISHED NICKEL | 9| 8 +Brand#12 |PROMO BURNISHED NICKEL | 49| 8 +Brand#12 |PROMO BURNISHED TIN | 9| 8 +Brand#12 |PROMO BURNISHED TIN | 14| 8 +Brand#12 |PROMO BURNISHED TIN | 45| 8 +Brand#12 |PROMO PLATED BRASS | 36| 8 +Brand#12 |PROMO PLATED BRASS | 45| 8 +Brand#12 |PROMO PLATED BRASS | 49| 8 +Brand#12 |PROMO PLATED COPPER | 23| 8 +Brand#12 |PROMO PLATED COPPER | 36| 8 +Brand#12 |PROMO PLATED NICKEL | 14| 8 +Brand#12 |PROMO PLATED STEEL | 3| 8 +Brand#12 |PROMO PLATED STEEL | 49| 8 +Brand#12 |PROMO PLATED TIN | 3| 8 +Brand#12 |PROMO PLATED TIN | 9| 8 +Brand#12 |PROMO PLATED TIN | 23| 8 +Brand#12 |PROMO PLATED TIN | 36| 8 +Brand#12 |PROMO POLISHED BRASS | 3| 8 +Brand#12 |PROMO POLISHED BRASS | 9| 8 +Brand#12 |PROMO POLISHED BRASS | 19| 8 +Brand#12 |PROMO POLISHED COPPER | 19| 8 +Brand#12 |PROMO POLISHED COPPER | 23| 8 +Brand#12 |PROMO POLISHED NICKEL | 3| 8 +Brand#12 |PROMO POLISHED NICKEL | 19| 8 +Brand#12 |PROMO POLISHED STEEL | 3| 8 +Brand#12 |PROMO POLISHED STEEL | 19| 8 +Brand#12 |PROMO POLISHED STEEL | 36| 8 +Brand#12 |PROMO POLISHED TIN | 23| 8 +Brand#12 |PROMO POLISHED TIN | 36| 8 +Brand#12 |SMALL ANODIZED BRASS | 36| 8 +Brand#12 |SMALL ANODIZED COPPER | 9| 8 +Brand#12 |SMALL ANODIZED STEEL | 9| 8 +Brand#12 |SMALL ANODIZED STEEL | 45| 8 +Brand#12 |SMALL ANODIZED STEEL | 49| 8 +Brand#12 |SMALL ANODIZED TIN | 14| 8 +Brand#12 |SMALL ANODIZED TIN | 19| 8 +Brand#12 |SMALL ANODIZED TIN | 23| 8 +Brand#12 |SMALL BRUSHED BRASS | 14| 8 +Brand#12 |SMALL BRUSHED BRASS | 45| 8 +Brand#12 |SMALL BRUSHED COPPER | 36| 8 +Brand#12 |SMALL BRUSHED NICKEL | 14| 8 +Brand#12 |SMALL BRUSHED NICKEL | 19| 8 +Brand#12 |SMALL BRUSHED NICKEL | 23| 8 +Brand#12 |SMALL BRUSHED NICKEL | 45| 8 +Brand#12 |SMALL BRUSHED STEEL | 49| 8 +Brand#12 |SMALL BRUSHED TIN | 9| 8 +Brand#12 |SMALL BURNISHED BRASS | 9| 8 +Brand#12 |SMALL BURNISHED COPPER | 36| 8 +Brand#12 |SMALL BURNISHED COPPER | 49| 8 +Brand#12 |SMALL BURNISHED NICKEL | 3| 8 +Brand#12 |SMALL BURNISHED NICKEL | 9| 8 +Brand#12 |SMALL BURNISHED NICKEL | 19| 8 +Brand#12 |SMALL BURNISHED NICKEL | 36| 8 +Brand#12 |SMALL BURNISHED STEEL | 9| 8 +Brand#12 |SMALL BURNISHED TIN | 23| 8 +Brand#12 |SMALL BURNISHED TIN | 45| 8 +Brand#12 |SMALL PLATED BRASS | 45| 8 +Brand#12 |SMALL PLATED COPPER | 19| 8 +Brand#12 |SMALL PLATED NICKEL | 23| 8 +Brand#12 |SMALL PLATED TIN | 45| 8 +Brand#12 |SMALL POLISHED BRASS | 36| 8 +Brand#12 |SMALL POLISHED BRASS | 45| 8 +Brand#12 |SMALL POLISHED COPPER | 45| 8 +Brand#12 |SMALL POLISHED COPPER | 49| 8 +Brand#12 |SMALL POLISHED TIN | 36| 8 +Brand#12 |SMALL POLISHED TIN | 45| 8 +Brand#12 |STANDARD ANODIZED BRASS | 14| 8 +Brand#12 |STANDARD ANODIZED BRASS | 49| 8 +Brand#12 |STANDARD ANODIZED NICKEL | 19| 8 +Brand#12 |STANDARD ANODIZED NICKEL | 49| 8 +Brand#12 |STANDARD ANODIZED STEEL | 19| 8 +Brand#12 |STANDARD ANODIZED STEEL | 36| 8 +Brand#12 |STANDARD ANODIZED TIN | 36| 8 +Brand#12 |STANDARD BRUSHED BRASS | 36| 8 +Brand#12 |STANDARD BRUSHED BRASS | 45| 8 +Brand#12 |STANDARD BRUSHED COPPER | 9| 8 +Brand#12 |STANDARD BRUSHED COPPER | 36| 8 +Brand#12 |STANDARD BRUSHED NICKEL | 3| 8 +Brand#12 |STANDARD BRUSHED NICKEL | 14| 8 +Brand#12 |STANDARD BRUSHED NICKEL | 19| 8 +Brand#12 |STANDARD BRUSHED NICKEL | 36| 8 +Brand#12 |STANDARD BRUSHED NICKEL | 45| 8 +Brand#12 |STANDARD BRUSHED STEEL | 45| 8 +Brand#12 |STANDARD BRUSHED TIN | 14| 8 +Brand#12 |STANDARD BRUSHED TIN | 23| 8 +Brand#12 |STANDARD BURNISHED BRASS | 23| 8 +Brand#12 |STANDARD BURNISHED BRASS | 36| 8 +Brand#12 |STANDARD BURNISHED BRASS | 49| 8 +Brand#12 |STANDARD BURNISHED COPPER| 14| 8 +Brand#12 |STANDARD BURNISHED NICKEL| 14| 8 +Brand#12 |STANDARD BURNISHED STEEL | 14| 8 +Brand#12 |STANDARD BURNISHED TIN | 14| 8 +Brand#12 |STANDARD BURNISHED TIN | 23| 8 +Brand#12 |STANDARD BURNISHED TIN | 36| 8 +Brand#12 |STANDARD PLATED BRASS | 49| 8 +Brand#12 |STANDARD PLATED COPPER | 14| 8 +Brand#12 |STANDARD PLATED STEEL | 45| 8 +Brand#12 |STANDARD PLATED TIN | 9| 8 +Brand#12 |STANDARD PLATED TIN | 45| 8 +Brand#12 |STANDARD POLISHED COPPER | 3| 8 +Brand#12 |STANDARD POLISHED COPPER | 23| 8 +Brand#12 |STANDARD POLISHED COPPER | 36| 8 +Brand#12 |STANDARD POLISHED NICKEL | 36| 8 +Brand#12 |STANDARD POLISHED STEEL | 3| 8 +Brand#12 |STANDARD POLISHED STEEL | 14| 8 +Brand#12 |STANDARD POLISHED STEEL | 19| 8 +Brand#12 |STANDARD POLISHED STEEL | 45| 8 +Brand#13 |ECONOMY ANODIZED COPPER | 9| 8 +Brand#13 |ECONOMY ANODIZED COPPER | 23| 8 +Brand#13 |ECONOMY ANODIZED NICKEL | 3| 8 +Brand#13 |ECONOMY ANODIZED STEEL | 3| 8 +Brand#13 |ECONOMY BRUSHED COPPER | 3| 8 +Brand#13 |ECONOMY BRUSHED COPPER | 9| 8 +Brand#13 |ECONOMY BRUSHED COPPER | 49| 8 +Brand#13 |ECONOMY BRUSHED NICKEL | 49| 8 +Brand#13 |ECONOMY BRUSHED TIN | 14| 8 +Brand#13 |ECONOMY BRUSHED TIN | 23| 8 +Brand#13 |ECONOMY BURNISHED BRASS | 45| 8 +Brand#13 |ECONOMY BURNISHED NICKEL | 9| 8 +Brand#13 |ECONOMY BURNISHED STEEL | 3| 8 +Brand#13 |ECONOMY BURNISHED STEEL | 36| 8 +Brand#13 |ECONOMY BURNISHED TIN | 49| 8 +Brand#13 |ECONOMY PLATED BRASS | 36| 8 +Brand#13 |ECONOMY PLATED COPPER | 3| 8 +Brand#13 |ECONOMY PLATED COPPER | 9| 8 +Brand#13 |ECONOMY PLATED COPPER | 19| 8 +Brand#13 |ECONOMY PLATED NICKEL | 14| 8 +Brand#13 |ECONOMY PLATED STEEL | 45| 8 +Brand#13 |ECONOMY PLATED TIN | 3| 8 +Brand#13 |ECONOMY PLATED TIN | 23| 8 +Brand#13 |ECONOMY POLISHED BRASS | 9| 8 +Brand#13 |ECONOMY POLISHED BRASS | 36| 8 +Brand#13 |ECONOMY POLISHED COPPER | 9| 8 +Brand#13 |ECONOMY POLISHED COPPER | 49| 8 +Brand#13 |ECONOMY POLISHED STEEL | 3| 8 +Brand#13 |ECONOMY POLISHED STEEL | 23| 8 +Brand#13 |ECONOMY POLISHED STEEL | 45| 8 +Brand#13 |ECONOMY POLISHED STEEL | 49| 8 +Brand#13 |ECONOMY POLISHED TIN | 3| 8 +Brand#13 |ECONOMY POLISHED TIN | 36| 8 +Brand#13 |LARGE ANODIZED COPPER | 3| 8 +Brand#13 |LARGE ANODIZED COPPER | 19| 8 +Brand#13 |LARGE ANODIZED STEEL | 19| 8 +Brand#13 |LARGE ANODIZED STEEL | 45| 8 +Brand#13 |LARGE ANODIZED TIN | 45| 8 +Brand#13 |LARGE BRUSHED BRASS | 9| 8 +Brand#13 |LARGE BRUSHED BRASS | 19| 8 +Brand#13 |LARGE BRUSHED BRASS | 45| 8 +Brand#13 |LARGE BRUSHED BRASS | 49| 8 +Brand#13 |LARGE BRUSHED COPPER | 45| 8 +Brand#13 |LARGE BRUSHED COPPER | 49| 8 +Brand#13 |LARGE BRUSHED NICKEL | 9| 8 +Brand#13 |LARGE BRUSHED STEEL | 19| 8 +Brand#13 |LARGE BRUSHED STEEL | 36| 8 +Brand#13 |LARGE BRUSHED TIN | 9| 8 +Brand#13 |LARGE BURNISHED BRASS | 3| 8 +Brand#13 |LARGE BURNISHED COPPER | 3| 8 +Brand#13 |LARGE BURNISHED COPPER | 23| 8 +Brand#13 |LARGE BURNISHED NICKEL | 14| 8 +Brand#13 |LARGE BURNISHED STEEL | 14| 8 +Brand#13 |LARGE BURNISHED STEEL | 45| 8 +Brand#13 |LARGE PLATED BRASS | 9| 8 +Brand#13 |LARGE PLATED COPPER | 14| 8 +Brand#13 |LARGE PLATED NICKEL | 19| 8 +Brand#13 |LARGE PLATED STEEL | 3| 8 +Brand#13 |LARGE PLATED STEEL | 36| 8 +Brand#13 |LARGE PLATED TIN | 14| 8 +Brand#13 |LARGE PLATED TIN | 45| 8 +Brand#13 |LARGE POLISHED BRASS | 23| 8 +Brand#13 |LARGE POLISHED NICKEL | 45| 8 +Brand#13 |LARGE POLISHED STEEL | 36| 8 +Brand#13 |LARGE POLISHED TIN | 3| 8 +Brand#13 |LARGE POLISHED TIN | 9| 8 +Brand#13 |LARGE POLISHED TIN | 14| 8 +Brand#13 |LARGE POLISHED TIN | 45| 8 +Brand#13 |MEDIUM ANODIZED STEEL | 23| 8 +Brand#13 |MEDIUM ANODIZED TIN | 9| 8 +Brand#13 |MEDIUM ANODIZED TIN | 45| 8 +Brand#13 |MEDIUM BRUSHED BRASS | 14| 8 +Brand#13 |MEDIUM BRUSHED BRASS | 36| 8 +Brand#13 |MEDIUM BRUSHED BRASS | 49| 8 +Brand#13 |MEDIUM BRUSHED COPPER | 23| 8 +Brand#13 |MEDIUM BRUSHED COPPER | 49| 8 +Brand#13 |MEDIUM BRUSHED NICKEL | 19| 8 +Brand#13 |MEDIUM BRUSHED STEEL | 14| 8 +Brand#13 |MEDIUM BRUSHED TIN | 9| 8 +Brand#13 |MEDIUM BURNISHED BRASS | 19| 8 +Brand#13 |MEDIUM BURNISHED COPPER | 3| 8 +Brand#13 |MEDIUM BURNISHED COPPER | 19| 8 +Brand#13 |MEDIUM BURNISHED COPPER | 23| 8 +Brand#13 |MEDIUM BURNISHED NICKEL | 9| 8 +Brand#13 |MEDIUM BURNISHED NICKEL | 23| 8 +Brand#13 |MEDIUM BURNISHED STEEL | 14| 8 +Brand#13 |MEDIUM BURNISHED STEEL | 19| 8 +Brand#13 |MEDIUM BURNISHED STEEL | 45| 8 +Brand#13 |MEDIUM BURNISHED STEEL | 49| 8 +Brand#13 |MEDIUM BURNISHED TIN | 45| 8 +Brand#13 |MEDIUM BURNISHED TIN | 49| 8 +Brand#13 |MEDIUM PLATED BRASS | 19| 8 +Brand#13 |MEDIUM PLATED BRASS | 23| 8 +Brand#13 |MEDIUM PLATED COPPER | 14| 8 +Brand#13 |MEDIUM PLATED COPPER | 19| 8 +Brand#13 |MEDIUM PLATED NICKEL | 3| 8 +Brand#13 |MEDIUM PLATED NICKEL | 36| 8 +Brand#13 |MEDIUM PLATED STEEL | 3| 8 +Brand#13 |MEDIUM PLATED STEEL | 9| 8 +Brand#13 |MEDIUM PLATED STEEL | 19| 8 +Brand#13 |MEDIUM PLATED STEEL | 36| 8 +Brand#13 |MEDIUM PLATED TIN | 36| 8 +Brand#13 |PROMO ANODIZED BRASS | 3| 8 +Brand#13 |PROMO ANODIZED COPPER | 9| 8 +Brand#13 |PROMO ANODIZED COPPER | 14| 8 +Brand#13 |PROMO ANODIZED COPPER | 23| 8 +Brand#13 |PROMO ANODIZED NICKEL | 3| 8 +Brand#13 |PROMO ANODIZED NICKEL | 9| 8 +Brand#13 |PROMO ANODIZED NICKEL | 45| 8 +Brand#13 |PROMO ANODIZED STEEL | 19| 8 +Brand#13 |PROMO ANODIZED TIN | 36| 8 +Brand#13 |PROMO ANODIZED TIN | 49| 8 +Brand#13 |PROMO BRUSHED BRASS | 3| 8 +Brand#13 |PROMO BRUSHED BRASS | 23| 8 +Brand#13 |PROMO BRUSHED BRASS | 49| 8 +Brand#13 |PROMO BRUSHED COPPER | 14| 8 +Brand#13 |PROMO BRUSHED COPPER | 19| 8 +Brand#13 |PROMO BRUSHED COPPER | 49| 8 +Brand#13 |PROMO BRUSHED NICKEL | 14| 8 +Brand#13 |PROMO BRUSHED TIN | 45| 8 +Brand#13 |PROMO BURNISHED BRASS | 9| 8 +Brand#13 |PROMO BURNISHED BRASS | 23| 8 +Brand#13 |PROMO BURNISHED BRASS | 36| 8 +Brand#13 |PROMO BURNISHED COPPER | 9| 8 +Brand#13 |PROMO BURNISHED COPPER | 23| 8 +Brand#13 |PROMO BURNISHED COPPER | 45| 8 +Brand#13 |PROMO BURNISHED NICKEL | 9| 8 +Brand#13 |PROMO BURNISHED STEEL | 9| 8 +Brand#13 |PROMO BURNISHED STEEL | 14| 8 +Brand#13 |PROMO BURNISHED STEEL | 23| 8 +Brand#13 |PROMO BURNISHED STEEL | 45| 8 +Brand#13 |PROMO BURNISHED TIN | 9| 8 +Brand#13 |PROMO BURNISHED TIN | 14| 8 +Brand#13 |PROMO BURNISHED TIN | 19| 8 +Brand#13 |PROMO PLATED BRASS | 14| 8 +Brand#13 |PROMO PLATED BRASS | 49| 8 +Brand#13 |PROMO PLATED NICKEL | 14| 8 +Brand#13 |PROMO PLATED NICKEL | 36| 8 +Brand#13 |PROMO PLATED STEEL | 9| 8 +Brand#13 |PROMO PLATED TIN | 14| 8 +Brand#13 |PROMO PLATED TIN | 23| 8 +Brand#13 |PROMO POLISHED BRASS | 3| 8 +Brand#13 |PROMO POLISHED BRASS | 19| 8 +Brand#13 |PROMO POLISHED BRASS | 45| 8 +Brand#13 |PROMO POLISHED BRASS | 49| 8 +Brand#13 |PROMO POLISHED NICKEL | 23| 8 +Brand#13 |PROMO POLISHED STEEL | 36| 8 +Brand#13 |PROMO POLISHED STEEL | 45| 8 +Brand#13 |PROMO POLISHED TIN | 19| 8 +Brand#13 |PROMO POLISHED TIN | 36| 8 +Brand#13 |SMALL ANODIZED BRASS | 14| 8 +Brand#13 |SMALL ANODIZED COPPER | 9| 8 +Brand#13 |SMALL ANODIZED COPPER | 23| 8 +Brand#13 |SMALL ANODIZED NICKEL | 14| 8 +Brand#13 |SMALL ANODIZED NICKEL | 45| 8 +Brand#13 |SMALL ANODIZED STEEL | 14| 8 +Brand#13 |SMALL ANODIZED STEEL | 23| 8 +Brand#13 |SMALL ANODIZED TIN | 45| 8 +Brand#13 |SMALL BRUSHED BRASS | 9| 8 +Brand#13 |SMALL BRUSHED BRASS | 14| 8 +Brand#13 |SMALL BRUSHED BRASS | 36| 8 +Brand#13 |SMALL BRUSHED BRASS | 49| 8 +Brand#13 |SMALL BRUSHED COPPER | 23| 8 +Brand#13 |SMALL BRUSHED COPPER | 36| 8 +Brand#13 |SMALL BRUSHED NICKEL | 9| 8 +Brand#13 |SMALL BRUSHED NICKEL | 19| 8 +Brand#13 |SMALL BRUSHED STEEL | 23| 8 +Brand#13 |SMALL BRUSHED STEEL | 49| 8 +Brand#13 |SMALL BRUSHED TIN | 9| 8 +Brand#13 |SMALL BRUSHED TIN | 49| 8 +Brand#13 |SMALL BURNISHED BRASS | 19| 8 +Brand#13 |SMALL BURNISHED BRASS | 45| 8 +Brand#13 |SMALL BURNISHED COPPER | 9| 8 +Brand#13 |SMALL BURNISHED NICKEL | 19| 8 +Brand#13 |SMALL BURNISHED NICKEL | 45| 8 +Brand#13 |SMALL BURNISHED STEEL | 23| 8 +Brand#13 |SMALL BURNISHED TIN | 9| 8 +Brand#13 |SMALL BURNISHED TIN | 14| 8 +Brand#13 |SMALL BURNISHED TIN | 36| 8 +Brand#13 |SMALL PLATED BRASS | 9| 8 +Brand#13 |SMALL PLATED BRASS | 49| 8 +Brand#13 |SMALL PLATED COPPER | 3| 8 +Brand#13 |SMALL PLATED COPPER | 23| 8 +Brand#13 |SMALL PLATED COPPER | 36| 8 +Brand#13 |SMALL PLATED NICKEL | 14| 8 +Brand#13 |SMALL PLATED STEEL | 19| 8 +Brand#13 |SMALL PLATED TIN | 19| 8 +Brand#13 |SMALL POLISHED BRASS | 45| 8 +Brand#13 |SMALL POLISHED COPPER | 3| 8 +Brand#13 |SMALL POLISHED COPPER | 49| 8 +Brand#13 |SMALL POLISHED NICKEL | 3| 8 +Brand#13 |SMALL POLISHED STEEL | 36| 8 +Brand#13 |SMALL POLISHED STEEL | 45| 8 +Brand#13 |SMALL POLISHED STEEL | 49| 8 +Brand#13 |STANDARD ANODIZED BRASS | 49| 8 +Brand#13 |STANDARD ANODIZED COPPER | 14| 8 +Brand#13 |STANDARD ANODIZED COPPER | 36| 8 +Brand#13 |STANDARD ANODIZED NICKEL | 19| 8 +Brand#13 |STANDARD ANODIZED STEEL | 14| 8 +Brand#13 |STANDARD ANODIZED TIN | 9| 8 +Brand#13 |STANDARD ANODIZED TIN | 36| 8 +Brand#13 |STANDARD BRUSHED BRASS | 36| 8 +Brand#13 |STANDARD BRUSHED COPPER | 3| 8 +Brand#13 |STANDARD BRUSHED NICKEL | 14| 8 +Brand#13 |STANDARD BRUSHED NICKEL | 49| 8 +Brand#13 |STANDARD BRUSHED STEEL | 9| 8 +Brand#13 |STANDARD BRUSHED STEEL | 49| 8 +Brand#13 |STANDARD BRUSHED TIN | 9| 8 +Brand#13 |STANDARD BURNISHED BRASS | 23| 8 +Brand#13 |STANDARD BURNISHED BRASS | 36| 8 +Brand#13 |STANDARD BURNISHED COPPER| 49| 8 +Brand#13 |STANDARD BURNISHED NICKEL| 3| 8 +Brand#13 |STANDARD BURNISHED NICKEL| 14| 8 +Brand#13 |STANDARD PLATED BRASS | 9| 8 +Brand#13 |STANDARD PLATED BRASS | 23| 8 +Brand#13 |STANDARD PLATED BRASS | 49| 8 +Brand#13 |STANDARD PLATED COPPER | 36| 8 +Brand#13 |STANDARD PLATED NICKEL | 3| 8 +Brand#13 |STANDARD PLATED STEEL | 14| 8 +Brand#13 |STANDARD PLATED STEEL | 49| 8 +Brand#13 |STANDARD PLATED TIN | 14| 8 +Brand#13 |STANDARD PLATED TIN | 36| 8 +Brand#13 |STANDARD POLISHED COPPER | 14| 8 +Brand#13 |STANDARD POLISHED NICKEL | 3| 8 +Brand#13 |STANDARD POLISHED NICKEL | 36| 8 +Brand#13 |STANDARD POLISHED STEEL | 14| 8 +Brand#13 |STANDARD POLISHED STEEL | 23| 8 +Brand#13 |STANDARD POLISHED STEEL | 36| 8 +Brand#13 |STANDARD POLISHED TIN | 23| 8 +Brand#14 |ECONOMY ANODIZED BRASS | 36| 8 +Brand#14 |ECONOMY ANODIZED COPPER | 36| 8 +Brand#14 |ECONOMY ANODIZED COPPER | 45| 8 +Brand#14 |ECONOMY ANODIZED NICKEL | 23| 8 +Brand#14 |ECONOMY ANODIZED NICKEL | 45| 8 +Brand#14 |ECONOMY ANODIZED STEEL | 3| 8 +Brand#14 |ECONOMY ANODIZED STEEL | 9| 8 +Brand#14 |ECONOMY ANODIZED STEEL | 19| 8 +Brand#14 |ECONOMY ANODIZED TIN | 19| 8 +Brand#14 |ECONOMY BRUSHED BRASS | 3| 8 +Brand#14 |ECONOMY BRUSHED BRASS | 14| 8 +Brand#14 |ECONOMY BRUSHED BRASS | 49| 8 +Brand#14 |ECONOMY BRUSHED NICKEL | 14| 8 +Brand#14 |ECONOMY BRUSHED STEEL | 49| 8 +Brand#14 |ECONOMY BRUSHED TIN | 3| 8 +Brand#14 |ECONOMY BRUSHED TIN | 49| 8 +Brand#14 |ECONOMY BURNISHED BRASS | 14| 8 +Brand#14 |ECONOMY BURNISHED COPPER | 36| 8 +Brand#14 |ECONOMY BURNISHED STEEL | 23| 8 +Brand#14 |ECONOMY BURNISHED STEEL | 36| 8 +Brand#14 |ECONOMY BURNISHED TIN | 19| 8 +Brand#14 |ECONOMY BURNISHED TIN | 23| 8 +Brand#14 |ECONOMY BURNISHED TIN | 36| 8 +Brand#14 |ECONOMY PLATED BRASS | 36| 8 +Brand#14 |ECONOMY PLATED COPPER | 3| 8 +Brand#14 |ECONOMY PLATED COPPER | 9| 8 +Brand#14 |ECONOMY PLATED NICKEL | 3| 8 +Brand#14 |ECONOMY PLATED STEEL | 36| 8 +Brand#14 |ECONOMY PLATED TIN | 9| 8 +Brand#14 |ECONOMY POLISHED BRASS | 3| 8 +Brand#14 |ECONOMY POLISHED BRASS | 23| 8 +Brand#14 |ECONOMY POLISHED BRASS | 36| 8 +Brand#14 |ECONOMY POLISHED COPPER | 14| 8 +Brand#14 |ECONOMY POLISHED STEEL | 3| 8 +Brand#14 |ECONOMY POLISHED TIN | 19| 8 +Brand#14 |LARGE ANODIZED BRASS | 19| 8 +Brand#14 |LARGE ANODIZED COPPER | 23| 8 +Brand#14 |LARGE ANODIZED NICKEL | 9| 8 +Brand#14 |LARGE ANODIZED NICKEL | 49| 8 +Brand#14 |LARGE ANODIZED STEEL | 3| 8 +Brand#14 |LARGE ANODIZED STEEL | 45| 8 +Brand#14 |LARGE ANODIZED TIN | 9| 8 +Brand#14 |LARGE ANODIZED TIN | 19| 8 +Brand#14 |LARGE ANODIZED TIN | 23| 8 +Brand#14 |LARGE BRUSHED BRASS | 23| 8 +Brand#14 |LARGE BRUSHED BRASS | 45| 8 +Brand#14 |LARGE BRUSHED COPPER | 49| 8 +Brand#14 |LARGE BRUSHED NICKEL | 23| 8 +Brand#14 |LARGE BRUSHED NICKEL | 45| 8 +Brand#14 |LARGE BRUSHED TIN | 9| 8 +Brand#14 |LARGE BURNISHED BRASS | 14| 8 +Brand#14 |LARGE BURNISHED COPPER | 19| 8 +Brand#14 |LARGE BURNISHED NICKEL | 3| 8 +Brand#14 |LARGE BURNISHED NICKEL | 49| 8 +Brand#14 |LARGE BURNISHED STEEL | 3| 8 +Brand#14 |LARGE BURNISHED STEEL | 9| 8 +Brand#14 |LARGE BURNISHED STEEL | 14| 8 +Brand#14 |LARGE BURNISHED STEEL | 19| 8 +Brand#14 |LARGE BURNISHED STEEL | 45| 8 +Brand#14 |LARGE BURNISHED TIN | 19| 8 +Brand#14 |LARGE BURNISHED TIN | 23| 8 +Brand#14 |LARGE BURNISHED TIN | 45| 8 +Brand#14 |LARGE PLATED BRASS | 23| 8 +Brand#14 |LARGE PLATED COPPER | 36| 8 +Brand#14 |LARGE PLATED NICKEL | 23| 8 +Brand#14 |LARGE PLATED NICKEL | 49| 8 +Brand#14 |LARGE PLATED STEEL | 49| 8 +Brand#14 |LARGE POLISHED BRASS | 3| 8 +Brand#14 |LARGE POLISHED BRASS | 9| 8 +Brand#14 |LARGE POLISHED BRASS | 14| 8 +Brand#14 |LARGE POLISHED BRASS | 19| 8 +Brand#14 |LARGE POLISHED BRASS | 36| 8 +Brand#14 |LARGE POLISHED COPPER | 9| 8 +Brand#14 |LARGE POLISHED COPPER | 23| 8 +Brand#14 |LARGE POLISHED NICKEL | 14| 8 +Brand#14 |LARGE POLISHED NICKEL | 36| 8 +Brand#14 |LARGE POLISHED STEEL | 23| 8 +Brand#14 |LARGE POLISHED TIN | 36| 8 +Brand#14 |LARGE POLISHED TIN | 45| 8 +Brand#14 |LARGE POLISHED TIN | 49| 8 +Brand#14 |MEDIUM ANODIZED BRASS | 14| 8 +Brand#14 |MEDIUM ANODIZED COPPER | 9| 8 +Brand#14 |MEDIUM ANODIZED COPPER | 19| 8 +Brand#14 |MEDIUM ANODIZED COPPER | 36| 8 +Brand#14 |MEDIUM ANODIZED COPPER | 49| 8 +Brand#14 |MEDIUM ANODIZED NICKEL | 9| 8 +Brand#14 |MEDIUM ANODIZED NICKEL | 36| 8 +Brand#14 |MEDIUM BRUSHED COPPER | 9| 8 +Brand#14 |MEDIUM BRUSHED COPPER | 23| 8 +Brand#14 |MEDIUM BRUSHED STEEL | 49| 8 +Brand#14 |MEDIUM BRUSHED TIN | 3| 8 +Brand#14 |MEDIUM BRUSHED TIN | 9| 8 +Brand#14 |MEDIUM BURNISHED BRASS | 19| 8 +Brand#14 |MEDIUM BURNISHED BRASS | 23| 8 +Brand#14 |MEDIUM BURNISHED NICKEL | 9| 8 +Brand#14 |MEDIUM BURNISHED NICKEL | 19| 8 +Brand#14 |MEDIUM BURNISHED NICKEL | 23| 8 +Brand#14 |MEDIUM BURNISHED NICKEL | 49| 8 +Brand#14 |MEDIUM BURNISHED STEEL | 36| 8 +Brand#14 |MEDIUM BURNISHED STEEL | 49| 8 +Brand#14 |MEDIUM BURNISHED TIN | 49| 8 +Brand#14 |MEDIUM PLATED BRASS | 9| 8 +Brand#14 |MEDIUM PLATED BRASS | 23| 8 +Brand#14 |MEDIUM PLATED BRASS | 36| 8 +Brand#14 |MEDIUM PLATED BRASS | 45| 8 +Brand#14 |MEDIUM PLATED BRASS | 49| 8 +Brand#14 |MEDIUM PLATED NICKEL | 23| 8 +Brand#14 |MEDIUM PLATED STEEL | 36| 8 +Brand#14 |MEDIUM PLATED STEEL | 49| 8 +Brand#14 |MEDIUM PLATED TIN | 3| 8 +Brand#14 |MEDIUM PLATED TIN | 14| 8 +Brand#14 |MEDIUM PLATED TIN | 45| 8 +Brand#14 |PROMO ANODIZED BRASS | 23| 8 +Brand#14 |PROMO ANODIZED BRASS | 36| 8 +Brand#14 |PROMO ANODIZED COPPER | 19| 8 +Brand#14 |PROMO ANODIZED COPPER | 36| 8 +Brand#14 |PROMO ANODIZED COPPER | 45| 8 +Brand#14 |PROMO ANODIZED STEEL | 45| 8 +Brand#14 |PROMO ANODIZED TIN | 14| 8 +Brand#14 |PROMO ANODIZED TIN | 19| 8 +Brand#14 |PROMO BRUSHED BRASS | 14| 8 +Brand#14 |PROMO BRUSHED BRASS | 19| 8 +Brand#14 |PROMO BRUSHED BRASS | 36| 8 +Brand#14 |PROMO BRUSHED BRASS | 45| 8 +Brand#14 |PROMO BRUSHED COPPER | 23| 8 +Brand#14 |PROMO BRUSHED COPPER | 49| 8 +Brand#14 |PROMO BRUSHED NICKEL | 19| 8 +Brand#14 |PROMO BRUSHED NICKEL | 36| 8 +Brand#14 |PROMO BRUSHED STEEL | 9| 8 +Brand#14 |PROMO BRUSHED STEEL | 36| 8 +Brand#14 |PROMO BRUSHED STEEL | 49| 8 +Brand#14 |PROMO BURNISHED BRASS | 9| 8 +Brand#14 |PROMO BURNISHED BRASS | 23| 8 +Brand#14 |PROMO BURNISHED BRASS | 36| 8 +Brand#14 |PROMO BURNISHED BRASS | 45| 8 +Brand#14 |PROMO BURNISHED NICKEL | 9| 8 +Brand#14 |PROMO BURNISHED STEEL | 36| 8 +Brand#14 |PROMO BURNISHED TIN | 49| 8 +Brand#14 |PROMO PLATED BRASS | 14| 8 +Brand#14 |PROMO PLATED BRASS | 45| 8 +Brand#14 |PROMO PLATED COPPER | 23| 8 +Brand#14 |PROMO PLATED NICKEL | 9| 8 +Brand#14 |PROMO PLATED STEEL | 3| 8 +Brand#14 |PROMO PLATED STEEL | 14| 8 +Brand#14 |PROMO PLATED STEEL | 19| 8 +Brand#14 |PROMO PLATED STEEL | 49| 8 +Brand#14 |PROMO PLATED TIN | 3| 8 +Brand#14 |PROMO PLATED TIN | 9| 8 +Brand#14 |PROMO POLISHED BRASS | 36| 8 +Brand#14 |PROMO POLISHED COPPER | 3| 8 +Brand#14 |PROMO POLISHED NICKEL | 3| 8 +Brand#14 |PROMO POLISHED NICKEL | 45| 8 +Brand#14 |PROMO POLISHED TIN | 9| 8 +Brand#14 |PROMO POLISHED TIN | 49| 8 +Brand#14 |SMALL ANODIZED BRASS | 9| 8 +Brand#14 |SMALL ANODIZED BRASS | 14| 8 +Brand#14 |SMALL ANODIZED COPPER | 14| 8 +Brand#14 |SMALL ANODIZED NICKEL | 36| 8 +Brand#14 |SMALL ANODIZED STEEL | 23| 8 +Brand#14 |SMALL ANODIZED TIN | 19| 8 +Brand#14 |SMALL BRUSHED BRASS | 19| 8 +Brand#14 |SMALL BRUSHED BRASS | 45| 8 +Brand#14 |SMALL BRUSHED COPPER | 36| 8 +Brand#14 |SMALL BRUSHED COPPER | 49| 8 +Brand#14 |SMALL BRUSHED TIN | 9| 8 +Brand#14 |SMALL BRUSHED TIN | 14| 8 +Brand#14 |SMALL BRUSHED TIN | 36| 8 +Brand#14 |SMALL BURNISHED BRASS | 19| 8 +Brand#14 |SMALL BURNISHED BRASS | 45| 8 +Brand#14 |SMALL BURNISHED COPPER | 14| 8 +Brand#14 |SMALL BURNISHED COPPER | 36| 8 +Brand#14 |SMALL BURNISHED NICKEL | 36| 8 +Brand#14 |SMALL BURNISHED NICKEL | 45| 8 +Brand#14 |SMALL BURNISHED STEEL | 14| 8 +Brand#14 |SMALL BURNISHED STEEL | 45| 8 +Brand#14 |SMALL BURNISHED TIN | 19| 8 +Brand#14 |SMALL BURNISHED TIN | 23| 8 +Brand#14 |SMALL PLATED BRASS | 14| 8 +Brand#14 |SMALL PLATED COPPER | 23| 8 +Brand#14 |SMALL PLATED NICKEL | 19| 8 +Brand#14 |SMALL PLATED STEEL | 14| 8 +Brand#14 |SMALL PLATED STEEL | 36| 8 +Brand#14 |SMALL PLATED TIN | 9| 8 +Brand#14 |SMALL PLATED TIN | 49| 8 +Brand#14 |SMALL POLISHED BRASS | 19| 8 +Brand#14 |SMALL POLISHED BRASS | 36| 8 +Brand#14 |SMALL POLISHED BRASS | 45| 8 +Brand#14 |SMALL POLISHED COPPER | 3| 8 +Brand#14 |SMALL POLISHED NICKEL | 9| 8 +Brand#14 |SMALL POLISHED NICKEL | 19| 8 +Brand#14 |SMALL POLISHED STEEL | 49| 8 +Brand#14 |SMALL POLISHED TIN | 3| 8 +Brand#14 |SMALL POLISHED TIN | 36| 8 +Brand#14 |STANDARD ANODIZED BRASS | 3| 8 +Brand#14 |STANDARD ANODIZED COPPER | 3| 8 +Brand#14 |STANDARD ANODIZED COPPER | 23| 8 +Brand#14 |STANDARD ANODIZED STEEL | 9| 8 +Brand#14 |STANDARD BRUSHED BRASS | 19| 8 +Brand#14 |STANDARD BRUSHED COPPER | 3| 8 +Brand#14 |STANDARD BRUSHED NICKEL | 3| 8 +Brand#14 |STANDARD BRUSHED STEEL | 23| 8 +Brand#14 |STANDARD BRUSHED TIN | 9| 8 +Brand#14 |STANDARD BRUSHED TIN | 45| 8 +Brand#14 |STANDARD BRUSHED TIN | 49| 8 +Brand#14 |STANDARD BURNISHED BRASS | 9| 8 +Brand#14 |STANDARD BURNISHED BRASS | 49| 8 +Brand#14 |STANDARD BURNISHED COPPER| 14| 8 +Brand#14 |STANDARD BURNISHED NICKEL| 3| 8 +Brand#14 |STANDARD BURNISHED NICKEL| 19| 8 +Brand#14 |STANDARD BURNISHED NICKEL| 23| 8 +Brand#14 |STANDARD BURNISHED STEEL | 14| 8 +Brand#14 |STANDARD BURNISHED STEEL | 19| 8 +Brand#14 |STANDARD BURNISHED STEEL | 23| 8 +Brand#14 |STANDARD BURNISHED TIN | 3| 8 +Brand#14 |STANDARD BURNISHED TIN | 9| 8 +Brand#14 |STANDARD PLATED BRASS | 9| 8 +Brand#14 |STANDARD PLATED BRASS | 45| 8 +Brand#14 |STANDARD PLATED COPPER | 14| 8 +Brand#14 |STANDARD PLATED NICKEL | 14| 8 +Brand#14 |STANDARD PLATED STEEL | 23| 8 +Brand#14 |STANDARD PLATED TIN | 3| 8 +Brand#14 |STANDARD POLISHED BRASS | 19| 8 +Brand#14 |STANDARD POLISHED COPPER | 3| 8 +Brand#14 |STANDARD POLISHED COPPER | 49| 8 +Brand#14 |STANDARD POLISHED NICKEL | 3| 8 +Brand#14 |STANDARD POLISHED NICKEL | 9| 8 +Brand#14 |STANDARD POLISHED NICKEL | 19| 8 +Brand#15 |ECONOMY ANODIZED BRASS | 3| 8 +Brand#15 |ECONOMY ANODIZED BRASS | 9| 8 +Brand#15 |ECONOMY ANODIZED STEEL | 3| 8 +Brand#15 |ECONOMY ANODIZED STEEL | 14| 8 +Brand#15 |ECONOMY ANODIZED STEEL | 36| 8 +Brand#15 |ECONOMY ANODIZED TIN | 36| 8 +Brand#15 |ECONOMY BRUSHED BRASS | 19| 8 +Brand#15 |ECONOMY BRUSHED NICKEL | 3| 8 +Brand#15 |ECONOMY BRUSHED NICKEL | 36| 8 +Brand#15 |ECONOMY BRUSHED NICKEL | 45| 8 +Brand#15 |ECONOMY BRUSHED STEEL | 23| 8 +Brand#15 |ECONOMY BRUSHED STEEL | 45| 8 +Brand#15 |ECONOMY BRUSHED TIN | 36| 8 +Brand#15 |ECONOMY BRUSHED TIN | 49| 8 +Brand#15 |ECONOMY BURNISHED BRASS | 9| 8 +Brand#15 |ECONOMY BURNISHED BRASS | 14| 8 +Brand#15 |ECONOMY BURNISHED COPPER | 23| 8 +Brand#15 |ECONOMY BURNISHED COPPER | 45| 8 +Brand#15 |ECONOMY BURNISHED NICKEL | 49| 8 +Brand#15 |ECONOMY PLATED BRASS | 14| 8 +Brand#15 |ECONOMY PLATED COPPER | 36| 8 +Brand#15 |ECONOMY PLATED COPPER | 45| 8 +Brand#15 |ECONOMY POLISHED COPPER | 49| 8 +Brand#15 |ECONOMY POLISHED NICKEL | 9| 8 +Brand#15 |ECONOMY POLISHED NICKEL | 14| 8 +Brand#15 |ECONOMY POLISHED STEEL | 49| 8 +Brand#15 |LARGE ANODIZED BRASS | 9| 8 +Brand#15 |LARGE ANODIZED BRASS | 36| 8 +Brand#15 |LARGE ANODIZED COPPER | 23| 8 +Brand#15 |LARGE ANODIZED COPPER | 36| 8 +Brand#15 |LARGE ANODIZED COPPER | 45| 8 +Brand#15 |LARGE ANODIZED NICKEL | 23| 8 +Brand#15 |LARGE ANODIZED NICKEL | 49| 8 +Brand#15 |LARGE ANODIZED TIN | 14| 8 +Brand#15 |LARGE BRUSHED BRASS | 9| 8 +Brand#15 |LARGE BRUSHED COPPER | 3| 8 +Brand#15 |LARGE BRUSHED COPPER | 14| 8 +Brand#15 |LARGE BRUSHED STEEL | 19| 8 +Brand#15 |LARGE BRUSHED STEEL | 23| 8 +Brand#15 |LARGE BRUSHED TIN | 3| 8 +Brand#15 |LARGE BRUSHED TIN | 9| 8 +Brand#15 |LARGE BRUSHED TIN | 19| 8 +Brand#15 |LARGE BRUSHED TIN | 49| 8 +Brand#15 |LARGE BURNISHED BRASS | 9| 8 +Brand#15 |LARGE BURNISHED BRASS | 14| 8 +Brand#15 |LARGE BURNISHED BRASS | 19| 8 +Brand#15 |LARGE BURNISHED COPPER | 23| 8 +Brand#15 |LARGE BURNISHED COPPER | 45| 8 +Brand#15 |LARGE BURNISHED NICKEL | 36| 8 +Brand#15 |LARGE BURNISHED STEEL | 36| 8 +Brand#15 |LARGE BURNISHED STEEL | 49| 8 +Brand#15 |LARGE BURNISHED TIN | 49| 8 +Brand#15 |LARGE PLATED COPPER | 19| 8 +Brand#15 |LARGE PLATED COPPER | 45| 8 +Brand#15 |LARGE PLATED NICKEL | 14| 8 +Brand#15 |LARGE PLATED STEEL | 9| 8 +Brand#15 |LARGE PLATED TIN | 49| 8 +Brand#15 |LARGE POLISHED BRASS | 23| 8 +Brand#15 |LARGE POLISHED STEEL | 36| 8 +Brand#15 |LARGE POLISHED STEEL | 49| 8 +Brand#15 |LARGE POLISHED TIN | 19| 8 +Brand#15 |MEDIUM ANODIZED BRASS | 3| 8 +Brand#15 |MEDIUM ANODIZED BRASS | 9| 8 +Brand#15 |MEDIUM ANODIZED BRASS | 19| 8 +Brand#15 |MEDIUM ANODIZED BRASS | 23| 8 +Brand#15 |MEDIUM ANODIZED COPPER | 36| 8 +Brand#15 |MEDIUM ANODIZED NICKEL | 45| 8 +Brand#15 |MEDIUM ANODIZED STEEL | 23| 8 +Brand#15 |MEDIUM ANODIZED TIN | 14| 8 +Brand#15 |MEDIUM ANODIZED TIN | 19| 8 +Brand#15 |MEDIUM ANODIZED TIN | 23| 8 +Brand#15 |MEDIUM BRUSHED BRASS | 3| 8 +Brand#15 |MEDIUM BRUSHED BRASS | 23| 8 +Brand#15 |MEDIUM BRUSHED COPPER | 49| 8 +Brand#15 |MEDIUM BRUSHED NICKEL | 9| 8 +Brand#15 |MEDIUM BRUSHED TIN | 9| 8 +Brand#15 |MEDIUM BRUSHED TIN | 23| 8 +Brand#15 |MEDIUM BURNISHED BRASS | 45| 8 +Brand#15 |MEDIUM BURNISHED COPPER | 3| 8 +Brand#15 |MEDIUM BURNISHED COPPER | 49| 8 +Brand#15 |MEDIUM BURNISHED NICKEL | 19| 8 +Brand#15 |MEDIUM BURNISHED NICKEL | 36| 8 +Brand#15 |MEDIUM BURNISHED NICKEL | 49| 8 +Brand#15 |MEDIUM BURNISHED STEEL | 23| 8 +Brand#15 |MEDIUM BURNISHED STEEL | 49| 8 +Brand#15 |MEDIUM BURNISHED TIN | 45| 8 +Brand#15 |MEDIUM PLATED BRASS | 36| 8 +Brand#15 |MEDIUM PLATED NICKEL | 23| 8 +Brand#15 |MEDIUM PLATED NICKEL | 49| 8 +Brand#15 |MEDIUM PLATED STEEL | 9| 8 +Brand#15 |MEDIUM PLATED STEEL | 19| 8 +Brand#15 |MEDIUM PLATED STEEL | 49| 8 +Brand#15 |MEDIUM PLATED TIN | 19| 8 +Brand#15 |MEDIUM PLATED TIN | 49| 8 +Brand#15 |PROMO ANODIZED BRASS | 14| 8 +Brand#15 |PROMO ANODIZED BRASS | 36| 8 +Brand#15 |PROMO ANODIZED COPPER | 45| 8 +Brand#15 |PROMO ANODIZED NICKEL | 36| 8 +Brand#15 |PROMO ANODIZED NICKEL | 49| 8 +Brand#15 |PROMO ANODIZED STEEL | 14| 8 +Brand#15 |PROMO BRUSHED BRASS | 14| 8 +Brand#15 |PROMO BRUSHED COPPER | 9| 8 +Brand#15 |PROMO BRUSHED COPPER | 19| 8 +Brand#15 |PROMO BRUSHED NICKEL | 9| 8 +Brand#15 |PROMO BRUSHED NICKEL | 19| 8 +Brand#15 |PROMO BRUSHED NICKEL | 23| 8 +Brand#15 |PROMO BRUSHED STEEL | 14| 8 +Brand#15 |PROMO BRUSHED STEEL | 23| 8 +Brand#15 |PROMO BRUSHED STEEL | 49| 8 +Brand#15 |PROMO BRUSHED TIN | 3| 8 +Brand#15 |PROMO BRUSHED TIN | 23| 8 +Brand#15 |PROMO BRUSHED TIN | 36| 8 +Brand#15 |PROMO BURNISHED BRASS | 23| 8 +Brand#15 |PROMO BURNISHED BRASS | 36| 8 +Brand#15 |PROMO BURNISHED COPPER | 3| 8 +Brand#15 |PROMO BURNISHED COPPER | 9| 8 +Brand#15 |PROMO BURNISHED COPPER | 19| 8 +Brand#15 |PROMO BURNISHED NICKEL | 23| 8 +Brand#15 |PROMO BURNISHED NICKEL | 36| 8 +Brand#15 |PROMO BURNISHED NICKEL | 49| 8 +Brand#15 |PROMO BURNISHED STEEL | 9| 8 +Brand#15 |PROMO BURNISHED TIN | 14| 8 +Brand#15 |PROMO BURNISHED TIN | 45| 8 +Brand#15 |PROMO PLATED BRASS | 36| 8 +Brand#15 |PROMO PLATED BRASS | 49| 8 +Brand#15 |PROMO PLATED COPPER | 3| 8 +Brand#15 |PROMO PLATED COPPER | 9| 8 +Brand#15 |PROMO PLATED COPPER | 14| 8 +Brand#15 |PROMO PLATED NICKEL | 36| 8 +Brand#15 |PROMO PLATED NICKEL | 45| 8 +Brand#15 |PROMO PLATED STEEL | 14| 8 +Brand#15 |PROMO PLATED TIN | 3| 8 +Brand#15 |PROMO PLATED TIN | 9| 8 +Brand#15 |PROMO PLATED TIN | 19| 8 +Brand#15 |PROMO POLISHED COPPER | 3| 8 +Brand#15 |PROMO POLISHED COPPER | 14| 8 +Brand#15 |PROMO POLISHED COPPER | 19| 8 +Brand#15 |PROMO POLISHED COPPER | 49| 8 +Brand#15 |PROMO POLISHED NICKEL | 19| 8 +Brand#15 |PROMO POLISHED STEEL | 3| 8 +Brand#15 |PROMO POLISHED STEEL | 14| 8 +Brand#15 |PROMO POLISHED STEEL | 19| 8 +Brand#15 |PROMO POLISHED TIN | 23| 8 +Brand#15 |SMALL ANODIZED BRASS | 14| 8 +Brand#15 |SMALL ANODIZED BRASS | 19| 8 +Brand#15 |SMALL ANODIZED NICKEL | 3| 8 +Brand#15 |SMALL ANODIZED NICKEL | 14| 8 +Brand#15 |SMALL ANODIZED NICKEL | 36| 8 +Brand#15 |SMALL ANODIZED STEEL | 3| 8 +Brand#15 |SMALL ANODIZED TIN | 45| 8 +Brand#15 |SMALL BRUSHED BRASS | 3| 8 +Brand#15 |SMALL BRUSHED BRASS | 9| 8 +Brand#15 |SMALL BRUSHED BRASS | 19| 8 +Brand#15 |SMALL BRUSHED NICKEL | 9| 8 +Brand#15 |SMALL BRUSHED NICKEL | 49| 8 +Brand#15 |SMALL BRUSHED STEEL | 14| 8 +Brand#15 |SMALL BRUSHED STEEL | 23| 8 +Brand#15 |SMALL BRUSHED TIN | 9| 8 +Brand#15 |SMALL BRUSHED TIN | 23| 8 +Brand#15 |SMALL BRUSHED TIN | 36| 8 +Brand#15 |SMALL BRUSHED TIN | 45| 8 +Brand#15 |SMALL BURNISHED BRASS | 19| 8 +Brand#15 |SMALL BURNISHED COPPER | 14| 8 +Brand#15 |SMALL BURNISHED COPPER | 49| 8 +Brand#15 |SMALL BURNISHED NICKEL | 3| 8 +Brand#15 |SMALL BURNISHED NICKEL | 9| 8 +Brand#15 |SMALL BURNISHED NICKEL | 36| 8 +Brand#15 |SMALL BURNISHED STEEL | 9| 8 +Brand#15 |SMALL BURNISHED STEEL | 19| 8 +Brand#15 |SMALL BURNISHED TIN | 14| 8 +Brand#15 |SMALL BURNISHED TIN | 19| 8 +Brand#15 |SMALL BURNISHED TIN | 23| 8 +Brand#15 |SMALL PLATED STEEL | 3| 8 +Brand#15 |SMALL PLATED STEEL | 9| 8 +Brand#15 |SMALL PLATED TIN | 9| 8 +Brand#15 |SMALL POLISHED COPPER | 3| 8 +Brand#15 |SMALL POLISHED COPPER | 9| 8 +Brand#15 |SMALL POLISHED NICKEL | 14| 8 +Brand#15 |SMALL POLISHED STEEL | 3| 8 +Brand#15 |SMALL POLISHED STEEL | 9| 8 +Brand#15 |SMALL POLISHED STEEL | 23| 8 +Brand#15 |SMALL POLISHED STEEL | 36| 8 +Brand#15 |SMALL POLISHED TIN | 9| 8 +Brand#15 |SMALL POLISHED TIN | 19| 8 +Brand#15 |SMALL POLISHED TIN | 45| 8 +Brand#15 |STANDARD ANODIZED BRASS | 19| 8 +Brand#15 |STANDARD ANODIZED BRASS | 23| 8 +Brand#15 |STANDARD ANODIZED COPPER | 3| 8 +Brand#15 |STANDARD ANODIZED COPPER | 23| 8 +Brand#15 |STANDARD ANODIZED COPPER | 36| 8 +Brand#15 |STANDARD BRUSHED COPPER | 23| 8 +Brand#15 |STANDARD BRUSHED NICKEL | 9| 8 +Brand#15 |STANDARD BRUSHED NICKEL | 19| 8 +Brand#15 |STANDARD BRUSHED STEEL | 49| 8 +Brand#15 |STANDARD BRUSHED TIN | 45| 8 +Brand#15 |STANDARD BURNISHED BRASS | 23| 8 +Brand#15 |STANDARD BURNISHED NICKEL| 9| 8 +Brand#15 |STANDARD BURNISHED NICKEL| 14| 8 +Brand#15 |STANDARD BURNISHED NICKEL| 49| 8 +Brand#15 |STANDARD BURNISHED STEEL | 45| 8 +Brand#15 |STANDARD PLATED BRASS | 14| 8 +Brand#15 |STANDARD PLATED BRASS | 36| 8 +Brand#15 |STANDARD PLATED COPPER | 9| 8 +Brand#15 |STANDARD PLATED NICKEL | 9| 8 +Brand#15 |STANDARD PLATED STEEL | 23| 8 +Brand#15 |STANDARD POLISHED BRASS | 3| 8 +Brand#15 |STANDARD POLISHED BRASS | 9| 8 +Brand#15 |STANDARD POLISHED BRASS | 14| 8 +Brand#15 |STANDARD POLISHED COPPER | 3| 8 +Brand#15 |STANDARD POLISHED COPPER | 23| 8 +Brand#15 |STANDARD POLISHED NICKEL | 14| 8 +Brand#15 |STANDARD POLISHED NICKEL | 36| 8 +Brand#15 |STANDARD POLISHED NICKEL | 45| 8 +Brand#15 |STANDARD POLISHED TIN | 3| 8 +Brand#15 |STANDARD POLISHED TIN | 36| 8 +Brand#21 |ECONOMY ANODIZED BRASS | 14| 8 +Brand#21 |ECONOMY ANODIZED COPPER | 3| 8 +Brand#21 |ECONOMY ANODIZED COPPER | 14| 8 +Brand#21 |ECONOMY ANODIZED COPPER | 36| 8 +Brand#21 |ECONOMY ANODIZED NICKEL | 14| 8 +Brand#21 |ECONOMY ANODIZED NICKEL | 36| 8 +Brand#21 |ECONOMY ANODIZED NICKEL | 45| 8 +Brand#21 |ECONOMY ANODIZED STEEL | 36| 8 +Brand#21 |ECONOMY ANODIZED STEEL | 49| 8 +Brand#21 |ECONOMY ANODIZED TIN | 9| 8 +Brand#21 |ECONOMY BRUSHED BRASS | 14| 8 +Brand#21 |ECONOMY BRUSHED BRASS | 36| 8 +Brand#21 |ECONOMY BRUSHED COPPER | 45| 8 +Brand#21 |ECONOMY BRUSHED NICKEL | 36| 8 +Brand#21 |ECONOMY BURNISHED BRASS | 3| 8 +Brand#21 |ECONOMY BURNISHED NICKEL | 3| 8 +Brand#21 |ECONOMY BURNISHED NICKEL | 49| 8 +Brand#21 |ECONOMY BURNISHED STEEL | 23| 8 +Brand#21 |ECONOMY BURNISHED STEEL | 36| 8 +Brand#21 |ECONOMY BURNISHED TIN | 14| 8 +Brand#21 |ECONOMY BURNISHED TIN | 19| 8 +Brand#21 |ECONOMY BURNISHED TIN | 45| 8 +Brand#21 |ECONOMY PLATED BRASS | 9| 8 +Brand#21 |ECONOMY PLATED NICKEL | 49| 8 +Brand#21 |ECONOMY PLATED STEEL | 19| 8 +Brand#21 |ECONOMY PLATED STEEL | 23| 8 +Brand#21 |ECONOMY POLISHED BRASS | 23| 8 +Brand#21 |ECONOMY POLISHED COPPER | 3| 8 +Brand#21 |ECONOMY POLISHED NICKEL | 3| 8 +Brand#21 |ECONOMY POLISHED NICKEL | 19| 8 +Brand#21 |ECONOMY POLISHED NICKEL | 36| 8 +Brand#21 |ECONOMY POLISHED STEEL | 36| 8 +Brand#21 |ECONOMY POLISHED STEEL | 49| 8 +Brand#21 |ECONOMY POLISHED TIN | 3| 8 +Brand#21 |ECONOMY POLISHED TIN | 45| 8 +Brand#21 |LARGE ANODIZED BRASS | 45| 8 +Brand#21 |LARGE ANODIZED NICKEL | 9| 8 +Brand#21 |LARGE ANODIZED NICKEL | 19| 8 +Brand#21 |LARGE ANODIZED NICKEL | 49| 8 +Brand#21 |LARGE ANODIZED STEEL | 3| 8 +Brand#21 |LARGE ANODIZED STEEL | 36| 8 +Brand#21 |LARGE BRUSHED BRASS | 19| 8 +Brand#21 |LARGE BRUSHED BRASS | 23| 8 +Brand#21 |LARGE BRUSHED BRASS | 45| 8 +Brand#21 |LARGE BRUSHED COPPER | 19| 8 +Brand#21 |LARGE BRUSHED NICKEL | 14| 8 +Brand#21 |LARGE BRUSHED NICKEL | 45| 8 +Brand#21 |LARGE BRUSHED STEEL | 45| 8 +Brand#21 |LARGE BRUSHED TIN | 9| 8 +Brand#21 |LARGE BRUSHED TIN | 19| 8 +Brand#21 |LARGE BRUSHED TIN | 36| 8 +Brand#21 |LARGE BURNISHED COPPER | 3| 8 +Brand#21 |LARGE BURNISHED COPPER | 9| 8 +Brand#21 |LARGE BURNISHED COPPER | 14| 8 +Brand#21 |LARGE BURNISHED COPPER | 19| 8 +Brand#21 |LARGE BURNISHED COPPER | 23| 8 +Brand#21 |LARGE BURNISHED NICKEL | 9| 8 +Brand#21 |LARGE BURNISHED NICKEL | 36| 8 +Brand#21 |LARGE BURNISHED STEEL | 14| 8 +Brand#21 |LARGE BURNISHED STEEL | 45| 8 +Brand#21 |LARGE BURNISHED STEEL | 49| 8 +Brand#21 |LARGE BURNISHED TIN | 14| 8 +Brand#21 |LARGE BURNISHED TIN | 49| 8 +Brand#21 |LARGE PLATED BRASS | 19| 8 +Brand#21 |LARGE PLATED BRASS | 23| 8 +Brand#21 |LARGE PLATED NICKEL | 23| 8 +Brand#21 |LARGE PLATED STEEL | 3| 8 +Brand#21 |LARGE PLATED STEEL | 19| 8 +Brand#21 |LARGE PLATED STEEL | 45| 8 +Brand#21 |LARGE PLATED TIN | 9| 8 +Brand#21 |LARGE PLATED TIN | 23| 8 +Brand#21 |LARGE POLISHED BRASS | 36| 8 +Brand#21 |LARGE POLISHED BRASS | 49| 8 +Brand#21 |LARGE POLISHED COPPER | 23| 8 +Brand#21 |LARGE POLISHED NICKEL | 3| 8 +Brand#21 |LARGE POLISHED NICKEL | 23| 8 +Brand#21 |LARGE POLISHED NICKEL | 45| 8 +Brand#21 |LARGE POLISHED STEEL | 3| 8 +Brand#21 |LARGE POLISHED STEEL | 9| 8 +Brand#21 |LARGE POLISHED STEEL | 23| 8 +Brand#21 |LARGE POLISHED TIN | 3| 8 +Brand#21 |LARGE POLISHED TIN | 19| 8 +Brand#21 |LARGE POLISHED TIN | 45| 8 +Brand#21 |MEDIUM ANODIZED BRASS | 3| 8 +Brand#21 |MEDIUM ANODIZED BRASS | 14| 8 +Brand#21 |MEDIUM ANODIZED BRASS | 23| 8 +Brand#21 |MEDIUM ANODIZED NICKEL | 36| 8 +Brand#21 |MEDIUM ANODIZED TIN | 9| 8 +Brand#21 |MEDIUM ANODIZED TIN | 14| 8 +Brand#21 |MEDIUM ANODIZED TIN | 23| 8 +Brand#21 |MEDIUM ANODIZED TIN | 45| 8 +Brand#21 |MEDIUM BRUSHED BRASS | 45| 8 +Brand#21 |MEDIUM BRUSHED BRASS | 49| 8 +Brand#21 |MEDIUM BRUSHED COPPER | 3| 8 +Brand#21 |MEDIUM BRUSHED COPPER | 14| 8 +Brand#21 |MEDIUM BRUSHED NICKEL | 14| 8 +Brand#21 |MEDIUM BRUSHED STEEL | 23| 8 +Brand#21 |MEDIUM BRUSHED STEEL | 45| 8 +Brand#21 |MEDIUM BURNISHED BRASS | 36| 8 +Brand#21 |MEDIUM BURNISHED NICKEL | 14| 8 +Brand#21 |MEDIUM BURNISHED STEEL | 23| 8 +Brand#21 |MEDIUM PLATED BRASS | 45| 8 +Brand#21 |MEDIUM PLATED COPPER | 23| 8 +Brand#21 |MEDIUM PLATED COPPER | 49| 8 +Brand#21 |MEDIUM PLATED TIN | 36| 8 +Brand#21 |PROMO ANODIZED BRASS | 14| 8 +Brand#21 |PROMO ANODIZED BRASS | 19| 8 +Brand#21 |PROMO ANODIZED COPPER | 14| 8 +Brand#21 |PROMO ANODIZED COPPER | 23| 8 +Brand#21 |PROMO ANODIZED COPPER | 45| 8 +Brand#21 |PROMO ANODIZED NICKEL | 14| 8 +Brand#21 |PROMO ANODIZED NICKEL | 23| 8 +Brand#21 |PROMO ANODIZED STEEL | 3| 8 +Brand#21 |PROMO ANODIZED STEEL | 9| 8 +Brand#21 |PROMO ANODIZED TIN | 23| 8 +Brand#21 |PROMO BRUSHED BRASS | 23| 8 +Brand#21 |PROMO BRUSHED BRASS | 45| 8 +Brand#21 |PROMO BRUSHED BRASS | 49| 8 +Brand#21 |PROMO BRUSHED NICKEL | 45| 8 +Brand#21 |PROMO BRUSHED STEEL | 23| 8 +Brand#21 |PROMO BURNISHED BRASS | 19| 8 +Brand#21 |PROMO BURNISHED BRASS | 23| 8 +Brand#21 |PROMO BURNISHED COPPER | 9| 8 +Brand#21 |PROMO BURNISHED COPPER | 49| 8 +Brand#21 |PROMO BURNISHED NICKEL | 19| 8 +Brand#21 |PROMO BURNISHED NICKEL | 23| 8 +Brand#21 |PROMO BURNISHED STEEL | 23| 8 +Brand#21 |PROMO PLATED BRASS | 14| 8 +Brand#21 |PROMO PLATED BRASS | 23| 8 +Brand#21 |PROMO PLATED COPPER | 3| 8 +Brand#21 |PROMO PLATED NICKEL | 3| 8 +Brand#21 |PROMO PLATED STEEL | 9| 8 +Brand#21 |PROMO PLATED STEEL | 23| 8 +Brand#21 |PROMO PLATED STEEL | 49| 8 +Brand#21 |PROMO PLATED TIN | 3| 8 +Brand#21 |PROMO POLISHED COPPER | 14| 8 +Brand#21 |PROMO POLISHED STEEL | 19| 8 +Brand#21 |PROMO POLISHED STEEL | 23| 8 +Brand#21 |PROMO POLISHED STEEL | 45| 8 +Brand#21 |SMALL ANODIZED BRASS | 45| 8 +Brand#21 |SMALL ANODIZED COPPER | 49| 8 +Brand#21 |SMALL ANODIZED NICKEL | 9| 8 +Brand#21 |SMALL ANODIZED NICKEL | 14| 8 +Brand#21 |SMALL ANODIZED NICKEL | 19| 8 +Brand#21 |SMALL ANODIZED NICKEL | 23| 8 +Brand#21 |SMALL ANODIZED NICKEL | 45| 8 +Brand#21 |SMALL ANODIZED STEEL | 49| 8 +Brand#21 |SMALL ANODIZED TIN | 9| 8 +Brand#21 |SMALL ANODIZED TIN | 14| 8 +Brand#21 |SMALL ANODIZED TIN | 19| 8 +Brand#21 |SMALL ANODIZED TIN | 36| 8 +Brand#21 |SMALL ANODIZED TIN | 49| 8 +Brand#21 |SMALL BRUSHED BRASS | 36| 8 +Brand#21 |SMALL BRUSHED NICKEL | 36| 8 +Brand#21 |SMALL BRUSHED STEEL | 14| 8 +Brand#21 |SMALL BRUSHED TIN | 45| 8 +Brand#21 |SMALL BURNISHED BRASS | 36| 8 +Brand#21 |SMALL BURNISHED COPPER | 3| 8 +Brand#21 |SMALL BURNISHED COPPER | 14| 8 +Brand#21 |SMALL BURNISHED COPPER | 19| 8 +Brand#21 |SMALL BURNISHED COPPER | 45| 8 +Brand#21 |SMALL BURNISHED NICKEL | 14| 8 +Brand#21 |SMALL BURNISHED STEEL | 49| 8 +Brand#21 |SMALL PLATED BRASS | 14| 8 +Brand#21 |SMALL PLATED COPPER | 3| 8 +Brand#21 |SMALL PLATED COPPER | 9| 8 +Brand#21 |SMALL PLATED COPPER | 45| 8 +Brand#21 |SMALL PLATED NICKEL | 3| 8 +Brand#21 |SMALL PLATED STEEL | 3| 8 +Brand#21 |SMALL PLATED STEEL | 9| 8 +Brand#21 |SMALL PLATED TIN | 19| 8 +Brand#21 |SMALL POLISHED STEEL | 36| 8 +Brand#21 |STANDARD ANODIZED BRASS | 3| 8 +Brand#21 |STANDARD ANODIZED BRASS | 36| 8 +Brand#21 |STANDARD ANODIZED BRASS | 45| 8 +Brand#21 |STANDARD ANODIZED COPPER | 23| 8 +Brand#21 |STANDARD ANODIZED STEEL | 36| 8 +Brand#21 |STANDARD ANODIZED STEEL | 49| 8 +Brand#21 |STANDARD ANODIZED TIN | 9| 8 +Brand#21 |STANDARD ANODIZED TIN | 49| 8 +Brand#21 |STANDARD BRUSHED BRASS | 14| 8 +Brand#21 |STANDARD BRUSHED BRASS | 45| 8 +Brand#21 |STANDARD BRUSHED COPPER | 23| 8 +Brand#21 |STANDARD BRUSHED NICKEL | 19| 8 +Brand#21 |STANDARD BRUSHED STEEL | 9| 8 +Brand#21 |STANDARD BRUSHED STEEL | 49| 8 +Brand#21 |STANDARD BRUSHED TIN | 23| 8 +Brand#21 |STANDARD BRUSHED TIN | 36| 8 +Brand#21 |STANDARD BRUSHED TIN | 45| 8 +Brand#21 |STANDARD BURNISHED BRASS | 36| 8 +Brand#21 |STANDARD BURNISHED COPPER| 9| 8 +Brand#21 |STANDARD BURNISHED COPPER| 19| 8 +Brand#21 |STANDARD BURNISHED NICKEL| 9| 8 +Brand#21 |STANDARD BURNISHED NICKEL| 23| 8 +Brand#21 |STANDARD BURNISHED NICKEL| 45| 8 +Brand#21 |STANDARD BURNISHED STEEL | 14| 8 +Brand#21 |STANDARD BURNISHED STEEL | 36| 8 +Brand#21 |STANDARD BURNISHED STEEL | 45| 8 +Brand#21 |STANDARD BURNISHED TIN | 19| 8 +Brand#21 |STANDARD BURNISHED TIN | 36| 8 +Brand#21 |STANDARD PLATED BRASS | 14| 8 +Brand#21 |STANDARD PLATED BRASS | 19| 8 +Brand#21 |STANDARD PLATED BRASS | 49| 8 +Brand#21 |STANDARD PLATED COPPER | 19| 8 +Brand#21 |STANDARD PLATED COPPER | 23| 8 +Brand#21 |STANDARD PLATED COPPER | 49| 8 +Brand#21 |STANDARD PLATED NICKEL | 3| 8 +Brand#21 |STANDARD PLATED NICKEL | 45| 8 +Brand#21 |STANDARD PLATED TIN | 14| 8 +Brand#21 |STANDARD PLATED TIN | 49| 8 +Brand#21 |STANDARD POLISHED BRASS | 9| 8 +Brand#21 |STANDARD POLISHED BRASS | 19| 8 +Brand#21 |STANDARD POLISHED BRASS | 45| 8 +Brand#21 |STANDARD POLISHED COPPER | 23| 8 +Brand#21 |STANDARD POLISHED NICKEL | 14| 8 +Brand#21 |STANDARD POLISHED NICKEL | 23| 8 +Brand#21 |STANDARD POLISHED STEEL | 19| 8 +Brand#21 |STANDARD POLISHED TIN | 36| 8 +Brand#22 |ECONOMY ANODIZED BRASS | 3| 8 +Brand#22 |ECONOMY ANODIZED BRASS | 36| 8 +Brand#22 |ECONOMY ANODIZED COPPER | 14| 8 +Brand#22 |ECONOMY ANODIZED STEEL | 23| 8 +Brand#22 |ECONOMY ANODIZED TIN | 36| 8 +Brand#22 |ECONOMY BRUSHED BRASS | 14| 8 +Brand#22 |ECONOMY BRUSHED COPPER | 3| 8 +Brand#22 |ECONOMY BRUSHED COPPER | 9| 8 +Brand#22 |ECONOMY BRUSHED COPPER | 23| 8 +Brand#22 |ECONOMY BRUSHED NICKEL | 3| 8 +Brand#22 |ECONOMY BRUSHED NICKEL | 9| 8 +Brand#22 |ECONOMY BRUSHED NICKEL | 14| 8 +Brand#22 |ECONOMY BRUSHED NICKEL | 45| 8 +Brand#22 |ECONOMY BRUSHED STEEL | 49| 8 +Brand#22 |ECONOMY BRUSHED TIN | 45| 8 +Brand#22 |ECONOMY BURNISHED NICKEL | 3| 8 +Brand#22 |ECONOMY BURNISHED NICKEL | 49| 8 +Brand#22 |ECONOMY BURNISHED STEEL | 9| 8 +Brand#22 |ECONOMY BURNISHED TIN | 23| 8 +Brand#22 |ECONOMY PLATED BRASS | 3| 8 +Brand#22 |ECONOMY PLATED STEEL | 3| 8 +Brand#22 |ECONOMY PLATED TIN | 9| 8 +Brand#22 |ECONOMY POLISHED COPPER | 49| 8 +Brand#22 |ECONOMY POLISHED NICKEL | 45| 8 +Brand#22 |ECONOMY POLISHED STEEL | 9| 8 +Brand#22 |ECONOMY POLISHED STEEL | 14| 8 +Brand#22 |ECONOMY POLISHED TIN | 14| 8 +Brand#22 |ECONOMY POLISHED TIN | 45| 8 +Brand#22 |LARGE ANODIZED BRASS | 14| 8 +Brand#22 |LARGE ANODIZED NICKEL | 23| 8 +Brand#22 |LARGE ANODIZED TIN | 19| 8 +Brand#22 |LARGE ANODIZED TIN | 23| 8 +Brand#22 |LARGE ANODIZED TIN | 49| 8 +Brand#22 |LARGE BRUSHED BRASS | 23| 8 +Brand#22 |LARGE BRUSHED BRASS | 36| 8 +Brand#22 |LARGE BRUSHED COPPER | 9| 8 +Brand#22 |LARGE BRUSHED NICKEL | 23| 8 +Brand#22 |LARGE BRUSHED NICKEL | 45| 8 +Brand#22 |LARGE BRUSHED STEEL | 23| 8 +Brand#22 |LARGE BURNISHED BRASS | 23| 8 +Brand#22 |LARGE BURNISHED COPPER | 9| 8 +Brand#22 |LARGE BURNISHED COPPER | 19| 8 +Brand#22 |LARGE BURNISHED NICKEL | 36| 8 +Brand#22 |LARGE BURNISHED NICKEL | 49| 8 +Brand#22 |LARGE BURNISHED STEEL | 9| 8 +Brand#22 |LARGE BURNISHED TIN | 45| 8 +Brand#22 |LARGE PLATED BRASS | 45| 8 +Brand#22 |LARGE PLATED COPPER | 45| 8 +Brand#22 |LARGE PLATED NICKEL | 9| 8 +Brand#22 |LARGE PLATED NICKEL | 19| 8 +Brand#22 |LARGE PLATED NICKEL | 23| 8 +Brand#22 |LARGE PLATED STEEL | 14| 8 +Brand#22 |LARGE PLATED STEEL | 19| 8 +Brand#22 |LARGE PLATED STEEL | 23| 8 +Brand#22 |LARGE PLATED TIN | 14| 8 +Brand#22 |LARGE POLISHED BRASS | 23| 8 +Brand#22 |LARGE POLISHED BRASS | 45| 8 +Brand#22 |LARGE POLISHED BRASS | 49| 8 +Brand#22 |LARGE POLISHED COPPER | 9| 8 +Brand#22 |LARGE POLISHED COPPER | 49| 8 +Brand#22 |LARGE POLISHED NICKEL | 45| 8 +Brand#22 |LARGE POLISHED NICKEL | 49| 8 +Brand#22 |LARGE POLISHED STEEL | 49| 8 +Brand#22 |LARGE POLISHED TIN | 49| 8 +Brand#22 |MEDIUM ANODIZED BRASS | 14| 8 +Brand#22 |MEDIUM ANODIZED BRASS | 49| 8 +Brand#22 |MEDIUM ANODIZED COPPER | 3| 8 +Brand#22 |MEDIUM ANODIZED COPPER | 14| 8 +Brand#22 |MEDIUM ANODIZED COPPER | 45| 8 +Brand#22 |MEDIUM ANODIZED NICKEL | 3| 8 +Brand#22 |MEDIUM ANODIZED NICKEL | 36| 8 +Brand#22 |MEDIUM ANODIZED STEEL | 36| 8 +Brand#22 |MEDIUM ANODIZED TIN | 45| 8 +Brand#22 |MEDIUM BRUSHED BRASS | 45| 8 +Brand#22 |MEDIUM BRUSHED BRASS | 49| 8 +Brand#22 |MEDIUM BRUSHED COPPER | 3| 8 +Brand#22 |MEDIUM BRUSHED COPPER | 45| 8 +Brand#22 |MEDIUM BRUSHED COPPER | 49| 8 +Brand#22 |MEDIUM BRUSHED STEEL | 36| 8 +Brand#22 |MEDIUM BURNISHED BRASS | 9| 8 +Brand#22 |MEDIUM BURNISHED BRASS | 45| 8 +Brand#22 |MEDIUM BURNISHED BRASS | 49| 8 +Brand#22 |MEDIUM BURNISHED COPPER | 14| 8 +Brand#22 |MEDIUM BURNISHED COPPER | 23| 8 +Brand#22 |MEDIUM BURNISHED COPPER | 36| 8 +Brand#22 |MEDIUM BURNISHED COPPER | 45| 8 +Brand#22 |MEDIUM BURNISHED COPPER | 49| 8 +Brand#22 |MEDIUM BURNISHED NICKEL | 3| 8 +Brand#22 |MEDIUM BURNISHED NICKEL | 14| 8 +Brand#22 |MEDIUM BURNISHED STEEL | 3| 8 +Brand#22 |MEDIUM BURNISHED STEEL | 9| 8 +Brand#22 |MEDIUM BURNISHED STEEL | 45| 8 +Brand#22 |MEDIUM BURNISHED TIN | 3| 8 +Brand#22 |MEDIUM BURNISHED TIN | 9| 8 +Brand#22 |MEDIUM BURNISHED TIN | 19| 8 +Brand#22 |MEDIUM BURNISHED TIN | 49| 8 +Brand#22 |MEDIUM PLATED COPPER | 19| 8 +Brand#22 |MEDIUM PLATED NICKEL | 3| 8 +Brand#22 |MEDIUM PLATED NICKEL | 45| 8 +Brand#22 |MEDIUM PLATED NICKEL | 49| 8 +Brand#22 |MEDIUM PLATED STEEL | 3| 8 +Brand#22 |MEDIUM PLATED STEEL | 9| 8 +Brand#22 |MEDIUM PLATED STEEL | 19| 8 +Brand#22 |MEDIUM PLATED TIN | 49| 8 +Brand#22 |PROMO ANODIZED BRASS | 3| 8 +Brand#22 |PROMO ANODIZED BRASS | 9| 8 +Brand#22 |PROMO ANODIZED BRASS | 45| 8 +Brand#22 |PROMO ANODIZED COPPER | 3| 8 +Brand#22 |PROMO ANODIZED COPPER | 9| 8 +Brand#22 |PROMO ANODIZED COPPER | 23| 8 +Brand#22 |PROMO ANODIZED NICKEL | 9| 8 +Brand#22 |PROMO ANODIZED NICKEL | 45| 8 +Brand#22 |PROMO ANODIZED STEEL | 19| 8 +Brand#22 |PROMO ANODIZED TIN | 14| 8 +Brand#22 |PROMO ANODIZED TIN | 23| 8 +Brand#22 |PROMO BRUSHED BRASS | 3| 8 +Brand#22 |PROMO BRUSHED BRASS | 19| 8 +Brand#22 |PROMO BRUSHED BRASS | 23| 8 +Brand#22 |PROMO BRUSHED BRASS | 36| 8 +Brand#22 |PROMO BRUSHED NICKEL | 19| 8 +Brand#22 |PROMO BRUSHED NICKEL | 45| 8 +Brand#22 |PROMO BRUSHED STEEL | 36| 8 +Brand#22 |PROMO BRUSHED TIN | 3| 8 +Brand#22 |PROMO BURNISHED COPPER | 14| 8 +Brand#22 |PROMO BURNISHED COPPER | 36| 8 +Brand#22 |PROMO BURNISHED STEEL | 14| 8 +Brand#22 |PROMO BURNISHED STEEL | 36| 8 +Brand#22 |PROMO BURNISHED TIN | 3| 8 +Brand#22 |PROMO PLATED BRASS | 36| 8 +Brand#22 |PROMO PLATED BRASS | 45| 8 +Brand#22 |PROMO PLATED COPPER | 36| 8 +Brand#22 |PROMO PLATED NICKEL | 9| 8 +Brand#22 |PROMO PLATED NICKEL | 19| 8 +Brand#22 |PROMO PLATED NICKEL | 49| 8 +Brand#22 |PROMO PLATED STEEL | 45| 8 +Brand#22 |PROMO PLATED TIN | 3| 8 +Brand#22 |PROMO PLATED TIN | 23| 8 +Brand#22 |PROMO POLISHED BRASS | 3| 8 +Brand#22 |PROMO POLISHED BRASS | 9| 8 +Brand#22 |PROMO POLISHED NICKEL | 9| 8 +Brand#22 |PROMO POLISHED STEEL | 14| 8 +Brand#22 |PROMO POLISHED TIN | 3| 8 +Brand#22 |PROMO POLISHED TIN | 23| 8 +Brand#22 |PROMO POLISHED TIN | 49| 8 +Brand#22 |SMALL ANODIZED BRASS | 14| 8 +Brand#22 |SMALL ANODIZED COPPER | 3| 8 +Brand#22 |SMALL ANODIZED NICKEL | 3| 8 +Brand#22 |SMALL ANODIZED NICKEL | 36| 8 +Brand#22 |SMALL ANODIZED STEEL | 23| 8 +Brand#22 |SMALL BRUSHED BRASS | 36| 8 +Brand#22 |SMALL BRUSHED TIN | 19| 8 +Brand#22 |SMALL BRUSHED TIN | 23| 8 +Brand#22 |SMALL BURNISHED BRASS | 3| 8 +Brand#22 |SMALL BURNISHED NICKEL | 3| 8 +Brand#22 |SMALL BURNISHED NICKEL | 49| 8 +Brand#22 |SMALL BURNISHED STEEL | 9| 8 +Brand#22 |SMALL BURNISHED STEEL | 23| 8 +Brand#22 |SMALL BURNISHED STEEL | 49| 8 +Brand#22 |SMALL BURNISHED TIN | 45| 8 +Brand#22 |SMALL PLATED BRASS | 23| 8 +Brand#22 |SMALL PLATED COPPER | 14| 8 +Brand#22 |SMALL PLATED COPPER | 36| 8 +Brand#22 |SMALL PLATED NICKEL | 3| 8 +Brand#22 |SMALL PLATED NICKEL | 19| 8 +Brand#22 |SMALL PLATED STEEL | 3| 8 +Brand#22 |SMALL PLATED STEEL | 45| 8 +Brand#22 |SMALL POLISHED COPPER | 9| 8 +Brand#22 |SMALL POLISHED COPPER | 23| 8 +Brand#22 |SMALL POLISHED NICKEL | 14| 8 +Brand#22 |SMALL POLISHED NICKEL | 19| 8 +Brand#22 |SMALL POLISHED STEEL | 14| 8 +Brand#22 |SMALL POLISHED TIN | 14| 8 +Brand#22 |SMALL POLISHED TIN | 19| 8 +Brand#22 |STANDARD ANODIZED BRASS | 14| 8 +Brand#22 |STANDARD ANODIZED BRASS | 19| 8 +Brand#22 |STANDARD ANODIZED COPPER | 9| 8 +Brand#22 |STANDARD ANODIZED COPPER | 23| 8 +Brand#22 |STANDARD ANODIZED COPPER | 36| 8 +Brand#22 |STANDARD ANODIZED NICKEL | 3| 8 +Brand#22 |STANDARD ANODIZED NICKEL | 14| 8 +Brand#22 |STANDARD ANODIZED TIN | 3| 8 +Brand#22 |STANDARD ANODIZED TIN | 49| 8 +Brand#22 |STANDARD BRUSHED BRASS | 14| 8 +Brand#22 |STANDARD BRUSHED BRASS | 19| 8 +Brand#22 |STANDARD BRUSHED COPPER | 19| 8 +Brand#22 |STANDARD BRUSHED COPPER | 36| 8 +Brand#22 |STANDARD BRUSHED NICKEL | 9| 8 +Brand#22 |STANDARD BRUSHED NICKEL | 19| 8 +Brand#22 |STANDARD BRUSHED NICKEL | 23| 8 +Brand#22 |STANDARD BRUSHED STEEL | 9| 8 +Brand#22 |STANDARD BRUSHED STEEL | 14| 8 +Brand#22 |STANDARD BRUSHED STEEL | 19| 8 +Brand#22 |STANDARD BRUSHED STEEL | 36| 8 +Brand#22 |STANDARD BRUSHED TIN | 3| 8 +Brand#22 |STANDARD BRUSHED TIN | 45| 8 +Brand#22 |STANDARD BURNISHED BRASS | 14| 8 +Brand#22 |STANDARD BURNISHED NICKEL| 14| 8 +Brand#22 |STANDARD BURNISHED NICKEL| 45| 8 +Brand#22 |STANDARD BURNISHED STEEL | 36| 8 +Brand#22 |STANDARD BURNISHED STEEL | 45| 8 +Brand#22 |STANDARD PLATED BRASS | 14| 8 +Brand#22 |STANDARD PLATED COPPER | 19| 8 +Brand#22 |STANDARD PLATED NICKEL | 19| 8 +Brand#22 |STANDARD PLATED NICKEL | 36| 8 +Brand#22 |STANDARD PLATED STEEL | 9| 8 +Brand#22 |STANDARD PLATED STEEL | 14| 8 +Brand#22 |STANDARD POLISHED BRASS | 14| 8 +Brand#22 |STANDARD POLISHED NICKEL | 23| 8 +Brand#22 |STANDARD POLISHED NICKEL | 45| 8 +Brand#22 |STANDARD POLISHED STEEL | 36| 8 +Brand#22 |STANDARD POLISHED STEEL | 45| 8 +Brand#22 |STANDARD POLISHED STEEL | 49| 8 +Brand#22 |STANDARD POLISHED TIN | 9| 8 +Brand#22 |STANDARD POLISHED TIN | 19| 8 +Brand#23 |ECONOMY ANODIZED COPPER | 3| 8 +Brand#23 |ECONOMY ANODIZED NICKEL | 3| 8 +Brand#23 |ECONOMY ANODIZED NICKEL | 49| 8 +Brand#23 |ECONOMY ANODIZED STEEL | 14| 8 +Brand#23 |ECONOMY ANODIZED TIN | 14| 8 +Brand#23 |ECONOMY ANODIZED TIN | 19| 8 +Brand#23 |ECONOMY ANODIZED TIN | 45| 8 +Brand#23 |ECONOMY ANODIZED TIN | 49| 8 +Brand#23 |ECONOMY BRUSHED BRASS | 3| 8 +Brand#23 |ECONOMY BRUSHED BRASS | 36| 8 +Brand#23 |ECONOMY BRUSHED COPPER | 9| 8 +Brand#23 |ECONOMY BRUSHED TIN | 9| 8 +Brand#23 |ECONOMY BRUSHED TIN | 19| 8 +Brand#23 |ECONOMY BRUSHED TIN | 23| 8 +Brand#23 |ECONOMY BURNISHED BRASS | 9| 8 +Brand#23 |ECONOMY BURNISHED BRASS | 14| 8 +Brand#23 |ECONOMY BURNISHED COPPER | 14| 8 +Brand#23 |ECONOMY BURNISHED NICKEL | 9| 8 +Brand#23 |ECONOMY BURNISHED NICKEL | 23| 8 +Brand#23 |ECONOMY BURNISHED STEEL | 45| 8 +Brand#23 |ECONOMY PLATED BRASS | 23| 8 +Brand#23 |ECONOMY PLATED COPPER | 23| 8 +Brand#23 |ECONOMY PLATED NICKEL | 3| 8 +Brand#23 |ECONOMY PLATED NICKEL | 23| 8 +Brand#23 |ECONOMY PLATED STEEL | 19| 8 +Brand#23 |ECONOMY PLATED TIN | 3| 8 +Brand#23 |ECONOMY PLATED TIN | 19| 8 +Brand#23 |ECONOMY PLATED TIN | 23| 8 +Brand#23 |ECONOMY PLATED TIN | 36| 8 +Brand#23 |ECONOMY POLISHED BRASS | 36| 8 +Brand#23 |ECONOMY POLISHED COPPER | 3| 8 +Brand#23 |ECONOMY POLISHED COPPER | 14| 8 +Brand#23 |ECONOMY POLISHED COPPER | 49| 8 +Brand#23 |ECONOMY POLISHED STEEL | 3| 8 +Brand#23 |ECONOMY POLISHED STEEL | 23| 8 +Brand#23 |ECONOMY POLISHED STEEL | 36| 8 +Brand#23 |ECONOMY POLISHED TIN | 45| 8 +Brand#23 |LARGE ANODIZED BRASS | 9| 8 +Brand#23 |LARGE ANODIZED BRASS | 14| 8 +Brand#23 |LARGE ANODIZED COPPER | 9| 8 +Brand#23 |LARGE ANODIZED COPPER | 45| 8 +Brand#23 |LARGE ANODIZED COPPER | 49| 8 +Brand#23 |LARGE ANODIZED STEEL | 19| 8 +Brand#23 |LARGE ANODIZED STEEL | 36| 8 +Brand#23 |LARGE BRUSHED BRASS | 9| 8 +Brand#23 |LARGE BRUSHED NICKEL | 3| 8 +Brand#23 |LARGE BRUSHED NICKEL | 45| 8 +Brand#23 |LARGE BURNISHED COPPER | 3| 8 +Brand#23 |LARGE BURNISHED COPPER | 9| 8 +Brand#23 |LARGE BURNISHED NICKEL | 9| 8 +Brand#23 |LARGE BURNISHED NICKEL | 19| 8 +Brand#23 |LARGE BURNISHED STEEL | 3| 8 +Brand#23 |LARGE BURNISHED STEEL | 9| 8 +Brand#23 |LARGE BURNISHED STEEL | 14| 8 +Brand#23 |LARGE BURNISHED STEEL | 49| 8 +Brand#23 |LARGE PLATED BRASS | 3| 8 +Brand#23 |LARGE PLATED BRASS | 9| 8 +Brand#23 |LARGE PLATED BRASS | 14| 8 +Brand#23 |LARGE PLATED COPPER | 19| 8 +Brand#23 |LARGE PLATED NICKEL | 23| 8 +Brand#23 |LARGE PLATED NICKEL | 49| 8 +Brand#23 |LARGE PLATED STEEL | 3| 8 +Brand#23 |LARGE PLATED STEEL | 14| 8 +Brand#23 |LARGE PLATED STEEL | 45| 8 +Brand#23 |LARGE POLISHED NICKEL | 3| 8 +Brand#23 |LARGE POLISHED NICKEL | 49| 8 +Brand#23 |LARGE POLISHED TIN | 9| 8 +Brand#23 |LARGE POLISHED TIN | 14| 8 +Brand#23 |LARGE POLISHED TIN | 36| 8 +Brand#23 |LARGE POLISHED TIN | 49| 8 +Brand#23 |MEDIUM ANODIZED COPPER | 45| 8 +Brand#23 |MEDIUM ANODIZED NICKEL | 3| 8 +Brand#23 |MEDIUM ANODIZED NICKEL | 14| 8 +Brand#23 |MEDIUM ANODIZED STEEL | 3| 8 +Brand#23 |MEDIUM ANODIZED STEEL | 19| 8 +Brand#23 |MEDIUM ANODIZED STEEL | 49| 8 +Brand#23 |MEDIUM ANODIZED TIN | 14| 8 +Brand#23 |MEDIUM ANODIZED TIN | 23| 8 +Brand#23 |MEDIUM ANODIZED TIN | 45| 8 +Brand#23 |MEDIUM BRUSHED BRASS | 45| 8 +Brand#23 |MEDIUM BRUSHED COPPER | 19| 8 +Brand#23 |MEDIUM BRUSHED COPPER | 23| 8 +Brand#23 |MEDIUM BRUSHED NICKEL | 3| 8 +Brand#23 |MEDIUM BRUSHED NICKEL | 14| 8 +Brand#23 |MEDIUM BRUSHED TIN | 14| 8 +Brand#23 |MEDIUM BRUSHED TIN | 45| 8 +Brand#23 |MEDIUM BURNISHED BRASS | 3| 8 +Brand#23 |MEDIUM BURNISHED BRASS | 9| 8 +Brand#23 |MEDIUM BURNISHED BRASS | 14| 8 +Brand#23 |MEDIUM BURNISHED COPPER | 14| 8 +Brand#23 |MEDIUM BURNISHED COPPER | 23| 8 +Brand#23 |MEDIUM BURNISHED COPPER | 36| 8 +Brand#23 |MEDIUM BURNISHED STEEL | 9| 8 +Brand#23 |MEDIUM BURNISHED STEEL | 14| 8 +Brand#23 |MEDIUM BURNISHED TIN | 9| 8 +Brand#23 |MEDIUM BURNISHED TIN | 14| 8 +Brand#23 |MEDIUM PLATED BRASS | 9| 8 +Brand#23 |MEDIUM PLATED BRASS | 14| 8 +Brand#23 |MEDIUM PLATED BRASS | 19| 8 +Brand#23 |MEDIUM PLATED NICKEL | 3| 8 +Brand#23 |MEDIUM PLATED NICKEL | 9| 8 +Brand#23 |MEDIUM PLATED NICKEL | 23| 8 +Brand#23 |MEDIUM PLATED NICKEL | 36| 8 +Brand#23 |MEDIUM PLATED STEEL | 23| 8 +Brand#23 |MEDIUM PLATED TIN | 49| 8 +Brand#23 |PROMO ANODIZED COPPER | 3| 8 +Brand#23 |PROMO ANODIZED COPPER | 36| 8 +Brand#23 |PROMO ANODIZED COPPER | 45| 8 +Brand#23 |PROMO ANODIZED NICKEL | 45| 8 +Brand#23 |PROMO ANODIZED TIN | 14| 8 +Brand#23 |PROMO BRUSHED BRASS | 19| 8 +Brand#23 |PROMO BRUSHED BRASS | 36| 8 +Brand#23 |PROMO BRUSHED COPPER | 14| 8 +Brand#23 |PROMO BRUSHED NICKEL | 3| 8 +Brand#23 |PROMO BRUSHED NICKEL | 49| 8 +Brand#23 |PROMO BRUSHED TIN | 9| 8 +Brand#23 |PROMO BRUSHED TIN | 49| 8 +Brand#23 |PROMO BURNISHED BRASS | 14| 8 +Brand#23 |PROMO BURNISHED BRASS | 45| 8 +Brand#23 |PROMO BURNISHED COPPER | 49| 8 +Brand#23 |PROMO BURNISHED NICKEL | 9| 8 +Brand#23 |PROMO BURNISHED NICKEL | 23| 8 +Brand#23 |PROMO BURNISHED STEEL | 14| 8 +Brand#23 |PROMO BURNISHED TIN | 14| 8 +Brand#23 |PROMO BURNISHED TIN | 49| 8 +Brand#23 |PROMO PLATED BRASS | 14| 8 +Brand#23 |PROMO PLATED COPPER | 14| 8 +Brand#23 |PROMO PLATED NICKEL | 23| 8 +Brand#23 |PROMO PLATED NICKEL | 45| 8 +Brand#23 |PROMO PLATED STEEL | 3| 8 +Brand#23 |PROMO PLATED STEEL | 49| 8 +Brand#23 |PROMO PLATED TIN | 3| 8 +Brand#23 |PROMO PLATED TIN | 23| 8 +Brand#23 |PROMO PLATED TIN | 36| 8 +Brand#23 |PROMO PLATED TIN | 45| 8 +Brand#23 |PROMO POLISHED BRASS | 14| 8 +Brand#23 |PROMO POLISHED COPPER | 23| 8 +Brand#23 |PROMO POLISHED NICKEL | 19| 8 +Brand#23 |PROMO POLISHED NICKEL | 23| 8 +Brand#23 |PROMO POLISHED NICKEL | 36| 8 +Brand#23 |PROMO POLISHED STEEL | 3| 8 +Brand#23 |PROMO POLISHED STEEL | 14| 8 +Brand#23 |PROMO POLISHED TIN | 23| 8 +Brand#23 |PROMO POLISHED TIN | 49| 8 +Brand#23 |SMALL ANODIZED BRASS | 36| 8 +Brand#23 |SMALL ANODIZED BRASS | 49| 8 +Brand#23 |SMALL ANODIZED COPPER | 14| 8 +Brand#23 |SMALL ANODIZED STEEL | 14| 8 +Brand#23 |SMALL ANODIZED STEEL | 23| 8 +Brand#23 |SMALL ANODIZED TIN | 3| 8 +Brand#23 |SMALL BRUSHED BRASS | 49| 8 +Brand#23 |SMALL BRUSHED COPPER | 23| 8 +Brand#23 |SMALL BRUSHED COPPER | 45| 8 +Brand#23 |SMALL BRUSHED NICKEL | 3| 8 +Brand#23 |SMALL BRUSHED STEEL | 23| 8 +Brand#23 |SMALL BRUSHED STEEL | 45| 8 +Brand#23 |SMALL BRUSHED STEEL | 49| 8 +Brand#23 |SMALL BRUSHED TIN | 3| 8 +Brand#23 |SMALL BRUSHED TIN | 14| 8 +Brand#23 |SMALL BURNISHED BRASS | 3| 8 +Brand#23 |SMALL BURNISHED BRASS | 9| 8 +Brand#23 |SMALL BURNISHED BRASS | 49| 8 +Brand#23 |SMALL BURNISHED COPPER | 45| 8 +Brand#23 |SMALL BURNISHED NICKEL | 3| 8 +Brand#23 |SMALL BURNISHED NICKEL | 49| 8 +Brand#23 |SMALL BURNISHED STEEL | 19| 8 +Brand#23 |SMALL BURNISHED STEEL | 49| 8 +Brand#23 |SMALL PLATED BRASS | 3| 8 +Brand#23 |SMALL PLATED BRASS | 45| 8 +Brand#23 |SMALL PLATED COPPER | 14| 8 +Brand#23 |SMALL PLATED COPPER | 36| 8 +Brand#23 |SMALL PLATED COPPER | 45| 8 +Brand#23 |SMALL PLATED NICKEL | 23| 8 +Brand#23 |SMALL PLATED STEEL | 19| 8 +Brand#23 |SMALL PLATED STEEL | 36| 8 +Brand#23 |SMALL PLATED STEEL | 49| 8 +Brand#23 |SMALL PLATED TIN | 19| 8 +Brand#23 |SMALL PLATED TIN | 23| 8 +Brand#23 |SMALL PLATED TIN | 45| 8 +Brand#23 |SMALL PLATED TIN | 49| 8 +Brand#23 |SMALL POLISHED BRASS | 19| 8 +Brand#23 |SMALL POLISHED BRASS | 49| 8 +Brand#23 |SMALL POLISHED COPPER | 9| 8 +Brand#23 |SMALL POLISHED NICKEL | 3| 8 +Brand#23 |SMALL POLISHED NICKEL | 23| 8 +Brand#23 |SMALL POLISHED NICKEL | 49| 8 +Brand#23 |STANDARD ANODIZED BRASS | 19| 8 +Brand#23 |STANDARD ANODIZED COPPER | 19| 8 +Brand#23 |STANDARD ANODIZED COPPER | 23| 8 +Brand#23 |STANDARD ANODIZED NICKEL | 9| 8 +Brand#23 |STANDARD ANODIZED NICKEL | 23| 8 +Brand#23 |STANDARD ANODIZED STEEL | 9| 8 +Brand#23 |STANDARD ANODIZED STEEL | 14| 8 +Brand#23 |STANDARD ANODIZED STEEL | 45| 8 +Brand#23 |STANDARD ANODIZED STEEL | 49| 8 +Brand#23 |STANDARD BRUSHED BRASS | 19| 8 +Brand#23 |STANDARD BRUSHED BRASS | 36| 8 +Brand#23 |STANDARD BRUSHED COPPER | 36| 8 +Brand#23 |STANDARD BRUSHED NICKEL | 9| 8 +Brand#23 |STANDARD BRUSHED NICKEL | 19| 8 +Brand#23 |STANDARD BRUSHED STEEL | 14| 8 +Brand#23 |STANDARD BRUSHED STEEL | 49| 8 +Brand#23 |STANDARD BRUSHED TIN | 3| 8 +Brand#23 |STANDARD BRUSHED TIN | 36| 8 +Brand#23 |STANDARD BURNISHED BRASS | 45| 8 +Brand#23 |STANDARD BURNISHED COPPER| 3| 8 +Brand#23 |STANDARD BURNISHED COPPER| 36| 8 +Brand#23 |STANDARD BURNISHED NICKEL| 45| 8 +Brand#23 |STANDARD BURNISHED TIN | 49| 8 +Brand#23 |STANDARD PLATED BRASS | 23| 8 +Brand#23 |STANDARD PLATED COPPER | 3| 8 +Brand#23 |STANDARD PLATED COPPER | 14| 8 +Brand#23 |STANDARD PLATED COPPER | 23| 8 +Brand#23 |STANDARD PLATED COPPER | 36| 8 +Brand#23 |STANDARD PLATED STEEL | 3| 8 +Brand#23 |STANDARD PLATED STEEL | 19| 8 +Brand#23 |STANDARD PLATED STEEL | 36| 8 +Brand#23 |STANDARD PLATED STEEL | 49| 8 +Brand#23 |STANDARD PLATED TIN | 3| 8 +Brand#23 |STANDARD PLATED TIN | 23| 8 +Brand#23 |STANDARD POLISHED BRASS | 36| 8 +Brand#23 |STANDARD POLISHED BRASS | 49| 8 +Brand#23 |STANDARD POLISHED COPPER | 14| 8 +Brand#23 |STANDARD POLISHED COPPER | 45| 8 +Brand#23 |STANDARD POLISHED TIN | 3| 8 +Brand#24 |ECONOMY ANODIZED BRASS | 23| 8 +Brand#24 |ECONOMY ANODIZED COPPER | 9| 8 +Brand#24 |ECONOMY ANODIZED COPPER | 14| 8 +Brand#24 |ECONOMY ANODIZED NICKEL | 9| 8 +Brand#24 |ECONOMY ANODIZED NICKEL | 14| 8 +Brand#24 |ECONOMY ANODIZED NICKEL | 19| 8 +Brand#24 |ECONOMY ANODIZED NICKEL | 36| 8 +Brand#24 |ECONOMY ANODIZED STEEL | 3| 8 +Brand#24 |ECONOMY ANODIZED STEEL | 36| 8 +Brand#24 |ECONOMY ANODIZED TIN | 19| 8 +Brand#24 |ECONOMY ANODIZED TIN | 36| 8 +Brand#24 |ECONOMY BRUSHED BRASS | 19| 8 +Brand#24 |ECONOMY BRUSHED COPPER | 23| 8 +Brand#24 |ECONOMY BRUSHED NICKEL | 9| 8 +Brand#24 |ECONOMY BRUSHED NICKEL | 45| 8 +Brand#24 |ECONOMY BRUSHED NICKEL | 49| 8 +Brand#24 |ECONOMY BRUSHED STEEL | 36| 8 +Brand#24 |ECONOMY BRUSHED TIN | 9| 8 +Brand#24 |ECONOMY BRUSHED TIN | 14| 8 +Brand#24 |ECONOMY BRUSHED TIN | 36| 8 +Brand#24 |ECONOMY BURNISHED BRASS | 23| 8 +Brand#24 |ECONOMY BURNISHED BRASS | 49| 8 +Brand#24 |ECONOMY BURNISHED COPPER | 45| 8 +Brand#24 |ECONOMY BURNISHED NICKEL | 14| 8 +Brand#24 |ECONOMY BURNISHED NICKEL | 19| 8 +Brand#24 |ECONOMY BURNISHED STEEL | 9| 8 +Brand#24 |ECONOMY BURNISHED STEEL | 36| 8 +Brand#24 |ECONOMY BURNISHED STEEL | 49| 8 +Brand#24 |ECONOMY PLATED BRASS | 49| 8 +Brand#24 |ECONOMY PLATED COPPER | 36| 8 +Brand#24 |ECONOMY PLATED COPPER | 45| 8 +Brand#24 |ECONOMY PLATED NICKEL | 9| 8 +Brand#24 |ECONOMY PLATED NICKEL | 36| 8 +Brand#24 |ECONOMY PLATED STEEL | 14| 8 +Brand#24 |ECONOMY POLISHED BRASS | 3| 8 +Brand#24 |ECONOMY POLISHED BRASS | 9| 8 +Brand#24 |ECONOMY POLISHED BRASS | 14| 8 +Brand#24 |ECONOMY POLISHED BRASS | 36| 8 +Brand#24 |ECONOMY POLISHED COPPER | 23| 8 +Brand#24 |ECONOMY POLISHED NICKEL | 23| 8 +Brand#24 |ECONOMY POLISHED NICKEL | 36| 8 +Brand#24 |ECONOMY POLISHED STEEL | 23| 8 +Brand#24 |ECONOMY POLISHED STEEL | 36| 8 +Brand#24 |ECONOMY POLISHED TIN | 9| 8 +Brand#24 |ECONOMY POLISHED TIN | 23| 8 +Brand#24 |LARGE ANODIZED COPPER | 23| 8 +Brand#24 |LARGE ANODIZED NICKEL | 3| 8 +Brand#24 |LARGE ANODIZED NICKEL | 23| 8 +Brand#24 |LARGE ANODIZED NICKEL | 49| 8 +Brand#24 |LARGE ANODIZED STEEL | 14| 8 +Brand#24 |LARGE ANODIZED STEEL | 49| 8 +Brand#24 |LARGE ANODIZED TIN | 9| 8 +Brand#24 |LARGE BRUSHED COPPER | 19| 8 +Brand#24 |LARGE BRUSHED COPPER | 49| 8 +Brand#24 |LARGE BRUSHED NICKEL | 36| 8 +Brand#24 |LARGE BRUSHED STEEL | 9| 8 +Brand#24 |LARGE BRUSHED STEEL | 19| 8 +Brand#24 |LARGE BRUSHED TIN | 45| 8 +Brand#24 |LARGE BRUSHED TIN | 49| 8 +Brand#24 |LARGE BURNISHED BRASS | 3| 8 +Brand#24 |LARGE BURNISHED BRASS | 23| 8 +Brand#24 |LARGE BURNISHED COPPER | 3| 8 +Brand#24 |LARGE BURNISHED NICKEL | 14| 8 +Brand#24 |LARGE BURNISHED NICKEL | 19| 8 +Brand#24 |LARGE BURNISHED TIN | 45| 8 +Brand#24 |LARGE PLATED BRASS | 9| 8 +Brand#24 |LARGE PLATED BRASS | 23| 8 +Brand#24 |LARGE PLATED COPPER | 45| 8 +Brand#24 |LARGE PLATED COPPER | 49| 8 +Brand#24 |LARGE PLATED NICKEL | 14| 8 +Brand#24 |LARGE PLATED NICKEL | 49| 8 +Brand#24 |LARGE PLATED STEEL | 19| 8 +Brand#24 |LARGE PLATED STEEL | 36| 8 +Brand#24 |LARGE PLATED TIN | 19| 8 +Brand#24 |LARGE POLISHED BRASS | 3| 8 +Brand#24 |LARGE POLISHED BRASS | 14| 8 +Brand#24 |LARGE POLISHED BRASS | 36| 8 +Brand#24 |LARGE POLISHED NICKEL | 9| 8 +Brand#24 |LARGE POLISHED NICKEL | 19| 8 +Brand#24 |LARGE POLISHED NICKEL | 36| 8 +Brand#24 |LARGE POLISHED STEEL | 23| 8 +Brand#24 |LARGE POLISHED STEEL | 49| 8 +Brand#24 |MEDIUM ANODIZED BRASS | 45| 8 +Brand#24 |MEDIUM ANODIZED BRASS | 49| 8 +Brand#24 |MEDIUM ANODIZED COPPER | 45| 8 +Brand#24 |MEDIUM ANODIZED NICKEL | 36| 8 +Brand#24 |MEDIUM ANODIZED STEEL | 9| 8 +Brand#24 |MEDIUM ANODIZED STEEL | 23| 8 +Brand#24 |MEDIUM ANODIZED STEEL | 49| 8 +Brand#24 |MEDIUM BRUSHED BRASS | 3| 8 +Brand#24 |MEDIUM BRUSHED COPPER | 14| 8 +Brand#24 |MEDIUM BRUSHED TIN | 49| 8 +Brand#24 |MEDIUM BURNISHED BRASS | 9| 8 +Brand#24 |MEDIUM BURNISHED COPPER | 3| 8 +Brand#24 |MEDIUM BURNISHED COPPER | 9| 8 +Brand#24 |MEDIUM BURNISHED NICKEL | 36| 8 +Brand#24 |MEDIUM BURNISHED NICKEL | 45| 8 +Brand#24 |MEDIUM BURNISHED STEEL | 19| 8 +Brand#24 |MEDIUM BURNISHED STEEL | 36| 8 +Brand#24 |MEDIUM PLATED BRASS | 19| 8 +Brand#24 |MEDIUM PLATED BRASS | 23| 8 +Brand#24 |MEDIUM PLATED COPPER | 3| 8 +Brand#24 |MEDIUM PLATED COPPER | 9| 8 +Brand#24 |MEDIUM PLATED COPPER | 23| 8 +Brand#24 |MEDIUM PLATED COPPER | 45| 8 +Brand#24 |MEDIUM PLATED NICKEL | 3| 8 +Brand#24 |MEDIUM PLATED NICKEL | 19| 8 +Brand#24 |MEDIUM PLATED STEEL | 14| 8 +Brand#24 |MEDIUM PLATED STEEL | 19| 8 +Brand#24 |PROMO ANODIZED BRASS | 3| 8 +Brand#24 |PROMO ANODIZED NICKEL | 14| 8 +Brand#24 |PROMO ANODIZED STEEL | 9| 8 +Brand#24 |PROMO ANODIZED STEEL | 45| 8 +Brand#24 |PROMO BRUSHED BRASS | 19| 8 +Brand#24 |PROMO BRUSHED COPPER | 3| 8 +Brand#24 |PROMO BRUSHED COPPER | 45| 8 +Brand#24 |PROMO BRUSHED NICKEL | 19| 8 +Brand#24 |PROMO BRUSHED NICKEL | 45| 8 +Brand#24 |PROMO BRUSHED NICKEL | 49| 8 +Brand#24 |PROMO BRUSHED STEEL | 19| 8 +Brand#24 |PROMO BRUSHED TIN | 14| 8 +Brand#24 |PROMO BURNISHED BRASS | 49| 8 +Brand#24 |PROMO BURNISHED STEEL | 3| 8 +Brand#24 |PROMO PLATED BRASS | 3| 8 +Brand#24 |PROMO PLATED BRASS | 9| 8 +Brand#24 |PROMO PLATED BRASS | 19| 8 +Brand#24 |PROMO PLATED BRASS | 49| 8 +Brand#24 |PROMO PLATED COPPER | 9| 8 +Brand#24 |PROMO PLATED NICKEL | 9| 8 +Brand#24 |PROMO PLATED STEEL | 36| 8 +Brand#24 |PROMO PLATED TIN | 23| 8 +Brand#24 |PROMO PLATED TIN | 49| 8 +Brand#24 |PROMO POLISHED BRASS | 45| 8 +Brand#24 |PROMO POLISHED COPPER | 49| 8 +Brand#24 |PROMO POLISHED NICKEL | 45| 8 +Brand#24 |PROMO POLISHED NICKEL | 49| 8 +Brand#24 |PROMO POLISHED STEEL | 14| 8 +Brand#24 |PROMO POLISHED STEEL | 36| 8 +Brand#24 |PROMO POLISHED TIN | 3| 8 +Brand#24 |PROMO POLISHED TIN | 14| 8 +Brand#24 |PROMO POLISHED TIN | 45| 8 +Brand#24 |SMALL ANODIZED BRASS | 19| 8 +Brand#24 |SMALL ANODIZED BRASS | 23| 8 +Brand#24 |SMALL ANODIZED COPPER | 36| 8 +Brand#24 |SMALL ANODIZED NICKEL | 9| 8 +Brand#24 |SMALL ANODIZED NICKEL | 45| 8 +Brand#24 |SMALL ANODIZED NICKEL | 49| 8 +Brand#24 |SMALL ANODIZED STEEL | 45| 8 +Brand#24 |SMALL ANODIZED TIN | 9| 8 +Brand#24 |SMALL ANODIZED TIN | 23| 8 +Brand#24 |SMALL ANODIZED TIN | 36| 8 +Brand#24 |SMALL BRUSHED BRASS | 9| 8 +Brand#24 |SMALL BRUSHED COPPER | 19| 8 +Brand#24 |SMALL BRUSHED NICKEL | 36| 8 +Brand#24 |SMALL BRUSHED STEEL | 9| 8 +Brand#24 |SMALL BRUSHED STEEL | 19| 8 +Brand#24 |SMALL BRUSHED STEEL | 36| 8 +Brand#24 |SMALL BRUSHED TIN | 3| 8 +Brand#24 |SMALL BRUSHED TIN | 14| 8 +Brand#24 |SMALL BRUSHED TIN | 36| 8 +Brand#24 |SMALL BRUSHED TIN | 49| 8 +Brand#24 |SMALL BURNISHED BRASS | 19| 8 +Brand#24 |SMALL BURNISHED BRASS | 36| 8 +Brand#24 |SMALL BURNISHED BRASS | 49| 8 +Brand#24 |SMALL BURNISHED NICKEL | 19| 8 +Brand#24 |SMALL BURNISHED NICKEL | 23| 8 +Brand#24 |SMALL BURNISHED NICKEL | 36| 8 +Brand#24 |SMALL BURNISHED TIN | 9| 8 +Brand#24 |SMALL PLATED BRASS | 23| 8 +Brand#24 |SMALL PLATED BRASS | 36| 8 +Brand#24 |SMALL PLATED COPPER | 3| 8 +Brand#24 |SMALL PLATED COPPER | 23| 8 +Brand#24 |SMALL PLATED NICKEL | 49| 8 +Brand#24 |SMALL PLATED STEEL | 3| 8 +Brand#24 |SMALL PLATED STEEL | 14| 8 +Brand#24 |SMALL PLATED STEEL | 49| 8 +Brand#24 |SMALL PLATED TIN | 3| 8 +Brand#24 |SMALL PLATED TIN | 14| 8 +Brand#24 |SMALL POLISHED BRASS | 14| 8 +Brand#24 |SMALL POLISHED BRASS | 23| 8 +Brand#24 |SMALL POLISHED NICKEL | 3| 8 +Brand#24 |SMALL POLISHED NICKEL | 9| 8 +Brand#24 |SMALL POLISHED NICKEL | 36| 8 +Brand#24 |SMALL POLISHED NICKEL | 45| 8 +Brand#24 |SMALL POLISHED STEEL | 9| 8 +Brand#24 |SMALL POLISHED TIN | 3| 8 +Brand#24 |STANDARD ANODIZED BRASS | 14| 8 +Brand#24 |STANDARD ANODIZED BRASS | 23| 8 +Brand#24 |STANDARD ANODIZED BRASS | 49| 8 +Brand#24 |STANDARD ANODIZED COPPER | 14| 8 +Brand#24 |STANDARD ANODIZED NICKEL | 36| 8 +Brand#24 |STANDARD ANODIZED STEEL | 3| 8 +Brand#24 |STANDARD ANODIZED STEEL | 14| 8 +Brand#24 |STANDARD BRUSHED BRASS | 3| 8 +Brand#24 |STANDARD BRUSHED BRASS | 36| 8 +Brand#24 |STANDARD BRUSHED COPPER | 9| 8 +Brand#24 |STANDARD BRUSHED COPPER | 23| 8 +Brand#24 |STANDARD BRUSHED NICKEL | 45| 8 +Brand#24 |STANDARD BRUSHED STEEL | 19| 8 +Brand#24 |STANDARD BRUSHED TIN | 14| 8 +Brand#24 |STANDARD BURNISHED NICKEL| 9| 8 +Brand#24 |STANDARD BURNISHED NICKEL| 23| 8 +Brand#24 |STANDARD BURNISHED NICKEL| 36| 8 +Brand#24 |STANDARD BURNISHED STEEL | 9| 8 +Brand#24 |STANDARD BURNISHED STEEL | 45| 8 +Brand#24 |STANDARD BURNISHED TIN | 14| 8 +Brand#24 |STANDARD BURNISHED TIN | 23| 8 +Brand#24 |STANDARD PLATED BRASS | 14| 8 +Brand#24 |STANDARD PLATED COPPER | 14| 8 +Brand#24 |STANDARD PLATED NICKEL | 3| 8 +Brand#24 |STANDARD PLATED NICKEL | 14| 8 +Brand#24 |STANDARD PLATED NICKEL | 45| 8 +Brand#24 |STANDARD PLATED STEEL | 19| 8 +Brand#24 |STANDARD PLATED STEEL | 49| 8 +Brand#24 |STANDARD PLATED TIN | 36| 8 +Brand#24 |STANDARD PLATED TIN | 45| 8 +Brand#24 |STANDARD POLISHED BRASS | 49| 8 +Brand#24 |STANDARD POLISHED COPPER | 23| 8 +Brand#24 |STANDARD POLISHED COPPER | 45| 8 +Brand#24 |STANDARD POLISHED NICKEL | 3| 8 +Brand#24 |STANDARD POLISHED NICKEL | 14| 8 +Brand#24 |STANDARD POLISHED STEEL | 3| 8 +Brand#24 |STANDARD POLISHED STEEL | 9| 8 +Brand#24 |STANDARD POLISHED STEEL | 19| 8 +Brand#24 |STANDARD POLISHED STEEL | 23| 8 +Brand#25 |ECONOMY ANODIZED BRASS | 49| 8 +Brand#25 |ECONOMY ANODIZED COPPER | 9| 8 +Brand#25 |ECONOMY ANODIZED COPPER | 23| 8 +Brand#25 |ECONOMY ANODIZED NICKEL | 9| 8 +Brand#25 |ECONOMY ANODIZED NICKEL | 19| 8 +Brand#25 |ECONOMY ANODIZED NICKEL | 45| 8 +Brand#25 |ECONOMY ANODIZED STEEL | 3| 8 +Brand#25 |ECONOMY ANODIZED STEEL | 19| 8 +Brand#25 |ECONOMY ANODIZED TIN | 49| 8 +Brand#25 |ECONOMY BRUSHED BRASS | 36| 8 +Brand#25 |ECONOMY BRUSHED NICKEL | 36| 8 +Brand#25 |ECONOMY BRUSHED STEEL | 49| 8 +Brand#25 |ECONOMY BURNISHED COPPER | 9| 8 +Brand#25 |ECONOMY BURNISHED COPPER | 14| 8 +Brand#25 |ECONOMY BURNISHED COPPER | 45| 8 +Brand#25 |ECONOMY BURNISHED NICKEL | 36| 8 +Brand#25 |ECONOMY BURNISHED STEEL | 9| 8 +Brand#25 |ECONOMY PLATED NICKEL | 45| 8 +Brand#25 |ECONOMY PLATED STEEL | 49| 8 +Brand#25 |ECONOMY PLATED TIN | 3| 8 +Brand#25 |ECONOMY PLATED TIN | 19| 8 +Brand#25 |ECONOMY PLATED TIN | 36| 8 +Brand#25 |ECONOMY POLISHED BRASS | 36| 8 +Brand#25 |ECONOMY POLISHED BRASS | 45| 8 +Brand#25 |ECONOMY POLISHED COPPER | 9| 8 +Brand#25 |ECONOMY POLISHED TIN | 36| 8 +Brand#25 |LARGE ANODIZED BRASS | 45| 8 +Brand#25 |LARGE ANODIZED NICKEL | 36| 8 +Brand#25 |LARGE ANODIZED STEEL | 3| 8 +Brand#25 |LARGE BRUSHED BRASS | 3| 8 +Brand#25 |LARGE BRUSHED NICKEL | 19| 8 +Brand#25 |LARGE BURNISHED BRASS | 9| 8 +Brand#25 |LARGE BURNISHED BRASS | 49| 8 +Brand#25 |LARGE BURNISHED COPPER | 3| 8 +Brand#25 |LARGE BURNISHED COPPER | 14| 8 +Brand#25 |LARGE BURNISHED COPPER | 19| 8 +Brand#25 |LARGE BURNISHED COPPER | 45| 8 +Brand#25 |LARGE BURNISHED TIN | 3| 8 +Brand#25 |LARGE BURNISHED TIN | 9| 8 +Brand#25 |LARGE PLATED COPPER | 36| 8 +Brand#25 |LARGE PLATED NICKEL | 36| 8 +Brand#25 |LARGE PLATED STEEL | 9| 8 +Brand#25 |LARGE PLATED STEEL | 23| 8 +Brand#25 |LARGE PLATED STEEL | 49| 8 +Brand#25 |LARGE PLATED TIN | 3| 8 +Brand#25 |LARGE PLATED TIN | 9| 8 +Brand#25 |LARGE PLATED TIN | 19| 8 +Brand#25 |LARGE PLATED TIN | 45| 8 +Brand#25 |LARGE POLISHED BRASS | 9| 8 +Brand#25 |LARGE POLISHED BRASS | 19| 8 +Brand#25 |LARGE POLISHED COPPER | 14| 8 +Brand#25 |LARGE POLISHED COPPER | 23| 8 +Brand#25 |LARGE POLISHED COPPER | 36| 8 +Brand#25 |LARGE POLISHED NICKEL | 14| 8 +Brand#25 |LARGE POLISHED STEEL | 9| 8 +Brand#25 |LARGE POLISHED STEEL | 36| 8 +Brand#25 |LARGE POLISHED STEEL | 45| 8 +Brand#25 |MEDIUM ANODIZED COPPER | 9| 8 +Brand#25 |MEDIUM ANODIZED COPPER | 36| 8 +Brand#25 |MEDIUM ANODIZED NICKEL | 9| 8 +Brand#25 |MEDIUM ANODIZED NICKEL | 36| 8 +Brand#25 |MEDIUM ANODIZED STEEL | 3| 8 +Brand#25 |MEDIUM ANODIZED TIN | 9| 8 +Brand#25 |MEDIUM ANODIZED TIN | 49| 8 +Brand#25 |MEDIUM BRUSHED COPPER | 14| 8 +Brand#25 |MEDIUM BRUSHED COPPER | 45| 8 +Brand#25 |MEDIUM BRUSHED COPPER | 49| 8 +Brand#25 |MEDIUM BRUSHED NICKEL | 49| 8 +Brand#25 |MEDIUM BRUSHED STEEL | 45| 8 +Brand#25 |MEDIUM BURNISHED BRASS | 3| 8 +Brand#25 |MEDIUM BURNISHED BRASS | 19| 8 +Brand#25 |MEDIUM BURNISHED BRASS | 36| 8 +Brand#25 |MEDIUM BURNISHED BRASS | 45| 8 +Brand#25 |MEDIUM BURNISHED COPPER | 14| 8 +Brand#25 |MEDIUM BURNISHED COPPER | 19| 8 +Brand#25 |MEDIUM BURNISHED COPPER | 45| 8 +Brand#25 |MEDIUM BURNISHED NICKEL | 3| 8 +Brand#25 |MEDIUM BURNISHED NICKEL | 9| 8 +Brand#25 |MEDIUM BURNISHED STEEL | 3| 8 +Brand#25 |MEDIUM BURNISHED STEEL | 45| 8 +Brand#25 |MEDIUM BURNISHED STEEL | 49| 8 +Brand#25 |MEDIUM BURNISHED TIN | 9| 8 +Brand#25 |MEDIUM PLATED BRASS | 14| 8 +Brand#25 |MEDIUM PLATED BRASS | 45| 8 +Brand#25 |MEDIUM PLATED COPPER | 49| 8 +Brand#25 |MEDIUM PLATED NICKEL | 9| 8 +Brand#25 |MEDIUM PLATED NICKEL | 19| 8 +Brand#25 |MEDIUM PLATED NICKEL | 23| 8 +Brand#25 |MEDIUM PLATED NICKEL | 36| 8 +Brand#25 |MEDIUM PLATED NICKEL | 45| 8 +Brand#25 |MEDIUM PLATED TIN | 3| 8 +Brand#25 |PROMO ANODIZED BRASS | 23| 8 +Brand#25 |PROMO ANODIZED BRASS | 45| 8 +Brand#25 |PROMO ANODIZED COPPER | 19| 8 +Brand#25 |PROMO ANODIZED COPPER | 45| 8 +Brand#25 |PROMO ANODIZED TIN | 3| 8 +Brand#25 |PROMO ANODIZED TIN | 14| 8 +Brand#25 |PROMO ANODIZED TIN | 19| 8 +Brand#25 |PROMO BRUSHED COPPER | 49| 8 +Brand#25 |PROMO BRUSHED NICKEL | 3| 8 +Brand#25 |PROMO BRUSHED NICKEL | 19| 8 +Brand#25 |PROMO BRUSHED TIN | 14| 8 +Brand#25 |PROMO BRUSHED TIN | 19| 8 +Brand#25 |PROMO BURNISHED COPPER | 14| 8 +Brand#25 |PROMO BURNISHED NICKEL | 3| 8 +Brand#25 |PROMO BURNISHED NICKEL | 36| 8 +Brand#25 |PROMO BURNISHED STEEL | 19| 8 +Brand#25 |PROMO BURNISHED TIN | 36| 8 +Brand#25 |PROMO PLATED BRASS | 14| 8 +Brand#25 |PROMO PLATED BRASS | 19| 8 +Brand#25 |PROMO PLATED NICKEL | 3| 8 +Brand#25 |PROMO PLATED NICKEL | 9| 8 +Brand#25 |PROMO PLATED NICKEL | 49| 8 +Brand#25 |PROMO PLATED STEEL | 23| 8 +Brand#25 |PROMO PLATED STEEL | 36| 8 +Brand#25 |PROMO POLISHED BRASS | 14| 8 +Brand#25 |PROMO POLISHED COPPER | 9| 8 +Brand#25 |PROMO POLISHED COPPER | 36| 8 +Brand#25 |PROMO POLISHED NICKEL | 19| 8 +Brand#25 |PROMO POLISHED NICKEL | 45| 8 +Brand#25 |PROMO POLISHED TIN | 19| 8 +Brand#25 |SMALL ANODIZED BRASS | 3| 8 +Brand#25 |SMALL ANODIZED BRASS | 9| 8 +Brand#25 |SMALL ANODIZED BRASS | 14| 8 +Brand#25 |SMALL ANODIZED BRASS | 23| 8 +Brand#25 |SMALL ANODIZED NICKEL | 14| 8 +Brand#25 |SMALL ANODIZED NICKEL | 19| 8 +Brand#25 |SMALL ANODIZED NICKEL | 49| 8 +Brand#25 |SMALL ANODIZED STEEL | 23| 8 +Brand#25 |SMALL ANODIZED TIN | 3| 8 +Brand#25 |SMALL ANODIZED TIN | 45| 8 +Brand#25 |SMALL BRUSHED BRASS | 3| 8 +Brand#25 |SMALL BRUSHED BRASS | 49| 8 +Brand#25 |SMALL BRUSHED COPPER | 36| 8 +Brand#25 |SMALL BRUSHED NICKEL | 9| 8 +Brand#25 |SMALL BRUSHED NICKEL | 49| 8 +Brand#25 |SMALL BRUSHED STEEL | 3| 8 +Brand#25 |SMALL BRUSHED TIN | 36| 8 +Brand#25 |SMALL BRUSHED TIN | 49| 8 +Brand#25 |SMALL BURNISHED BRASS | 23| 8 +Brand#25 |SMALL BURNISHED COPPER | 45| 8 +Brand#25 |SMALL BURNISHED NICKEL | 23| 8 +Brand#25 |SMALL BURNISHED STEEL | 3| 8 +Brand#25 |SMALL BURNISHED TIN | 3| 8 +Brand#25 |SMALL PLATED COPPER | 36| 8 +Brand#25 |SMALL PLATED COPPER | 49| 8 +Brand#25 |SMALL PLATED STEEL | 23| 8 +Brand#25 |SMALL PLATED STEEL | 36| 8 +Brand#25 |SMALL PLATED STEEL | 45| 8 +Brand#25 |SMALL PLATED TIN | 49| 8 +Brand#25 |SMALL POLISHED BRASS | 3| 8 +Brand#25 |SMALL POLISHED BRASS | 19| 8 +Brand#25 |SMALL POLISHED BRASS | 23| 8 +Brand#25 |SMALL POLISHED BRASS | 45| 8 +Brand#25 |SMALL POLISHED COPPER | 3| 8 +Brand#25 |SMALL POLISHED NICKEL | 9| 8 +Brand#25 |SMALL POLISHED NICKEL | 14| 8 +Brand#25 |SMALL POLISHED NICKEL | 19| 8 +Brand#25 |SMALL POLISHED STEEL | 14| 8 +Brand#25 |SMALL POLISHED TIN | 45| 8 +Brand#25 |SMALL POLISHED TIN | 49| 8 +Brand#25 |STANDARD ANODIZED BRASS | 9| 8 +Brand#25 |STANDARD ANODIZED BRASS | 45| 8 +Brand#25 |STANDARD ANODIZED NICKEL | 14| 8 +Brand#25 |STANDARD ANODIZED NICKEL | 23| 8 +Brand#25 |STANDARD ANODIZED NICKEL | 49| 8 +Brand#25 |STANDARD ANODIZED STEEL | 3| 8 +Brand#25 |STANDARD ANODIZED STEEL | 9| 8 +Brand#25 |STANDARD ANODIZED TIN | 9| 8 +Brand#25 |STANDARD ANODIZED TIN | 14| 8 +Brand#25 |STANDARD ANODIZED TIN | 23| 8 +Brand#25 |STANDARD ANODIZED TIN | 49| 8 +Brand#25 |STANDARD BRUSHED COPPER | 3| 8 +Brand#25 |STANDARD BRUSHED COPPER | 36| 8 +Brand#25 |STANDARD BRUSHED NICKEL | 14| 8 +Brand#25 |STANDARD BRUSHED NICKEL | 19| 8 +Brand#25 |STANDARD BRUSHED TIN | 36| 8 +Brand#25 |STANDARD BURNISHED NICKEL| 9| 8 +Brand#25 |STANDARD BURNISHED NICKEL| 19| 8 +Brand#25 |STANDARD BURNISHED STEEL | 14| 8 +Brand#25 |STANDARD BURNISHED STEEL | 36| 8 +Brand#25 |STANDARD BURNISHED STEEL | 45| 8 +Brand#25 |STANDARD BURNISHED TIN | 14| 8 +Brand#25 |STANDARD BURNISHED TIN | 19| 8 +Brand#25 |STANDARD PLATED BRASS | 19| 8 +Brand#25 |STANDARD PLATED COPPER | 14| 8 +Brand#25 |STANDARD PLATED COPPER | 36| 8 +Brand#25 |STANDARD PLATED COPPER | 45| 8 +Brand#25 |STANDARD PLATED STEEL | 45| 8 +Brand#25 |STANDARD PLATED TIN | 49| 8 +Brand#25 |STANDARD POLISHED BRASS | 19| 8 +Brand#25 |STANDARD POLISHED BRASS | 49| 8 +Brand#25 |STANDARD POLISHED NICKEL | 3| 8 +Brand#25 |STANDARD POLISHED STEEL | 19| 8 +Brand#25 |STANDARD POLISHED TIN | 36| 8 +Brand#25 |STANDARD POLISHED TIN | 45| 8 +Brand#25 |STANDARD POLISHED TIN | 49| 8 +Brand#31 |ECONOMY ANODIZED COPPER | 23| 8 +Brand#31 |ECONOMY ANODIZED NICKEL | 9| 8 +Brand#31 |ECONOMY ANODIZED NICKEL | 14| 8 +Brand#31 |ECONOMY ANODIZED STEEL | 3| 8 +Brand#31 |ECONOMY ANODIZED TIN | 3| 8 +Brand#31 |ECONOMY ANODIZED TIN | 19| 8 +Brand#31 |ECONOMY BRUSHED COPPER | 3| 8 +Brand#31 |ECONOMY BRUSHED COPPER | 9| 8 +Brand#31 |ECONOMY BRUSHED NICKEL | 9| 8 +Brand#31 |ECONOMY BRUSHED STEEL | 3| 8 +Brand#31 |ECONOMY BRUSHED STEEL | 19| 8 +Brand#31 |ECONOMY BRUSHED TIN | 23| 8 +Brand#31 |ECONOMY BURNISHED COPPER | 45| 8 +Brand#31 |ECONOMY BURNISHED STEEL | 3| 8 +Brand#31 |ECONOMY BURNISHED STEEL | 14| 8 +Brand#31 |ECONOMY BURNISHED STEEL | 19| 8 +Brand#31 |ECONOMY BURNISHED TIN | 3| 8 +Brand#31 |ECONOMY BURNISHED TIN | 45| 8 +Brand#31 |ECONOMY BURNISHED TIN | 49| 8 +Brand#31 |ECONOMY PLATED BRASS | 36| 8 +Brand#31 |ECONOMY PLATED COPPER | 19| 8 +Brand#31 |ECONOMY PLATED COPPER | 49| 8 +Brand#31 |ECONOMY PLATED NICKEL | 36| 8 +Brand#31 |ECONOMY PLATED STEEL | 23| 8 +Brand#31 |ECONOMY PLATED TIN | 3| 8 +Brand#31 |ECONOMY PLATED TIN | 36| 8 +Brand#31 |ECONOMY POLISHED BRASS | 9| 8 +Brand#31 |ECONOMY POLISHED BRASS | 23| 8 +Brand#31 |ECONOMY POLISHED COPPER | 49| 8 +Brand#31 |ECONOMY POLISHED NICKEL | 9| 8 +Brand#31 |ECONOMY POLISHED NICKEL | 45| 8 +Brand#31 |ECONOMY POLISHED STEEL | 49| 8 +Brand#31 |ECONOMY POLISHED TIN | 45| 8 +Brand#31 |LARGE ANODIZED BRASS | 3| 8 +Brand#31 |LARGE ANODIZED BRASS | 14| 8 +Brand#31 |LARGE ANODIZED BRASS | 36| 8 +Brand#31 |LARGE ANODIZED COPPER | 23| 8 +Brand#31 |LARGE ANODIZED COPPER | 45| 8 +Brand#31 |LARGE ANODIZED NICKEL | 49| 8 +Brand#31 |LARGE ANODIZED STEEL | 3| 8 +Brand#31 |LARGE ANODIZED STEEL | 9| 8 +Brand#31 |LARGE ANODIZED TIN | 9| 8 +Brand#31 |LARGE BRUSHED BRASS | 45| 8 +Brand#31 |LARGE BRUSHED BRASS | 49| 8 +Brand#31 |LARGE BRUSHED COPPER | 19| 8 +Brand#31 |LARGE BRUSHED NICKEL | 14| 8 +Brand#31 |LARGE BRUSHED NICKEL | 23| 8 +Brand#31 |LARGE BRUSHED STEEL | 14| 8 +Brand#31 |LARGE BRUSHED TIN | 45| 8 +Brand#31 |LARGE BURNISHED BRASS | 19| 8 +Brand#31 |LARGE BURNISHED BRASS | 23| 8 +Brand#31 |LARGE BURNISHED COPPER | 23| 8 +Brand#31 |LARGE BURNISHED COPPER | 45| 8 +Brand#31 |LARGE BURNISHED NICKEL | 9| 8 +Brand#31 |LARGE BURNISHED NICKEL | 19| 8 +Brand#31 |LARGE BURNISHED STEEL | 9| 8 +Brand#31 |LARGE BURNISHED STEEL | 36| 8 +Brand#31 |LARGE BURNISHED TIN | 14| 8 +Brand#31 |LARGE BURNISHED TIN | 23| 8 +Brand#31 |LARGE BURNISHED TIN | 49| 8 +Brand#31 |LARGE PLATED BRASS | 14| 8 +Brand#31 |LARGE PLATED COPPER | 19| 8 +Brand#31 |LARGE PLATED TIN | 9| 8 +Brand#31 |LARGE PLATED TIN | 36| 8 +Brand#31 |LARGE POLISHED BRASS | 45| 8 +Brand#31 |LARGE POLISHED COPPER | 3| 8 +Brand#31 |LARGE POLISHED COPPER | 9| 8 +Brand#31 |LARGE POLISHED COPPER | 19| 8 +Brand#31 |LARGE POLISHED NICKEL | 14| 8 +Brand#31 |LARGE POLISHED NICKEL | 19| 8 +Brand#31 |LARGE POLISHED TIN | 14| 8 +Brand#31 |LARGE POLISHED TIN | 19| 8 +Brand#31 |LARGE POLISHED TIN | 23| 8 +Brand#31 |MEDIUM ANODIZED BRASS | 19| 8 +Brand#31 |MEDIUM ANODIZED BRASS | 23| 8 +Brand#31 |MEDIUM ANODIZED COPPER | 14| 8 +Brand#31 |MEDIUM ANODIZED COPPER | 19| 8 +Brand#31 |MEDIUM ANODIZED STEEL | 49| 8 +Brand#31 |MEDIUM ANODIZED TIN | 19| 8 +Brand#31 |MEDIUM BRUSHED BRASS | 19| 8 +Brand#31 |MEDIUM BRUSHED BRASS | 36| 8 +Brand#31 |MEDIUM BRUSHED COPPER | 9| 8 +Brand#31 |MEDIUM BRUSHED COPPER | 23| 8 +Brand#31 |MEDIUM BRUSHED COPPER | 49| 8 +Brand#31 |MEDIUM BRUSHED STEEL | 23| 8 +Brand#31 |MEDIUM BRUSHED TIN | 49| 8 +Brand#31 |MEDIUM BURNISHED BRASS | 49| 8 +Brand#31 |MEDIUM BURNISHED NICKEL | 9| 8 +Brand#31 |MEDIUM BURNISHED NICKEL | 19| 8 +Brand#31 |MEDIUM BURNISHED NICKEL | 45| 8 +Brand#31 |MEDIUM BURNISHED STEEL | 19| 8 +Brand#31 |MEDIUM BURNISHED TIN | 3| 8 +Brand#31 |MEDIUM BURNISHED TIN | 14| 8 +Brand#31 |MEDIUM BURNISHED TIN | 23| 8 +Brand#31 |MEDIUM PLATED BRASS | 3| 8 +Brand#31 |MEDIUM PLATED COPPER | 14| 8 +Brand#31 |MEDIUM PLATED COPPER | 19| 8 +Brand#31 |MEDIUM PLATED TIN | 19| 8 +Brand#31 |PROMO ANODIZED BRASS | 3| 8 +Brand#31 |PROMO ANODIZED BRASS | 9| 8 +Brand#31 |PROMO ANODIZED BRASS | 14| 8 +Brand#31 |PROMO ANODIZED BRASS | 23| 8 +Brand#31 |PROMO ANODIZED COPPER | 23| 8 +Brand#31 |PROMO ANODIZED NICKEL | 3| 8 +Brand#31 |PROMO ANODIZED NICKEL | 36| 8 +Brand#31 |PROMO ANODIZED NICKEL | 45| 8 +Brand#31 |PROMO ANODIZED STEEL | 9| 8 +Brand#31 |PROMO ANODIZED STEEL | 49| 8 +Brand#31 |PROMO BRUSHED BRASS | 19| 8 +Brand#31 |PROMO BRUSHED BRASS | 23| 8 +Brand#31 |PROMO BRUSHED BRASS | 36| 8 +Brand#31 |PROMO BRUSHED COPPER | 45| 8 +Brand#31 |PROMO BRUSHED NICKEL | 23| 8 +Brand#31 |PROMO BRUSHED NICKEL | 49| 8 +Brand#31 |PROMO BRUSHED STEEL | 49| 8 +Brand#31 |PROMO BRUSHED TIN | 9| 8 +Brand#31 |PROMO BRUSHED TIN | 36| 8 +Brand#31 |PROMO BURNISHED COPPER | 3| 8 +Brand#31 |PROMO BURNISHED COPPER | 14| 8 +Brand#31 |PROMO BURNISHED COPPER | 19| 8 +Brand#31 |PROMO BURNISHED COPPER | 36| 8 +Brand#31 |PROMO BURNISHED NICKEL | 45| 8 +Brand#31 |PROMO BURNISHED STEEL | 19| 8 +Brand#31 |PROMO PLATED COPPER | 19| 8 +Brand#31 |PROMO PLATED COPPER | 36| 8 +Brand#31 |PROMO PLATED COPPER | 49| 8 +Brand#31 |PROMO PLATED NICKEL | 36| 8 +Brand#31 |PROMO PLATED STEEL | 19| 8 +Brand#31 |PROMO PLATED STEEL | 23| 8 +Brand#31 |PROMO PLATED TIN | 3| 8 +Brand#31 |PROMO PLATED TIN | 49| 8 +Brand#31 |PROMO POLISHED BRASS | 3| 8 +Brand#31 |PROMO POLISHED BRASS | 49| 8 +Brand#31 |PROMO POLISHED NICKEL | 3| 8 +Brand#31 |PROMO POLISHED TIN | 9| 8 +Brand#31 |PROMO POLISHED TIN | 45| 8 +Brand#31 |SMALL ANODIZED BRASS | 9| 8 +Brand#31 |SMALL ANODIZED BRASS | 36| 8 +Brand#31 |SMALL ANODIZED COPPER | 36| 8 +Brand#31 |SMALL ANODIZED COPPER | 45| 8 +Brand#31 |SMALL ANODIZED NICKEL | 14| 8 +Brand#31 |SMALL ANODIZED NICKEL | 49| 8 +Brand#31 |SMALL ANODIZED STEEL | 9| 8 +Brand#31 |SMALL ANODIZED STEEL | 45| 8 +Brand#31 |SMALL ANODIZED TIN | 23| 8 +Brand#31 |SMALL BRUSHED BRASS | 23| 8 +Brand#31 |SMALL BRUSHED NICKEL | 19| 8 +Brand#31 |SMALL BRUSHED NICKEL | 49| 8 +Brand#31 |SMALL BRUSHED STEEL | 36| 8 +Brand#31 |SMALL BRUSHED STEEL | 49| 8 +Brand#31 |SMALL BRUSHED TIN | 9| 8 +Brand#31 |SMALL BRUSHED TIN | 45| 8 +Brand#31 |SMALL BURNISHED NICKEL | 23| 8 +Brand#31 |SMALL BURNISHED STEEL | 3| 8 +Brand#31 |SMALL BURNISHED STEEL | 9| 8 +Brand#31 |SMALL BURNISHED STEEL | 19| 8 +Brand#31 |SMALL BURNISHED STEEL | 23| 8 +Brand#31 |SMALL BURNISHED STEEL | 36| 8 +Brand#31 |SMALL BURNISHED STEEL | 49| 8 +Brand#31 |SMALL BURNISHED TIN | 36| 8 +Brand#31 |SMALL PLATED BRASS | 23| 8 +Brand#31 |SMALL PLATED COPPER | 14| 8 +Brand#31 |SMALL PLATED COPPER | 19| 8 +Brand#31 |SMALL PLATED NICKEL | 36| 8 +Brand#31 |SMALL PLATED STEEL | 14| 8 +Brand#31 |SMALL PLATED STEEL | 36| 8 +Brand#31 |SMALL PLATED TIN | 3| 8 +Brand#31 |SMALL PLATED TIN | 36| 8 +Brand#31 |SMALL POLISHED BRASS | 3| 8 +Brand#31 |SMALL POLISHED BRASS | 14| 8 +Brand#31 |SMALL POLISHED BRASS | 19| 8 +Brand#31 |SMALL POLISHED COPPER | 3| 8 +Brand#31 |SMALL POLISHED COPPER | 36| 8 +Brand#31 |SMALL POLISHED NICKEL | 14| 8 +Brand#31 |SMALL POLISHED STEEL | 49| 8 +Brand#31 |SMALL POLISHED TIN | 23| 8 +Brand#31 |SMALL POLISHED TIN | 49| 8 +Brand#31 |STANDARD ANODIZED NICKEL | 9| 8 +Brand#31 |STANDARD ANODIZED NICKEL | 14| 8 +Brand#31 |STANDARD ANODIZED NICKEL | 45| 8 +Brand#31 |STANDARD ANODIZED STEEL | 19| 8 +Brand#31 |STANDARD ANODIZED STEEL | 45| 8 +Brand#31 |STANDARD ANODIZED TIN | 3| 8 +Brand#31 |STANDARD ANODIZED TIN | 9| 8 +Brand#31 |STANDARD ANODIZED TIN | 19| 8 +Brand#31 |STANDARD ANODIZED TIN | 36| 8 +Brand#31 |STANDARD BRUSHED BRASS | 9| 8 +Brand#31 |STANDARD BRUSHED BRASS | 36| 8 +Brand#31 |STANDARD BRUSHED COPPER | 36| 8 +Brand#31 |STANDARD BRUSHED NICKEL | 36| 8 +Brand#31 |STANDARD BRUSHED STEEL | 19| 8 +Brand#31 |STANDARD BRUSHED STEEL | 45| 8 +Brand#31 |STANDARD BRUSHED TIN | 23| 8 +Brand#31 |STANDARD BURNISHED BRASS | 9| 8 +Brand#31 |STANDARD BURNISHED BRASS | 36| 8 +Brand#31 |STANDARD BURNISHED COPPER| 9| 8 +Brand#31 |STANDARD BURNISHED COPPER| 14| 8 +Brand#31 |STANDARD BURNISHED NICKEL| 45| 8 +Brand#31 |STANDARD BURNISHED STEEL | 9| 8 +Brand#31 |STANDARD BURNISHED STEEL | 23| 8 +Brand#31 |STANDARD BURNISHED TIN | 3| 8 +Brand#31 |STANDARD BURNISHED TIN | 9| 8 +Brand#31 |STANDARD BURNISHED TIN | 36| 8 +Brand#31 |STANDARD PLATED COPPER | 3| 8 +Brand#31 |STANDARD PLATED COPPER | 23| 8 +Brand#31 |STANDARD PLATED COPPER | 49| 8 +Brand#31 |STANDARD PLATED NICKEL | 3| 8 +Brand#31 |STANDARD PLATED NICKEL | 9| 8 +Brand#31 |STANDARD PLATED NICKEL | 36| 8 +Brand#31 |STANDARD PLATED TIN | 14| 8 +Brand#31 |STANDARD PLATED TIN | 19| 8 +Brand#31 |STANDARD POLISHED BRASS | 14| 8 +Brand#31 |STANDARD POLISHED COPPER | 3| 8 +Brand#31 |STANDARD POLISHED COPPER | 23| 8 +Brand#31 |STANDARD POLISHED NICKEL | 19| 8 +Brand#31 |STANDARD POLISHED STEEL | 14| 8 +Brand#31 |STANDARD POLISHED TIN | 36| 8 +Brand#31 |STANDARD POLISHED TIN | 45| 8 +Brand#32 |ECONOMY ANODIZED BRASS | 19| 8 +Brand#32 |ECONOMY ANODIZED BRASS | 45| 8 +Brand#32 |ECONOMY ANODIZED COPPER | 49| 8 +Brand#32 |ECONOMY ANODIZED NICKEL | 14| 8 +Brand#32 |ECONOMY ANODIZED NICKEL | 19| 8 +Brand#32 |ECONOMY ANODIZED STEEL | 3| 8 +Brand#32 |ECONOMY ANODIZED STEEL | 45| 8 +Brand#32 |ECONOMY ANODIZED TIN | 49| 8 +Brand#32 |ECONOMY BRUSHED COPPER | 23| 8 +Brand#32 |ECONOMY BRUSHED NICKEL | 36| 8 +Brand#32 |ECONOMY BRUSHED STEEL | 14| 8 +Brand#32 |ECONOMY BRUSHED TIN | 14| 8 +Brand#32 |ECONOMY BRUSHED TIN | 45| 8 +Brand#32 |ECONOMY BURNISHED COPPER | 9| 8 +Brand#32 |ECONOMY BURNISHED NICKEL | 9| 8 +Brand#32 |ECONOMY BURNISHED NICKEL | 14| 8 +Brand#32 |ECONOMY BURNISHED NICKEL | 19| 8 +Brand#32 |ECONOMY BURNISHED NICKEL | 23| 8 +Brand#32 |ECONOMY BURNISHED STEEL | 14| 8 +Brand#32 |ECONOMY BURNISHED STEEL | 19| 8 +Brand#32 |ECONOMY BURNISHED STEEL | 36| 8 +Brand#32 |ECONOMY BURNISHED TIN | 9| 8 +Brand#32 |ECONOMY BURNISHED TIN | 45| 8 +Brand#32 |ECONOMY BURNISHED TIN | 49| 8 +Brand#32 |ECONOMY PLATED NICKEL | 3| 8 +Brand#32 |ECONOMY PLATED NICKEL | 14| 8 +Brand#32 |ECONOMY PLATED STEEL | 49| 8 +Brand#32 |ECONOMY PLATED TIN | 9| 8 +Brand#32 |ECONOMY POLISHED BRASS | 14| 8 +Brand#32 |ECONOMY POLISHED BRASS | 19| 8 +Brand#32 |ECONOMY POLISHED BRASS | 49| 8 +Brand#32 |ECONOMY POLISHED COPPER | 3| 8 +Brand#32 |ECONOMY POLISHED COPPER | 23| 8 +Brand#32 |ECONOMY POLISHED NICKEL | 9| 8 +Brand#32 |ECONOMY POLISHED NICKEL | 23| 8 +Brand#32 |ECONOMY POLISHED NICKEL | 49| 8 +Brand#32 |ECONOMY POLISHED STEEL | 19| 8 +Brand#32 |ECONOMY POLISHED STEEL | 36| 8 +Brand#32 |ECONOMY POLISHED TIN | 9| 8 +Brand#32 |LARGE ANODIZED BRASS | 9| 8 +Brand#32 |LARGE ANODIZED NICKEL | 45| 8 +Brand#32 |LARGE ANODIZED STEEL | 3| 8 +Brand#32 |LARGE ANODIZED STEEL | 49| 8 +Brand#32 |LARGE ANODIZED TIN | 9| 8 +Brand#32 |LARGE BRUSHED BRASS | 3| 8 +Brand#32 |LARGE BRUSHED COPPER | 9| 8 +Brand#32 |LARGE BRUSHED COPPER | 14| 8 +Brand#32 |LARGE BRUSHED NICKEL | 45| 8 +Brand#32 |LARGE BRUSHED TIN | 36| 8 +Brand#32 |LARGE BURNISHED BRASS | 9| 8 +Brand#32 |LARGE BURNISHED BRASS | 23| 8 +Brand#32 |LARGE BURNISHED BRASS | 36| 8 +Brand#32 |LARGE BURNISHED NICKEL | 3| 8 +Brand#32 |LARGE BURNISHED STEEL | 49| 8 +Brand#32 |LARGE BURNISHED TIN | 23| 8 +Brand#32 |LARGE BURNISHED TIN | 45| 8 +Brand#32 |LARGE BURNISHED TIN | 49| 8 +Brand#32 |LARGE PLATED BRASS | 14| 8 +Brand#32 |LARGE PLATED BRASS | 45| 8 +Brand#32 |LARGE PLATED BRASS | 49| 8 +Brand#32 |LARGE PLATED NICKEL | 14| 8 +Brand#32 |LARGE PLATED STEEL | 19| 8 +Brand#32 |LARGE PLATED TIN | 14| 8 +Brand#32 |LARGE POLISHED BRASS | 45| 8 +Brand#32 |LARGE POLISHED COPPER | 3| 8 +Brand#32 |LARGE POLISHED COPPER | 9| 8 +Brand#32 |LARGE POLISHED STEEL | 49| 8 +Brand#32 |LARGE POLISHED TIN | 14| 8 +Brand#32 |LARGE POLISHED TIN | 49| 8 +Brand#32 |MEDIUM ANODIZED BRASS | 3| 8 +Brand#32 |MEDIUM ANODIZED BRASS | 23| 8 +Brand#32 |MEDIUM ANODIZED COPPER | 3| 8 +Brand#32 |MEDIUM ANODIZED STEEL | 9| 8 +Brand#32 |MEDIUM ANODIZED TIN | 9| 8 +Brand#32 |MEDIUM BRUSHED BRASS | 3| 8 +Brand#32 |MEDIUM BRUSHED BRASS | 36| 8 +Brand#32 |MEDIUM BRUSHED COPPER | 23| 8 +Brand#32 |MEDIUM BRUSHED TIN | 3| 8 +Brand#32 |MEDIUM BRUSHED TIN | 23| 8 +Brand#32 |MEDIUM BURNISHED BRASS | 19| 8 +Brand#32 |MEDIUM BURNISHED BRASS | 45| 8 +Brand#32 |MEDIUM BURNISHED BRASS | 49| 8 +Brand#32 |MEDIUM BURNISHED COPPER | 9| 8 +Brand#32 |MEDIUM BURNISHED COPPER | 36| 8 +Brand#32 |MEDIUM BURNISHED NICKEL | 49| 8 +Brand#32 |MEDIUM BURNISHED STEEL | 9| 8 +Brand#32 |MEDIUM BURNISHED TIN | 9| 8 +Brand#32 |MEDIUM PLATED BRASS | 3| 8 +Brand#32 |MEDIUM PLATED COPPER | 3| 8 +Brand#32 |MEDIUM PLATED COPPER | 9| 8 +Brand#32 |MEDIUM PLATED COPPER | 23| 8 +Brand#32 |MEDIUM PLATED NICKEL | 23| 8 +Brand#32 |MEDIUM PLATED STEEL | 3| 8 +Brand#32 |MEDIUM PLATED STEEL | 9| 8 +Brand#32 |PROMO ANODIZED BRASS | 3| 8 +Brand#32 |PROMO ANODIZED BRASS | 9| 8 +Brand#32 |PROMO ANODIZED COPPER | 19| 8 +Brand#32 |PROMO ANODIZED NICKEL | 9| 8 +Brand#32 |PROMO ANODIZED NICKEL | 14| 8 +Brand#32 |PROMO ANODIZED NICKEL | 19| 8 +Brand#32 |PROMO ANODIZED STEEL | 3| 8 +Brand#32 |PROMO ANODIZED STEEL | 23| 8 +Brand#32 |PROMO BRUSHED BRASS | 36| 8 +Brand#32 |PROMO BRUSHED COPPER | 23| 8 +Brand#32 |PROMO BRUSHED NICKEL | 23| 8 +Brand#32 |PROMO BRUSHED NICKEL | 36| 8 +Brand#32 |PROMO BRUSHED STEEL | 3| 8 +Brand#32 |PROMO BRUSHED TIN | 23| 8 +Brand#32 |PROMO BURNISHED BRASS | 14| 8 +Brand#32 |PROMO BURNISHED BRASS | 45| 8 +Brand#32 |PROMO BURNISHED COPPER | 3| 8 +Brand#32 |PROMO BURNISHED COPPER | 19| 8 +Brand#32 |PROMO BURNISHED COPPER | 49| 8 +Brand#32 |PROMO BURNISHED NICKEL | 3| 8 +Brand#32 |PROMO BURNISHED NICKEL | 19| 8 +Brand#32 |PROMO BURNISHED NICKEL | 49| 8 +Brand#32 |PROMO BURNISHED TIN | 3| 8 +Brand#32 |PROMO BURNISHED TIN | 14| 8 +Brand#32 |PROMO BURNISHED TIN | 45| 8 +Brand#32 |PROMO PLATED BRASS | 9| 8 +Brand#32 |PROMO PLATED COPPER | 19| 8 +Brand#32 |PROMO PLATED NICKEL | 49| 8 +Brand#32 |PROMO PLATED STEEL | 14| 8 +Brand#32 |PROMO PLATED TIN | 19| 8 +Brand#32 |PROMO POLISHED BRASS | 3| 8 +Brand#32 |PROMO POLISHED BRASS | 23| 8 +Brand#32 |PROMO POLISHED BRASS | 49| 8 +Brand#32 |PROMO POLISHED NICKEL | 3| 8 +Brand#32 |PROMO POLISHED NICKEL | 36| 8 +Brand#32 |PROMO POLISHED STEEL | 3| 8 +Brand#32 |PROMO POLISHED TIN | 3| 8 +Brand#32 |PROMO POLISHED TIN | 9| 8 +Brand#32 |PROMO POLISHED TIN | 14| 8 +Brand#32 |PROMO POLISHED TIN | 36| 8 +Brand#32 |SMALL ANODIZED BRASS | 3| 8 +Brand#32 |SMALL ANODIZED BRASS | 49| 8 +Brand#32 |SMALL ANODIZED COPPER | 23| 8 +Brand#32 |SMALL ANODIZED STEEL | 3| 8 +Brand#32 |SMALL ANODIZED STEEL | 23| 8 +Brand#32 |SMALL ANODIZED TIN | 49| 8 +Brand#32 |SMALL BRUSHED BRASS | 36| 8 +Brand#32 |SMALL BRUSHED COPPER | 14| 8 +Brand#32 |SMALL BRUSHED COPPER | 23| 8 +Brand#32 |SMALL BRUSHED NICKEL | 3| 8 +Brand#32 |SMALL BRUSHED NICKEL | 36| 8 +Brand#32 |SMALL BRUSHED STEEL | 9| 8 +Brand#32 |SMALL BURNISHED BRASS | 9| 8 +Brand#32 |SMALL BURNISHED BRASS | 36| 8 +Brand#32 |SMALL BURNISHED BRASS | 49| 8 +Brand#32 |SMALL BURNISHED COPPER | 45| 8 +Brand#32 |SMALL BURNISHED NICKEL | 9| 8 +Brand#32 |SMALL BURNISHED STEEL | 3| 8 +Brand#32 |SMALL BURNISHED STEEL | 9| 8 +Brand#32 |SMALL BURNISHED STEEL | 14| 8 +Brand#32 |SMALL BURNISHED STEEL | 23| 8 +Brand#32 |SMALL BURNISHED TIN | 19| 8 +Brand#32 |SMALL BURNISHED TIN | 45| 8 +Brand#32 |SMALL PLATED COPPER | 23| 8 +Brand#32 |SMALL PLATED COPPER | 36| 8 +Brand#32 |SMALL PLATED NICKEL | 9| 8 +Brand#32 |SMALL PLATED STEEL | 3| 8 +Brand#32 |SMALL PLATED STEEL | 19| 8 +Brand#32 |SMALL PLATED TIN | 23| 8 +Brand#32 |SMALL PLATED TIN | 36| 8 +Brand#32 |SMALL PLATED TIN | 45| 8 +Brand#32 |SMALL POLISHED BRASS | 3| 8 +Brand#32 |SMALL POLISHED BRASS | 45| 8 +Brand#32 |SMALL POLISHED COPPER | 9| 8 +Brand#32 |SMALL POLISHED COPPER | 14| 8 +Brand#32 |SMALL POLISHED NICKEL | 49| 8 +Brand#32 |SMALL POLISHED STEEL | 3| 8 +Brand#32 |SMALL POLISHED STEEL | 14| 8 +Brand#32 |SMALL POLISHED STEEL | 45| 8 +Brand#32 |SMALL POLISHED TIN | 19| 8 +Brand#32 |SMALL POLISHED TIN | 45| 8 +Brand#32 |STANDARD ANODIZED BRASS | 19| 8 +Brand#32 |STANDARD ANODIZED NICKEL | 19| 8 +Brand#32 |STANDARD ANODIZED STEEL | 9| 8 +Brand#32 |STANDARD ANODIZED STEEL | 45| 8 +Brand#32 |STANDARD ANODIZED STEEL | 49| 8 +Brand#32 |STANDARD ANODIZED TIN | 36| 8 +Brand#32 |STANDARD BRUSHED COPPER | 36| 8 +Brand#32 |STANDARD BRUSHED NICKEL | 14| 8 +Brand#32 |STANDARD BRUSHED NICKEL | 19| 8 +Brand#32 |STANDARD BRUSHED STEEL | 3| 8 +Brand#32 |STANDARD BRUSHED TIN | 45| 8 +Brand#32 |STANDARD BURNISHED BRASS | 14| 8 +Brand#32 |STANDARD BURNISHED NICKEL| 14| 8 +Brand#32 |STANDARD BURNISHED NICKEL| 23| 8 +Brand#32 |STANDARD BURNISHED NICKEL| 49| 8 +Brand#32 |STANDARD BURNISHED STEEL | 14| 8 +Brand#32 |STANDARD BURNISHED STEEL | 45| 8 +Brand#32 |STANDARD BURNISHED STEEL | 49| 8 +Brand#32 |STANDARD BURNISHED TIN | 14| 8 +Brand#32 |STANDARD BURNISHED TIN | 23| 8 +Brand#32 |STANDARD PLATED BRASS | 3| 8 +Brand#32 |STANDARD PLATED BRASS | 9| 8 +Brand#32 |STANDARD PLATED COPPER | 9| 8 +Brand#32 |STANDARD PLATED COPPER | 14| 8 +Brand#32 |STANDARD PLATED NICKEL | 19| 8 +Brand#32 |STANDARD PLATED TIN | 9| 8 +Brand#32 |STANDARD POLISHED COPPER | 3| 8 +Brand#32 |STANDARD POLISHED COPPER | 23| 8 +Brand#32 |STANDARD POLISHED NICKEL | 9| 8 +Brand#32 |STANDARD POLISHED NICKEL | 14| 8 +Brand#32 |STANDARD POLISHED NICKEL | 49| 8 +Brand#32 |STANDARD POLISHED STEEL | 23| 8 +Brand#33 |ECONOMY ANODIZED BRASS | 3| 8 +Brand#33 |ECONOMY ANODIZED BRASS | 19| 8 +Brand#33 |ECONOMY ANODIZED COPPER | 3| 8 +Brand#33 |ECONOMY ANODIZED COPPER | 9| 8 +Brand#33 |ECONOMY ANODIZED COPPER | 23| 8 +Brand#33 |ECONOMY ANODIZED NICKEL | 3| 8 +Brand#33 |ECONOMY ANODIZED NICKEL | 23| 8 +Brand#33 |ECONOMY ANODIZED STEEL | 19| 8 +Brand#33 |ECONOMY BRUSHED BRASS | 14| 8 +Brand#33 |ECONOMY BRUSHED BRASS | 45| 8 +Brand#33 |ECONOMY BRUSHED COPPER | 9| 8 +Brand#33 |ECONOMY BRUSHED COPPER | 23| 8 +Brand#33 |ECONOMY BRUSHED COPPER | 45| 8 +Brand#33 |ECONOMY BRUSHED NICKEL | 14| 8 +Brand#33 |ECONOMY BRUSHED NICKEL | 45| 8 +Brand#33 |ECONOMY BRUSHED STEEL | 19| 8 +Brand#33 |ECONOMY BRUSHED TIN | 3| 8 +Brand#33 |ECONOMY BURNISHED BRASS | 3| 8 +Brand#33 |ECONOMY BURNISHED BRASS | 45| 8 +Brand#33 |ECONOMY BURNISHED BRASS | 49| 8 +Brand#33 |ECONOMY BURNISHED COPPER | 3| 8 +Brand#33 |ECONOMY BURNISHED COPPER | 14| 8 +Brand#33 |ECONOMY BURNISHED COPPER | 19| 8 +Brand#33 |ECONOMY BURNISHED STEEL | 9| 8 +Brand#33 |ECONOMY BURNISHED STEEL | 36| 8 +Brand#33 |ECONOMY BURNISHED TIN | 9| 8 +Brand#33 |ECONOMY PLATED BRASS | 3| 8 +Brand#33 |ECONOMY PLATED BRASS | 9| 8 +Brand#33 |ECONOMY PLATED BRASS | 19| 8 +Brand#33 |ECONOMY PLATED COPPER | 9| 8 +Brand#33 |ECONOMY PLATED COPPER | 14| 8 +Brand#33 |ECONOMY PLATED COPPER | 23| 8 +Brand#33 |ECONOMY PLATED COPPER | 49| 8 +Brand#33 |ECONOMY PLATED NICKEL | 3| 8 +Brand#33 |ECONOMY PLATED NICKEL | 36| 8 +Brand#33 |ECONOMY PLATED STEEL | 9| 8 +Brand#33 |ECONOMY PLATED STEEL | 19| 8 +Brand#33 |ECONOMY PLATED STEEL | 23| 8 +Brand#33 |ECONOMY POLISHED BRASS | 19| 8 +Brand#33 |ECONOMY POLISHED NICKEL | 14| 8 +Brand#33 |ECONOMY POLISHED STEEL | 9| 8 +Brand#33 |LARGE ANODIZED BRASS | 9| 8 +Brand#33 |LARGE ANODIZED BRASS | 49| 8 +Brand#33 |LARGE ANODIZED COPPER | 3| 8 +Brand#33 |LARGE ANODIZED COPPER | 19| 8 +Brand#33 |LARGE ANODIZED NICKEL | 19| 8 +Brand#33 |LARGE ANODIZED NICKEL | 45| 8 +Brand#33 |LARGE ANODIZED NICKEL | 49| 8 +Brand#33 |LARGE BRUSHED BRASS | 3| 8 +Brand#33 |LARGE BRUSHED BRASS | 14| 8 +Brand#33 |LARGE BRUSHED BRASS | 19| 8 +Brand#33 |LARGE BRUSHED BRASS | 49| 8 +Brand#33 |LARGE BRUSHED COPPER | 19| 8 +Brand#33 |LARGE BRUSHED COPPER | 49| 8 +Brand#33 |LARGE BRUSHED NICKEL | 9| 8 +Brand#33 |LARGE BRUSHED NICKEL | 14| 8 +Brand#33 |LARGE BRUSHED NICKEL | 49| 8 +Brand#33 |LARGE BRUSHED STEEL | 36| 8 +Brand#33 |LARGE BRUSHED TIN | 3| 8 +Brand#33 |LARGE BRUSHED TIN | 23| 8 +Brand#33 |LARGE BURNISHED BRASS | 3| 8 +Brand#33 |LARGE BURNISHED BRASS | 9| 8 +Brand#33 |LARGE BURNISHED COPPER | 14| 8 +Brand#33 |LARGE BURNISHED NICKEL | 3| 8 +Brand#33 |LARGE BURNISHED TIN | 9| 8 +Brand#33 |LARGE BURNISHED TIN | 14| 8 +Brand#33 |LARGE BURNISHED TIN | 45| 8 +Brand#33 |LARGE PLATED BRASS | 3| 8 +Brand#33 |LARGE PLATED BRASS | 45| 8 +Brand#33 |LARGE PLATED NICKEL | 3| 8 +Brand#33 |LARGE PLATED STEEL | 3| 8 +Brand#33 |LARGE PLATED STEEL | 14| 8 +Brand#33 |LARGE PLATED TIN | 36| 8 +Brand#33 |LARGE POLISHED BRASS | 23| 8 +Brand#33 |LARGE POLISHED NICKEL | 3| 8 +Brand#33 |LARGE POLISHED NICKEL | 14| 8 +Brand#33 |LARGE POLISHED STEEL | 36| 8 +Brand#33 |LARGE POLISHED TIN | 9| 8 +Brand#33 |LARGE POLISHED TIN | 36| 8 +Brand#33 |MEDIUM ANODIZED BRASS | 3| 8 +Brand#33 |MEDIUM ANODIZED COPPER | 3| 8 +Brand#33 |MEDIUM ANODIZED COPPER | 9| 8 +Brand#33 |MEDIUM ANODIZED STEEL | 3| 8 +Brand#33 |MEDIUM ANODIZED STEEL | 9| 8 +Brand#33 |MEDIUM ANODIZED TIN | 9| 8 +Brand#33 |MEDIUM ANODIZED TIN | 23| 8 +Brand#33 |MEDIUM ANODIZED TIN | 36| 8 +Brand#33 |MEDIUM BRUSHED BRASS | 19| 8 +Brand#33 |MEDIUM BRUSHED BRASS | 23| 8 +Brand#33 |MEDIUM BRUSHED COPPER | 14| 8 +Brand#33 |MEDIUM BRUSHED NICKEL | 23| 8 +Brand#33 |MEDIUM BRUSHED NICKEL | 45| 8 +Brand#33 |MEDIUM BURNISHED BRASS | 3| 8 +Brand#33 |MEDIUM BURNISHED COPPER | 14| 8 +Brand#33 |MEDIUM BURNISHED COPPER | 45| 8 +Brand#33 |MEDIUM BURNISHED COPPER | 49| 8 +Brand#33 |MEDIUM BURNISHED NICKEL | 9| 8 +Brand#33 |MEDIUM BURNISHED NICKEL | 19| 8 +Brand#33 |MEDIUM BURNISHED STEEL | 14| 8 +Brand#33 |MEDIUM BURNISHED TIN | 36| 8 +Brand#33 |MEDIUM PLATED BRASS | 3| 8 +Brand#33 |MEDIUM PLATED BRASS | 19| 8 +Brand#33 |MEDIUM PLATED NICKEL | 3| 8 +Brand#33 |MEDIUM PLATED NICKEL | 9| 8 +Brand#33 |MEDIUM PLATED NICKEL | 23| 8 +Brand#33 |MEDIUM PLATED NICKEL | 36| 8 +Brand#33 |MEDIUM PLATED NICKEL | 45| 8 +Brand#33 |MEDIUM PLATED STEEL | 14| 8 +Brand#33 |MEDIUM PLATED TIN | 14| 8 +Brand#33 |PROMO ANODIZED BRASS | 3| 8 +Brand#33 |PROMO ANODIZED BRASS | 9| 8 +Brand#33 |PROMO ANODIZED BRASS | 49| 8 +Brand#33 |PROMO ANODIZED COPPER | 14| 8 +Brand#33 |PROMO ANODIZED COPPER | 19| 8 +Brand#33 |PROMO ANODIZED NICKEL | 45| 8 +Brand#33 |PROMO ANODIZED STEEL | 9| 8 +Brand#33 |PROMO ANODIZED TIN | 45| 8 +Brand#33 |PROMO BRUSHED COPPER | 3| 8 +Brand#33 |PROMO BRUSHED COPPER | 9| 8 +Brand#33 |PROMO BRUSHED COPPER | 45| 8 +Brand#33 |PROMO BRUSHED COPPER | 49| 8 +Brand#33 |PROMO BRUSHED NICKEL | 14| 8 +Brand#33 |PROMO BRUSHED NICKEL | 36| 8 +Brand#33 |PROMO BRUSHED NICKEL | 49| 8 +Brand#33 |PROMO BRUSHED STEEL | 9| 8 +Brand#33 |PROMO BRUSHED STEEL | 49| 8 +Brand#33 |PROMO BRUSHED TIN | 3| 8 +Brand#33 |PROMO BRUSHED TIN | 9| 8 +Brand#33 |PROMO BURNISHED BRASS | 45| 8 +Brand#33 |PROMO BURNISHED BRASS | 49| 8 +Brand#33 |PROMO BURNISHED COPPER | 3| 8 +Brand#33 |PROMO BURNISHED COPPER | 23| 8 +Brand#33 |PROMO BURNISHED NICKEL | 3| 8 +Brand#33 |PROMO BURNISHED NICKEL | 14| 8 +Brand#33 |PROMO BURNISHED NICKEL | 19| 8 +Brand#33 |PROMO BURNISHED STEEL | 3| 8 +Brand#33 |PROMO BURNISHED STEEL | 14| 8 +Brand#33 |PROMO BURNISHED STEEL | 19| 8 +Brand#33 |PROMO BURNISHED STEEL | 36| 8 +Brand#33 |PROMO BURNISHED STEEL | 45| 8 +Brand#33 |PROMO PLATED BRASS | 3| 8 +Brand#33 |PROMO PLATED BRASS | 9| 8 +Brand#33 |PROMO PLATED BRASS | 45| 8 +Brand#33 |PROMO PLATED COPPER | 14| 8 +Brand#33 |PROMO PLATED COPPER | 19| 8 +Brand#33 |PROMO PLATED COPPER | 45| 8 +Brand#33 |PROMO PLATED NICKEL | 45| 8 +Brand#33 |PROMO PLATED STEEL | 9| 8 +Brand#33 |PROMO POLISHED BRASS | 3| 8 +Brand#33 |PROMO POLISHED BRASS | 9| 8 +Brand#33 |PROMO POLISHED BRASS | 14| 8 +Brand#33 |PROMO POLISHED BRASS | 36| 8 +Brand#33 |PROMO POLISHED BRASS | 49| 8 +Brand#33 |PROMO POLISHED COPPER | 45| 8 +Brand#33 |PROMO POLISHED NICKEL | 9| 8 +Brand#33 |PROMO POLISHED NICKEL | 49| 8 +Brand#33 |PROMO POLISHED STEEL | 3| 8 +Brand#33 |PROMO POLISHED STEEL | 19| 8 +Brand#33 |PROMO POLISHED TIN | 14| 8 +Brand#33 |PROMO POLISHED TIN | 45| 8 +Brand#33 |PROMO POLISHED TIN | 49| 8 +Brand#33 |SMALL ANODIZED BRASS | 23| 8 +Brand#33 |SMALL ANODIZED COPPER | 3| 8 +Brand#33 |SMALL ANODIZED COPPER | 14| 8 +Brand#33 |SMALL ANODIZED COPPER | 45| 8 +Brand#33 |SMALL ANODIZED COPPER | 49| 8 +Brand#33 |SMALL ANODIZED NICKEL | 3| 8 +Brand#33 |SMALL ANODIZED NICKEL | 45| 8 +Brand#33 |SMALL ANODIZED STEEL | 3| 8 +Brand#33 |SMALL ANODIZED STEEL | 9| 8 +Brand#33 |SMALL ANODIZED TIN | 49| 8 +Brand#33 |SMALL BRUSHED BRASS | 9| 8 +Brand#33 |SMALL BRUSHED BRASS | 23| 8 +Brand#33 |SMALL BRUSHED BRASS | 49| 8 +Brand#33 |SMALL BRUSHED STEEL | 3| 8 +Brand#33 |SMALL BRUSHED TIN | 9| 8 +Brand#33 |SMALL BRUSHED TIN | 19| 8 +Brand#33 |SMALL BURNISHED BRASS | 9| 8 +Brand#33 |SMALL BURNISHED BRASS | 14| 8 +Brand#33 |SMALL BURNISHED BRASS | 23| 8 +Brand#33 |SMALL BURNISHED COPPER | 36| 8 +Brand#33 |SMALL BURNISHED STEEL | 9| 8 +Brand#33 |SMALL BURNISHED STEEL | 14| 8 +Brand#33 |SMALL BURNISHED TIN | 23| 8 +Brand#33 |SMALL BURNISHED TIN | 36| 8 +Brand#33 |SMALL BURNISHED TIN | 45| 8 +Brand#33 |SMALL PLATED BRASS | 9| 8 +Brand#33 |SMALL PLATED BRASS | 49| 8 +Brand#33 |SMALL PLATED NICKEL | 14| 8 +Brand#33 |SMALL PLATED NICKEL | 19| 8 +Brand#33 |SMALL PLATED NICKEL | 36| 8 +Brand#33 |SMALL PLATED STEEL | 14| 8 +Brand#33 |SMALL PLATED STEEL | 23| 8 +Brand#33 |SMALL PLATED TIN | 23| 8 +Brand#33 |SMALL PLATED TIN | 36| 8 +Brand#33 |SMALL PLATED TIN | 49| 8 +Brand#33 |SMALL POLISHED BRASS | 9| 8 +Brand#33 |SMALL POLISHED BRASS | 23| 8 +Brand#33 |SMALL POLISHED BRASS | 45| 8 +Brand#33 |SMALL POLISHED COPPER | 3| 8 +Brand#33 |SMALL POLISHED STEEL | 23| 8 +Brand#33 |SMALL POLISHED STEEL | 49| 8 +Brand#33 |SMALL POLISHED TIN | 19| 8 +Brand#33 |SMALL POLISHED TIN | 23| 8 +Brand#33 |SMALL POLISHED TIN | 45| 8 +Brand#33 |STANDARD ANODIZED COPPER | 3| 8 +Brand#33 |STANDARD ANODIZED COPPER | 19| 8 +Brand#33 |STANDARD ANODIZED COPPER | 23| 8 +Brand#33 |STANDARD ANODIZED COPPER | 49| 8 +Brand#33 |STANDARD ANODIZED NICKEL | 45| 8 +Brand#33 |STANDARD ANODIZED STEEL | 19| 8 +Brand#33 |STANDARD ANODIZED STEEL | 36| 8 +Brand#33 |STANDARD ANODIZED STEEL | 49| 8 +Brand#33 |STANDARD ANODIZED TIN | 23| 8 +Brand#33 |STANDARD ANODIZED TIN | 49| 8 +Brand#33 |STANDARD BRUSHED BRASS | 9| 8 +Brand#33 |STANDARD BRUSHED COPPER | 3| 8 +Brand#33 |STANDARD BRUSHED COPPER | 19| 8 +Brand#33 |STANDARD BRUSHED COPPER | 36| 8 +Brand#33 |STANDARD BRUSHED NICKEL | 23| 8 +Brand#33 |STANDARD BRUSHED NICKEL | 49| 8 +Brand#33 |STANDARD BRUSHED STEEL | 9| 8 +Brand#33 |STANDARD BRUSHED TIN | 19| 8 +Brand#33 |STANDARD BURNISHED BRASS | 14| 8 +Brand#33 |STANDARD BURNISHED BRASS | 23| 8 +Brand#33 |STANDARD BURNISHED BRASS | 49| 8 +Brand#33 |STANDARD BURNISHED COPPER| 19| 8 +Brand#33 |STANDARD BURNISHED NICKEL| 36| 8 +Brand#33 |STANDARD BURNISHED STEEL | 36| 8 +Brand#33 |STANDARD PLATED BRASS | 14| 8 +Brand#33 |STANDARD PLATED BRASS | 36| 8 +Brand#33 |STANDARD PLATED BRASS | 45| 8 +Brand#33 |STANDARD PLATED BRASS | 49| 8 +Brand#33 |STANDARD PLATED COPPER | 14| 8 +Brand#33 |STANDARD PLATED COPPER | 19| 8 +Brand#33 |STANDARD PLATED COPPER | 45| 8 +Brand#33 |STANDARD PLATED COPPER | 49| 8 +Brand#33 |STANDARD PLATED NICKEL | 36| 8 +Brand#33 |STANDARD PLATED STEEL | 3| 8 +Brand#33 |STANDARD PLATED STEEL | 9| 8 +Brand#33 |STANDARD PLATED STEEL | 23| 8 +Brand#33 |STANDARD PLATED STEEL | 49| 8 +Brand#33 |STANDARD PLATED TIN | 14| 8 +Brand#33 |STANDARD PLATED TIN | 49| 8 +Brand#33 |STANDARD POLISHED BRASS | 19| 8 +Brand#33 |STANDARD POLISHED COPPER | 3| 8 +Brand#33 |STANDARD POLISHED COPPER | 9| 8 +Brand#33 |STANDARD POLISHED COPPER | 23| 8 +Brand#33 |STANDARD POLISHED NICKEL | 14| 8 +Brand#33 |STANDARD POLISHED STEEL | 14| 8 +Brand#33 |STANDARD POLISHED STEEL | 19| 8 +Brand#33 |STANDARD POLISHED STEEL | 49| 8 +Brand#34 |ECONOMY ANODIZED BRASS | 14| 8 +Brand#34 |ECONOMY ANODIZED COPPER | 9| 8 +Brand#34 |ECONOMY ANODIZED COPPER | 14| 8 +Brand#34 |ECONOMY ANODIZED COPPER | 45| 8 +Brand#34 |ECONOMY ANODIZED STEEL | 49| 8 +Brand#34 |ECONOMY ANODIZED TIN | 19| 8 +Brand#34 |ECONOMY ANODIZED TIN | 23| 8 +Brand#34 |ECONOMY ANODIZED TIN | 36| 8 +Brand#34 |ECONOMY ANODIZED TIN | 49| 8 +Brand#34 |ECONOMY BRUSHED BRASS | 9| 8 +Brand#34 |ECONOMY BRUSHED BRASS | 14| 8 +Brand#34 |ECONOMY BRUSHED BRASS | 36| 8 +Brand#34 |ECONOMY BRUSHED COPPER | 3| 8 +Brand#34 |ECONOMY BRUSHED NICKEL | 23| 8 +Brand#34 |ECONOMY BRUSHED STEEL | 3| 8 +Brand#34 |ECONOMY BRUSHED STEEL | 19| 8 +Brand#34 |ECONOMY BRUSHED TIN | 14| 8 +Brand#34 |ECONOMY BURNISHED NICKEL | 45| 8 +Brand#34 |ECONOMY BURNISHED TIN | 3| 8 +Brand#34 |ECONOMY BURNISHED TIN | 9| 8 +Brand#34 |ECONOMY BURNISHED TIN | 19| 8 +Brand#34 |ECONOMY PLATED BRASS | 9| 8 +Brand#34 |ECONOMY PLATED BRASS | 14| 8 +Brand#34 |ECONOMY PLATED BRASS | 45| 8 +Brand#34 |ECONOMY PLATED COPPER | 49| 8 +Brand#34 |ECONOMY PLATED NICKEL | 23| 8 +Brand#34 |ECONOMY PLATED NICKEL | 36| 8 +Brand#34 |ECONOMY PLATED NICKEL | 45| 8 +Brand#34 |ECONOMY PLATED STEEL | 3| 8 +Brand#34 |ECONOMY PLATED STEEL | 9| 8 +Brand#34 |ECONOMY PLATED TIN | 45| 8 +Brand#34 |ECONOMY POLISHED BRASS | 14| 8 +Brand#34 |ECONOMY POLISHED BRASS | 19| 8 +Brand#34 |ECONOMY POLISHED BRASS | 36| 8 +Brand#34 |ECONOMY POLISHED COPPER | 14| 8 +Brand#34 |ECONOMY POLISHED COPPER | 19| 8 +Brand#34 |ECONOMY POLISHED COPPER | 45| 8 +Brand#34 |ECONOMY POLISHED STEEL | 14| 8 +Brand#34 |ECONOMY POLISHED STEEL | 23| 8 +Brand#34 |ECONOMY POLISHED STEEL | 45| 8 +Brand#34 |LARGE ANODIZED TIN | 36| 8 +Brand#34 |LARGE BRUSHED BRASS | 14| 8 +Brand#34 |LARGE BRUSHED BRASS | 49| 8 +Brand#34 |LARGE BRUSHED STEEL | 19| 8 +Brand#34 |LARGE BRUSHED STEEL | 49| 8 +Brand#34 |LARGE BRUSHED TIN | 9| 8 +Brand#34 |LARGE BURNISHED BRASS | 36| 8 +Brand#34 |LARGE BURNISHED BRASS | 45| 8 +Brand#34 |LARGE BURNISHED COPPER | 3| 8 +Brand#34 |LARGE BURNISHED COPPER | 14| 8 +Brand#34 |LARGE BURNISHED COPPER | 36| 8 +Brand#34 |LARGE BURNISHED NICKEL | 3| 8 +Brand#34 |LARGE BURNISHED STEEL | 19| 8 +Brand#34 |LARGE BURNISHED TIN | 3| 8 +Brand#34 |LARGE PLATED COPPER | 3| 8 +Brand#34 |LARGE PLATED COPPER | 14| 8 +Brand#34 |LARGE PLATED COPPER | 36| 8 +Brand#34 |LARGE PLATED COPPER | 49| 8 +Brand#34 |LARGE PLATED NICKEL | 14| 8 +Brand#34 |LARGE PLATED STEEL | 23| 8 +Brand#34 |LARGE PLATED TIN | 19| 8 +Brand#34 |LARGE POLISHED BRASS | 23| 8 +Brand#34 |LARGE POLISHED COPPER | 9| 8 +Brand#34 |LARGE POLISHED NICKEL | 19| 8 +Brand#34 |LARGE POLISHED STEEL | 3| 8 +Brand#34 |LARGE POLISHED STEEL | 23| 8 +Brand#34 |LARGE POLISHED STEEL | 36| 8 +Brand#34 |LARGE POLISHED TIN | 19| 8 +Brand#34 |LARGE POLISHED TIN | 36| 8 +Brand#34 |LARGE POLISHED TIN | 45| 8 +Brand#34 |MEDIUM ANODIZED BRASS | 9| 8 +Brand#34 |MEDIUM ANODIZED BRASS | 23| 8 +Brand#34 |MEDIUM ANODIZED BRASS | 49| 8 +Brand#34 |MEDIUM ANODIZED STEEL | 49| 8 +Brand#34 |MEDIUM ANODIZED TIN | 9| 8 +Brand#34 |MEDIUM ANODIZED TIN | 14| 8 +Brand#34 |MEDIUM BRUSHED COPPER | 19| 8 +Brand#34 |MEDIUM BRUSHED COPPER | 45| 8 +Brand#34 |MEDIUM BRUSHED COPPER | 49| 8 +Brand#34 |MEDIUM BRUSHED NICKEL | 23| 8 +Brand#34 |MEDIUM BRUSHED STEEL | 36| 8 +Brand#34 |MEDIUM BRUSHED TIN | 9| 8 +Brand#34 |MEDIUM BURNISHED BRASS | 49| 8 +Brand#34 |MEDIUM BURNISHED COPPER | 3| 8 +Brand#34 |MEDIUM BURNISHED NICKEL | 36| 8 +Brand#34 |MEDIUM BURNISHED TIN | 3| 8 +Brand#34 |MEDIUM PLATED BRASS | 19| 8 +Brand#34 |MEDIUM PLATED COPPER | 14| 8 +Brand#34 |MEDIUM PLATED COPPER | 49| 8 +Brand#34 |MEDIUM PLATED STEEL | 14| 8 +Brand#34 |MEDIUM PLATED STEEL | 23| 8 +Brand#34 |MEDIUM PLATED TIN | 14| 8 +Brand#34 |MEDIUM PLATED TIN | 19| 8 +Brand#34 |MEDIUM PLATED TIN | 36| 8 +Brand#34 |PROMO ANODIZED BRASS | 3| 8 +Brand#34 |PROMO ANODIZED COPPER | 14| 8 +Brand#34 |PROMO ANODIZED COPPER | 45| 8 +Brand#34 |PROMO ANODIZED NICKEL | 14| 8 +Brand#34 |PROMO ANODIZED NICKEL | 19| 8 +Brand#34 |PROMO ANODIZED STEEL | 14| 8 +Brand#34 |PROMO ANODIZED STEEL | 23| 8 +Brand#34 |PROMO ANODIZED TIN | 3| 8 +Brand#34 |PROMO ANODIZED TIN | 9| 8 +Brand#34 |PROMO ANODIZED TIN | 14| 8 +Brand#34 |PROMO BRUSHED BRASS | 9| 8 +Brand#34 |PROMO BRUSHED BRASS | 19| 8 +Brand#34 |PROMO BRUSHED BRASS | 23| 8 +Brand#34 |PROMO BRUSHED BRASS | 45| 8 +Brand#34 |PROMO BRUSHED COPPER | 14| 8 +Brand#34 |PROMO BRUSHED STEEL | 36| 8 +Brand#34 |PROMO BRUSHED TIN | 3| 8 +Brand#34 |PROMO BRUSHED TIN | 45| 8 +Brand#34 |PROMO BURNISHED BRASS | 14| 8 +Brand#34 |PROMO BURNISHED BRASS | 36| 8 +Brand#34 |PROMO BURNISHED NICKEL | 19| 8 +Brand#34 |PROMO BURNISHED STEEL | 9| 8 +Brand#34 |PROMO BURNISHED STEEL | 45| 8 +Brand#34 |PROMO BURNISHED STEEL | 49| 8 +Brand#34 |PROMO BURNISHED TIN | 14| 8 +Brand#34 |PROMO BURNISHED TIN | 36| 8 +Brand#34 |PROMO PLATED BRASS | 9| 8 +Brand#34 |PROMO PLATED BRASS | 23| 8 +Brand#34 |PROMO PLATED BRASS | 49| 8 +Brand#34 |PROMO PLATED NICKEL | 23| 8 +Brand#34 |PROMO PLATED STEEL | 9| 8 +Brand#34 |PROMO PLATED STEEL | 14| 8 +Brand#34 |PROMO POLISHED BRASS | 23| 8 +Brand#34 |PROMO POLISHED COPPER | 14| 8 +Brand#34 |PROMO POLISHED NICKEL | 19| 8 +Brand#34 |PROMO POLISHED STEEL | 9| 8 +Brand#34 |PROMO POLISHED STEEL | 19| 8 +Brand#34 |PROMO POLISHED STEEL | 49| 8 +Brand#34 |PROMO POLISHED TIN | 9| 8 +Brand#34 |PROMO POLISHED TIN | 45| 8 +Brand#34 |SMALL ANODIZED BRASS | 49| 8 +Brand#34 |SMALL ANODIZED NICKEL | 23| 8 +Brand#34 |SMALL ANODIZED NICKEL | 36| 8 +Brand#34 |SMALL ANODIZED NICKEL | 49| 8 +Brand#34 |SMALL ANODIZED STEEL | 49| 8 +Brand#34 |SMALL ANODIZED TIN | 49| 8 +Brand#34 |SMALL BRUSHED BRASS | 14| 8 +Brand#34 |SMALL BRUSHED NICKEL | 14| 8 +Brand#34 |SMALL BRUSHED NICKEL | 45| 8 +Brand#34 |SMALL BRUSHED STEEL | 9| 8 +Brand#34 |SMALL BRUSHED STEEL | 14| 8 +Brand#34 |SMALL BURNISHED BRASS | 19| 8 +Brand#34 |SMALL BURNISHED BRASS | 45| 8 +Brand#34 |SMALL BURNISHED COPPER | 14| 8 +Brand#34 |SMALL BURNISHED COPPER | 23| 8 +Brand#34 |SMALL BURNISHED NICKEL | 3| 8 +Brand#34 |SMALL BURNISHED NICKEL | 49| 8 +Brand#34 |SMALL BURNISHED TIN | 36| 8 +Brand#34 |SMALL BURNISHED TIN | 49| 8 +Brand#34 |SMALL PLATED BRASS | 23| 8 +Brand#34 |SMALL PLATED COPPER | 19| 8 +Brand#34 |SMALL PLATED COPPER | 23| 8 +Brand#34 |SMALL PLATED COPPER | 49| 8 +Brand#34 |SMALL PLATED NICKEL | 3| 8 +Brand#34 |SMALL PLATED NICKEL | 9| 8 +Brand#34 |SMALL PLATED NICKEL | 23| 8 +Brand#34 |SMALL PLATED STEEL | 9| 8 +Brand#34 |SMALL PLATED STEEL | 45| 8 +Brand#34 |SMALL PLATED TIN | 14| 8 +Brand#34 |SMALL PLATED TIN | 19| 8 +Brand#34 |SMALL POLISHED BRASS | 14| 8 +Brand#34 |SMALL POLISHED COPPER | 9| 8 +Brand#34 |SMALL POLISHED COPPER | 45| 8 +Brand#34 |STANDARD ANODIZED BRASS | 14| 8 +Brand#34 |STANDARD ANODIZED BRASS | 23| 8 +Brand#34 |STANDARD ANODIZED COPPER | 3| 8 +Brand#34 |STANDARD ANODIZED COPPER | 45| 8 +Brand#34 |STANDARD ANODIZED NICKEL | 3| 8 +Brand#34 |STANDARD ANODIZED NICKEL | 9| 8 +Brand#34 |STANDARD ANODIZED NICKEL | 23| 8 +Brand#34 |STANDARD ANODIZED NICKEL | 36| 8 +Brand#34 |STANDARD ANODIZED STEEL | 3| 8 +Brand#34 |STANDARD ANODIZED STEEL | 45| 8 +Brand#34 |STANDARD ANODIZED TIN | 36| 8 +Brand#34 |STANDARD BRUSHED COPPER | 49| 8 +Brand#34 |STANDARD BRUSHED NICKEL | 19| 8 +Brand#34 |STANDARD BRUSHED NICKEL | 45| 8 +Brand#34 |STANDARD BRUSHED TIN | 49| 8 +Brand#34 |STANDARD BURNISHED BRASS | 14| 8 +Brand#34 |STANDARD BURNISHED BRASS | 19| 8 +Brand#34 |STANDARD BURNISHED BRASS | 49| 8 +Brand#34 |STANDARD BURNISHED COPPER| 9| 8 +Brand#34 |STANDARD BURNISHED COPPER| 45| 8 +Brand#34 |STANDARD BURNISHED NICKEL| 14| 8 +Brand#34 |STANDARD BURNISHED NICKEL| 49| 8 +Brand#34 |STANDARD BURNISHED STEEL | 9| 8 +Brand#34 |STANDARD BURNISHED TIN | 9| 8 +Brand#34 |STANDARD BURNISHED TIN | 23| 8 +Brand#34 |STANDARD PLATED NICKEL | 3| 8 +Brand#34 |STANDARD PLATED NICKEL | 19| 8 +Brand#34 |STANDARD PLATED NICKEL | 36| 8 +Brand#34 |STANDARD PLATED STEEL | 23| 8 +Brand#34 |STANDARD PLATED STEEL | 49| 8 +Brand#34 |STANDARD PLATED TIN | 14| 8 +Brand#34 |STANDARD PLATED TIN | 19| 8 +Brand#34 |STANDARD PLATED TIN | 45| 8 +Brand#34 |STANDARD POLISHED BRASS | 9| 8 +Brand#34 |STANDARD POLISHED BRASS | 19| 8 +Brand#34 |STANDARD POLISHED COPPER | 36| 8 +Brand#34 |STANDARD POLISHED NICKEL | 36| 8 +Brand#34 |STANDARD POLISHED STEEL | 3| 8 +Brand#34 |STANDARD POLISHED STEEL | 9| 8 +Brand#34 |STANDARD POLISHED STEEL | 23| 8 +Brand#34 |STANDARD POLISHED TIN | 3| 8 +Brand#35 |ECONOMY ANODIZED BRASS | 23| 8 +Brand#35 |ECONOMY ANODIZED COPPER | 3| 8 +Brand#35 |ECONOMY ANODIZED COPPER | 49| 8 +Brand#35 |ECONOMY ANODIZED NICKEL | 3| 8 +Brand#35 |ECONOMY ANODIZED NICKEL | 9| 8 +Brand#35 |ECONOMY ANODIZED NICKEL | 49| 8 +Brand#35 |ECONOMY ANODIZED STEEL | 36| 8 +Brand#35 |ECONOMY ANODIZED TIN | 19| 8 +Brand#35 |ECONOMY ANODIZED TIN | 23| 8 +Brand#35 |ECONOMY BRUSHED BRASS | 3| 8 +Brand#35 |ECONOMY BRUSHED COPPER | 23| 8 +Brand#35 |ECONOMY BRUSHED NICKEL | 14| 8 +Brand#35 |ECONOMY BRUSHED STEEL | 23| 8 +Brand#35 |ECONOMY BRUSHED STEEL | 36| 8 +Brand#35 |ECONOMY BRUSHED STEEL | 45| 8 +Brand#35 |ECONOMY BRUSHED TIN | 3| 8 +Brand#35 |ECONOMY BRUSHED TIN | 9| 8 +Brand#35 |ECONOMY BRUSHED TIN | 23| 8 +Brand#35 |ECONOMY BRUSHED TIN | 36| 8 +Brand#35 |ECONOMY BURNISHED BRASS | 3| 8 +Brand#35 |ECONOMY BURNISHED COPPER | 19| 8 +Brand#35 |ECONOMY BURNISHED COPPER | 23| 8 +Brand#35 |ECONOMY BURNISHED NICKEL | 23| 8 +Brand#35 |ECONOMY BURNISHED TIN | 3| 8 +Brand#35 |ECONOMY BURNISHED TIN | 23| 8 +Brand#35 |ECONOMY BURNISHED TIN | 45| 8 +Brand#35 |ECONOMY PLATED BRASS | 45| 8 +Brand#35 |ECONOMY PLATED BRASS | 49| 8 +Brand#35 |ECONOMY PLATED COPPER | 19| 8 +Brand#35 |ECONOMY PLATED NICKEL | 3| 8 +Brand#35 |ECONOMY PLATED NICKEL | 23| 8 +Brand#35 |ECONOMY PLATED STEEL | 19| 8 +Brand#35 |ECONOMY PLATED STEEL | 23| 8 +Brand#35 |ECONOMY PLATED TIN | 36| 8 +Brand#35 |ECONOMY POLISHED BRASS | 14| 8 +Brand#35 |ECONOMY POLISHED BRASS | 36| 8 +Brand#35 |ECONOMY POLISHED COPPER | 3| 8 +Brand#35 |ECONOMY POLISHED COPPER | 14| 8 +Brand#35 |ECONOMY POLISHED NICKEL | 9| 8 +Brand#35 |ECONOMY POLISHED TIN | 19| 8 +Brand#35 |LARGE ANODIZED BRASS | 23| 8 +Brand#35 |LARGE ANODIZED BRASS | 36| 8 +Brand#35 |LARGE ANODIZED COPPER | 9| 8 +Brand#35 |LARGE ANODIZED COPPER | 14| 8 +Brand#35 |LARGE ANODIZED STEEL | 9| 8 +Brand#35 |LARGE ANODIZED STEEL | 19| 8 +Brand#35 |LARGE ANODIZED STEEL | 23| 8 +Brand#35 |LARGE ANODIZED TIN | 9| 8 +Brand#35 |LARGE ANODIZED TIN | 14| 8 +Brand#35 |LARGE BRUSHED BRASS | 23| 8 +Brand#35 |LARGE BRUSHED COPPER | 9| 8 +Brand#35 |LARGE BRUSHED COPPER | 19| 8 +Brand#35 |LARGE BRUSHED STEEL | 14| 8 +Brand#35 |LARGE BRUSHED STEEL | 19| 8 +Brand#35 |LARGE BRUSHED TIN | 23| 8 +Brand#35 |LARGE BRUSHED TIN | 45| 8 +Brand#35 |LARGE BURNISHED BRASS | 14| 8 +Brand#35 |LARGE BURNISHED BRASS | 19| 8 +Brand#35 |LARGE BURNISHED BRASS | 36| 8 +Brand#35 |LARGE BURNISHED COPPER | 3| 8 +Brand#35 |LARGE BURNISHED NICKEL | 14| 8 +Brand#35 |LARGE BURNISHED NICKEL | 23| 8 +Brand#35 |LARGE BURNISHED TIN | 3| 8 +Brand#35 |LARGE BURNISHED TIN | 9| 8 +Brand#35 |LARGE BURNISHED TIN | 19| 8 +Brand#35 |LARGE BURNISHED TIN | 23| 8 +Brand#35 |LARGE BURNISHED TIN | 36| 8 +Brand#35 |LARGE PLATED BRASS | 9| 8 +Brand#35 |LARGE PLATED BRASS | 45| 8 +Brand#35 |LARGE PLATED COPPER | 23| 8 +Brand#35 |LARGE PLATED NICKEL | 3| 8 +Brand#35 |LARGE PLATED NICKEL | 19| 8 +Brand#35 |LARGE PLATED STEEL | 23| 8 +Brand#35 |LARGE PLATED TIN | 9| 8 +Brand#35 |LARGE PLATED TIN | 45| 8 +Brand#35 |LARGE POLISHED BRASS | 19| 8 +Brand#35 |LARGE POLISHED BRASS | 49| 8 +Brand#35 |LARGE POLISHED STEEL | 14| 8 +Brand#35 |LARGE POLISHED STEEL | 36| 8 +Brand#35 |LARGE POLISHED TIN | 9| 8 +Brand#35 |LARGE POLISHED TIN | 14| 8 +Brand#35 |MEDIUM ANODIZED COPPER | 9| 8 +Brand#35 |MEDIUM ANODIZED STEEL | 3| 8 +Brand#35 |MEDIUM ANODIZED TIN | 14| 8 +Brand#35 |MEDIUM ANODIZED TIN | 45| 8 +Brand#35 |MEDIUM ANODIZED TIN | 49| 8 +Brand#35 |MEDIUM BRUSHED BRASS | 19| 8 +Brand#35 |MEDIUM BRUSHED BRASS | 23| 8 +Brand#35 |MEDIUM BRUSHED COPPER | 19| 8 +Brand#35 |MEDIUM BRUSHED COPPER | 36| 8 +Brand#35 |MEDIUM BRUSHED NICKEL | 9| 8 +Brand#35 |MEDIUM BRUSHED STEEL | 3| 8 +Brand#35 |MEDIUM BRUSHED TIN | 14| 8 +Brand#35 |MEDIUM BRUSHED TIN | 19| 8 +Brand#35 |MEDIUM BURNISHED BRASS | 49| 8 +Brand#35 |MEDIUM BURNISHED STEEL | 45| 8 +Brand#35 |MEDIUM BURNISHED TIN | 9| 8 +Brand#35 |MEDIUM BURNISHED TIN | 19| 8 +Brand#35 |MEDIUM BURNISHED TIN | 23| 8 +Brand#35 |MEDIUM BURNISHED TIN | 36| 8 +Brand#35 |MEDIUM BURNISHED TIN | 45| 8 +Brand#35 |MEDIUM PLATED BRASS | 3| 8 +Brand#35 |MEDIUM PLATED BRASS | 23| 8 +Brand#35 |MEDIUM PLATED BRASS | 36| 8 +Brand#35 |MEDIUM PLATED COPPER | 3| 8 +Brand#35 |MEDIUM PLATED COPPER | 9| 8 +Brand#35 |MEDIUM PLATED COPPER | 19| 8 +Brand#35 |MEDIUM PLATED NICKEL | 49| 8 +Brand#35 |MEDIUM PLATED STEEL | 14| 8 +Brand#35 |MEDIUM PLATED STEEL | 23| 8 +Brand#35 |MEDIUM PLATED STEEL | 36| 8 +Brand#35 |MEDIUM PLATED TIN | 23| 8 +Brand#35 |PROMO ANODIZED BRASS | 3| 8 +Brand#35 |PROMO ANODIZED COPPER | 3| 8 +Brand#35 |PROMO ANODIZED COPPER | 36| 8 +Brand#35 |PROMO ANODIZED NICKEL | 36| 8 +Brand#35 |PROMO ANODIZED NICKEL | 45| 8 +Brand#35 |PROMO ANODIZED NICKEL | 49| 8 +Brand#35 |PROMO ANODIZED STEEL | 45| 8 +Brand#35 |PROMO ANODIZED TIN | 14| 8 +Brand#35 |PROMO BRUSHED BRASS | 14| 8 +Brand#35 |PROMO BRUSHED BRASS | 45| 8 +Brand#35 |PROMO BRUSHED COPPER | 3| 8 +Brand#35 |PROMO BRUSHED COPPER | 14| 8 +Brand#35 |PROMO BRUSHED NICKEL | 9| 8 +Brand#35 |PROMO BRUSHED STEEL | 9| 8 +Brand#35 |PROMO BRUSHED TIN | 19| 8 +Brand#35 |PROMO BRUSHED TIN | 45| 8 +Brand#35 |PROMO BURNISHED BRASS | 3| 8 +Brand#35 |PROMO BURNISHED BRASS | 19| 8 +Brand#35 |PROMO BURNISHED COPPER | 9| 8 +Brand#35 |PROMO BURNISHED COPPER | 14| 8 +Brand#35 |PROMO BURNISHED COPPER | 19| 8 +Brand#35 |PROMO BURNISHED NICKEL | 14| 8 +Brand#35 |PROMO BURNISHED TIN | 3| 8 +Brand#35 |PROMO BURNISHED TIN | 45| 8 +Brand#35 |PROMO PLATED BRASS | 19| 8 +Brand#35 |PROMO PLATED COPPER | 23| 8 +Brand#35 |PROMO PLATED NICKEL | 9| 8 +Brand#35 |PROMO PLATED NICKEL | 23| 8 +Brand#35 |PROMO PLATED NICKEL | 45| 8 +Brand#35 |PROMO PLATED STEEL | 9| 8 +Brand#35 |PROMO PLATED STEEL | 23| 8 +Brand#35 |PROMO PLATED STEEL | 36| 8 +Brand#35 |PROMO PLATED TIN | 3| 8 +Brand#35 |PROMO PLATED TIN | 9| 8 +Brand#35 |PROMO PLATED TIN | 19| 8 +Brand#35 |PROMO PLATED TIN | 36| 8 +Brand#35 |PROMO PLATED TIN | 45| 8 +Brand#35 |PROMO POLISHED BRASS | 3| 8 +Brand#35 |PROMO POLISHED BRASS | 9| 8 +Brand#35 |PROMO POLISHED BRASS | 23| 8 +Brand#35 |PROMO POLISHED NICKEL | 9| 8 +Brand#35 |PROMO POLISHED NICKEL | 23| 8 +Brand#35 |PROMO POLISHED TIN | 3| 8 +Brand#35 |PROMO POLISHED TIN | 23| 8 +Brand#35 |PROMO POLISHED TIN | 45| 8 +Brand#35 |SMALL ANODIZED BRASS | 49| 8 +Brand#35 |SMALL ANODIZED NICKEL | 9| 8 +Brand#35 |SMALL ANODIZED NICKEL | 19| 8 +Brand#35 |SMALL ANODIZED STEEL | 19| 8 +Brand#35 |SMALL ANODIZED TIN | 14| 8 +Brand#35 |SMALL ANODIZED TIN | 36| 8 +Brand#35 |SMALL BRUSHED BRASS | 14| 8 +Brand#35 |SMALL BRUSHED COPPER | 49| 8 +Brand#35 |SMALL BRUSHED NICKEL | 3| 8 +Brand#35 |SMALL BRUSHED NICKEL | 9| 8 +Brand#35 |SMALL BRUSHED NICKEL | 49| 8 +Brand#35 |SMALL BRUSHED STEEL | 9| 8 +Brand#35 |SMALL BRUSHED STEEL | 23| 8 +Brand#35 |SMALL BRUSHED STEEL | 36| 8 +Brand#35 |SMALL BRUSHED STEEL | 49| 8 +Brand#35 |SMALL BRUSHED TIN | 19| 8 +Brand#35 |SMALL BRUSHED TIN | 23| 8 +Brand#35 |SMALL BURNISHED COPPER | 49| 8 +Brand#35 |SMALL BURNISHED NICKEL | 9| 8 +Brand#35 |SMALL BURNISHED STEEL | 3| 8 +Brand#35 |SMALL BURNISHED STEEL | 14| 8 +Brand#35 |SMALL BURNISHED STEEL | 23| 8 +Brand#35 |SMALL BURNISHED STEEL | 36| 8 +Brand#35 |SMALL PLATED COPPER | 45| 8 +Brand#35 |SMALL PLATED NICKEL | 9| 8 +Brand#35 |SMALL PLATED NICKEL | 23| 8 +Brand#35 |SMALL PLATED NICKEL | 36| 8 +Brand#35 |SMALL PLATED NICKEL | 45| 8 +Brand#35 |SMALL PLATED STEEL | 3| 8 +Brand#35 |SMALL PLATED STEEL | 14| 8 +Brand#35 |SMALL PLATED TIN | 9| 8 +Brand#35 |SMALL POLISHED BRASS | 9| 8 +Brand#35 |SMALL POLISHED BRASS | 23| 8 +Brand#35 |SMALL POLISHED BRASS | 36| 8 +Brand#35 |SMALL POLISHED COPPER | 3| 8 +Brand#35 |SMALL POLISHED COPPER | 23| 8 +Brand#35 |SMALL POLISHED COPPER | 45| 8 +Brand#35 |SMALL POLISHED COPPER | 49| 8 +Brand#35 |SMALL POLISHED NICKEL | 14| 8 +Brand#35 |SMALL POLISHED NICKEL | 19| 8 +Brand#35 |SMALL POLISHED STEEL | 23| 8 +Brand#35 |SMALL POLISHED STEEL | 49| 8 +Brand#35 |SMALL POLISHED TIN | 9| 8 +Brand#35 |SMALL POLISHED TIN | 23| 8 +Brand#35 |SMALL POLISHED TIN | 45| 8 +Brand#35 |SMALL POLISHED TIN | 49| 8 +Brand#35 |STANDARD ANODIZED BRASS | 14| 8 +Brand#35 |STANDARD ANODIZED BRASS | 19| 8 +Brand#35 |STANDARD ANODIZED COPPER | 14| 8 +Brand#35 |STANDARD ANODIZED COPPER | 36| 8 +Brand#35 |STANDARD ANODIZED COPPER | 45| 8 +Brand#35 |STANDARD ANODIZED NICKEL | 14| 8 +Brand#35 |STANDARD ANODIZED NICKEL | 49| 8 +Brand#35 |STANDARD ANODIZED STEEL | 14| 8 +Brand#35 |STANDARD ANODIZED TIN | 23| 8 +Brand#35 |STANDARD ANODIZED TIN | 45| 8 +Brand#35 |STANDARD ANODIZED TIN | 49| 8 +Brand#35 |STANDARD BRUSHED BRASS | 19| 8 +Brand#35 |STANDARD BRUSHED BRASS | 23| 8 +Brand#35 |STANDARD BRUSHED BRASS | 36| 8 +Brand#35 |STANDARD BRUSHED COPPER | 14| 8 +Brand#35 |STANDARD BRUSHED COPPER | 23| 8 +Brand#35 |STANDARD BRUSHED COPPER | 36| 8 +Brand#35 |STANDARD BRUSHED NICKEL | 14| 8 +Brand#35 |STANDARD BRUSHED NICKEL | 49| 8 +Brand#35 |STANDARD BRUSHED TIN | 3| 8 +Brand#35 |STANDARD BURNISHED BRASS | 45| 8 +Brand#35 |STANDARD BURNISHED COPPER| 36| 8 +Brand#35 |STANDARD BURNISHED NICKEL| 9| 8 +Brand#35 |STANDARD BURNISHED NICKEL| 14| 8 +Brand#35 |STANDARD BURNISHED NICKEL| 49| 8 +Brand#35 |STANDARD BURNISHED STEEL | 14| 8 +Brand#35 |STANDARD BURNISHED TIN | 36| 8 +Brand#35 |STANDARD PLATED BRASS | 23| 8 +Brand#35 |STANDARD PLATED COPPER | 3| 8 +Brand#35 |STANDARD PLATED COPPER | 19| 8 +Brand#35 |STANDARD PLATED COPPER | 36| 8 +Brand#35 |STANDARD PLATED NICKEL | 14| 8 +Brand#35 |STANDARD PLATED TIN | 19| 8 +Brand#35 |STANDARD PLATED TIN | 23| 8 +Brand#35 |STANDARD PLATED TIN | 49| 8 +Brand#35 |STANDARD POLISHED BRASS | 19| 8 +Brand#35 |STANDARD POLISHED BRASS | 36| 8 +Brand#35 |STANDARD POLISHED NICKEL | 23| 8 +Brand#35 |STANDARD POLISHED STEEL | 14| 8 +Brand#35 |STANDARD POLISHED STEEL | 45| 8 +Brand#35 |STANDARD POLISHED STEEL | 49| 8 +Brand#35 |STANDARD POLISHED TIN | 45| 8 +Brand#41 |ECONOMY ANODIZED BRASS | 14| 8 +Brand#41 |ECONOMY ANODIZED BRASS | 19| 8 +Brand#41 |ECONOMY ANODIZED COPPER | 23| 8 +Brand#41 |ECONOMY ANODIZED NICKEL | 19| 8 +Brand#41 |ECONOMY ANODIZED NICKEL | 45| 8 +Brand#41 |ECONOMY ANODIZED STEEL | 45| 8 +Brand#41 |ECONOMY BRUSHED BRASS | 3| 8 +Brand#41 |ECONOMY BRUSHED BRASS | 14| 8 +Brand#41 |ECONOMY BRUSHED BRASS | 36| 8 +Brand#41 |ECONOMY BRUSHED COPPER | 3| 8 +Brand#41 |ECONOMY BRUSHED COPPER | 14| 8 +Brand#41 |ECONOMY BRUSHED COPPER | 19| 8 +Brand#41 |ECONOMY BRUSHED NICKEL | 19| 8 +Brand#41 |ECONOMY BRUSHED NICKEL | 36| 8 +Brand#41 |ECONOMY BRUSHED NICKEL | 45| 8 +Brand#41 |ECONOMY BRUSHED STEEL | 3| 8 +Brand#41 |ECONOMY BRUSHED STEEL | 45| 8 +Brand#41 |ECONOMY BRUSHED TIN | 14| 8 +Brand#41 |ECONOMY BRUSHED TIN | 36| 8 +Brand#41 |ECONOMY BURNISHED BRASS | 3| 8 +Brand#41 |ECONOMY BURNISHED BRASS | 45| 8 +Brand#41 |ECONOMY BURNISHED COPPER | 9| 8 +Brand#41 |ECONOMY BURNISHED NICKEL | 45| 8 +Brand#41 |ECONOMY BURNISHED NICKEL | 49| 8 +Brand#41 |ECONOMY BURNISHED STEEL | 23| 8 +Brand#41 |ECONOMY BURNISHED TIN | 3| 8 +Brand#41 |ECONOMY PLATED BRASS | 49| 8 +Brand#41 |ECONOMY PLATED COPPER | 14| 8 +Brand#41 |ECONOMY PLATED NICKEL | 14| 8 +Brand#41 |ECONOMY PLATED NICKEL | 45| 8 +Brand#41 |ECONOMY PLATED STEEL | 9| 8 +Brand#41 |ECONOMY PLATED STEEL | 23| 8 +Brand#41 |ECONOMY PLATED STEEL | 45| 8 +Brand#41 |ECONOMY PLATED TIN | 19| 8 +Brand#41 |ECONOMY PLATED TIN | 49| 8 +Brand#41 |ECONOMY POLISHED BRASS | 14| 8 +Brand#41 |ECONOMY POLISHED BRASS | 23| 8 +Brand#41 |ECONOMY POLISHED BRASS | 49| 8 +Brand#41 |ECONOMY POLISHED COPPER | 14| 8 +Brand#41 |ECONOMY POLISHED NICKEL | 49| 8 +Brand#41 |ECONOMY POLISHED TIN | 45| 8 +Brand#41 |ECONOMY POLISHED TIN | 49| 8 +Brand#41 |LARGE ANODIZED BRASS | 3| 8 +Brand#41 |LARGE ANODIZED BRASS | 45| 8 +Brand#41 |LARGE ANODIZED COPPER | 14| 8 +Brand#41 |LARGE ANODIZED NICKEL | 3| 8 +Brand#41 |LARGE ANODIZED STEEL | 14| 8 +Brand#41 |LARGE ANODIZED STEEL | 36| 8 +Brand#41 |LARGE ANODIZED TIN | 45| 8 +Brand#41 |LARGE BRUSHED BRASS | 23| 8 +Brand#41 |LARGE BRUSHED COPPER | 49| 8 +Brand#41 |LARGE BRUSHED TIN | 14| 8 +Brand#41 |LARGE BRUSHED TIN | 19| 8 +Brand#41 |LARGE BRUSHED TIN | 49| 8 +Brand#41 |LARGE BURNISHED BRASS | 19| 8 +Brand#41 |LARGE BURNISHED COPPER | 14| 8 +Brand#41 |LARGE BURNISHED COPPER | 49| 8 +Brand#41 |LARGE BURNISHED NICKEL | 14| 8 +Brand#41 |LARGE BURNISHED STEEL | 3| 8 +Brand#41 |LARGE BURNISHED STEEL | 14| 8 +Brand#41 |LARGE BURNISHED STEEL | 45| 8 +Brand#41 |LARGE BURNISHED STEEL | 49| 8 +Brand#41 |LARGE BURNISHED TIN | 3| 8 +Brand#41 |LARGE BURNISHED TIN | 9| 8 +Brand#41 |LARGE BURNISHED TIN | 36| 8 +Brand#41 |LARGE PLATED BRASS | 3| 8 +Brand#41 |LARGE PLATED BRASS | 14| 8 +Brand#41 |LARGE PLATED BRASS | 19| 8 +Brand#41 |LARGE PLATED BRASS | 45| 8 +Brand#41 |LARGE PLATED COPPER | 14| 8 +Brand#41 |LARGE PLATED COPPER | 23| 8 +Brand#41 |LARGE PLATED NICKEL | 3| 8 +Brand#41 |LARGE PLATED NICKEL | 9| 8 +Brand#41 |LARGE PLATED NICKEL | 36| 8 +Brand#41 |LARGE PLATED STEEL | 3| 8 +Brand#41 |LARGE PLATED STEEL | 23| 8 +Brand#41 |LARGE PLATED STEEL | 36| 8 +Brand#41 |LARGE PLATED STEEL | 49| 8 +Brand#41 |LARGE PLATED TIN | 3| 8 +Brand#41 |LARGE POLISHED BRASS | 19| 8 +Brand#41 |LARGE POLISHED COPPER | 3| 8 +Brand#41 |LARGE POLISHED COPPER | 19| 8 +Brand#41 |LARGE POLISHED COPPER | 49| 8 +Brand#41 |LARGE POLISHED NICKEL | 23| 8 +Brand#41 |LARGE POLISHED STEEL | 14| 8 +Brand#41 |LARGE POLISHED TIN | 9| 8 +Brand#41 |LARGE POLISHED TIN | 14| 8 +Brand#41 |MEDIUM ANODIZED BRASS | 3| 8 +Brand#41 |MEDIUM ANODIZED BRASS | 9| 8 +Brand#41 |MEDIUM ANODIZED BRASS | 36| 8 +Brand#41 |MEDIUM ANODIZED COPPER | 23| 8 +Brand#41 |MEDIUM ANODIZED NICKEL | 19| 8 +Brand#41 |MEDIUM ANODIZED NICKEL | 36| 8 +Brand#41 |MEDIUM ANODIZED STEEL | 23| 8 +Brand#41 |MEDIUM ANODIZED STEEL | 45| 8 +Brand#41 |MEDIUM ANODIZED TIN | 9| 8 +Brand#41 |MEDIUM ANODIZED TIN | 19| 8 +Brand#41 |MEDIUM ANODIZED TIN | 45| 8 +Brand#41 |MEDIUM BRUSHED BRASS | 3| 8 +Brand#41 |MEDIUM BRUSHED BRASS | 14| 8 +Brand#41 |MEDIUM BRUSHED BRASS | 45| 8 +Brand#41 |MEDIUM BRUSHED COPPER | 3| 8 +Brand#41 |MEDIUM BRUSHED COPPER | 14| 8 +Brand#41 |MEDIUM BRUSHED STEEL | 45| 8 +Brand#41 |MEDIUM BRUSHED STEEL | 49| 8 +Brand#41 |MEDIUM BRUSHED TIN | 9| 8 +Brand#41 |MEDIUM BRUSHED TIN | 23| 8 +Brand#41 |MEDIUM BRUSHED TIN | 49| 8 +Brand#41 |MEDIUM BURNISHED BRASS | 36| 8 +Brand#41 |MEDIUM BURNISHED COPPER | 9| 8 +Brand#41 |MEDIUM BURNISHED STEEL | 3| 8 +Brand#41 |MEDIUM BURNISHED STEEL | 45| 8 +Brand#41 |MEDIUM PLATED BRASS | 45| 8 +Brand#41 |MEDIUM PLATED COPPER | 9| 8 +Brand#41 |MEDIUM PLATED COPPER | 49| 8 +Brand#41 |MEDIUM PLATED NICKEL | 19| 8 +Brand#41 |MEDIUM PLATED NICKEL | 45| 8 +Brand#41 |MEDIUM PLATED STEEL | 9| 8 +Brand#41 |MEDIUM PLATED STEEL | 23| 8 +Brand#41 |PROMO ANODIZED COPPER | 14| 8 +Brand#41 |PROMO ANODIZED NICKEL | 3| 8 +Brand#41 |PROMO ANODIZED NICKEL | 19| 8 +Brand#41 |PROMO ANODIZED STEEL | 9| 8 +Brand#41 |PROMO ANODIZED TIN | 36| 8 +Brand#41 |PROMO BRUSHED BRASS | 9| 8 +Brand#41 |PROMO BRUSHED BRASS | 14| 8 +Brand#41 |PROMO BRUSHED BRASS | 19| 8 +Brand#41 |PROMO BRUSHED BRASS | 23| 8 +Brand#41 |PROMO BRUSHED BRASS | 36| 8 +Brand#41 |PROMO BRUSHED COPPER | 36| 8 +Brand#41 |PROMO BRUSHED STEEL | 9| 8 +Brand#41 |PROMO BRUSHED STEEL | 36| 8 +Brand#41 |PROMO BRUSHED STEEL | 49| 8 +Brand#41 |PROMO BRUSHED TIN | 9| 8 +Brand#41 |PROMO BRUSHED TIN | 49| 8 +Brand#41 |PROMO BURNISHED BRASS | 9| 8 +Brand#41 |PROMO BURNISHED BRASS | 14| 8 +Brand#41 |PROMO BURNISHED COPPER | 36| 8 +Brand#41 |PROMO BURNISHED COPPER | 45| 8 +Brand#41 |PROMO BURNISHED NICKEL | 36| 8 +Brand#41 |PROMO BURNISHED STEEL | 14| 8 +Brand#41 |PROMO BURNISHED STEEL | 36| 8 +Brand#41 |PROMO BURNISHED TIN | 3| 8 +Brand#41 |PROMO BURNISHED TIN | 23| 8 +Brand#41 |PROMO PLATED BRASS | 14| 8 +Brand#41 |PROMO PLATED BRASS | 36| 8 +Brand#41 |PROMO PLATED COPPER | 14| 8 +Brand#41 |PROMO PLATED COPPER | 23| 8 +Brand#41 |PROMO PLATED NICKEL | 49| 8 +Brand#41 |PROMO PLATED STEEL | 3| 8 +Brand#41 |PROMO PLATED STEEL | 14| 8 +Brand#41 |PROMO PLATED TIN | 45| 8 +Brand#41 |PROMO POLISHED BRASS | 9| 8 +Brand#41 |PROMO POLISHED COPPER | 3| 8 +Brand#41 |PROMO POLISHED COPPER | 19| 8 +Brand#41 |PROMO POLISHED COPPER | 49| 8 +Brand#41 |PROMO POLISHED NICKEL | 3| 8 +Brand#41 |PROMO POLISHED STEEL | 49| 8 +Brand#41 |PROMO POLISHED TIN | 14| 8 +Brand#41 |PROMO POLISHED TIN | 45| 8 +Brand#41 |SMALL ANODIZED BRASS | 14| 8 +Brand#41 |SMALL ANODIZED BRASS | 36| 8 +Brand#41 |SMALL ANODIZED COPPER | 49| 8 +Brand#41 |SMALL ANODIZED NICKEL | 14| 8 +Brand#41 |SMALL ANODIZED NICKEL | 19| 8 +Brand#41 |SMALL ANODIZED TIN | 3| 8 +Brand#41 |SMALL ANODIZED TIN | 9| 8 +Brand#41 |SMALL ANODIZED TIN | 23| 8 +Brand#41 |SMALL BRUSHED BRASS | 9| 8 +Brand#41 |SMALL BRUSHED BRASS | 23| 8 +Brand#41 |SMALL BRUSHED COPPER | 45| 8 +Brand#41 |SMALL BRUSHED COPPER | 49| 8 +Brand#41 |SMALL BRUSHED NICKEL | 14| 8 +Brand#41 |SMALL BRUSHED NICKEL | 36| 8 +Brand#41 |SMALL BRUSHED STEEL | 19| 8 +Brand#41 |SMALL BRUSHED TIN | 3| 8 +Brand#41 |SMALL BRUSHED TIN | 19| 8 +Brand#41 |SMALL BURNISHED BRASS | 14| 8 +Brand#41 |SMALL BURNISHED BRASS | 19| 8 +Brand#41 |SMALL BURNISHED COPPER | 9| 8 +Brand#41 |SMALL BURNISHED COPPER | 19| 8 +Brand#41 |SMALL BURNISHED NICKEL | 3| 8 +Brand#41 |SMALL BURNISHED NICKEL | 19| 8 +Brand#41 |SMALL BURNISHED NICKEL | 45| 8 +Brand#41 |SMALL BURNISHED STEEL | 9| 8 +Brand#41 |SMALL BURNISHED STEEL | 23| 8 +Brand#41 |SMALL BURNISHED STEEL | 45| 8 +Brand#41 |SMALL BURNISHED STEEL | 49| 8 +Brand#41 |SMALL BURNISHED TIN | 14| 8 +Brand#41 |SMALL PLATED BRASS | 3| 8 +Brand#41 |SMALL PLATED COPPER | 9| 8 +Brand#41 |SMALL PLATED COPPER | 14| 8 +Brand#41 |SMALL PLATED NICKEL | 3| 8 +Brand#41 |SMALL PLATED NICKEL | 36| 8 +Brand#41 |SMALL PLATED STEEL | 9| 8 +Brand#41 |SMALL PLATED STEEL | 36| 8 +Brand#41 |SMALL PLATED TIN | 19| 8 +Brand#41 |SMALL PLATED TIN | 49| 8 +Brand#41 |SMALL POLISHED BRASS | 45| 8 +Brand#41 |SMALL POLISHED COPPER | 3| 8 +Brand#41 |SMALL POLISHED COPPER | 14| 8 +Brand#41 |SMALL POLISHED COPPER | 23| 8 +Brand#41 |SMALL POLISHED NICKEL | 3| 8 +Brand#41 |SMALL POLISHED STEEL | 49| 8 +Brand#41 |SMALL POLISHED TIN | 9| 8 +Brand#41 |SMALL POLISHED TIN | 45| 8 +Brand#41 |STANDARD ANODIZED COPPER | 3| 8 +Brand#41 |STANDARD ANODIZED COPPER | 23| 8 +Brand#41 |STANDARD ANODIZED NICKEL | 3| 8 +Brand#41 |STANDARD ANODIZED NICKEL | 9| 8 +Brand#41 |STANDARD ANODIZED STEEL | 45| 8 +Brand#41 |STANDARD ANODIZED STEEL | 49| 8 +Brand#41 |STANDARD ANODIZED TIN | 19| 8 +Brand#41 |STANDARD ANODIZED TIN | 23| 8 +Brand#41 |STANDARD BRUSHED BRASS | 9| 8 +Brand#41 |STANDARD BRUSHED NICKEL | 3| 8 +Brand#41 |STANDARD BRUSHED NICKEL | 9| 8 +Brand#41 |STANDARD BRUSHED STEEL | 45| 8 +Brand#41 |STANDARD BRUSHED TIN | 9| 8 +Brand#41 |STANDARD BRUSHED TIN | 19| 8 +Brand#41 |STANDARD BRUSHED TIN | 45| 8 +Brand#41 |STANDARD BRUSHED TIN | 49| 8 +Brand#41 |STANDARD BURNISHED BRASS | 14| 8 +Brand#41 |STANDARD BURNISHED BRASS | 36| 8 +Brand#41 |STANDARD BURNISHED COPPER| 9| 8 +Brand#41 |STANDARD BURNISHED COPPER| 14| 8 +Brand#41 |STANDARD BURNISHED NICKEL| 19| 8 +Brand#41 |STANDARD BURNISHED STEEL | 3| 8 +Brand#41 |STANDARD BURNISHED STEEL | 49| 8 +Brand#41 |STANDARD BURNISHED TIN | 19| 8 +Brand#41 |STANDARD BURNISHED TIN | 45| 8 +Brand#41 |STANDARD PLATED BRASS | 19| 8 +Brand#41 |STANDARD PLATED NICKEL | 14| 8 +Brand#41 |STANDARD PLATED NICKEL | 19| 8 +Brand#41 |STANDARD PLATED NICKEL | 49| 8 +Brand#41 |STANDARD PLATED STEEL | 3| 8 +Brand#41 |STANDARD PLATED STEEL | 19| 8 +Brand#41 |STANDARD PLATED STEEL | 49| 8 +Brand#41 |STANDARD PLATED TIN | 45| 8 +Brand#41 |STANDARD PLATED TIN | 49| 8 +Brand#41 |STANDARD POLISHED BRASS | 14| 8 +Brand#41 |STANDARD POLISHED BRASS | 36| 8 +Brand#41 |STANDARD POLISHED COPPER | 14| 8 +Brand#41 |STANDARD POLISHED NICKEL | 36| 8 +Brand#41 |STANDARD POLISHED STEEL | 3| 8 +Brand#41 |STANDARD POLISHED STEEL | 36| 8 +Brand#41 |STANDARD POLISHED TIN | 19| 8 +Brand#41 |STANDARD POLISHED TIN | 45| 8 +Brand#42 |ECONOMY ANODIZED BRASS | 9| 8 +Brand#42 |ECONOMY ANODIZED BRASS | 19| 8 +Brand#42 |ECONOMY ANODIZED BRASS | 23| 8 +Brand#42 |ECONOMY ANODIZED COPPER | 23| 8 +Brand#42 |ECONOMY ANODIZED COPPER | 49| 8 +Brand#42 |ECONOMY ANODIZED NICKEL | 19| 8 +Brand#42 |ECONOMY ANODIZED NICKEL | 36| 8 +Brand#42 |ECONOMY ANODIZED STEEL | 49| 8 +Brand#42 |ECONOMY BRUSHED COPPER | 3| 8 +Brand#42 |ECONOMY BRUSHED NICKEL | 14| 8 +Brand#42 |ECONOMY BRUSHED STEEL | 23| 8 +Brand#42 |ECONOMY BRUSHED STEEL | 49| 8 +Brand#42 |ECONOMY BRUSHED TIN | 9| 8 +Brand#42 |ECONOMY BRUSHED TIN | 19| 8 +Brand#42 |ECONOMY BRUSHED TIN | 49| 8 +Brand#42 |ECONOMY BURNISHED COPPER | 3| 8 +Brand#42 |ECONOMY BURNISHED COPPER | 49| 8 +Brand#42 |ECONOMY BURNISHED NICKEL | 3| 8 +Brand#42 |ECONOMY BURNISHED TIN | 14| 8 +Brand#42 |ECONOMY BURNISHED TIN | 45| 8 +Brand#42 |ECONOMY PLATED BRASS | 9| 8 +Brand#42 |ECONOMY PLATED COPPER | 23| 8 +Brand#42 |ECONOMY PLATED COPPER | 36| 8 +Brand#42 |ECONOMY PLATED NICKEL | 19| 8 +Brand#42 |ECONOMY PLATED NICKEL | 49| 8 +Brand#42 |ECONOMY PLATED STEEL | 49| 8 +Brand#42 |ECONOMY PLATED TIN | 3| 8 +Brand#42 |ECONOMY POLISHED BRASS | 9| 8 +Brand#42 |ECONOMY POLISHED NICKEL | 49| 8 +Brand#42 |ECONOMY POLISHED STEEL | 9| 8 +Brand#42 |ECONOMY POLISHED STEEL | 36| 8 +Brand#42 |ECONOMY POLISHED TIN | 36| 8 +Brand#42 |LARGE ANODIZED BRASS | 3| 8 +Brand#42 |LARGE ANODIZED BRASS | 23| 8 +Brand#42 |LARGE ANODIZED COPPER | 3| 8 +Brand#42 |LARGE ANODIZED COPPER | 14| 8 +Brand#42 |LARGE ANODIZED COPPER | 49| 8 +Brand#42 |LARGE ANODIZED NICKEL | 9| 8 +Brand#42 |LARGE ANODIZED NICKEL | 45| 8 +Brand#42 |LARGE ANODIZED NICKEL | 49| 8 +Brand#42 |LARGE ANODIZED STEEL | 3| 8 +Brand#42 |LARGE ANODIZED STEEL | 9| 8 +Brand#42 |LARGE ANODIZED TIN | 14| 8 +Brand#42 |LARGE ANODIZED TIN | 45| 8 +Brand#42 |LARGE BRUSHED BRASS | 49| 8 +Brand#42 |LARGE BRUSHED COPPER | 9| 8 +Brand#42 |LARGE BRUSHED NICKEL | 19| 8 +Brand#42 |LARGE BRUSHED NICKEL | 36| 8 +Brand#42 |LARGE BRUSHED NICKEL | 49| 8 +Brand#42 |LARGE BRUSHED TIN | 23| 8 +Brand#42 |LARGE BRUSHED TIN | 49| 8 +Brand#42 |LARGE BURNISHED BRASS | 3| 8 +Brand#42 |LARGE BURNISHED BRASS | 49| 8 +Brand#42 |LARGE BURNISHED TIN | 45| 8 +Brand#42 |LARGE PLATED COPPER | 9| 8 +Brand#42 |LARGE PLATED COPPER | 45| 8 +Brand#42 |LARGE PLATED NICKEL | 45| 8 +Brand#42 |LARGE PLATED TIN | 3| 8 +Brand#42 |LARGE PLATED TIN | 45| 8 +Brand#42 |LARGE POLISHED COPPER | 49| 8 +Brand#42 |LARGE POLISHED NICKEL | 23| 8 +Brand#42 |LARGE POLISHED NICKEL | 36| 8 +Brand#42 |LARGE POLISHED STEEL | 3| 8 +Brand#42 |LARGE POLISHED TIN | 3| 8 +Brand#42 |LARGE POLISHED TIN | 19| 8 +Brand#42 |LARGE POLISHED TIN | 45| 8 +Brand#42 |MEDIUM ANODIZED BRASS | 9| 8 +Brand#42 |MEDIUM ANODIZED BRASS | 49| 8 +Brand#42 |MEDIUM ANODIZED COPPER | 3| 8 +Brand#42 |MEDIUM ANODIZED COPPER | 19| 8 +Brand#42 |MEDIUM ANODIZED COPPER | 49| 8 +Brand#42 |MEDIUM ANODIZED NICKEL | 36| 8 +Brand#42 |MEDIUM ANODIZED STEEL | 3| 8 +Brand#42 |MEDIUM ANODIZED TIN | 14| 8 +Brand#42 |MEDIUM ANODIZED TIN | 36| 8 +Brand#42 |MEDIUM ANODIZED TIN | 45| 8 +Brand#42 |MEDIUM BRUSHED COPPER | 14| 8 +Brand#42 |MEDIUM BRUSHED COPPER | 49| 8 +Brand#42 |MEDIUM BRUSHED NICKEL | 14| 8 +Brand#42 |MEDIUM BRUSHED STEEL | 36| 8 +Brand#42 |MEDIUM BRUSHED STEEL | 49| 8 +Brand#42 |MEDIUM BURNISHED BRASS | 45| 8 +Brand#42 |MEDIUM BURNISHED COPPER | 3| 8 +Brand#42 |MEDIUM BURNISHED NICKEL | 14| 8 +Brand#42 |MEDIUM BURNISHED STEEL | 9| 8 +Brand#42 |MEDIUM BURNISHED STEEL | 14| 8 +Brand#42 |MEDIUM BURNISHED STEEL | 36| 8 +Brand#42 |MEDIUM BURNISHED TIN | 3| 8 +Brand#42 |MEDIUM PLATED BRASS | 49| 8 +Brand#42 |MEDIUM PLATED COPPER | 3| 8 +Brand#42 |MEDIUM PLATED COPPER | 49| 8 +Brand#42 |MEDIUM PLATED NICKEL | 9| 8 +Brand#42 |MEDIUM PLATED STEEL | 9| 8 +Brand#42 |MEDIUM PLATED STEEL | 14| 8 +Brand#42 |MEDIUM PLATED STEEL | 36| 8 +Brand#42 |MEDIUM PLATED TIN | 9| 8 +Brand#42 |MEDIUM PLATED TIN | 14| 8 +Brand#42 |PROMO ANODIZED BRASS | 9| 8 +Brand#42 |PROMO ANODIZED BRASS | 36| 8 +Brand#42 |PROMO ANODIZED BRASS | 45| 8 +Brand#42 |PROMO ANODIZED COPPER | 3| 8 +Brand#42 |PROMO ANODIZED COPPER | 23| 8 +Brand#42 |PROMO ANODIZED COPPER | 45| 8 +Brand#42 |PROMO ANODIZED NICKEL | 9| 8 +Brand#42 |PROMO ANODIZED TIN | 3| 8 +Brand#42 |PROMO BRUSHED COPPER | 14| 8 +Brand#42 |PROMO BRUSHED STEEL | 19| 8 +Brand#42 |PROMO BRUSHED STEEL | 23| 8 +Brand#42 |PROMO BRUSHED STEEL | 45| 8 +Brand#42 |PROMO BURNISHED BRASS | 14| 8 +Brand#42 |PROMO BURNISHED BRASS | 45| 8 +Brand#42 |PROMO BURNISHED BRASS | 49| 8 +Brand#42 |PROMO BURNISHED COPPER | 45| 8 +Brand#42 |PROMO BURNISHED NICKEL | 36| 8 +Brand#42 |PROMO PLATED NICKEL | 23| 8 +Brand#42 |PROMO PLATED STEEL | 45| 8 +Brand#42 |PROMO PLATED TIN | 9| 8 +Brand#42 |PROMO PLATED TIN | 19| 8 +Brand#42 |PROMO PLATED TIN | 23| 8 +Brand#42 |PROMO PLATED TIN | 36| 8 +Brand#42 |PROMO PLATED TIN | 45| 8 +Brand#42 |PROMO POLISHED BRASS | 19| 8 +Brand#42 |PROMO POLISHED BRASS | 23| 8 +Brand#42 |PROMO POLISHED BRASS | 45| 8 +Brand#42 |PROMO POLISHED COPPER | 36| 8 +Brand#42 |PROMO POLISHED NICKEL | 3| 8 +Brand#42 |PROMO POLISHED NICKEL | 9| 8 +Brand#42 |PROMO POLISHED STEEL | 9| 8 +Brand#42 |PROMO POLISHED STEEL | 23| 8 +Brand#42 |PROMO POLISHED TIN | 3| 8 +Brand#42 |PROMO POLISHED TIN | 9| 8 +Brand#42 |SMALL ANODIZED BRASS | 19| 8 +Brand#42 |SMALL ANODIZED COPPER | 14| 8 +Brand#42 |SMALL ANODIZED COPPER | 19| 8 +Brand#42 |SMALL ANODIZED COPPER | 36| 8 +Brand#42 |SMALL ANODIZED NICKEL | 14| 8 +Brand#42 |SMALL ANODIZED NICKEL | 23| 8 +Brand#42 |SMALL ANODIZED NICKEL | 45| 8 +Brand#42 |SMALL ANODIZED STEEL | 3| 8 +Brand#42 |SMALL ANODIZED STEEL | 9| 8 +Brand#42 |SMALL ANODIZED STEEL | 36| 8 +Brand#42 |SMALL ANODIZED TIN | 3| 8 +Brand#42 |SMALL ANODIZED TIN | 19| 8 +Brand#42 |SMALL BRUSHED COPPER | 9| 8 +Brand#42 |SMALL BRUSHED COPPER | 36| 8 +Brand#42 |SMALL BRUSHED NICKEL | 23| 8 +Brand#42 |SMALL BRUSHED STEEL | 3| 8 +Brand#42 |SMALL BRUSHED STEEL | 9| 8 +Brand#42 |SMALL BRUSHED STEEL | 14| 8 +Brand#42 |SMALL BRUSHED STEEL | 36| 8 +Brand#42 |SMALL BRUSHED STEEL | 45| 8 +Brand#42 |SMALL BRUSHED TIN | 9| 8 +Brand#42 |SMALL BRUSHED TIN | 14| 8 +Brand#42 |SMALL BRUSHED TIN | 45| 8 +Brand#42 |SMALL BRUSHED TIN | 49| 8 +Brand#42 |SMALL BURNISHED BRASS | 23| 8 +Brand#42 |SMALL BURNISHED NICKEL | 19| 8 +Brand#42 |SMALL BURNISHED STEEL | 14| 8 +Brand#42 |SMALL PLATED BRASS | 19| 8 +Brand#42 |SMALL PLATED COPPER | 36| 8 +Brand#42 |SMALL PLATED STEEL | 3| 8 +Brand#42 |SMALL PLATED STEEL | 23| 8 +Brand#42 |SMALL PLATED STEEL | 36| 8 +Brand#42 |SMALL PLATED TIN | 14| 8 +Brand#42 |SMALL PLATED TIN | 19| 8 +Brand#42 |SMALL PLATED TIN | 36| 8 +Brand#42 |SMALL POLISHED BRASS | 23| 8 +Brand#42 |SMALL POLISHED BRASS | 45| 8 +Brand#42 |SMALL POLISHED COPPER | 23| 8 +Brand#42 |SMALL POLISHED COPPER | 45| 8 +Brand#42 |SMALL POLISHED NICKEL | 14| 8 +Brand#42 |SMALL POLISHED NICKEL | 19| 8 +Brand#42 |SMALL POLISHED NICKEL | 45| 8 +Brand#42 |SMALL POLISHED STEEL | 49| 8 +Brand#42 |SMALL POLISHED TIN | 14| 8 +Brand#42 |SMALL POLISHED TIN | 36| 8 +Brand#42 |SMALL POLISHED TIN | 49| 8 +Brand#42 |STANDARD ANODIZED BRASS | 36| 8 +Brand#42 |STANDARD ANODIZED COPPER | 14| 8 +Brand#42 |STANDARD ANODIZED STEEL | 3| 8 +Brand#42 |STANDARD ANODIZED STEEL | 9| 8 +Brand#42 |STANDARD ANODIZED STEEL | 45| 8 +Brand#42 |STANDARD ANODIZED TIN | 3| 8 +Brand#42 |STANDARD BRUSHED BRASS | 3| 8 +Brand#42 |STANDARD BRUSHED BRASS | 9| 8 +Brand#42 |STANDARD BRUSHED BRASS | 23| 8 +Brand#42 |STANDARD BRUSHED COPPER | 36| 8 +Brand#42 |STANDARD BRUSHED COPPER | 49| 8 +Brand#42 |STANDARD BRUSHED NICKEL | 23| 8 +Brand#42 |STANDARD BRUSHED NICKEL | 49| 8 +Brand#42 |STANDARD BRUSHED STEEL | 23| 8 +Brand#42 |STANDARD BRUSHED TIN | 49| 8 +Brand#42 |STANDARD BURNISHED BRASS | 9| 8 +Brand#42 |STANDARD BURNISHED BRASS | 14| 8 +Brand#42 |STANDARD BURNISHED BRASS | 49| 8 +Brand#42 |STANDARD BURNISHED NICKEL| 14| 8 +Brand#42 |STANDARD BURNISHED NICKEL| 49| 8 +Brand#42 |STANDARD BURNISHED STEEL | 36| 8 +Brand#42 |STANDARD BURNISHED TIN | 9| 8 +Brand#42 |STANDARD PLATED COPPER | 49| 8 +Brand#42 |STANDARD PLATED NICKEL | 14| 8 +Brand#42 |STANDARD PLATED NICKEL | 45| 8 +Brand#42 |STANDARD PLATED STEEL | 14| 8 +Brand#42 |STANDARD PLATED STEEL | 19| 8 +Brand#42 |STANDARD PLATED STEEL | 36| 8 +Brand#42 |STANDARD PLATED STEEL | 45| 8 +Brand#42 |STANDARD PLATED TIN | 9| 8 +Brand#42 |STANDARD PLATED TIN | 14| 8 +Brand#42 |STANDARD POLISHED BRASS | 19| 8 +Brand#42 |STANDARD POLISHED BRASS | 36| 8 +Brand#42 |STANDARD POLISHED COPPER | 14| 8 +Brand#42 |STANDARD POLISHED COPPER | 19| 8 +Brand#42 |STANDARD POLISHED COPPER | 49| 8 +Brand#42 |STANDARD POLISHED NICKEL | 14| 8 +Brand#42 |STANDARD POLISHED NICKEL | 23| 8 +Brand#42 |STANDARD POLISHED STEEL | 23| 8 +Brand#42 |STANDARD POLISHED TIN | 14| 8 +Brand#42 |STANDARD POLISHED TIN | 23| 8 +Brand#42 |STANDARD POLISHED TIN | 36| 8 +Brand#43 |ECONOMY ANODIZED BRASS | 3| 8 +Brand#43 |ECONOMY ANODIZED NICKEL | 3| 8 +Brand#43 |ECONOMY ANODIZED NICKEL | 49| 8 +Brand#43 |ECONOMY ANODIZED STEEL | 23| 8 +Brand#43 |ECONOMY ANODIZED STEEL | 36| 8 +Brand#43 |ECONOMY ANODIZED TIN | 49| 8 +Brand#43 |ECONOMY BRUSHED COPPER | 45| 8 +Brand#43 |ECONOMY BRUSHED NICKEL | 9| 8 +Brand#43 |ECONOMY BRUSHED NICKEL | 14| 8 +Brand#43 |ECONOMY BRUSHED NICKEL | 19| 8 +Brand#43 |ECONOMY BRUSHED NICKEL | 49| 8 +Brand#43 |ECONOMY BRUSHED TIN | 36| 8 +Brand#43 |ECONOMY BRUSHED TIN | 45| 8 +Brand#43 |ECONOMY BURNISHED BRASS | 19| 8 +Brand#43 |ECONOMY BURNISHED COPPER | 14| 8 +Brand#43 |ECONOMY BURNISHED COPPER | 36| 8 +Brand#43 |ECONOMY BURNISHED NICKEL | 9| 8 +Brand#43 |ECONOMY BURNISHED NICKEL | 14| 8 +Brand#43 |ECONOMY BURNISHED NICKEL | 23| 8 +Brand#43 |ECONOMY BURNISHED NICKEL | 45| 8 +Brand#43 |ECONOMY BURNISHED STEEL | 3| 8 +Brand#43 |ECONOMY BURNISHED STEEL | 36| 8 +Brand#43 |ECONOMY BURNISHED TIN | 3| 8 +Brand#43 |ECONOMY BURNISHED TIN | 49| 8 +Brand#43 |ECONOMY PLATED COPPER | 19| 8 +Brand#43 |ECONOMY PLATED NICKEL | 9| 8 +Brand#43 |ECONOMY PLATED STEEL | 19| 8 +Brand#43 |ECONOMY PLATED TIN | 9| 8 +Brand#43 |ECONOMY PLATED TIN | 19| 8 +Brand#43 |ECONOMY POLISHED BRASS | 19| 8 +Brand#43 |ECONOMY POLISHED COPPER | 19| 8 +Brand#43 |ECONOMY POLISHED COPPER | 36| 8 +Brand#43 |ECONOMY POLISHED NICKEL | 19| 8 +Brand#43 |ECONOMY POLISHED NICKEL | 36| 8 +Brand#43 |ECONOMY POLISHED STEEL | 3| 8 +Brand#43 |ECONOMY POLISHED TIN | 9| 8 +Brand#43 |ECONOMY POLISHED TIN | 36| 8 +Brand#43 |ECONOMY POLISHED TIN | 45| 8 +Brand#43 |LARGE ANODIZED BRASS | 14| 8 +Brand#43 |LARGE ANODIZED BRASS | 36| 8 +Brand#43 |LARGE ANODIZED COPPER | 19| 8 +Brand#43 |LARGE ANODIZED NICKEL | 3| 8 +Brand#43 |LARGE ANODIZED NICKEL | 23| 8 +Brand#43 |LARGE ANODIZED NICKEL | 36| 8 +Brand#43 |LARGE ANODIZED STEEL | 23| 8 +Brand#43 |LARGE ANODIZED STEEL | 49| 8 +Brand#43 |LARGE ANODIZED TIN | 19| 8 +Brand#43 |LARGE BRUSHED BRASS | 23| 8 +Brand#43 |LARGE BRUSHED COPPER | 19| 8 +Brand#43 |LARGE BRUSHED COPPER | 36| 8 +Brand#43 |LARGE BRUSHED NICKEL | 14| 8 +Brand#43 |LARGE BRUSHED NICKEL | 19| 8 +Brand#43 |LARGE BRUSHED NICKEL | 36| 8 +Brand#43 |LARGE BRUSHED NICKEL | 49| 8 +Brand#43 |LARGE BRUSHED STEEL | 3| 8 +Brand#43 |LARGE BRUSHED TIN | 23| 8 +Brand#43 |LARGE BURNISHED BRASS | 9| 8 +Brand#43 |LARGE BURNISHED BRASS | 14| 8 +Brand#43 |LARGE BURNISHED BRASS | 49| 8 +Brand#43 |LARGE BURNISHED COPPER | 3| 8 +Brand#43 |LARGE BURNISHED NICKEL | 36| 8 +Brand#43 |LARGE BURNISHED TIN | 23| 8 +Brand#43 |LARGE PLATED BRASS | 9| 8 +Brand#43 |LARGE PLATED BRASS | 45| 8 +Brand#43 |LARGE PLATED COPPER | 36| 8 +Brand#43 |LARGE PLATED NICKEL | 3| 8 +Brand#43 |LARGE PLATED NICKEL | 14| 8 +Brand#43 |LARGE PLATED NICKEL | 49| 8 +Brand#43 |LARGE PLATED STEEL | 3| 8 +Brand#43 |LARGE PLATED STEEL | 14| 8 +Brand#43 |LARGE PLATED STEEL | 49| 8 +Brand#43 |LARGE PLATED TIN | 23| 8 +Brand#43 |LARGE PLATED TIN | 36| 8 +Brand#43 |LARGE PLATED TIN | 45| 8 +Brand#43 |LARGE POLISHED BRASS | 36| 8 +Brand#43 |LARGE POLISHED COPPER | 3| 8 +Brand#43 |LARGE POLISHED COPPER | 14| 8 +Brand#43 |LARGE POLISHED COPPER | 36| 8 +Brand#43 |LARGE POLISHED NICKEL | 3| 8 +Brand#43 |LARGE POLISHED STEEL | 9| 8 +Brand#43 |LARGE POLISHED STEEL | 14| 8 +Brand#43 |LARGE POLISHED STEEL | 19| 8 +Brand#43 |MEDIUM ANODIZED BRASS | 49| 8 +Brand#43 |MEDIUM ANODIZED COPPER | 19| 8 +Brand#43 |MEDIUM ANODIZED COPPER | 23| 8 +Brand#43 |MEDIUM ANODIZED NICKEL | 3| 8 +Brand#43 |MEDIUM ANODIZED STEEL | 9| 8 +Brand#43 |MEDIUM ANODIZED STEEL | 19| 8 +Brand#43 |MEDIUM ANODIZED STEEL | 36| 8 +Brand#43 |MEDIUM BRUSHED BRASS | 9| 8 +Brand#43 |MEDIUM BRUSHED BRASS | 14| 8 +Brand#43 |MEDIUM BRUSHED COPPER | 45| 8 +Brand#43 |MEDIUM BRUSHED STEEL | 19| 8 +Brand#43 |MEDIUM BRUSHED STEEL | 49| 8 +Brand#43 |MEDIUM BRUSHED TIN | 49| 8 +Brand#43 |MEDIUM BURNISHED BRASS | 19| 8 +Brand#43 |MEDIUM BURNISHED NICKEL | 19| 8 +Brand#43 |MEDIUM BURNISHED NICKEL | 36| 8 +Brand#43 |MEDIUM BURNISHED STEEL | 23| 8 +Brand#43 |MEDIUM BURNISHED TIN | 14| 8 +Brand#43 |MEDIUM BURNISHED TIN | 36| 8 +Brand#43 |MEDIUM PLATED BRASS | 19| 8 +Brand#43 |MEDIUM PLATED BRASS | 36| 8 +Brand#43 |MEDIUM PLATED COPPER | 3| 8 +Brand#43 |MEDIUM PLATED COPPER | 49| 8 +Brand#43 |MEDIUM PLATED NICKEL | 36| 8 +Brand#43 |MEDIUM PLATED NICKEL | 45| 8 +Brand#43 |MEDIUM PLATED TIN | 45| 8 +Brand#43 |PROMO ANODIZED BRASS | 3| 8 +Brand#43 |PROMO ANODIZED BRASS | 9| 8 +Brand#43 |PROMO ANODIZED BRASS | 45| 8 +Brand#43 |PROMO ANODIZED NICKEL | 14| 8 +Brand#43 |PROMO ANODIZED NICKEL | 45| 8 +Brand#43 |PROMO ANODIZED STEEL | 49| 8 +Brand#43 |PROMO ANODIZED TIN | 3| 8 +Brand#43 |PROMO ANODIZED TIN | 14| 8 +Brand#43 |PROMO ANODIZED TIN | 19| 8 +Brand#43 |PROMO ANODIZED TIN | 49| 8 +Brand#43 |PROMO BRUSHED BRASS | 3| 8 +Brand#43 |PROMO BRUSHED BRASS | 45| 8 +Brand#43 |PROMO BRUSHED COPPER | 23| 8 +Brand#43 |PROMO BRUSHED NICKEL | 14| 8 +Brand#43 |PROMO BRUSHED NICKEL | 19| 8 +Brand#43 |PROMO BRUSHED STEEL | 14| 8 +Brand#43 |PROMO BURNISHED BRASS | 3| 8 +Brand#43 |PROMO BURNISHED BRASS | 49| 8 +Brand#43 |PROMO BURNISHED COPPER | 14| 8 +Brand#43 |PROMO BURNISHED NICKEL | 49| 8 +Brand#43 |PROMO BURNISHED STEEL | 49| 8 +Brand#43 |PROMO BURNISHED TIN | 9| 8 +Brand#43 |PROMO BURNISHED TIN | 36| 8 +Brand#43 |PROMO BURNISHED TIN | 49| 8 +Brand#43 |PROMO PLATED BRASS | 14| 8 +Brand#43 |PROMO PLATED COPPER | 45| 8 +Brand#43 |PROMO PLATED NICKEL | 45| 8 +Brand#43 |PROMO PLATED STEEL | 45| 8 +Brand#43 |PROMO PLATED TIN | 23| 8 +Brand#43 |PROMO POLISHED BRASS | 23| 8 +Brand#43 |PROMO POLISHED COPPER | 3| 8 +Brand#43 |PROMO POLISHED COPPER | 14| 8 +Brand#43 |PROMO POLISHED COPPER | 36| 8 +Brand#43 |PROMO POLISHED NICKEL | 14| 8 +Brand#43 |PROMO POLISHED NICKEL | 19| 8 +Brand#43 |PROMO POLISHED STEEL | 14| 8 +Brand#43 |PROMO POLISHED STEEL | 23| 8 +Brand#43 |PROMO POLISHED TIN | 3| 8 +Brand#43 |PROMO POLISHED TIN | 36| 8 +Brand#43 |SMALL ANODIZED BRASS | 19| 8 +Brand#43 |SMALL ANODIZED COPPER | 14| 8 +Brand#43 |SMALL ANODIZED COPPER | 19| 8 +Brand#43 |SMALL ANODIZED COPPER | 49| 8 +Brand#43 |SMALL ANODIZED NICKEL | 14| 8 +Brand#43 |SMALL ANODIZED NICKEL | 45| 8 +Brand#43 |SMALL ANODIZED STEEL | 49| 8 +Brand#43 |SMALL ANODIZED TIN | 49| 8 +Brand#43 |SMALL BRUSHED COPPER | 19| 8 +Brand#43 |SMALL BRUSHED COPPER | 49| 8 +Brand#43 |SMALL BRUSHED NICKEL | 9| 8 +Brand#43 |SMALL BRUSHED NICKEL | 49| 8 +Brand#43 |SMALL BRUSHED STEEL | 45| 8 +Brand#43 |SMALL BRUSHED TIN | 3| 8 +Brand#43 |SMALL BURNISHED COPPER | 23| 8 +Brand#43 |SMALL BURNISHED STEEL | 9| 8 +Brand#43 |SMALL BURNISHED STEEL | 45| 8 +Brand#43 |SMALL BURNISHED TIN | 9| 8 +Brand#43 |SMALL BURNISHED TIN | 49| 8 +Brand#43 |SMALL PLATED BRASS | 23| 8 +Brand#43 |SMALL PLATED BRASS | 45| 8 +Brand#43 |SMALL PLATED COPPER | 45| 8 +Brand#43 |SMALL PLATED NICKEL | 3| 8 +Brand#43 |SMALL PLATED NICKEL | 19| 8 +Brand#43 |SMALL PLATED NICKEL | 23| 8 +Brand#43 |SMALL PLATED NICKEL | 45| 8 +Brand#43 |SMALL PLATED NICKEL | 49| 8 +Brand#43 |SMALL PLATED STEEL | 14| 8 +Brand#43 |SMALL PLATED STEEL | 36| 8 +Brand#43 |SMALL PLATED TIN | 14| 8 +Brand#43 |SMALL POLISHED BRASS | 9| 8 +Brand#43 |SMALL POLISHED BRASS | 19| 8 +Brand#43 |SMALL POLISHED COPPER | 9| 8 +Brand#43 |SMALL POLISHED COPPER | 19| 8 +Brand#43 |SMALL POLISHED NICKEL | 3| 8 +Brand#43 |SMALL POLISHED NICKEL | 36| 8 +Brand#43 |SMALL POLISHED STEEL | 45| 8 +Brand#43 |SMALL POLISHED STEEL | 49| 8 +Brand#43 |SMALL POLISHED TIN | 36| 8 +Brand#43 |STANDARD ANODIZED COPPER | 3| 8 +Brand#43 |STANDARD ANODIZED COPPER | 9| 8 +Brand#43 |STANDARD ANODIZED COPPER | 14| 8 +Brand#43 |STANDARD ANODIZED COPPER | 49| 8 +Brand#43 |STANDARD ANODIZED NICKEL | 49| 8 +Brand#43 |STANDARD ANODIZED STEEL | 3| 8 +Brand#43 |STANDARD ANODIZED STEEL | 14| 8 +Brand#43 |STANDARD ANODIZED STEEL | 45| 8 +Brand#43 |STANDARD ANODIZED STEEL | 49| 8 +Brand#43 |STANDARD ANODIZED TIN | 14| 8 +Brand#43 |STANDARD BRUSHED BRASS | 14| 8 +Brand#43 |STANDARD BRUSHED BRASS | 36| 8 +Brand#43 |STANDARD BRUSHED NICKEL | 49| 8 +Brand#43 |STANDARD BRUSHED TIN | 19| 8 +Brand#43 |STANDARD BRUSHED TIN | 45| 8 +Brand#43 |STANDARD BRUSHED TIN | 49| 8 +Brand#43 |STANDARD BURNISHED BRASS | 23| 8 +Brand#43 |STANDARD BURNISHED BRASS | 49| 8 +Brand#43 |STANDARD BURNISHED COPPER| 9| 8 +Brand#43 |STANDARD BURNISHED COPPER| 14| 8 +Brand#43 |STANDARD BURNISHED COPPER| 45| 8 +Brand#43 |STANDARD BURNISHED NICKEL| 19| 8 +Brand#43 |STANDARD BURNISHED NICKEL| 49| 8 +Brand#43 |STANDARD BURNISHED STEEL | 9| 8 +Brand#43 |STANDARD BURNISHED STEEL | 19| 8 +Brand#43 |STANDARD BURNISHED STEEL | 45| 8 +Brand#43 |STANDARD BURNISHED TIN | 19| 8 +Brand#43 |STANDARD PLATED COPPER | 36| 8 +Brand#43 |STANDARD PLATED NICKEL | 19| 8 +Brand#43 |STANDARD PLATED NICKEL | 49| 8 +Brand#43 |STANDARD PLATED TIN | 14| 8 +Brand#43 |STANDARD PLATED TIN | 23| 8 +Brand#43 |STANDARD POLISHED BRASS | 19| 8 +Brand#43 |STANDARD POLISHED COPPER | 45| 8 +Brand#43 |STANDARD POLISHED NICKEL | 3| 8 +Brand#43 |STANDARD POLISHED NICKEL | 14| 8 +Brand#43 |STANDARD POLISHED NICKEL | 23| 8 +Brand#43 |STANDARD POLISHED NICKEL | 36| 8 +Brand#43 |STANDARD POLISHED NICKEL | 45| 8 +Brand#43 |STANDARD POLISHED STEEL | 14| 8 +Brand#43 |STANDARD POLISHED STEEL | 49| 8 +Brand#44 |ECONOMY ANODIZED BRASS | 3| 8 +Brand#44 |ECONOMY ANODIZED COPPER | 23| 8 +Brand#44 |ECONOMY ANODIZED COPPER | 49| 8 +Brand#44 |ECONOMY ANODIZED NICKEL | 23| 8 +Brand#44 |ECONOMY ANODIZED STEEL | 19| 8 +Brand#44 |ECONOMY ANODIZED STEEL | 45| 8 +Brand#44 |ECONOMY ANODIZED TIN | 14| 8 +Brand#44 |ECONOMY ANODIZED TIN | 36| 8 +Brand#44 |ECONOMY BRUSHED COPPER | 23| 8 +Brand#44 |ECONOMY BRUSHED STEEL | 9| 8 +Brand#44 |ECONOMY BRUSHED STEEL | 19| 8 +Brand#44 |ECONOMY BRUSHED TIN | 19| 8 +Brand#44 |ECONOMY BRUSHED TIN | 49| 8 +Brand#44 |ECONOMY BURNISHED COPPER | 3| 8 +Brand#44 |ECONOMY BURNISHED COPPER | 9| 8 +Brand#44 |ECONOMY BURNISHED COPPER | 14| 8 +Brand#44 |ECONOMY BURNISHED COPPER | 23| 8 +Brand#44 |ECONOMY BURNISHED COPPER | 49| 8 +Brand#44 |ECONOMY BURNISHED NICKEL | 23| 8 +Brand#44 |ECONOMY BURNISHED NICKEL | 49| 8 +Brand#44 |ECONOMY BURNISHED STEEL | 9| 8 +Brand#44 |ECONOMY BURNISHED STEEL | 19| 8 +Brand#44 |ECONOMY BURNISHED STEEL | 49| 8 +Brand#44 |ECONOMY BURNISHED TIN | 3| 8 +Brand#44 |ECONOMY BURNISHED TIN | 19| 8 +Brand#44 |ECONOMY BURNISHED TIN | 45| 8 +Brand#44 |ECONOMY PLATED COPPER | 45| 8 +Brand#44 |ECONOMY PLATED NICKEL | 23| 8 +Brand#44 |ECONOMY PLATED STEEL | 14| 8 +Brand#44 |ECONOMY PLATED STEEL | 23| 8 +Brand#44 |ECONOMY PLATED STEEL | 36| 8 +Brand#44 |ECONOMY PLATED TIN | 19| 8 +Brand#44 |ECONOMY POLISHED BRASS | 23| 8 +Brand#44 |ECONOMY POLISHED BRASS | 36| 8 +Brand#44 |ECONOMY POLISHED COPPER | 9| 8 +Brand#44 |ECONOMY POLISHED COPPER | 19| 8 +Brand#44 |ECONOMY POLISHED NICKEL | 23| 8 +Brand#44 |ECONOMY POLISHED NICKEL | 36| 8 +Brand#44 |ECONOMY POLISHED NICKEL | 45| 8 +Brand#44 |ECONOMY POLISHED NICKEL | 49| 8 +Brand#44 |ECONOMY POLISHED STEEL | 9| 8 +Brand#44 |ECONOMY POLISHED STEEL | 49| 8 +Brand#44 |ECONOMY POLISHED TIN | 3| 8 +Brand#44 |ECONOMY POLISHED TIN | 19| 8 +Brand#44 |LARGE ANODIZED BRASS | 3| 8 +Brand#44 |LARGE ANODIZED BRASS | 23| 8 +Brand#44 |LARGE ANODIZED BRASS | 49| 8 +Brand#44 |LARGE ANODIZED COPPER | 9| 8 +Brand#44 |LARGE ANODIZED COPPER | 45| 8 +Brand#44 |LARGE ANODIZED NICKEL | 49| 8 +Brand#44 |LARGE ANODIZED STEEL | 19| 8 +Brand#44 |LARGE ANODIZED TIN | 14| 8 +Brand#44 |LARGE BRUSHED BRASS | 14| 8 +Brand#44 |LARGE BRUSHED COPPER | 14| 8 +Brand#44 |LARGE BRUSHED NICKEL | 19| 8 +Brand#44 |LARGE BRUSHED NICKEL | 23| 8 +Brand#44 |LARGE BRUSHED NICKEL | 45| 8 +Brand#44 |LARGE BRUSHED TIN | 23| 8 +Brand#44 |LARGE BURNISHED COPPER | 9| 8 +Brand#44 |LARGE BURNISHED COPPER | 19| 8 +Brand#44 |LARGE BURNISHED COPPER | 23| 8 +Brand#44 |LARGE BURNISHED NICKEL | 36| 8 +Brand#44 |LARGE BURNISHED NICKEL | 49| 8 +Brand#44 |LARGE BURNISHED STEEL | 23| 8 +Brand#44 |LARGE BURNISHED STEEL | 49| 8 +Brand#44 |LARGE BURNISHED TIN | 14| 8 +Brand#44 |LARGE PLATED BRASS | 19| 8 +Brand#44 |LARGE PLATED COPPER | 14| 8 +Brand#44 |LARGE PLATED COPPER | 19| 8 +Brand#44 |LARGE PLATED NICKEL | 9| 8 +Brand#44 |LARGE PLATED NICKEL | 23| 8 +Brand#44 |LARGE PLATED STEEL | 23| 8 +Brand#44 |LARGE PLATED TIN | 14| 8 +Brand#44 |LARGE PLATED TIN | 19| 8 +Brand#44 |LARGE PLATED TIN | 36| 8 +Brand#44 |LARGE PLATED TIN | 49| 8 +Brand#44 |LARGE POLISHED BRASS | 9| 8 +Brand#44 |LARGE POLISHED BRASS | 19| 8 +Brand#44 |LARGE POLISHED BRASS | 23| 8 +Brand#44 |LARGE POLISHED COPPER | 9| 8 +Brand#44 |LARGE POLISHED COPPER | 49| 8 +Brand#44 |LARGE POLISHED NICKEL | 23| 8 +Brand#44 |LARGE POLISHED NICKEL | 36| 8 +Brand#44 |LARGE POLISHED STEEL | 45| 8 +Brand#44 |LARGE POLISHED TIN | 9| 8 +Brand#44 |MEDIUM ANODIZED BRASS | 36| 8 +Brand#44 |MEDIUM ANODIZED COPPER | 14| 8 +Brand#44 |MEDIUM ANODIZED COPPER | 49| 8 +Brand#44 |MEDIUM ANODIZED NICKEL | 19| 8 +Brand#44 |MEDIUM ANODIZED NICKEL | 45| 8 +Brand#44 |MEDIUM ANODIZED STEEL | 9| 8 +Brand#44 |MEDIUM ANODIZED STEEL | 23| 8 +Brand#44 |MEDIUM ANODIZED TIN | 45| 8 +Brand#44 |MEDIUM BRUSHED COPPER | 14| 8 +Brand#44 |MEDIUM BRUSHED NICKEL | 14| 8 +Brand#44 |MEDIUM BRUSHED STEEL | 14| 8 +Brand#44 |MEDIUM BRUSHED STEEL | 19| 8 +Brand#44 |MEDIUM BURNISHED BRASS | 3| 8 +Brand#44 |MEDIUM BURNISHED BRASS | 45| 8 +Brand#44 |MEDIUM BURNISHED COPPER | 45| 8 +Brand#44 |MEDIUM BURNISHED NICKEL | 3| 8 +Brand#44 |MEDIUM BURNISHED NICKEL | 14| 8 +Brand#44 |MEDIUM BURNISHED STEEL | 23| 8 +Brand#44 |MEDIUM BURNISHED TIN | 19| 8 +Brand#44 |MEDIUM BURNISHED TIN | 23| 8 +Brand#44 |MEDIUM PLATED BRASS | 3| 8 +Brand#44 |MEDIUM PLATED BRASS | 23| 8 +Brand#44 |MEDIUM PLATED COPPER | 3| 8 +Brand#44 |MEDIUM PLATED NICKEL | 23| 8 +Brand#44 |MEDIUM PLATED NICKEL | 49| 8 +Brand#44 |PROMO ANODIZED BRASS | 3| 8 +Brand#44 |PROMO ANODIZED BRASS | 14| 8 +Brand#44 |PROMO ANODIZED BRASS | 49| 8 +Brand#44 |PROMO ANODIZED COPPER | 23| 8 +Brand#44 |PROMO ANODIZED NICKEL | 23| 8 +Brand#44 |PROMO ANODIZED NICKEL | 36| 8 +Brand#44 |PROMO ANODIZED STEEL | 9| 8 +Brand#44 |PROMO ANODIZED STEEL | 49| 8 +Brand#44 |PROMO BRUSHED BRASS | 9| 8 +Brand#44 |PROMO BRUSHED COPPER | 9| 8 +Brand#44 |PROMO BRUSHED COPPER | 23| 8 +Brand#44 |PROMO BRUSHED COPPER | 36| 8 +Brand#44 |PROMO BRUSHED NICKEL | 23| 8 +Brand#44 |PROMO BRUSHED NICKEL | 45| 8 +Brand#44 |PROMO BRUSHED STEEL | 3| 8 +Brand#44 |PROMO BRUSHED STEEL | 9| 8 +Brand#44 |PROMO BRUSHED STEEL | 45| 8 +Brand#44 |PROMO BRUSHED STEEL | 49| 8 +Brand#44 |PROMO BRUSHED TIN | 3| 8 +Brand#44 |PROMO BRUSHED TIN | 19| 8 +Brand#44 |PROMO BRUSHED TIN | 45| 8 +Brand#44 |PROMO BURNISHED BRASS | 36| 8 +Brand#44 |PROMO BURNISHED NICKEL | 3| 8 +Brand#44 |PROMO BURNISHED STEEL | 9| 8 +Brand#44 |PROMO BURNISHED STEEL | 19| 8 +Brand#44 |PROMO BURNISHED STEEL | 49| 8 +Brand#44 |PROMO PLATED BRASS | 23| 8 +Brand#44 |PROMO PLATED NICKEL | 9| 8 +Brand#44 |PROMO PLATED NICKEL | 23| 8 +Brand#44 |PROMO PLATED STEEL | 23| 8 +Brand#44 |PROMO PLATED STEEL | 49| 8 +Brand#44 |PROMO PLATED TIN | 14| 8 +Brand#44 |PROMO PLATED TIN | 36| 8 +Brand#44 |PROMO POLISHED BRASS | 36| 8 +Brand#44 |PROMO POLISHED COPPER | 9| 8 +Brand#44 |PROMO POLISHED NICKEL | 45| 8 +Brand#44 |PROMO POLISHED STEEL | 9| 8 +Brand#44 |PROMO POLISHED STEEL | 45| 8 +Brand#44 |PROMO POLISHED TIN | 14| 8 +Brand#44 |PROMO POLISHED TIN | 23| 8 +Brand#44 |PROMO POLISHED TIN | 36| 8 +Brand#44 |PROMO POLISHED TIN | 45| 8 +Brand#44 |PROMO POLISHED TIN | 49| 8 +Brand#44 |SMALL ANODIZED BRASS | 3| 8 +Brand#44 |SMALL ANODIZED BRASS | 9| 8 +Brand#44 |SMALL ANODIZED BRASS | 36| 8 +Brand#44 |SMALL ANODIZED COPPER | 14| 8 +Brand#44 |SMALL ANODIZED COPPER | 19| 8 +Brand#44 |SMALL ANODIZED COPPER | 23| 8 +Brand#44 |SMALL ANODIZED NICKEL | 23| 8 +Brand#44 |SMALL ANODIZED TIN | 14| 8 +Brand#44 |SMALL ANODIZED TIN | 19| 8 +Brand#44 |SMALL ANODIZED TIN | 23| 8 +Brand#44 |SMALL ANODIZED TIN | 45| 8 +Brand#44 |SMALL BRUSHED BRASS | 14| 8 +Brand#44 |SMALL BRUSHED COPPER | 23| 8 +Brand#44 |SMALL BRUSHED TIN | 36| 8 +Brand#44 |SMALL BURNISHED BRASS | 3| 8 +Brand#44 |SMALL BURNISHED BRASS | 36| 8 +Brand#44 |SMALL BURNISHED BRASS | 49| 8 +Brand#44 |SMALL BURNISHED NICKEL | 14| 8 +Brand#44 |SMALL BURNISHED NICKEL | 45| 8 +Brand#44 |SMALL BURNISHED TIN | 9| 8 +Brand#44 |SMALL BURNISHED TIN | 23| 8 +Brand#44 |SMALL BURNISHED TIN | 49| 8 +Brand#44 |SMALL PLATED BRASS | 36| 8 +Brand#44 |SMALL PLATED COPPER | 14| 8 +Brand#44 |SMALL PLATED NICKEL | 45| 8 +Brand#44 |SMALL PLATED NICKEL | 49| 8 +Brand#44 |SMALL PLATED TIN | 19| 8 +Brand#44 |SMALL POLISHED COPPER | 9| 8 +Brand#44 |SMALL POLISHED COPPER | 49| 8 +Brand#44 |SMALL POLISHED NICKEL | 9| 8 +Brand#44 |SMALL POLISHED NICKEL | 14| 8 +Brand#44 |SMALL POLISHED NICKEL | 19| 8 +Brand#44 |SMALL POLISHED NICKEL | 23| 8 +Brand#44 |SMALL POLISHED NICKEL | 45| 8 +Brand#44 |SMALL POLISHED STEEL | 3| 8 +Brand#44 |SMALL POLISHED TIN | 3| 8 +Brand#44 |SMALL POLISHED TIN | 14| 8 +Brand#44 |SMALL POLISHED TIN | 19| 8 +Brand#44 |SMALL POLISHED TIN | 23| 8 +Brand#44 |SMALL POLISHED TIN | 45| 8 +Brand#44 |STANDARD ANODIZED COPPER | 3| 8 +Brand#44 |STANDARD ANODIZED COPPER | 19| 8 +Brand#44 |STANDARD ANODIZED STEEL | 14| 8 +Brand#44 |STANDARD ANODIZED STEEL | 45| 8 +Brand#44 |STANDARD ANODIZED TIN | 23| 8 +Brand#44 |STANDARD ANODIZED TIN | 36| 8 +Brand#44 |STANDARD BRUSHED BRASS | 14| 8 +Brand#44 |STANDARD BRUSHED BRASS | 45| 8 +Brand#44 |STANDARD BRUSHED COPPER | 19| 8 +Brand#44 |STANDARD BRUSHED NICKEL | 23| 8 +Brand#44 |STANDARD BRUSHED STEEL | 23| 8 +Brand#44 |STANDARD BRUSHED STEEL | 49| 8 +Brand#44 |STANDARD BRUSHED TIN | 9| 8 +Brand#44 |STANDARD BRUSHED TIN | 19| 8 +Brand#44 |STANDARD BRUSHED TIN | 23| 8 +Brand#44 |STANDARD BURNISHED BRASS | 9| 8 +Brand#44 |STANDARD BURNISHED BRASS | 49| 8 +Brand#44 |STANDARD BURNISHED COPPER| 45| 8 +Brand#44 |STANDARD BURNISHED NICKEL| 19| 8 +Brand#44 |STANDARD BURNISHED NICKEL| 23| 8 +Brand#44 |STANDARD BURNISHED STEEL | 3| 8 +Brand#44 |STANDARD BURNISHED STEEL | 14| 8 +Brand#44 |STANDARD BURNISHED STEEL | 45| 8 +Brand#44 |STANDARD BURNISHED TIN | 19| 8 +Brand#44 |STANDARD PLATED BRASS | 9| 8 +Brand#44 |STANDARD PLATED BRASS | 45| 8 +Brand#44 |STANDARD PLATED COPPER | 9| 8 +Brand#44 |STANDARD PLATED COPPER | 23| 8 +Brand#44 |STANDARD PLATED COPPER | 49| 8 +Brand#44 |STANDARD PLATED NICKEL | 14| 8 +Brand#44 |STANDARD PLATED NICKEL | 19| 8 +Brand#44 |STANDARD PLATED TIN | 19| 8 +Brand#44 |STANDARD PLATED TIN | 49| 8 +Brand#44 |STANDARD POLISHED COPPER | 14| 8 +Brand#44 |STANDARD POLISHED COPPER | 19| 8 +Brand#44 |STANDARD POLISHED COPPER | 45| 8 +Brand#44 |STANDARD POLISHED COPPER | 49| 8 +Brand#44 |STANDARD POLISHED NICKEL | 36| 8 +Brand#44 |STANDARD POLISHED TIN | 9| 8 +Brand#44 |STANDARD POLISHED TIN | 19| 8 +Brand#51 |ECONOMY ANODIZED BRASS | 49| 8 +Brand#51 |ECONOMY ANODIZED COPPER | 3| 8 +Brand#51 |ECONOMY ANODIZED NICKEL | 3| 8 +Brand#51 |ECONOMY ANODIZED NICKEL | 23| 8 +Brand#51 |ECONOMY ANODIZED STEEL | 36| 8 +Brand#51 |ECONOMY ANODIZED STEEL | 45| 8 +Brand#51 |ECONOMY ANODIZED STEEL | 49| 8 +Brand#51 |ECONOMY ANODIZED TIN | 23| 8 +Brand#51 |ECONOMY BRUSHED BRASS | 3| 8 +Brand#51 |ECONOMY BRUSHED COPPER | 36| 8 +Brand#51 |ECONOMY BRUSHED COPPER | 45| 8 +Brand#51 |ECONOMY BRUSHED NICKEL | 14| 8 +Brand#51 |ECONOMY BRUSHED NICKEL | 19| 8 +Brand#51 |ECONOMY BRUSHED STEEL | 9| 8 +Brand#51 |ECONOMY BRUSHED STEEL | 14| 8 +Brand#51 |ECONOMY BRUSHED STEEL | 49| 8 +Brand#51 |ECONOMY BRUSHED TIN | 19| 8 +Brand#51 |ECONOMY BURNISHED BRASS | 14| 8 +Brand#51 |ECONOMY BURNISHED STEEL | 14| 8 +Brand#51 |ECONOMY BURNISHED STEEL | 19| 8 +Brand#51 |ECONOMY BURNISHED STEEL | 36| 8 +Brand#51 |ECONOMY BURNISHED TIN | 14| 8 +Brand#51 |ECONOMY BURNISHED TIN | 45| 8 +Brand#51 |ECONOMY PLATED BRASS | 3| 8 +Brand#51 |ECONOMY PLATED BRASS | 23| 8 +Brand#51 |ECONOMY PLATED BRASS | 36| 8 +Brand#51 |ECONOMY PLATED COPPER | 49| 8 +Brand#51 |ECONOMY PLATED NICKEL | 9| 8 +Brand#51 |ECONOMY PLATED NICKEL | 14| 8 +Brand#51 |ECONOMY PLATED NICKEL | 49| 8 +Brand#51 |ECONOMY PLATED TIN | 36| 8 +Brand#51 |ECONOMY PLATED TIN | 49| 8 +Brand#51 |ECONOMY POLISHED BRASS | 14| 8 +Brand#51 |ECONOMY POLISHED BRASS | 36| 8 +Brand#51 |ECONOMY POLISHED BRASS | 49| 8 +Brand#51 |ECONOMY POLISHED COPPER | 9| 8 +Brand#51 |ECONOMY POLISHED NICKEL | 19| 8 +Brand#51 |ECONOMY POLISHED NICKEL | 36| 8 +Brand#51 |ECONOMY POLISHED STEEL | 3| 8 +Brand#51 |ECONOMY POLISHED STEEL | 9| 8 +Brand#51 |ECONOMY POLISHED STEEL | 14| 8 +Brand#51 |ECONOMY POLISHED STEEL | 36| 8 +Brand#51 |ECONOMY POLISHED TIN | 14| 8 +Brand#51 |ECONOMY POLISHED TIN | 19| 8 +Brand#51 |LARGE ANODIZED BRASS | 19| 8 +Brand#51 |LARGE ANODIZED BRASS | 23| 8 +Brand#51 |LARGE ANODIZED COPPER | 36| 8 +Brand#51 |LARGE ANODIZED COPPER | 49| 8 +Brand#51 |LARGE ANODIZED NICKEL | 14| 8 +Brand#51 |LARGE ANODIZED NICKEL | 45| 8 +Brand#51 |LARGE ANODIZED STEEL | 45| 8 +Brand#51 |LARGE ANODIZED TIN | 19| 8 +Brand#51 |LARGE BRUSHED BRASS | 9| 8 +Brand#51 |LARGE BRUSHED BRASS | 23| 8 +Brand#51 |LARGE BRUSHED COPPER | 23| 8 +Brand#51 |LARGE BRUSHED COPPER | 49| 8 +Brand#51 |LARGE BRUSHED NICKEL | 9| 8 +Brand#51 |LARGE BRUSHED NICKEL | 19| 8 +Brand#51 |LARGE BRUSHED NICKEL | 45| 8 +Brand#51 |LARGE BURNISHED BRASS | 3| 8 +Brand#51 |LARGE BURNISHED BRASS | 14| 8 +Brand#51 |LARGE BURNISHED BRASS | 36| 8 +Brand#51 |LARGE BURNISHED NICKEL | 23| 8 +Brand#51 |LARGE BURNISHED STEEL | 9| 8 +Brand#51 |LARGE BURNISHED STEEL | 36| 8 +Brand#51 |LARGE PLATED BRASS | 23| 8 +Brand#51 |LARGE PLATED COPPER | 49| 8 +Brand#51 |LARGE PLATED NICKEL | 3| 8 +Brand#51 |LARGE PLATED NICKEL | 36| 8 +Brand#51 |LARGE PLATED STEEL | 3| 8 +Brand#51 |LARGE PLATED TIN | 9| 8 +Brand#51 |LARGE PLATED TIN | 36| 8 +Brand#51 |LARGE POLISHED BRASS | 9| 8 +Brand#51 |LARGE POLISHED COPPER | 14| 8 +Brand#51 |LARGE POLISHED COPPER | 45| 8 +Brand#51 |LARGE POLISHED NICKEL | 14| 8 +Brand#51 |LARGE POLISHED STEEL | 3| 8 +Brand#51 |LARGE POLISHED TIN | 14| 8 +Brand#51 |LARGE POLISHED TIN | 23| 8 +Brand#51 |MEDIUM ANODIZED BRASS | 23| 8 +Brand#51 |MEDIUM ANODIZED BRASS | 49| 8 +Brand#51 |MEDIUM ANODIZED COPPER | 9| 8 +Brand#51 |MEDIUM ANODIZED COPPER | 45| 8 +Brand#51 |MEDIUM ANODIZED NICKEL | 9| 8 +Brand#51 |MEDIUM ANODIZED NICKEL | 14| 8 +Brand#51 |MEDIUM ANODIZED NICKEL | 36| 8 +Brand#51 |MEDIUM ANODIZED STEEL | 3| 8 +Brand#51 |MEDIUM ANODIZED STEEL | 36| 8 +Brand#51 |MEDIUM ANODIZED TIN | 3| 8 +Brand#51 |MEDIUM ANODIZED TIN | 19| 8 +Brand#51 |MEDIUM BRUSHED COPPER | 3| 8 +Brand#51 |MEDIUM BRUSHED COPPER | 45| 8 +Brand#51 |MEDIUM BRUSHED NICKEL | 14| 8 +Brand#51 |MEDIUM BURNISHED BRASS | 9| 8 +Brand#51 |MEDIUM BURNISHED COPPER | 3| 8 +Brand#51 |MEDIUM BURNISHED COPPER | 9| 8 +Brand#51 |MEDIUM BURNISHED COPPER | 19| 8 +Brand#51 |MEDIUM BURNISHED NICKEL | 9| 8 +Brand#51 |MEDIUM BURNISHED NICKEL | 23| 8 +Brand#51 |MEDIUM BURNISHED NICKEL | 36| 8 +Brand#51 |MEDIUM BURNISHED STEEL | 14| 8 +Brand#51 |MEDIUM BURNISHED STEEL | 49| 8 +Brand#51 |MEDIUM BURNISHED TIN | 9| 8 +Brand#51 |MEDIUM BURNISHED TIN | 49| 8 +Brand#51 |MEDIUM PLATED BRASS | 49| 8 +Brand#51 |MEDIUM PLATED COPPER | 9| 8 +Brand#51 |MEDIUM PLATED COPPER | 19| 8 +Brand#51 |MEDIUM PLATED NICKEL | 3| 8 +Brand#51 |MEDIUM PLATED NICKEL | 9| 8 +Brand#51 |MEDIUM PLATED STEEL | 9| 8 +Brand#51 |MEDIUM PLATED STEEL | 49| 8 +Brand#51 |PROMO ANODIZED COPPER | 49| 8 +Brand#51 |PROMO ANODIZED NICKEL | 19| 8 +Brand#51 |PROMO ANODIZED TIN | 14| 8 +Brand#51 |PROMO ANODIZED TIN | 19| 8 +Brand#51 |PROMO BRUSHED BRASS | 19| 8 +Brand#51 |PROMO BRUSHED NICKEL | 9| 8 +Brand#51 |PROMO BRUSHED NICKEL | 14| 8 +Brand#51 |PROMO BRUSHED STEEL | 49| 8 +Brand#51 |PROMO BRUSHED TIN | 45| 8 +Brand#51 |PROMO BURNISHED BRASS | 3| 8 +Brand#51 |PROMO BURNISHED BRASS | 19| 8 +Brand#51 |PROMO BURNISHED BRASS | 23| 8 +Brand#51 |PROMO BURNISHED NICKEL | 3| 8 +Brand#51 |PROMO BURNISHED STEEL | 14| 8 +Brand#51 |PROMO BURNISHED TIN | 3| 8 +Brand#51 |PROMO BURNISHED TIN | 36| 8 +Brand#51 |PROMO BURNISHED TIN | 45| 8 +Brand#51 |PROMO PLATED BRASS | 19| 8 +Brand#51 |PROMO PLATED BRASS | 49| 8 +Brand#51 |PROMO PLATED COPPER | 19| 8 +Brand#51 |PROMO PLATED NICKEL | 23| 8 +Brand#51 |PROMO PLATED STEEL | 3| 8 +Brand#51 |PROMO PLATED STEEL | 23| 8 +Brand#51 |PROMO PLATED STEEL | 49| 8 +Brand#51 |PROMO PLATED TIN | 3| 8 +Brand#51 |PROMO PLATED TIN | 19| 8 +Brand#51 |PROMO POLISHED BRASS | 3| 8 +Brand#51 |PROMO POLISHED BRASS | 9| 8 +Brand#51 |PROMO POLISHED BRASS | 19| 8 +Brand#51 |PROMO POLISHED BRASS | 23| 8 +Brand#51 |PROMO POLISHED COPPER | 9| 8 +Brand#51 |PROMO POLISHED COPPER | 14| 8 +Brand#51 |PROMO POLISHED STEEL | 36| 8 +Brand#51 |PROMO POLISHED STEEL | 45| 8 +Brand#51 |SMALL ANODIZED BRASS | 9| 8 +Brand#51 |SMALL ANODIZED COPPER | 49| 8 +Brand#51 |SMALL ANODIZED NICKEL | 14| 8 +Brand#51 |SMALL ANODIZED STEEL | 3| 8 +Brand#51 |SMALL ANODIZED STEEL | 14| 8 +Brand#51 |SMALL ANODIZED STEEL | 23| 8 +Brand#51 |SMALL ANODIZED STEEL | 45| 8 +Brand#51 |SMALL ANODIZED TIN | 19| 8 +Brand#51 |SMALL BRUSHED BRASS | 9| 8 +Brand#51 |SMALL BRUSHED COPPER | 3| 8 +Brand#51 |SMALL BRUSHED COPPER | 19| 8 +Brand#51 |SMALL BRUSHED COPPER | 45| 8 +Brand#51 |SMALL BRUSHED NICKEL | 23| 8 +Brand#51 |SMALL BRUSHED STEEL | 3| 8 +Brand#51 |SMALL BRUSHED STEEL | 9| 8 +Brand#51 |SMALL BRUSHED STEEL | 14| 8 +Brand#51 |SMALL BRUSHED TIN | 9| 8 +Brand#51 |SMALL BRUSHED TIN | 36| 8 +Brand#51 |SMALL BURNISHED BRASS | 36| 8 +Brand#51 |SMALL BURNISHED BRASS | 49| 8 +Brand#51 |SMALL BURNISHED COPPER | 14| 8 +Brand#51 |SMALL BURNISHED COPPER | 23| 8 +Brand#51 |SMALL BURNISHED NICKEL | 19| 8 +Brand#51 |SMALL BURNISHED NICKEL | 49| 8 +Brand#51 |SMALL BURNISHED STEEL | 14| 8 +Brand#51 |SMALL BURNISHED STEEL | 19| 8 +Brand#51 |SMALL BURNISHED TIN | 49| 8 +Brand#51 |SMALL PLATED COPPER | 45| 8 +Brand#51 |SMALL PLATED COPPER | 49| 8 +Brand#51 |SMALL PLATED NICKEL | 9| 8 +Brand#51 |SMALL PLATED STEEL | 36| 8 +Brand#51 |SMALL PLATED STEEL | 45| 8 +Brand#51 |SMALL PLATED TIN | 19| 8 +Brand#51 |SMALL POLISHED BRASS | 19| 8 +Brand#51 |SMALL POLISHED COPPER | 36| 8 +Brand#51 |SMALL POLISHED STEEL | 23| 8 +Brand#51 |SMALL POLISHED STEEL | 45| 8 +Brand#51 |SMALL POLISHED TIN | 49| 8 +Brand#51 |STANDARD ANODIZED BRASS | 19| 8 +Brand#51 |STANDARD ANODIZED BRASS | 36| 8 +Brand#51 |STANDARD ANODIZED NICKEL | 3| 8 +Brand#51 |STANDARD ANODIZED NICKEL | 9| 8 +Brand#51 |STANDARD ANODIZED NICKEL | 19| 8 +Brand#51 |STANDARD ANODIZED STEEL | 9| 8 +Brand#51 |STANDARD ANODIZED STEEL | 36| 8 +Brand#51 |STANDARD ANODIZED TIN | 9| 8 +Brand#51 |STANDARD ANODIZED TIN | 23| 8 +Brand#51 |STANDARD BRUSHED COPPER | 23| 8 +Brand#51 |STANDARD BRUSHED COPPER | 45| 8 +Brand#51 |STANDARD BRUSHED NICKEL | 19| 8 +Brand#51 |STANDARD BRUSHED NICKEL | 23| 8 +Brand#51 |STANDARD BRUSHED STEEL | 19| 8 +Brand#51 |STANDARD BURNISHED BRASS | 3| 8 +Brand#51 |STANDARD BURNISHED BRASS | 23| 8 +Brand#51 |STANDARD BURNISHED COPPER| 23| 8 +Brand#51 |STANDARD BURNISHED NICKEL| 14| 8 +Brand#51 |STANDARD BURNISHED NICKEL| 23| 8 +Brand#51 |STANDARD BURNISHED NICKEL| 36| 8 +Brand#51 |STANDARD BURNISHED NICKEL| 49| 8 +Brand#51 |STANDARD BURNISHED TIN | 14| 8 +Brand#51 |STANDARD PLATED BRASS | 49| 8 +Brand#51 |STANDARD PLATED COPPER | 19| 8 +Brand#51 |STANDARD PLATED COPPER | 45| 8 +Brand#51 |STANDARD PLATED NICKEL | 19| 8 +Brand#51 |STANDARD PLATED STEEL | 19| 8 +Brand#51 |STANDARD PLATED TIN | 9| 8 +Brand#51 |STANDARD POLISHED BRASS | 3| 8 +Brand#51 |STANDARD POLISHED BRASS | 45| 8 +Brand#51 |STANDARD POLISHED COPPER | 9| 8 +Brand#51 |STANDARD POLISHED COPPER | 49| 8 +Brand#51 |STANDARD POLISHED NICKEL | 3| 8 +Brand#51 |STANDARD POLISHED NICKEL | 49| 8 +Brand#51 |STANDARD POLISHED STEEL | 9| 8 +Brand#51 |STANDARD POLISHED STEEL | 14| 8 +Brand#51 |STANDARD POLISHED STEEL | 49| 8 +Brand#51 |STANDARD POLISHED TIN | 14| 8 +Brand#51 |STANDARD POLISHED TIN | 23| 8 +Brand#51 |STANDARD POLISHED TIN | 49| 8 +Brand#52 |ECONOMY ANODIZED BRASS | 14| 8 +Brand#52 |ECONOMY ANODIZED BRASS | 36| 8 +Brand#52 |ECONOMY ANODIZED NICKEL | 23| 8 +Brand#52 |ECONOMY ANODIZED STEEL | 3| 8 +Brand#52 |ECONOMY ANODIZED STEEL | 19| 8 +Brand#52 |ECONOMY ANODIZED TIN | 3| 8 +Brand#52 |ECONOMY ANODIZED TIN | 14| 8 +Brand#52 |ECONOMY ANODIZED TIN | 49| 8 +Brand#52 |ECONOMY BRUSHED BRASS | 36| 8 +Brand#52 |ECONOMY BRUSHED STEEL | 3| 8 +Brand#52 |ECONOMY BRUSHED STEEL | 14| 8 +Brand#52 |ECONOMY BRUSHED TIN | 9| 8 +Brand#52 |ECONOMY BRUSHED TIN | 36| 8 +Brand#52 |ECONOMY BRUSHED TIN | 49| 8 +Brand#52 |ECONOMY BURNISHED COPPER | 45| 8 +Brand#52 |ECONOMY BURNISHED NICKEL | 23| 8 +Brand#52 |ECONOMY BURNISHED STEEL | 9| 8 +Brand#52 |ECONOMY BURNISHED STEEL | 36| 8 +Brand#52 |ECONOMY BURNISHED TIN | 3| 8 +Brand#52 |ECONOMY PLATED BRASS | 3| 8 +Brand#52 |ECONOMY PLATED BRASS | 14| 8 +Brand#52 |ECONOMY PLATED BRASS | 23| 8 +Brand#52 |ECONOMY PLATED BRASS | 45| 8 +Brand#52 |ECONOMY PLATED COPPER | 49| 8 +Brand#52 |ECONOMY PLATED NICKEL | 3| 8 +Brand#52 |ECONOMY PLATED NICKEL | 49| 8 +Brand#52 |ECONOMY PLATED STEEL | 3| 8 +Brand#52 |ECONOMY PLATED STEEL | 14| 8 +Brand#52 |ECONOMY PLATED TIN | 3| 8 +Brand#52 |ECONOMY PLATED TIN | 19| 8 +Brand#52 |ECONOMY PLATED TIN | 23| 8 +Brand#52 |ECONOMY POLISHED BRASS | 23| 8 +Brand#52 |ECONOMY POLISHED BRASS | 45| 8 +Brand#52 |ECONOMY POLISHED BRASS | 49| 8 +Brand#52 |ECONOMY POLISHED NICKEL | 19| 8 +Brand#52 |ECONOMY POLISHED NICKEL | 23| 8 +Brand#52 |ECONOMY POLISHED NICKEL | 45| 8 +Brand#52 |ECONOMY POLISHED STEEL | 9| 8 +Brand#52 |ECONOMY POLISHED STEEL | 45| 8 +Brand#52 |ECONOMY POLISHED STEEL | 49| 8 +Brand#52 |ECONOMY POLISHED TIN | 19| 8 +Brand#52 |ECONOMY POLISHED TIN | 36| 8 +Brand#52 |ECONOMY POLISHED TIN | 49| 8 +Brand#52 |LARGE ANODIZED BRASS | 14| 8 +Brand#52 |LARGE ANODIZED STEEL | 9| 8 +Brand#52 |LARGE ANODIZED STEEL | 19| 8 +Brand#52 |LARGE ANODIZED STEEL | 36| 8 +Brand#52 |LARGE ANODIZED STEEL | 45| 8 +Brand#52 |LARGE ANODIZED TIN | 9| 8 +Brand#52 |LARGE ANODIZED TIN | 14| 8 +Brand#52 |LARGE ANODIZED TIN | 36| 8 +Brand#52 |LARGE BRUSHED BRASS | 19| 8 +Brand#52 |LARGE BRUSHED COPPER | 14| 8 +Brand#52 |LARGE BRUSHED COPPER | 49| 8 +Brand#52 |LARGE BRUSHED NICKEL | 36| 8 +Brand#52 |LARGE BRUSHED TIN | 19| 8 +Brand#52 |LARGE BRUSHED TIN | 49| 8 +Brand#52 |LARGE BURNISHED BRASS | 19| 8 +Brand#52 |LARGE BURNISHED BRASS | 49| 8 +Brand#52 |LARGE BURNISHED COPPER | 3| 8 +Brand#52 |LARGE BURNISHED COPPER | 23| 8 +Brand#52 |LARGE BURNISHED NICKEL | 3| 8 +Brand#52 |LARGE BURNISHED NICKEL | 9| 8 +Brand#52 |LARGE BURNISHED STEEL | 9| 8 +Brand#52 |LARGE BURNISHED STEEL | 14| 8 +Brand#52 |LARGE BURNISHED TIN | 14| 8 +Brand#52 |LARGE BURNISHED TIN | 45| 8 +Brand#52 |LARGE PLATED BRASS | 14| 8 +Brand#52 |LARGE PLATED COPPER | 3| 8 +Brand#52 |LARGE PLATED COPPER | 14| 8 +Brand#52 |LARGE PLATED COPPER | 45| 8 +Brand#52 |LARGE PLATED NICKEL | 14| 8 +Brand#52 |LARGE PLATED NICKEL | 49| 8 +Brand#52 |LARGE PLATED TIN | 45| 8 +Brand#52 |LARGE POLISHED COPPER | 14| 8 +Brand#52 |LARGE POLISHED NICKEL | 23| 8 +Brand#52 |LARGE POLISHED NICKEL | 49| 8 +Brand#52 |LARGE POLISHED TIN | 9| 8 +Brand#52 |MEDIUM ANODIZED BRASS | 3| 8 +Brand#52 |MEDIUM ANODIZED COPPER | 3| 8 +Brand#52 |MEDIUM ANODIZED COPPER | 14| 8 +Brand#52 |MEDIUM ANODIZED COPPER | 36| 8 +Brand#52 |MEDIUM ANODIZED COPPER | 49| 8 +Brand#52 |MEDIUM ANODIZED NICKEL | 23| 8 +Brand#52 |MEDIUM ANODIZED NICKEL | 45| 8 +Brand#52 |MEDIUM ANODIZED STEEL | 19| 8 +Brand#52 |MEDIUM ANODIZED STEEL | 45| 8 +Brand#52 |MEDIUM ANODIZED TIN | 19| 8 +Brand#52 |MEDIUM ANODIZED TIN | 49| 8 +Brand#52 |MEDIUM BRUSHED BRASS | 9| 8 +Brand#52 |MEDIUM BRUSHED COPPER | 3| 8 +Brand#52 |MEDIUM BRUSHED COPPER | 9| 8 +Brand#52 |MEDIUM BRUSHED NICKEL | 49| 8 +Brand#52 |MEDIUM BRUSHED STEEL | 23| 8 +Brand#52 |MEDIUM BRUSHED STEEL | 36| 8 +Brand#52 |MEDIUM BRUSHED STEEL | 45| 8 +Brand#52 |MEDIUM BRUSHED STEEL | 49| 8 +Brand#52 |MEDIUM BRUSHED TIN | 19| 8 +Brand#52 |MEDIUM BRUSHED TIN | 23| 8 +Brand#52 |MEDIUM BRUSHED TIN | 49| 8 +Brand#52 |MEDIUM BURNISHED COPPER | 36| 8 +Brand#52 |MEDIUM BURNISHED NICKEL | 14| 8 +Brand#52 |MEDIUM BURNISHED NICKEL | 19| 8 +Brand#52 |MEDIUM BURNISHED TIN | 9| 8 +Brand#52 |MEDIUM BURNISHED TIN | 19| 8 +Brand#52 |MEDIUM BURNISHED TIN | 49| 8 +Brand#52 |MEDIUM PLATED COPPER | 14| 8 +Brand#52 |MEDIUM PLATED COPPER | 19| 8 +Brand#52 |MEDIUM PLATED COPPER | 36| 8 +Brand#52 |MEDIUM PLATED NICKEL | 3| 8 +Brand#52 |MEDIUM PLATED STEEL | 36| 8 +Brand#52 |MEDIUM PLATED TIN | 3| 8 +Brand#52 |MEDIUM PLATED TIN | 9| 8 +Brand#52 |MEDIUM PLATED TIN | 14| 8 +Brand#52 |PROMO ANODIZED BRASS | 36| 8 +Brand#52 |PROMO ANODIZED COPPER | 19| 8 +Brand#52 |PROMO ANODIZED COPPER | 23| 8 +Brand#52 |PROMO ANODIZED COPPER | 36| 8 +Brand#52 |PROMO ANODIZED TIN | 9| 8 +Brand#52 |PROMO ANODIZED TIN | 23| 8 +Brand#52 |PROMO BRUSHED BRASS | 3| 8 +Brand#52 |PROMO BRUSHED BRASS | 14| 8 +Brand#52 |PROMO BRUSHED BRASS | 45| 8 +Brand#52 |PROMO BRUSHED COPPER | 45| 8 +Brand#52 |PROMO BRUSHED NICKEL | 45| 8 +Brand#52 |PROMO BRUSHED NICKEL | 49| 8 +Brand#52 |PROMO BRUSHED STEEL | 9| 8 +Brand#52 |PROMO BRUSHED STEEL | 14| 8 +Brand#52 |PROMO BRUSHED STEEL | 23| 8 +Brand#52 |PROMO BURNISHED BRASS | 14| 8 +Brand#52 |PROMO BURNISHED BRASS | 23| 8 +Brand#52 |PROMO BURNISHED COPPER | 45| 8 +Brand#52 |PROMO BURNISHED COPPER | 49| 8 +Brand#52 |PROMO BURNISHED NICKEL | 9| 8 +Brand#52 |PROMO BURNISHED NICKEL | 14| 8 +Brand#52 |PROMO BURNISHED NICKEL | 49| 8 +Brand#52 |PROMO PLATED BRASS | 3| 8 +Brand#52 |PROMO PLATED BRASS | 45| 8 +Brand#52 |PROMO PLATED BRASS | 49| 8 +Brand#52 |PROMO PLATED COPPER | 3| 8 +Brand#52 |PROMO PLATED COPPER | 9| 8 +Brand#52 |PROMO PLATED COPPER | 45| 8 +Brand#52 |PROMO PLATED NICKEL | 19| 8 +Brand#52 |PROMO PLATED NICKEL | 23| 8 +Brand#52 |PROMO PLATED NICKEL | 36| 8 +Brand#52 |PROMO PLATED NICKEL | 45| 8 +Brand#52 |PROMO PLATED STEEL | 3| 8 +Brand#52 |PROMO PLATED STEEL | 23| 8 +Brand#52 |PROMO PLATED STEEL | 49| 8 +Brand#52 |PROMO POLISHED BRASS | 36| 8 +Brand#52 |PROMO POLISHED COPPER | 23| 8 +Brand#52 |PROMO POLISHED COPPER | 49| 8 +Brand#52 |PROMO POLISHED NICKEL | 14| 8 +Brand#52 |PROMO POLISHED STEEL | 45| 8 +Brand#52 |PROMO POLISHED TIN | 3| 8 +Brand#52 |PROMO POLISHED TIN | 9| 8 +Brand#52 |PROMO POLISHED TIN | 14| 8 +Brand#52 |PROMO POLISHED TIN | 19| 8 +Brand#52 |PROMO POLISHED TIN | 45| 8 +Brand#52 |SMALL ANODIZED BRASS | 3| 8 +Brand#52 |SMALL ANODIZED BRASS | 14| 8 +Brand#52 |SMALL ANODIZED BRASS | 23| 8 +Brand#52 |SMALL ANODIZED COPPER | 23| 8 +Brand#52 |SMALL ANODIZED NICKEL | 45| 8 +Brand#52 |SMALL ANODIZED STEEL | 23| 8 +Brand#52 |SMALL ANODIZED TIN | 19| 8 +Brand#52 |SMALL ANODIZED TIN | 23| 8 +Brand#52 |SMALL ANODIZED TIN | 49| 8 +Brand#52 |SMALL BRUSHED BRASS | 9| 8 +Brand#52 |SMALL BRUSHED BRASS | 49| 8 +Brand#52 |SMALL BRUSHED COPPER | 23| 8 +Brand#52 |SMALL BRUSHED NICKEL | 19| 8 +Brand#52 |SMALL BRUSHED TIN | 3| 8 +Brand#52 |SMALL BRUSHED TIN | 19| 8 +Brand#52 |SMALL BRUSHED TIN | 45| 8 +Brand#52 |SMALL BRUSHED TIN | 49| 8 +Brand#52 |SMALL BURNISHED BRASS | 9| 8 +Brand#52 |SMALL BURNISHED BRASS | 45| 8 +Brand#52 |SMALL BURNISHED COPPER | 9| 8 +Brand#52 |SMALL BURNISHED COPPER | 45| 8 +Brand#52 |SMALL BURNISHED NICKEL | 3| 8 +Brand#52 |SMALL BURNISHED NICKEL | 14| 8 +Brand#52 |SMALL BURNISHED TIN | 36| 8 +Brand#52 |SMALL PLATED BRASS | 3| 8 +Brand#52 |SMALL PLATED BRASS | 45| 8 +Brand#52 |SMALL PLATED BRASS | 49| 8 +Brand#52 |SMALL PLATED COPPER | 49| 8 +Brand#52 |SMALL PLATED NICKEL | 14| 8 +Brand#52 |SMALL PLATED NICKEL | 36| 8 +Brand#52 |SMALL POLISHED BRASS | 23| 8 +Brand#52 |SMALL POLISHED COPPER | 9| 8 +Brand#52 |SMALL POLISHED COPPER | 36| 8 +Brand#52 |SMALL POLISHED COPPER | 45| 8 +Brand#52 |SMALL POLISHED STEEL | 3| 8 +Brand#52 |SMALL POLISHED STEEL | 9| 8 +Brand#52 |SMALL POLISHED STEEL | 49| 8 +Brand#52 |SMALL POLISHED TIN | 9| 8 +Brand#52 |SMALL POLISHED TIN | 14| 8 +Brand#52 |STANDARD ANODIZED BRASS | 49| 8 +Brand#52 |STANDARD ANODIZED COPPER | 3| 8 +Brand#52 |STANDARD ANODIZED COPPER | 9| 8 +Brand#52 |STANDARD ANODIZED COPPER | 19| 8 +Brand#52 |STANDARD ANODIZED COPPER | 36| 8 +Brand#52 |STANDARD ANODIZED COPPER | 45| 8 +Brand#52 |STANDARD ANODIZED STEEL | 3| 8 +Brand#52 |STANDARD ANODIZED STEEL | 23| 8 +Brand#52 |STANDARD ANODIZED STEEL | 49| 8 +Brand#52 |STANDARD ANODIZED TIN | 3| 8 +Brand#52 |STANDARD BRUSHED BRASS | 3| 8 +Brand#52 |STANDARD BRUSHED COPPER | 45| 8 +Brand#52 |STANDARD BRUSHED STEEL | 14| 8 +Brand#52 |STANDARD BRUSHED TIN | 9| 8 +Brand#52 |STANDARD BURNISHED BRASS | 49| 8 +Brand#52 |STANDARD BURNISHED COPPER| 19| 8 +Brand#52 |STANDARD BURNISHED COPPER| 23| 8 +Brand#52 |STANDARD BURNISHED STEEL | 3| 8 +Brand#52 |STANDARD BURNISHED TIN | 19| 8 +Brand#52 |STANDARD PLATED BRASS | 49| 8 +Brand#52 |STANDARD PLATED STEEL | 14| 8 +Brand#52 |STANDARD PLATED STEEL | 36| 8 +Brand#52 |STANDARD POLISHED BRASS | 3| 8 +Brand#52 |STANDARD POLISHED BRASS | 9| 8 +Brand#52 |STANDARD POLISHED BRASS | 49| 8 +Brand#52 |STANDARD POLISHED COPPER | 9| 8 +Brand#52 |STANDARD POLISHED COPPER | 14| 8 +Brand#52 |STANDARD POLISHED NICKEL | 45| 8 +Brand#52 |STANDARD POLISHED STEEL | 45| 8 +Brand#52 |STANDARD POLISHED TIN | 19| 8 +Brand#53 |ECONOMY ANODIZED BRASS | 9| 8 +Brand#53 |ECONOMY ANODIZED BRASS | 36| 8 +Brand#53 |ECONOMY ANODIZED BRASS | 45| 8 +Brand#53 |ECONOMY ANODIZED COPPER | 45| 8 +Brand#53 |ECONOMY ANODIZED NICKEL | 19| 8 +Brand#53 |ECONOMY ANODIZED STEEL | 45| 8 +Brand#53 |ECONOMY ANODIZED TIN | 14| 8 +Brand#53 |ECONOMY ANODIZED TIN | 36| 8 +Brand#53 |ECONOMY BRUSHED COPPER | 3| 8 +Brand#53 |ECONOMY BRUSHED NICKEL | 23| 8 +Brand#53 |ECONOMY BRUSHED STEEL | 23| 8 +Brand#53 |ECONOMY BRUSHED STEEL | 49| 8 +Brand#53 |ECONOMY BRUSHED TIN | 3| 8 +Brand#53 |ECONOMY BURNISHED BRASS | 9| 8 +Brand#53 |ECONOMY BURNISHED BRASS | 45| 8 +Brand#53 |ECONOMY BURNISHED COPPER | 9| 8 +Brand#53 |ECONOMY BURNISHED COPPER | 14| 8 +Brand#53 |ECONOMY BURNISHED COPPER | 19| 8 +Brand#53 |ECONOMY BURNISHED NICKEL | 3| 8 +Brand#53 |ECONOMY BURNISHED NICKEL | 14| 8 +Brand#53 |ECONOMY BURNISHED NICKEL | 36| 8 +Brand#53 |ECONOMY BURNISHED NICKEL | 45| 8 +Brand#53 |ECONOMY BURNISHED STEEL | 19| 8 +Brand#53 |ECONOMY BURNISHED STEEL | 23| 8 +Brand#53 |ECONOMY BURNISHED STEEL | 36| 8 +Brand#53 |ECONOMY BURNISHED TIN | 3| 8 +Brand#53 |ECONOMY BURNISHED TIN | 49| 8 +Brand#53 |ECONOMY PLATED BRASS | 14| 8 +Brand#53 |ECONOMY PLATED BRASS | 19| 8 +Brand#53 |ECONOMY PLATED COPPER | 3| 8 +Brand#53 |ECONOMY PLATED TIN | 19| 8 +Brand#53 |ECONOMY POLISHED COPPER | 14| 8 +Brand#53 |ECONOMY POLISHED COPPER | 19| 8 +Brand#53 |ECONOMY POLISHED NICKEL | 36| 8 +Brand#53 |ECONOMY POLISHED STEEL | 3| 8 +Brand#53 |ECONOMY POLISHED STEEL | 9| 8 +Brand#53 |LARGE ANODIZED BRASS | 19| 8 +Brand#53 |LARGE ANODIZED BRASS | 45| 8 +Brand#53 |LARGE ANODIZED STEEL | 45| 8 +Brand#53 |LARGE ANODIZED TIN | 23| 8 +Brand#53 |LARGE ANODIZED TIN | 45| 8 +Brand#53 |LARGE ANODIZED TIN | 49| 8 +Brand#53 |LARGE BRUSHED COPPER | 19| 8 +Brand#53 |LARGE BRUSHED COPPER | 45| 8 +Brand#53 |LARGE BRUSHED STEEL | 9| 8 +Brand#53 |LARGE BRUSHED STEEL | 45| 8 +Brand#53 |LARGE BRUSHED TIN | 3| 8 +Brand#53 |LARGE BRUSHED TIN | 9| 8 +Brand#53 |LARGE BRUSHED TIN | 36| 8 +Brand#53 |LARGE BURNISHED BRASS | 3| 8 +Brand#53 |LARGE BURNISHED NICKEL | 14| 8 +Brand#53 |LARGE BURNISHED NICKEL | 23| 8 +Brand#53 |LARGE BURNISHED STEEL | 3| 8 +Brand#53 |LARGE BURNISHED STEEL | 19| 8 +Brand#53 |LARGE BURNISHED STEEL | 23| 8 +Brand#53 |LARGE BURNISHED STEEL | 45| 8 +Brand#53 |LARGE BURNISHED TIN | 9| 8 +Brand#53 |LARGE PLATED BRASS | 9| 8 +Brand#53 |LARGE PLATED BRASS | 49| 8 +Brand#53 |LARGE PLATED NICKEL | 49| 8 +Brand#53 |LARGE PLATED STEEL | 45| 8 +Brand#53 |LARGE PLATED TIN | 23| 8 +Brand#53 |LARGE POLISHED BRASS | 3| 8 +Brand#53 |LARGE POLISHED BRASS | 23| 8 +Brand#53 |LARGE POLISHED COPPER | 23| 8 +Brand#53 |LARGE POLISHED NICKEL | 3| 8 +Brand#53 |LARGE POLISHED NICKEL | 14| 8 +Brand#53 |LARGE POLISHED NICKEL | 23| 8 +Brand#53 |LARGE POLISHED STEEL | 3| 8 +Brand#53 |LARGE POLISHED STEEL | 23| 8 +Brand#53 |LARGE POLISHED TIN | 9| 8 +Brand#53 |LARGE POLISHED TIN | 49| 8 +Brand#53 |MEDIUM ANODIZED BRASS | 3| 8 +Brand#53 |MEDIUM ANODIZED COPPER | 9| 8 +Brand#53 |MEDIUM ANODIZED COPPER | 45| 8 +Brand#53 |MEDIUM ANODIZED STEEL | 9| 8 +Brand#53 |MEDIUM ANODIZED STEEL | 23| 8 +Brand#53 |MEDIUM ANODIZED STEEL | 36| 8 +Brand#53 |MEDIUM ANODIZED TIN | 3| 8 +Brand#53 |MEDIUM BRUSHED COPPER | 9| 8 +Brand#53 |MEDIUM BRUSHED COPPER | 36| 8 +Brand#53 |MEDIUM BRUSHED NICKEL | 14| 8 +Brand#53 |MEDIUM BRUSHED NICKEL | 23| 8 +Brand#53 |MEDIUM BRUSHED STEEL | 45| 8 +Brand#53 |MEDIUM BRUSHED TIN | 9| 8 +Brand#53 |MEDIUM BURNISHED COPPER | 3| 8 +Brand#53 |MEDIUM BURNISHED COPPER | 14| 8 +Brand#53 |MEDIUM BURNISHED COPPER | 45| 8 +Brand#53 |MEDIUM BURNISHED NICKEL | 19| 8 +Brand#53 |MEDIUM BURNISHED NICKEL | 36| 8 +Brand#53 |MEDIUM BURNISHED STEEL | 14| 8 +Brand#53 |MEDIUM BURNISHED STEEL | 49| 8 +Brand#53 |MEDIUM BURNISHED TIN | 9| 8 +Brand#53 |MEDIUM BURNISHED TIN | 14| 8 +Brand#53 |MEDIUM PLATED BRASS | 9| 8 +Brand#53 |MEDIUM PLATED BRASS | 19| 8 +Brand#53 |MEDIUM PLATED NICKEL | 23| 8 +Brand#53 |MEDIUM PLATED NICKEL | 36| 8 +Brand#53 |MEDIUM PLATED NICKEL | 45| 8 +Brand#53 |MEDIUM PLATED STEEL | 19| 8 +Brand#53 |MEDIUM PLATED STEEL | 45| 8 +Brand#53 |PROMO ANODIZED BRASS | 19| 8 +Brand#53 |PROMO ANODIZED BRASS | 23| 8 +Brand#53 |PROMO ANODIZED BRASS | 36| 8 +Brand#53 |PROMO ANODIZED COPPER | 3| 8 +Brand#53 |PROMO ANODIZED COPPER | 9| 8 +Brand#53 |PROMO ANODIZED NICKEL | 36| 8 +Brand#53 |PROMO ANODIZED STEEL | 3| 8 +Brand#53 |PROMO ANODIZED STEEL | 14| 8 +Brand#53 |PROMO ANODIZED TIN | 19| 8 +Brand#53 |PROMO ANODIZED TIN | 49| 8 +Brand#53 |PROMO BRUSHED BRASS | 45| 8 +Brand#53 |PROMO BRUSHED COPPER | 9| 8 +Brand#53 |PROMO BRUSHED COPPER | 14| 8 +Brand#53 |PROMO BRUSHED NICKEL | 14| 8 +Brand#53 |PROMO BRUSHED NICKEL | 49| 8 +Brand#53 |PROMO BRUSHED STEEL | 3| 8 +Brand#53 |PROMO BRUSHED TIN | 23| 8 +Brand#53 |PROMO BURNISHED BRASS | 14| 8 +Brand#53 |PROMO BURNISHED BRASS | 23| 8 +Brand#53 |PROMO BURNISHED BRASS | 36| 8 +Brand#53 |PROMO BURNISHED COPPER | 14| 8 +Brand#53 |PROMO BURNISHED NICKEL | 14| 8 +Brand#53 |PROMO BURNISHED STEEL | 23| 8 +Brand#53 |PROMO BURNISHED TIN | 3| 8 +Brand#53 |PROMO BURNISHED TIN | 9| 8 +Brand#53 |PROMO BURNISHED TIN | 19| 8 +Brand#53 |PROMO BURNISHED TIN | 45| 8 +Brand#53 |PROMO PLATED BRASS | 45| 8 +Brand#53 |PROMO PLATED BRASS | 49| 8 +Brand#53 |PROMO PLATED COPPER | 23| 8 +Brand#53 |PROMO PLATED COPPER | 45| 8 +Brand#53 |PROMO PLATED COPPER | 49| 8 +Brand#53 |PROMO PLATED NICKEL | 49| 8 +Brand#53 |PROMO PLATED STEEL | 19| 8 +Brand#53 |PROMO PLATED TIN | 45| 8 +Brand#53 |PROMO PLATED TIN | 49| 8 +Brand#53 |PROMO POLISHED BRASS | 14| 8 +Brand#53 |PROMO POLISHED BRASS | 19| 8 +Brand#53 |PROMO POLISHED BRASS | 36| 8 +Brand#53 |PROMO POLISHED NICKEL | 19| 8 +Brand#53 |PROMO POLISHED NICKEL | 23| 8 +Brand#53 |PROMO POLISHED NICKEL | 45| 8 +Brand#53 |PROMO POLISHED STEEL | 3| 8 +Brand#53 |PROMO POLISHED STEEL | 9| 8 +Brand#53 |PROMO POLISHED TIN | 36| 8 +Brand#53 |PROMO POLISHED TIN | 45| 8 +Brand#53 |SMALL ANODIZED BRASS | 3| 8 +Brand#53 |SMALL ANODIZED BRASS | 9| 8 +Brand#53 |SMALL ANODIZED BRASS | 45| 8 +Brand#53 |SMALL ANODIZED COPPER | 3| 8 +Brand#53 |SMALL ANODIZED COPPER | 19| 8 +Brand#53 |SMALL ANODIZED COPPER | 23| 8 +Brand#53 |SMALL ANODIZED NICKEL | 9| 8 +Brand#53 |SMALL ANODIZED NICKEL | 19| 8 +Brand#53 |SMALL ANODIZED STEEL | 23| 8 +Brand#53 |SMALL ANODIZED STEEL | 45| 8 +Brand#53 |SMALL ANODIZED TIN | 36| 8 +Brand#53 |SMALL BRUSHED BRASS | 14| 8 +Brand#53 |SMALL BRUSHED BRASS | 36| 8 +Brand#53 |SMALL BRUSHED STEEL | 45| 8 +Brand#53 |SMALL BRUSHED TIN | 3| 8 +Brand#53 |SMALL BRUSHED TIN | 14| 8 +Brand#53 |SMALL BRUSHED TIN | 19| 8 +Brand#53 |SMALL BRUSHED TIN | 45| 8 +Brand#53 |SMALL BRUSHED TIN | 49| 8 +Brand#53 |SMALL BURNISHED BRASS | 45| 8 +Brand#53 |SMALL BURNISHED BRASS | 49| 8 +Brand#53 |SMALL BURNISHED COPPER | 19| 8 +Brand#53 |SMALL BURNISHED COPPER | 23| 8 +Brand#53 |SMALL BURNISHED COPPER | 36| 8 +Brand#53 |SMALL BURNISHED COPPER | 45| 8 +Brand#53 |SMALL BURNISHED COPPER | 49| 8 +Brand#53 |SMALL BURNISHED NICKEL | 14| 8 +Brand#53 |SMALL BURNISHED STEEL | 9| 8 +Brand#53 |SMALL BURNISHED STEEL | 36| 8 +Brand#53 |SMALL BURNISHED TIN | 14| 8 +Brand#53 |SMALL BURNISHED TIN | 23| 8 +Brand#53 |SMALL PLATED BRASS | 9| 8 +Brand#53 |SMALL PLATED BRASS | 36| 8 +Brand#53 |SMALL PLATED NICKEL | 9| 8 +Brand#53 |SMALL PLATED NICKEL | 14| 8 +Brand#53 |SMALL PLATED NICKEL | 23| 8 +Brand#53 |SMALL PLATED STEEL | 19| 8 +Brand#53 |SMALL PLATED STEEL | 23| 8 +Brand#53 |SMALL PLATED TIN | 9| 8 +Brand#53 |SMALL POLISHED BRASS | 36| 8 +Brand#53 |SMALL POLISHED COPPER | 23| 8 +Brand#53 |SMALL POLISHED NICKEL | 3| 8 +Brand#53 |SMALL POLISHED NICKEL | 19| 8 +Brand#53 |SMALL POLISHED STEEL | 3| 8 +Brand#53 |SMALL POLISHED STEEL | 23| 8 +Brand#53 |SMALL POLISHED TIN | 23| 8 +Brand#53 |SMALL POLISHED TIN | 36| 8 +Brand#53 |STANDARD ANODIZED BRASS | 14| 8 +Brand#53 |STANDARD ANODIZED BRASS | 23| 8 +Brand#53 |STANDARD ANODIZED BRASS | 45| 8 +Brand#53 |STANDARD ANODIZED COPPER | 36| 8 +Brand#53 |STANDARD ANODIZED NICKEL | 9| 8 +Brand#53 |STANDARD ANODIZED NICKEL | 19| 8 +Brand#53 |STANDARD ANODIZED STEEL | 9| 8 +Brand#53 |STANDARD ANODIZED STEEL | 19| 8 +Brand#53 |STANDARD ANODIZED STEEL | 45| 8 +Brand#53 |STANDARD ANODIZED TIN | 14| 8 +Brand#53 |STANDARD ANODIZED TIN | 49| 8 +Brand#53 |STANDARD BRUSHED BRASS | 14| 8 +Brand#53 |STANDARD BRUSHED BRASS | 19| 8 +Brand#53 |STANDARD BRUSHED COPPER | 49| 8 +Brand#53 |STANDARD BRUSHED NICKEL | 36| 8 +Brand#53 |STANDARD BRUSHED NICKEL | 45| 8 +Brand#53 |STANDARD BRUSHED NICKEL | 49| 8 +Brand#53 |STANDARD BRUSHED STEEL | 23| 8 +Brand#53 |STANDARD BURNISHED BRASS | 19| 8 +Brand#53 |STANDARD BURNISHED BRASS | 49| 8 +Brand#53 |STANDARD BURNISHED COPPER| 3| 8 +Brand#53 |STANDARD BURNISHED COPPER| 23| 8 +Brand#53 |STANDARD BURNISHED COPPER| 45| 8 +Brand#53 |STANDARD BURNISHED NICKEL| 49| 8 +Brand#53 |STANDARD BURNISHED STEEL | 19| 8 +Brand#53 |STANDARD BURNISHED STEEL | 23| 8 +Brand#53 |STANDARD BURNISHED TIN | 3| 8 +Brand#53 |STANDARD BURNISHED TIN | 14| 8 +Brand#53 |STANDARD BURNISHED TIN | 19| 8 +Brand#53 |STANDARD BURNISHED TIN | 36| 8 +Brand#53 |STANDARD PLATED BRASS | 19| 8 +Brand#53 |STANDARD PLATED COPPER | 3| 8 +Brand#53 |STANDARD PLATED NICKEL | 14| 8 +Brand#53 |STANDARD PLATED NICKEL | 36| 8 +Brand#53 |STANDARD PLATED STEEL | 14| 8 +Brand#53 |STANDARD PLATED STEEL | 23| 8 +Brand#53 |STANDARD PLATED STEEL | 45| 8 +Brand#53 |STANDARD PLATED TIN | 9| 8 +Brand#53 |STANDARD PLATED TIN | 14| 8 +Brand#53 |STANDARD PLATED TIN | 19| 8 +Brand#53 |STANDARD PLATED TIN | 23| 8 +Brand#53 |STANDARD POLISHED BRASS | 36| 8 +Brand#53 |STANDARD POLISHED NICKEL | 3| 8 +Brand#53 |STANDARD POLISHED NICKEL | 36| 8 +Brand#53 |STANDARD POLISHED NICKEL | 49| 8 +Brand#53 |STANDARD POLISHED TIN | 9| 8 +Brand#54 |ECONOMY ANODIZED NICKEL | 9| 8 +Brand#54 |ECONOMY ANODIZED NICKEL | 23| 8 +Brand#54 |ECONOMY ANODIZED STEEL | 19| 8 +Brand#54 |ECONOMY ANODIZED STEEL | 23| 8 +Brand#54 |ECONOMY ANODIZED TIN | 3| 8 +Brand#54 |ECONOMY ANODIZED TIN | 45| 8 +Brand#54 |ECONOMY BRUSHED BRASS | 14| 8 +Brand#54 |ECONOMY BRUSHED BRASS | 19| 8 +Brand#54 |ECONOMY BRUSHED BRASS | 23| 8 +Brand#54 |ECONOMY BRUSHED COPPER | 9| 8 +Brand#54 |ECONOMY BRUSHED COPPER | 45| 8 +Brand#54 |ECONOMY BRUSHED NICKEL | 9| 8 +Brand#54 |ECONOMY BRUSHED NICKEL | 23| 8 +Brand#54 |ECONOMY BRUSHED NICKEL | 36| 8 +Brand#54 |ECONOMY BRUSHED NICKEL | 49| 8 +Brand#54 |ECONOMY BRUSHED STEEL | 3| 8 +Brand#54 |ECONOMY BRUSHED STEEL | 14| 8 +Brand#54 |ECONOMY BURNISHED COPPER | 9| 8 +Brand#54 |ECONOMY BURNISHED COPPER | 36| 8 +Brand#54 |ECONOMY BURNISHED NICKEL | 36| 8 +Brand#54 |ECONOMY BURNISHED STEEL | 14| 8 +Brand#54 |ECONOMY BURNISHED STEEL | 36| 8 +Brand#54 |ECONOMY BURNISHED TIN | 9| 8 +Brand#54 |ECONOMY BURNISHED TIN | 14| 8 +Brand#54 |ECONOMY BURNISHED TIN | 23| 8 +Brand#54 |ECONOMY PLATED COPPER | 14| 8 +Brand#54 |ECONOMY PLATED COPPER | 19| 8 +Brand#54 |ECONOMY PLATED NICKEL | 23| 8 +Brand#54 |ECONOMY PLATED NICKEL | 45| 8 +Brand#54 |ECONOMY PLATED STEEL | 3| 8 +Brand#54 |ECONOMY PLATED STEEL | 19| 8 +Brand#54 |ECONOMY PLATED TIN | 23| 8 +Brand#54 |ECONOMY POLISHED BRASS | 23| 8 +Brand#54 |ECONOMY POLISHED BRASS | 36| 8 +Brand#54 |ECONOMY POLISHED BRASS | 49| 8 +Brand#54 |ECONOMY POLISHED COPPER | 9| 8 +Brand#54 |ECONOMY POLISHED COPPER | 19| 8 +Brand#54 |ECONOMY POLISHED COPPER | 23| 8 +Brand#54 |ECONOMY POLISHED COPPER | 45| 8 +Brand#54 |ECONOMY POLISHED STEEL | 14| 8 +Brand#54 |ECONOMY POLISHED STEEL | 19| 8 +Brand#54 |ECONOMY POLISHED STEEL | 23| 8 +Brand#54 |LARGE ANODIZED COPPER | 3| 8 +Brand#54 |LARGE ANODIZED COPPER | 45| 8 +Brand#54 |LARGE ANODIZED STEEL | 9| 8 +Brand#54 |LARGE ANODIZED STEEL | 14| 8 +Brand#54 |LARGE ANODIZED TIN | 23| 8 +Brand#54 |LARGE BRUSHED BRASS | 3| 8 +Brand#54 |LARGE BRUSHED BRASS | 14| 8 +Brand#54 |LARGE BRUSHED BRASS | 45| 8 +Brand#54 |LARGE BRUSHED COPPER | 14| 8 +Brand#54 |LARGE BRUSHED COPPER | 45| 8 +Brand#54 |LARGE BRUSHED NICKEL | 3| 8 +Brand#54 |LARGE BRUSHED STEEL | 36| 8 +Brand#54 |LARGE BRUSHED STEEL | 49| 8 +Brand#54 |LARGE BRUSHED TIN | 36| 8 +Brand#54 |LARGE BURNISHED BRASS | 23| 8 +Brand#54 |LARGE BURNISHED COPPER | 49| 8 +Brand#54 |LARGE BURNISHED NICKEL | 23| 8 +Brand#54 |LARGE BURNISHED NICKEL | 49| 8 +Brand#54 |LARGE BURNISHED STEEL | 49| 8 +Brand#54 |LARGE BURNISHED TIN | 14| 8 +Brand#54 |LARGE BURNISHED TIN | 49| 8 +Brand#54 |LARGE PLATED BRASS | 23| 8 +Brand#54 |LARGE PLATED BRASS | 45| 8 +Brand#54 |LARGE PLATED COPPER | 49| 8 +Brand#54 |LARGE PLATED STEEL | 3| 8 +Brand#54 |LARGE PLATED STEEL | 9| 8 +Brand#54 |LARGE PLATED STEEL | 19| 8 +Brand#54 |LARGE PLATED STEEL | 36| 8 +Brand#54 |LARGE PLATED TIN | 9| 8 +Brand#54 |LARGE POLISHED BRASS | 49| 8 +Brand#54 |LARGE POLISHED COPPER | 45| 8 +Brand#54 |LARGE POLISHED NICKEL | 14| 8 +Brand#54 |LARGE POLISHED STEEL | 3| 8 +Brand#54 |LARGE POLISHED STEEL | 14| 8 +Brand#54 |LARGE POLISHED STEEL | 19| 8 +Brand#54 |LARGE POLISHED TIN | 36| 8 +Brand#54 |MEDIUM ANODIZED BRASS | 9| 8 +Brand#54 |MEDIUM ANODIZED BRASS | 36| 8 +Brand#54 |MEDIUM ANODIZED BRASS | 49| 8 +Brand#54 |MEDIUM ANODIZED COPPER | 23| 8 +Brand#54 |MEDIUM ANODIZED NICKEL | 3| 8 +Brand#54 |MEDIUM ANODIZED NICKEL | 14| 8 +Brand#54 |MEDIUM ANODIZED NICKEL | 19| 8 +Brand#54 |MEDIUM ANODIZED NICKEL | 36| 8 +Brand#54 |MEDIUM ANODIZED STEEL | 3| 8 +Brand#54 |MEDIUM BRUSHED BRASS | 3| 8 +Brand#54 |MEDIUM BRUSHED BRASS | 14| 8 +Brand#54 |MEDIUM BRUSHED BRASS | 19| 8 +Brand#54 |MEDIUM BRUSHED BRASS | 45| 8 +Brand#54 |MEDIUM BRUSHED COPPER | 14| 8 +Brand#54 |MEDIUM BRUSHED COPPER | 19| 8 +Brand#54 |MEDIUM BRUSHED COPPER | 23| 8 +Brand#54 |MEDIUM BRUSHED NICKEL | 9| 8 +Brand#54 |MEDIUM BRUSHED STEEL | 9| 8 +Brand#54 |MEDIUM BRUSHED STEEL | 45| 8 +Brand#54 |MEDIUM BRUSHED TIN | 14| 8 +Brand#54 |MEDIUM BRUSHED TIN | 49| 8 +Brand#54 |MEDIUM BURNISHED BRASS | 23| 8 +Brand#54 |MEDIUM BURNISHED BRASS | 49| 8 +Brand#54 |MEDIUM BURNISHED COPPER | 3| 8 +Brand#54 |MEDIUM BURNISHED COPPER | 36| 8 +Brand#54 |MEDIUM BURNISHED COPPER | 45| 8 +Brand#54 |MEDIUM BURNISHED STEEL | 14| 8 +Brand#54 |MEDIUM BURNISHED STEEL | 19| 8 +Brand#54 |MEDIUM BURNISHED TIN | 9| 8 +Brand#54 |MEDIUM BURNISHED TIN | 19| 8 +Brand#54 |MEDIUM BURNISHED TIN | 36| 8 +Brand#54 |MEDIUM PLATED BRASS | 3| 8 +Brand#54 |MEDIUM PLATED BRASS | 23| 8 +Brand#54 |MEDIUM PLATED COPPER | 9| 8 +Brand#54 |MEDIUM PLATED COPPER | 45| 8 +Brand#54 |MEDIUM PLATED COPPER | 49| 8 +Brand#54 |MEDIUM PLATED NICKEL | 45| 8 +Brand#54 |MEDIUM PLATED TIN | 19| 8 +Brand#54 |MEDIUM PLATED TIN | 23| 8 +Brand#54 |PROMO ANODIZED BRASS | 3| 8 +Brand#54 |PROMO ANODIZED BRASS | 9| 8 +Brand#54 |PROMO ANODIZED BRASS | 14| 8 +Brand#54 |PROMO ANODIZED BRASS | 23| 8 +Brand#54 |PROMO ANODIZED BRASS | 36| 8 +Brand#54 |PROMO ANODIZED COPPER | 23| 8 +Brand#54 |PROMO ANODIZED STEEL | 36| 8 +Brand#54 |PROMO ANODIZED TIN | 19| 8 +Brand#54 |PROMO BRUSHED BRASS | 23| 8 +Brand#54 |PROMO BRUSHED BRASS | 49| 8 +Brand#54 |PROMO BRUSHED COPPER | 3| 8 +Brand#54 |PROMO BRUSHED COPPER | 45| 8 +Brand#54 |PROMO BRUSHED NICKEL | 3| 8 +Brand#54 |PROMO BRUSHED NICKEL | 23| 8 +Brand#54 |PROMO BRUSHED STEEL | 36| 8 +Brand#54 |PROMO BRUSHED STEEL | 49| 8 +Brand#54 |PROMO BRUSHED TIN | 45| 8 +Brand#54 |PROMO BURNISHED COPPER | 14| 8 +Brand#54 |PROMO BURNISHED NICKEL | 19| 8 +Brand#54 |PROMO BURNISHED NICKEL | 49| 8 +Brand#54 |PROMO BURNISHED STEEL | 3| 8 +Brand#54 |PROMO BURNISHED TIN | 3| 8 +Brand#54 |PROMO BURNISHED TIN | 14| 8 +Brand#54 |PROMO BURNISHED TIN | 45| 8 +Brand#54 |PROMO PLATED COPPER | 36| 8 +Brand#54 |PROMO PLATED COPPER | 45| 8 +Brand#54 |PROMO PLATED COPPER | 49| 8 +Brand#54 |PROMO PLATED NICKEL | 3| 8 +Brand#54 |PROMO PLATED NICKEL | 14| 8 +Brand#54 |PROMO PLATED NICKEL | 19| 8 +Brand#54 |PROMO PLATED STEEL | 45| 8 +Brand#54 |PROMO PLATED TIN | 14| 8 +Brand#54 |PROMO PLATED TIN | 23| 8 +Brand#54 |PROMO POLISHED BRASS | 14| 8 +Brand#54 |PROMO POLISHED BRASS | 36| 8 +Brand#54 |PROMO POLISHED COPPER | 14| 8 +Brand#54 |PROMO POLISHED COPPER | 23| 8 +Brand#54 |PROMO POLISHED NICKEL | 14| 8 +Brand#54 |PROMO POLISHED NICKEL | 19| 8 +Brand#54 |PROMO POLISHED NICKEL | 23| 8 +Brand#54 |PROMO POLISHED STEEL | 23| 8 +Brand#54 |PROMO POLISHED STEEL | 36| 8 +Brand#54 |PROMO POLISHED TIN | 49| 8 +Brand#54 |SMALL ANODIZED BRASS | 19| 8 +Brand#54 |SMALL ANODIZED COPPER | 23| 8 +Brand#54 |SMALL ANODIZED COPPER | 45| 8 +Brand#54 |SMALL ANODIZED NICKEL | 14| 8 +Brand#54 |SMALL ANODIZED STEEL | 9| 8 +Brand#54 |SMALL ANODIZED TIN | 14| 8 +Brand#54 |SMALL BRUSHED BRASS | 9| 8 +Brand#54 |SMALL BRUSHED BRASS | 49| 8 +Brand#54 |SMALL BRUSHED COPPER | 45| 8 +Brand#54 |SMALL BRUSHED TIN | 19| 8 +Brand#54 |SMALL BRUSHED TIN | 36| 8 +Brand#54 |SMALL BURNISHED BRASS | 9| 8 +Brand#54 |SMALL BURNISHED BRASS | 14| 8 +Brand#54 |SMALL BURNISHED COPPER | 3| 8 +Brand#54 |SMALL BURNISHED COPPER | 14| 8 +Brand#54 |SMALL BURNISHED STEEL | 9| 8 +Brand#54 |SMALL BURNISHED TIN | 23| 8 +Brand#54 |SMALL BURNISHED TIN | 49| 8 +Brand#54 |SMALL PLATED COPPER | 14| 8 +Brand#54 |SMALL PLATED COPPER | 23| 8 +Brand#54 |SMALL PLATED COPPER | 45| 8 +Brand#54 |SMALL PLATED NICKEL | 14| 8 +Brand#54 |SMALL PLATED STEEL | 49| 8 +Brand#54 |SMALL PLATED TIN | 14| 8 +Brand#54 |SMALL PLATED TIN | 23| 8 +Brand#54 |SMALL PLATED TIN | 36| 8 +Brand#54 |SMALL POLISHED BRASS | 9| 8 +Brand#54 |SMALL POLISHED BRASS | 36| 8 +Brand#54 |SMALL POLISHED COPPER | 3| 8 +Brand#54 |SMALL POLISHED COPPER | 49| 8 +Brand#54 |SMALL POLISHED NICKEL | 3| 8 +Brand#54 |SMALL POLISHED NICKEL | 14| 8 +Brand#54 |SMALL POLISHED NICKEL | 23| 8 +Brand#54 |SMALL POLISHED STEEL | 3| 8 +Brand#54 |SMALL POLISHED STEEL | 23| 8 +Brand#54 |SMALL POLISHED TIN | 45| 8 +Brand#54 |STANDARD ANODIZED BRASS | 9| 8 +Brand#54 |STANDARD ANODIZED BRASS | 45| 8 +Brand#54 |STANDARD ANODIZED COPPER | 9| 8 +Brand#54 |STANDARD ANODIZED COPPER | 23| 8 +Brand#54 |STANDARD ANODIZED STEEL | 3| 8 +Brand#54 |STANDARD ANODIZED STEEL | 14| 8 +Brand#54 |STANDARD ANODIZED STEEL | 23| 8 +Brand#54 |STANDARD ANODIZED TIN | 45| 8 +Brand#54 |STANDARD BRUSHED BRASS | 36| 8 +Brand#54 |STANDARD BRUSHED COPPER | 36| 8 +Brand#54 |STANDARD BRUSHED NICKEL | 14| 8 +Brand#54 |STANDARD BRUSHED NICKEL | 49| 8 +Brand#54 |STANDARD BRUSHED STEEL | 9| 8 +Brand#54 |STANDARD BRUSHED STEEL | 36| 8 +Brand#54 |STANDARD BRUSHED TIN | 19| 8 +Brand#54 |STANDARD BRUSHED TIN | 23| 8 +Brand#54 |STANDARD BRUSHED TIN | 49| 8 +Brand#54 |STANDARD BURNISHED BRASS | 45| 8 +Brand#54 |STANDARD BURNISHED COPPER| 9| 8 +Brand#54 |STANDARD BURNISHED COPPER| 19| 8 +Brand#54 |STANDARD BURNISHED NICKEL| 23| 8 +Brand#54 |STANDARD BURNISHED STEEL | 14| 8 +Brand#54 |STANDARD PLATED BRASS | 3| 8 +Brand#54 |STANDARD PLATED BRASS | 23| 8 +Brand#54 |STANDARD PLATED COPPER | 36| 8 +Brand#54 |STANDARD PLATED NICKEL | 36| 8 +Brand#54 |STANDARD PLATED STEEL | 45| 8 +Brand#54 |STANDARD PLATED TIN | 49| 8 +Brand#54 |STANDARD POLISHED BRASS | 49| 8 +Brand#54 |STANDARD POLISHED COPPER | 19| 8 +Brand#54 |STANDARD POLISHED COPPER | 23| 8 +Brand#54 |STANDARD POLISHED NICKEL | 36| 8 +Brand#54 |STANDARD POLISHED STEEL | 19| 8 +Brand#54 |STANDARD POLISHED TIN | 9| 8 +Brand#54 |STANDARD POLISHED TIN | 14| 8 +Brand#55 |ECONOMY ANODIZED COPPER | 23| 8 +Brand#55 |ECONOMY ANODIZED NICKEL | 9| 8 +Brand#55 |ECONOMY ANODIZED NICKEL | 14| 8 +Brand#55 |ECONOMY ANODIZED NICKEL | 45| 8 +Brand#55 |ECONOMY ANODIZED STEEL | 9| 8 +Brand#55 |ECONOMY ANODIZED STEEL | 49| 8 +Brand#55 |ECONOMY ANODIZED TIN | 9| 8 +Brand#55 |ECONOMY ANODIZED TIN | 14| 8 +Brand#55 |ECONOMY ANODIZED TIN | 19| 8 +Brand#55 |ECONOMY ANODIZED TIN | 23| 8 +Brand#55 |ECONOMY ANODIZED TIN | 36| 8 +Brand#55 |ECONOMY BRUSHED COPPER | 23| 8 +Brand#55 |ECONOMY BRUSHED STEEL | 49| 8 +Brand#55 |ECONOMY BRUSHED TIN | 3| 8 +Brand#55 |ECONOMY BRUSHED TIN | 23| 8 +Brand#55 |ECONOMY BURNISHED BRASS | 3| 8 +Brand#55 |ECONOMY BURNISHED BRASS | 14| 8 +Brand#55 |ECONOMY BURNISHED COPPER | 23| 8 +Brand#55 |ECONOMY BURNISHED NICKEL | 19| 8 +Brand#55 |ECONOMY BURNISHED NICKEL | 49| 8 +Brand#55 |ECONOMY BURNISHED STEEL | 9| 8 +Brand#55 |ECONOMY BURNISHED STEEL | 19| 8 +Brand#55 |ECONOMY BURNISHED STEEL | 49| 8 +Brand#55 |ECONOMY BURNISHED TIN | 45| 8 +Brand#55 |ECONOMY PLATED BRASS | 45| 8 +Brand#55 |ECONOMY PLATED COPPER | 49| 8 +Brand#55 |ECONOMY PLATED NICKEL | 19| 8 +Brand#55 |ECONOMY PLATED NICKEL | 36| 8 +Brand#55 |ECONOMY PLATED TIN | 23| 8 +Brand#55 |ECONOMY POLISHED BRASS | 19| 8 +Brand#55 |ECONOMY POLISHED BRASS | 23| 8 +Brand#55 |ECONOMY POLISHED COPPER | 23| 8 +Brand#55 |ECONOMY POLISHED COPPER | 45| 8 +Brand#55 |ECONOMY POLISHED NICKEL | 9| 8 +Brand#55 |ECONOMY POLISHED NICKEL | 14| 8 +Brand#55 |ECONOMY POLISHED NICKEL | 19| 8 +Brand#55 |ECONOMY POLISHED NICKEL | 45| 8 +Brand#55 |ECONOMY POLISHED TIN | 9| 8 +Brand#55 |LARGE ANODIZED BRASS | 36| 8 +Brand#55 |LARGE ANODIZED COPPER | 9| 8 +Brand#55 |LARGE ANODIZED COPPER | 36| 8 +Brand#55 |LARGE ANODIZED COPPER | 45| 8 +Brand#55 |LARGE ANODIZED NICKEL | 36| 8 +Brand#55 |LARGE ANODIZED STEEL | 9| 8 +Brand#55 |LARGE ANODIZED TIN | 14| 8 +Brand#55 |LARGE BRUSHED COPPER | 9| 8 +Brand#55 |LARGE BRUSHED COPPER | 19| 8 +Brand#55 |LARGE BRUSHED NICKEL | 14| 8 +Brand#55 |LARGE BRUSHED TIN | 9| 8 +Brand#55 |LARGE BURNISHED BRASS | 3| 8 +Brand#55 |LARGE BURNISHED BRASS | 49| 8 +Brand#55 |LARGE BURNISHED COPPER | 36| 8 +Brand#55 |LARGE BURNISHED COPPER | 49| 8 +Brand#55 |LARGE BURNISHED NICKEL | 19| 8 +Brand#55 |LARGE BURNISHED NICKEL | 45| 8 +Brand#55 |LARGE BURNISHED STEEL | 3| 8 +Brand#55 |LARGE BURNISHED STEEL | 23| 8 +Brand#55 |LARGE PLATED COPPER | 14| 8 +Brand#55 |LARGE PLATED NICKEL | 9| 8 +Brand#55 |LARGE PLATED STEEL | 19| 8 +Brand#55 |LARGE PLATED STEEL | 36| 8 +Brand#55 |LARGE PLATED STEEL | 49| 8 +Brand#55 |LARGE PLATED TIN | 9| 8 +Brand#55 |LARGE PLATED TIN | 14| 8 +Brand#55 |LARGE PLATED TIN | 36| 8 +Brand#55 |LARGE PLATED TIN | 45| 8 +Brand#55 |LARGE POLISHED BRASS | 3| 8 +Brand#55 |LARGE POLISHED COPPER | 9| 8 +Brand#55 |LARGE POLISHED COPPER | 36| 8 +Brand#55 |LARGE POLISHED TIN | 9| 8 +Brand#55 |LARGE POLISHED TIN | 45| 8 +Brand#55 |MEDIUM ANODIZED BRASS | 23| 8 +Brand#55 |MEDIUM ANODIZED COPPER | 14| 8 +Brand#55 |MEDIUM ANODIZED COPPER | 49| 8 +Brand#55 |MEDIUM ANODIZED NICKEL | 14| 8 +Brand#55 |MEDIUM ANODIZED NICKEL | 19| 8 +Brand#55 |MEDIUM ANODIZED NICKEL | 45| 8 +Brand#55 |MEDIUM ANODIZED STEEL | 3| 8 +Brand#55 |MEDIUM ANODIZED STEEL | 14| 8 +Brand#55 |MEDIUM ANODIZED TIN | 45| 8 +Brand#55 |MEDIUM BRUSHED COPPER | 23| 8 +Brand#55 |MEDIUM BRUSHED NICKEL | 9| 8 +Brand#55 |MEDIUM BRUSHED NICKEL | 36| 8 +Brand#55 |MEDIUM BRUSHED STEEL | 14| 8 +Brand#55 |MEDIUM BRUSHED STEEL | 36| 8 +Brand#55 |MEDIUM BRUSHED STEEL | 49| 8 +Brand#55 |MEDIUM BRUSHED TIN | 45| 8 +Brand#55 |MEDIUM BURNISHED COPPER | 23| 8 +Brand#55 |MEDIUM BURNISHED NICKEL | 23| 8 +Brand#55 |MEDIUM BURNISHED STEEL | 14| 8 +Brand#55 |MEDIUM BURNISHED STEEL | 36| 8 +Brand#55 |MEDIUM BURNISHED STEEL | 49| 8 +Brand#55 |MEDIUM BURNISHED TIN | 45| 8 +Brand#55 |MEDIUM PLATED BRASS | 23| 8 +Brand#55 |MEDIUM PLATED COPPER | 9| 8 +Brand#55 |MEDIUM PLATED COPPER | 45| 8 +Brand#55 |MEDIUM PLATED NICKEL | 49| 8 +Brand#55 |MEDIUM PLATED TIN | 3| 8 +Brand#55 |MEDIUM PLATED TIN | 14| 8 +Brand#55 |MEDIUM PLATED TIN | 36| 8 +Brand#55 |PROMO ANODIZED BRASS | 45| 8 +Brand#55 |PROMO ANODIZED BRASS | 49| 8 +Brand#55 |PROMO ANODIZED COPPER | 3| 8 +Brand#55 |PROMO ANODIZED COPPER | 45| 8 +Brand#55 |PROMO ANODIZED COPPER | 49| 8 +Brand#55 |PROMO ANODIZED NICKEL | 3| 8 +Brand#55 |PROMO ANODIZED NICKEL | 14| 8 +Brand#55 |PROMO ANODIZED NICKEL | 36| 8 +Brand#55 |PROMO ANODIZED STEEL | 3| 8 +Brand#55 |PROMO ANODIZED STEEL | 36| 8 +Brand#55 |PROMO ANODIZED STEEL | 49| 8 +Brand#55 |PROMO ANODIZED TIN | 36| 8 +Brand#55 |PROMO ANODIZED TIN | 49| 8 +Brand#55 |PROMO BRUSHED BRASS | 9| 8 +Brand#55 |PROMO BRUSHED COPPER | 9| 8 +Brand#55 |PROMO BRUSHED NICKEL | 36| 8 +Brand#55 |PROMO BRUSHED NICKEL | 49| 8 +Brand#55 |PROMO BRUSHED STEEL | 3| 8 +Brand#55 |PROMO BRUSHED STEEL | 9| 8 +Brand#55 |PROMO BRUSHED STEEL | 36| 8 +Brand#55 |PROMO BRUSHED STEEL | 45| 8 +Brand#55 |PROMO BRUSHED TIN | 49| 8 +Brand#55 |PROMO BURNISHED BRASS | 49| 8 +Brand#55 |PROMO BURNISHED COPPER | 14| 8 +Brand#55 |PROMO BURNISHED STEEL | 9| 8 +Brand#55 |PROMO BURNISHED TIN | 45| 8 +Brand#55 |PROMO BURNISHED TIN | 49| 8 +Brand#55 |PROMO PLATED BRASS | 9| 8 +Brand#55 |PROMO PLATED BRASS | 36| 8 +Brand#55 |PROMO PLATED BRASS | 45| 8 +Brand#55 |PROMO PLATED COPPER | 14| 8 +Brand#55 |PROMO PLATED COPPER | 23| 8 +Brand#55 |PROMO PLATED NICKEL | 14| 8 +Brand#55 |PROMO PLATED NICKEL | 49| 8 +Brand#55 |PROMO PLATED TIN | 36| 8 +Brand#55 |PROMO PLATED TIN | 45| 8 +Brand#55 |PROMO POLISHED BRASS | 3| 8 +Brand#55 |PROMO POLISHED COPPER | 36| 8 +Brand#55 |PROMO POLISHED STEEL | 3| 8 +Brand#55 |PROMO POLISHED STEEL | 14| 8 +Brand#55 |PROMO POLISHED STEEL | 36| 8 +Brand#55 |SMALL ANODIZED BRASS | 19| 8 +Brand#55 |SMALL ANODIZED COPPER | 14| 8 +Brand#55 |SMALL ANODIZED NICKEL | 3| 8 +Brand#55 |SMALL ANODIZED STEEL | 14| 8 +Brand#55 |SMALL ANODIZED STEEL | 19| 8 +Brand#55 |SMALL ANODIZED STEEL | 49| 8 +Brand#55 |SMALL ANODIZED TIN | 3| 8 +Brand#55 |SMALL BRUSHED BRASS | 19| 8 +Brand#55 |SMALL BRUSHED BRASS | 49| 8 +Brand#55 |SMALL BRUSHED COPPER | 14| 8 +Brand#55 |SMALL BRUSHED COPPER | 36| 8 +Brand#55 |SMALL BRUSHED COPPER | 45| 8 +Brand#55 |SMALL BRUSHED TIN | 23| 8 +Brand#55 |SMALL BURNISHED BRASS | 9| 8 +Brand#55 |SMALL BURNISHED COPPER | 45| 8 +Brand#55 |SMALL BURNISHED NICKEL | 3| 8 +Brand#55 |SMALL BURNISHED STEEL | 19| 8 +Brand#55 |SMALL BURNISHED STEEL | 23| 8 +Brand#55 |SMALL BURNISHED TIN | 3| 8 +Brand#55 |SMALL BURNISHED TIN | 14| 8 +Brand#55 |SMALL BURNISHED TIN | 19| 8 +Brand#55 |SMALL BURNISHED TIN | 36| 8 +Brand#55 |SMALL PLATED BRASS | 45| 8 +Brand#55 |SMALL PLATED COPPER | 9| 8 +Brand#55 |SMALL PLATED COPPER | 19| 8 +Brand#55 |SMALL PLATED COPPER | 23| 8 +Brand#55 |SMALL PLATED COPPER | 45| 8 +Brand#55 |SMALL PLATED NICKEL | 9| 8 +Brand#55 |SMALL PLATED NICKEL | 23| 8 +Brand#55 |SMALL PLATED STEEL | 49| 8 +Brand#55 |SMALL PLATED TIN | 3| 8 +Brand#55 |SMALL PLATED TIN | 9| 8 +Brand#55 |SMALL PLATED TIN | 14| 8 +Brand#55 |SMALL PLATED TIN | 49| 8 +Brand#55 |SMALL POLISHED BRASS | 14| 8 +Brand#55 |SMALL POLISHED COPPER | 3| 8 +Brand#55 |SMALL POLISHED TIN | 19| 8 +Brand#55 |SMALL POLISHED TIN | 49| 8 +Brand#55 |STANDARD ANODIZED BRASS | 14| 8 +Brand#55 |STANDARD ANODIZED BRASS | 36| 8 +Brand#55 |STANDARD ANODIZED COPPER | 23| 8 +Brand#55 |STANDARD ANODIZED NICKEL | 23| 8 +Brand#55 |STANDARD ANODIZED TIN | 19| 8 +Brand#55 |STANDARD ANODIZED TIN | 49| 8 +Brand#55 |STANDARD BRUSHED BRASS | 3| 8 +Brand#55 |STANDARD BRUSHED BRASS | 36| 8 +Brand#55 |STANDARD BRUSHED BRASS | 45| 8 +Brand#55 |STANDARD BRUSHED COPPER | 3| 8 +Brand#55 |STANDARD BRUSHED COPPER | 23| 8 +Brand#55 |STANDARD BRUSHED NICKEL | 19| 8 +Brand#55 |STANDARD BRUSHED TIN | 23| 8 +Brand#55 |STANDARD BURNISHED BRASS | 49| 8 +Brand#55 |STANDARD BURNISHED COPPER| 23| 8 +Brand#55 |STANDARD BURNISHED COPPER| 36| 8 +Brand#55 |STANDARD BURNISHED NICKEL| 3| 8 +Brand#55 |STANDARD BURNISHED NICKEL| 14| 8 +Brand#55 |STANDARD BURNISHED NICKEL| 36| 8 +Brand#55 |STANDARD BURNISHED NICKEL| 45| 8 +Brand#55 |STANDARD BURNISHED STEEL | 14| 8 +Brand#55 |STANDARD BURNISHED STEEL | 49| 8 +Brand#55 |STANDARD PLATED BRASS | 19| 8 +Brand#55 |STANDARD PLATED BRASS | 23| 8 +Brand#55 |STANDARD PLATED COPPER | 23| 8 +Brand#55 |STANDARD PLATED NICKEL | 49| 8 +Brand#55 |STANDARD PLATED TIN | 23| 8 +Brand#55 |STANDARD POLISHED BRASS | 19| 8 +Brand#55 |STANDARD POLISHED BRASS | 49| 8 +Brand#55 |STANDARD POLISHED COPPER | 9| 8 +Brand#55 |STANDARD POLISHED COPPER | 36| 8 +Brand#55 |STANDARD POLISHED STEEL | 9| 8 +Brand#55 |STANDARD POLISHED STEEL | 36| 8 +Brand#55 |STANDARD POLISHED STEEL | 45| 8 +Brand#55 |STANDARD POLISHED STEEL | 49| 8 +Brand#12 |PROMO ANODIZED NICKEL | 49| 7 +Brand#13 |LARGE PLATED TIN | 23| 7 +Brand#14 |PROMO PLATED BRASS | 19| 7 +Brand#22 |STANDARD POLISHED TIN | 3| 7 +Brand#23 |ECONOMY PLATED NICKEL | 19| 7 +Brand#23 |LARGE BURNISHED NICKEL | 14| 7 +Brand#24 |PROMO BRUSHED NICKEL | 14| 7 +Brand#31 |MEDIUM BURNISHED NICKEL | 23| 7 +Brand#32 |LARGE BRUSHED COPPER | 3| 7 +Brand#32 |LARGE POLISHED NICKEL | 23| 7 +Brand#32 |STANDARD BURNISHED STEEL | 19| 7 +Brand#33 |ECONOMY BRUSHED BRASS | 3| 7 +Brand#33 |PROMO PLATED NICKEL | 9| 7 +Brand#33 |SMALL ANODIZED COPPER | 23| 7 +Brand#41 |ECONOMY BRUSHED COPPER | 36| 7 +Brand#41 |PROMO POLISHED BRASS | 45| 7 +Brand#42 |MEDIUM PLATED STEEL | 45| 7 +Brand#42 |STANDARD PLATED COPPER | 19| 7 +Brand#43 |LARGE POLISHED COPPER | 19| 7 +Brand#44 |PROMO BURNISHED STEEL | 45| 7 +Brand#51 |STANDARD PLATED TIN | 45| 7 +Brand#52 |STANDARD ANODIZED STEEL | 14| 7 +Brand#53 |STANDARD ANODIZED NICKEL | 14| 7 +Brand#55 |ECONOMY POLISHED TIN | 19| 7 +Brand#55 |SMALL BURNISHED STEEL | 3| 7 +Brand#32 |MEDIUM BURNISHED STEEL | 3| 6 +Brand#11 |ECONOMY ANODIZED BRASS | 3| 4 +Brand#11 |ECONOMY ANODIZED BRASS | 45| 4 +Brand#11 |ECONOMY ANODIZED COPPER | 3| 4 +Brand#11 |ECONOMY ANODIZED COPPER | 19| 4 +Brand#11 |ECONOMY ANODIZED COPPER | 36| 4 +Brand#11 |ECONOMY ANODIZED COPPER | 45| 4 +Brand#11 |ECONOMY ANODIZED STEEL | 9| 4 +Brand#11 |ECONOMY ANODIZED STEEL | 14| 4 +Brand#11 |ECONOMY ANODIZED STEEL | 23| 4 +Brand#11 |ECONOMY ANODIZED STEEL | 45| 4 +Brand#11 |ECONOMY ANODIZED STEEL | 49| 4 +Brand#11 |ECONOMY ANODIZED TIN | 3| 4 +Brand#11 |ECONOMY ANODIZED TIN | 9| 4 +Brand#11 |ECONOMY ANODIZED TIN | 49| 4 +Brand#11 |ECONOMY BRUSHED BRASS | 3| 4 +Brand#11 |ECONOMY BRUSHED BRASS | 19| 4 +Brand#11 |ECONOMY BRUSHED COPPER | 3| 4 +Brand#11 |ECONOMY BRUSHED COPPER | 19| 4 +Brand#11 |ECONOMY BRUSHED NICKEL | 14| 4 +Brand#11 |ECONOMY BRUSHED STEEL | 3| 4 +Brand#11 |ECONOMY BRUSHED STEEL | 36| 4 +Brand#11 |ECONOMY BRUSHED TIN | 23| 4 +Brand#11 |ECONOMY BRUSHED TIN | 45| 4 +Brand#11 |ECONOMY BURNISHED BRASS | 3| 4 +Brand#11 |ECONOMY BURNISHED BRASS | 9| 4 +Brand#11 |ECONOMY BURNISHED BRASS | 14| 4 +Brand#11 |ECONOMY BURNISHED BRASS | 19| 4 +Brand#11 |ECONOMY BURNISHED BRASS | 49| 4 +Brand#11 |ECONOMY BURNISHED COPPER | 14| 4 +Brand#11 |ECONOMY BURNISHED COPPER | 23| 4 +Brand#11 |ECONOMY BURNISHED COPPER | 36| 4 +Brand#11 |ECONOMY BURNISHED NICKEL | 9| 4 +Brand#11 |ECONOMY BURNISHED NICKEL | 49| 4 +Brand#11 |ECONOMY BURNISHED STEEL | 14| 4 +Brand#11 |ECONOMY BURNISHED TIN | 19| 4 +Brand#11 |ECONOMY BURNISHED TIN | 23| 4 +Brand#11 |ECONOMY BURNISHED TIN | 45| 4 +Brand#11 |ECONOMY PLATED BRASS | 3| 4 +Brand#11 |ECONOMY PLATED BRASS | 9| 4 +Brand#11 |ECONOMY PLATED BRASS | 36| 4 +Brand#11 |ECONOMY PLATED BRASS | 49| 4 +Brand#11 |ECONOMY PLATED COPPER | 36| 4 +Brand#11 |ECONOMY PLATED NICKEL | 3| 4 +Brand#11 |ECONOMY PLATED NICKEL | 49| 4 +Brand#11 |ECONOMY PLATED STEEL | 3| 4 +Brand#11 |ECONOMY PLATED STEEL | 14| 4 +Brand#11 |ECONOMY PLATED STEEL | 19| 4 +Brand#11 |ECONOMY PLATED STEEL | 49| 4 +Brand#11 |ECONOMY PLATED TIN | 3| 4 +Brand#11 |ECONOMY PLATED TIN | 9| 4 +Brand#11 |ECONOMY PLATED TIN | 19| 4 +Brand#11 |ECONOMY PLATED TIN | 36| 4 +Brand#11 |ECONOMY POLISHED BRASS | 9| 4 +Brand#11 |ECONOMY POLISHED BRASS | 19| 4 +Brand#11 |ECONOMY POLISHED BRASS | 23| 4 +Brand#11 |ECONOMY POLISHED BRASS | 36| 4 +Brand#11 |ECONOMY POLISHED BRASS | 49| 4 +Brand#11 |ECONOMY POLISHED COPPER | 3| 4 +Brand#11 |ECONOMY POLISHED COPPER | 19| 4 +Brand#11 |ECONOMY POLISHED COPPER | 23| 4 +Brand#11 |ECONOMY POLISHED NICKEL | 36| 4 +Brand#11 |ECONOMY POLISHED NICKEL | 49| 4 +Brand#11 |ECONOMY POLISHED STEEL | 9| 4 +Brand#11 |ECONOMY POLISHED STEEL | 14| 4 +Brand#11 |ECONOMY POLISHED STEEL | 23| 4 +Brand#11 |ECONOMY POLISHED STEEL | 36| 4 +Brand#11 |ECONOMY POLISHED STEEL | 45| 4 +Brand#11 |ECONOMY POLISHED TIN | 49| 4 +Brand#11 |LARGE ANODIZED BRASS | 3| 4 +Brand#11 |LARGE ANODIZED BRASS | 9| 4 +Brand#11 |LARGE ANODIZED BRASS | 19| 4 +Brand#11 |LARGE ANODIZED BRASS | 23| 4 +Brand#11 |LARGE ANODIZED COPPER | 3| 4 +Brand#11 |LARGE ANODIZED COPPER | 9| 4 +Brand#11 |LARGE ANODIZED COPPER | 36| 4 +Brand#11 |LARGE ANODIZED COPPER | 45| 4 +Brand#11 |LARGE ANODIZED NICKEL | 23| 4 +Brand#11 |LARGE ANODIZED STEEL | 14| 4 +Brand#11 |LARGE ANODIZED STEEL | 49| 4 +Brand#11 |LARGE ANODIZED TIN | 3| 4 +Brand#11 |LARGE ANODIZED TIN | 9| 4 +Brand#11 |LARGE ANODIZED TIN | 14| 4 +Brand#11 |LARGE ANODIZED TIN | 19| 4 +Brand#11 |LARGE BRUSHED BRASS | 36| 4 +Brand#11 |LARGE BRUSHED BRASS | 45| 4 +Brand#11 |LARGE BRUSHED COPPER | 3| 4 +Brand#11 |LARGE BRUSHED NICKEL | 9| 4 +Brand#11 |LARGE BRUSHED NICKEL | 36| 4 +Brand#11 |LARGE BRUSHED NICKEL | 49| 4 +Brand#11 |LARGE BRUSHED STEEL | 3| 4 +Brand#11 |LARGE BRUSHED STEEL | 9| 4 +Brand#11 |LARGE BRUSHED STEEL | 23| 4 +Brand#11 |LARGE BRUSHED STEEL | 45| 4 +Brand#11 |LARGE BRUSHED TIN | 3| 4 +Brand#11 |LARGE BURNISHED BRASS | 9| 4 +Brand#11 |LARGE BURNISHED BRASS | 19| 4 +Brand#11 |LARGE BURNISHED BRASS | 36| 4 +Brand#11 |LARGE BURNISHED COPPER | 19| 4 +Brand#11 |LARGE BURNISHED COPPER | 45| 4 +Brand#11 |LARGE BURNISHED NICKEL | 3| 4 +Brand#11 |LARGE BURNISHED NICKEL | 49| 4 +Brand#11 |LARGE BURNISHED STEEL | 14| 4 +Brand#11 |LARGE BURNISHED STEEL | 23| 4 +Brand#11 |LARGE BURNISHED STEEL | 45| 4 +Brand#11 |LARGE BURNISHED TIN | 3| 4 +Brand#11 |LARGE BURNISHED TIN | 9| 4 +Brand#11 |LARGE BURNISHED TIN | 36| 4 +Brand#11 |LARGE BURNISHED TIN | 45| 4 +Brand#11 |LARGE PLATED BRASS | 9| 4 +Brand#11 |LARGE PLATED BRASS | 36| 4 +Brand#11 |LARGE PLATED BRASS | 45| 4 +Brand#11 |LARGE PLATED BRASS | 49| 4 +Brand#11 |LARGE PLATED COPPER | 3| 4 +Brand#11 |LARGE PLATED COPPER | 9| 4 +Brand#11 |LARGE PLATED COPPER | 14| 4 +Brand#11 |LARGE PLATED COPPER | 19| 4 +Brand#11 |LARGE PLATED COPPER | 23| 4 +Brand#11 |LARGE PLATED COPPER | 36| 4 +Brand#11 |LARGE PLATED COPPER | 45| 4 +Brand#11 |LARGE PLATED COPPER | 49| 4 +Brand#11 |LARGE PLATED NICKEL | 9| 4 +Brand#11 |LARGE PLATED NICKEL | 14| 4 +Brand#11 |LARGE PLATED NICKEL | 19| 4 +Brand#11 |LARGE PLATED NICKEL | 49| 4 +Brand#11 |LARGE PLATED STEEL | 9| 4 +Brand#11 |LARGE PLATED STEEL | 49| 4 +Brand#11 |LARGE PLATED TIN | 23| 4 +Brand#11 |LARGE PLATED TIN | 45| 4 +Brand#11 |LARGE POLISHED BRASS | 3| 4 +Brand#11 |LARGE POLISHED BRASS | 14| 4 +Brand#11 |LARGE POLISHED BRASS | 19| 4 +Brand#11 |LARGE POLISHED BRASS | 23| 4 +Brand#11 |LARGE POLISHED BRASS | 45| 4 +Brand#11 |LARGE POLISHED COPPER | 3| 4 +Brand#11 |LARGE POLISHED COPPER | 19| 4 +Brand#11 |LARGE POLISHED NICKEL | 49| 4 +Brand#11 |LARGE POLISHED STEEL | 14| 4 +Brand#11 |LARGE POLISHED STEEL | 23| 4 +Brand#11 |LARGE POLISHED STEEL | 45| 4 +Brand#11 |LARGE POLISHED TIN | 9| 4 +Brand#11 |LARGE POLISHED TIN | 14| 4 +Brand#11 |LARGE POLISHED TIN | 45| 4 +Brand#11 |LARGE POLISHED TIN | 49| 4 +Brand#11 |MEDIUM ANODIZED BRASS | 19| 4 +Brand#11 |MEDIUM ANODIZED COPPER | 3| 4 +Brand#11 |MEDIUM ANODIZED COPPER | 45| 4 +Brand#11 |MEDIUM ANODIZED COPPER | 49| 4 +Brand#11 |MEDIUM ANODIZED STEEL | 14| 4 +Brand#11 |MEDIUM ANODIZED STEEL | 23| 4 +Brand#11 |MEDIUM ANODIZED TIN | 14| 4 +Brand#11 |MEDIUM ANODIZED TIN | 19| 4 +Brand#11 |MEDIUM BRUSHED BRASS | 14| 4 +Brand#11 |MEDIUM BRUSHED BRASS | 36| 4 +Brand#11 |MEDIUM BRUSHED BRASS | 49| 4 +Brand#11 |MEDIUM BRUSHED COPPER | 36| 4 +Brand#11 |MEDIUM BRUSHED COPPER | 49| 4 +Brand#11 |MEDIUM BRUSHED NICKEL | 3| 4 +Brand#11 |MEDIUM BRUSHED NICKEL | 19| 4 +Brand#11 |MEDIUM BRUSHED NICKEL | 49| 4 +Brand#11 |MEDIUM BRUSHED STEEL | 14| 4 +Brand#11 |MEDIUM BRUSHED TIN | 3| 4 +Brand#11 |MEDIUM BRUSHED TIN | 9| 4 +Brand#11 |MEDIUM BRUSHED TIN | 49| 4 +Brand#11 |MEDIUM BURNISHED BRASS | 9| 4 +Brand#11 |MEDIUM BURNISHED BRASS | 14| 4 +Brand#11 |MEDIUM BURNISHED BRASS | 36| 4 +Brand#11 |MEDIUM BURNISHED COPPER | 3| 4 +Brand#11 |MEDIUM BURNISHED COPPER | 36| 4 +Brand#11 |MEDIUM BURNISHED NICKEL | 14| 4 +Brand#11 |MEDIUM BURNISHED NICKEL | 19| 4 +Brand#11 |MEDIUM BURNISHED NICKEL | 36| 4 +Brand#11 |MEDIUM BURNISHED NICKEL | 45| 4 +Brand#11 |MEDIUM BURNISHED STEEL | 23| 4 +Brand#11 |MEDIUM BURNISHED STEEL | 45| 4 +Brand#11 |MEDIUM BURNISHED STEEL | 49| 4 +Brand#11 |MEDIUM BURNISHED TIN | 23| 4 +Brand#11 |MEDIUM BURNISHED TIN | 45| 4 +Brand#11 |MEDIUM PLATED BRASS | 19| 4 +Brand#11 |MEDIUM PLATED COPPER | 23| 4 +Brand#11 |MEDIUM PLATED COPPER | 45| 4 +Brand#11 |MEDIUM PLATED COPPER | 49| 4 +Brand#11 |MEDIUM PLATED NICKEL | 36| 4 +Brand#11 |MEDIUM PLATED NICKEL | 49| 4 +Brand#11 |MEDIUM PLATED STEEL | 49| 4 +Brand#11 |MEDIUM PLATED TIN | 36| 4 +Brand#11 |MEDIUM PLATED TIN | 49| 4 +Brand#11 |PROMO ANODIZED BRASS | 3| 4 +Brand#11 |PROMO ANODIZED BRASS | 9| 4 +Brand#11 |PROMO ANODIZED BRASS | 14| 4 +Brand#11 |PROMO ANODIZED BRASS | 23| 4 +Brand#11 |PROMO ANODIZED COPPER | 3| 4 +Brand#11 |PROMO ANODIZED COPPER | 23| 4 +Brand#11 |PROMO ANODIZED COPPER | 45| 4 +Brand#11 |PROMO ANODIZED NICKEL | 14| 4 +Brand#11 |PROMO ANODIZED NICKEL | 19| 4 +Brand#11 |PROMO ANODIZED NICKEL | 23| 4 +Brand#11 |PROMO ANODIZED NICKEL | 49| 4 +Brand#11 |PROMO ANODIZED STEEL | 9| 4 +Brand#11 |PROMO ANODIZED STEEL | 14| 4 +Brand#11 |PROMO ANODIZED TIN | 14| 4 +Brand#11 |PROMO ANODIZED TIN | 45| 4 +Brand#11 |PROMO BRUSHED BRASS | 9| 4 +Brand#11 |PROMO BRUSHED BRASS | 14| 4 +Brand#11 |PROMO BRUSHED BRASS | 19| 4 +Brand#11 |PROMO BRUSHED BRASS | 23| 4 +Brand#11 |PROMO BRUSHED BRASS | 45| 4 +Brand#11 |PROMO BRUSHED COPPER | 3| 4 +Brand#11 |PROMO BRUSHED COPPER | 23| 4 +Brand#11 |PROMO BRUSHED COPPER | 45| 4 +Brand#11 |PROMO BRUSHED COPPER | 49| 4 +Brand#11 |PROMO BRUSHED NICKEL | 3| 4 +Brand#11 |PROMO BRUSHED NICKEL | 14| 4 +Brand#11 |PROMO BRUSHED NICKEL | 23| 4 +Brand#11 |PROMO BRUSHED NICKEL | 45| 4 +Brand#11 |PROMO BRUSHED NICKEL | 49| 4 +Brand#11 |PROMO BRUSHED STEEL | 3| 4 +Brand#11 |PROMO BRUSHED STEEL | 14| 4 +Brand#11 |PROMO BRUSHED STEEL | 19| 4 +Brand#11 |PROMO BRUSHED TIN | 3| 4 +Brand#11 |PROMO BRUSHED TIN | 9| 4 +Brand#11 |PROMO BRUSHED TIN | 23| 4 +Brand#11 |PROMO BRUSHED TIN | 49| 4 +Brand#11 |PROMO BURNISHED BRASS | 14| 4 +Brand#11 |PROMO BURNISHED BRASS | 45| 4 +Brand#11 |PROMO BURNISHED COPPER | 9| 4 +Brand#11 |PROMO BURNISHED COPPER | 19| 4 +Brand#11 |PROMO BURNISHED COPPER | 36| 4 +Brand#11 |PROMO BURNISHED NICKEL | 9| 4 +Brand#11 |PROMO BURNISHED NICKEL | 19| 4 +Brand#11 |PROMO BURNISHED NICKEL | 49| 4 +Brand#11 |PROMO BURNISHED STEEL | 3| 4 +Brand#11 |PROMO BURNISHED STEEL | 9| 4 +Brand#11 |PROMO BURNISHED TIN | 3| 4 +Brand#11 |PROMO BURNISHED TIN | 9| 4 +Brand#11 |PROMO BURNISHED TIN | 14| 4 +Brand#11 |PROMO BURNISHED TIN | 19| 4 +Brand#11 |PROMO BURNISHED TIN | 49| 4 +Brand#11 |PROMO PLATED BRASS | 3| 4 +Brand#11 |PROMO PLATED BRASS | 9| 4 +Brand#11 |PROMO PLATED BRASS | 36| 4 +Brand#11 |PROMO PLATED COPPER | 9| 4 +Brand#11 |PROMO PLATED COPPER | 23| 4 +Brand#11 |PROMO PLATED NICKEL | 19| 4 +Brand#11 |PROMO PLATED NICKEL | 23| 4 +Brand#11 |PROMO PLATED NICKEL | 36| 4 +Brand#11 |PROMO PLATED NICKEL | 45| 4 +Brand#11 |PROMO PLATED STEEL | 36| 4 +Brand#11 |PROMO PLATED STEEL | 45| 4 +Brand#11 |PROMO PLATED TIN | 45| 4 +Brand#11 |PROMO POLISHED BRASS | 9| 4 +Brand#11 |PROMO POLISHED BRASS | 45| 4 +Brand#11 |PROMO POLISHED BRASS | 49| 4 +Brand#11 |PROMO POLISHED COPPER | 3| 4 +Brand#11 |PROMO POLISHED COPPER | 36| 4 +Brand#11 |PROMO POLISHED COPPER | 49| 4 +Brand#11 |PROMO POLISHED NICKEL | 14| 4 +Brand#11 |PROMO POLISHED NICKEL | 19| 4 +Brand#11 |PROMO POLISHED STEEL | 9| 4 +Brand#11 |PROMO POLISHED STEEL | 14| 4 +Brand#11 |PROMO POLISHED STEEL | 36| 4 +Brand#11 |PROMO POLISHED TIN | 36| 4 +Brand#11 |PROMO POLISHED TIN | 45| 4 +Brand#11 |SMALL ANODIZED BRASS | 3| 4 +Brand#11 |SMALL ANODIZED BRASS | 14| 4 +Brand#11 |SMALL ANODIZED BRASS | 19| 4 +Brand#11 |SMALL ANODIZED BRASS | 36| 4 +Brand#11 |SMALL ANODIZED COPPER | 9| 4 +Brand#11 |SMALL ANODIZED COPPER | 23| 4 +Brand#11 |SMALL ANODIZED COPPER | 36| 4 +Brand#11 |SMALL ANODIZED NICKEL | 3| 4 +Brand#11 |SMALL ANODIZED NICKEL | 14| 4 +Brand#11 |SMALL ANODIZED NICKEL | 19| 4 +Brand#11 |SMALL ANODIZED NICKEL | 45| 4 +Brand#11 |SMALL ANODIZED STEEL | 19| 4 +Brand#11 |SMALL ANODIZED STEEL | 36| 4 +Brand#11 |SMALL ANODIZED TIN | 3| 4 +Brand#11 |SMALL ANODIZED TIN | 14| 4 +Brand#11 |SMALL ANODIZED TIN | 49| 4 +Brand#11 |SMALL BRUSHED BRASS | 3| 4 +Brand#11 |SMALL BRUSHED BRASS | 9| 4 +Brand#11 |SMALL BRUSHED BRASS | 14| 4 +Brand#11 |SMALL BRUSHED COPPER | 3| 4 +Brand#11 |SMALL BRUSHED COPPER | 23| 4 +Brand#11 |SMALL BRUSHED COPPER | 36| 4 +Brand#11 |SMALL BRUSHED COPPER | 45| 4 +Brand#11 |SMALL BRUSHED COPPER | 49| 4 +Brand#11 |SMALL BRUSHED STEEL | 9| 4 +Brand#11 |SMALL BRUSHED STEEL | 19| 4 +Brand#11 |SMALL BRUSHED STEEL | 36| 4 +Brand#11 |SMALL BRUSHED STEEL | 45| 4 +Brand#11 |SMALL BRUSHED TIN | 9| 4 +Brand#11 |SMALL BRUSHED TIN | 23| 4 +Brand#11 |SMALL BRUSHED TIN | 36| 4 +Brand#11 |SMALL BRUSHED TIN | 45| 4 +Brand#11 |SMALL BURNISHED BRASS | 3| 4 +Brand#11 |SMALL BURNISHED BRASS | 23| 4 +Brand#11 |SMALL BURNISHED BRASS | 36| 4 +Brand#11 |SMALL BURNISHED COPPER | 3| 4 +Brand#11 |SMALL BURNISHED COPPER | 14| 4 +Brand#11 |SMALL BURNISHED NICKEL | 36| 4 +Brand#11 |SMALL BURNISHED NICKEL | 45| 4 +Brand#11 |SMALL BURNISHED STEEL | 14| 4 +Brand#11 |SMALL BURNISHED STEEL | 23| 4 +Brand#11 |SMALL BURNISHED STEEL | 49| 4 +Brand#11 |SMALL BURNISHED TIN | 14| 4 +Brand#11 |SMALL BURNISHED TIN | 23| 4 +Brand#11 |SMALL BURNISHED TIN | 36| 4 +Brand#11 |SMALL BURNISHED TIN | 49| 4 +Brand#11 |SMALL PLATED BRASS | 9| 4 +Brand#11 |SMALL PLATED BRASS | 23| 4 +Brand#11 |SMALL PLATED COPPER | 3| 4 +Brand#11 |SMALL PLATED COPPER | 14| 4 +Brand#11 |SMALL PLATED COPPER | 36| 4 +Brand#11 |SMALL PLATED NICKEL | 3| 4 +Brand#11 |SMALL PLATED NICKEL | 14| 4 +Brand#11 |SMALL PLATED NICKEL | 19| 4 +Brand#11 |SMALL PLATED STEEL | 23| 4 +Brand#11 |SMALL PLATED STEEL | 36| 4 +Brand#11 |SMALL PLATED TIN | 49| 4 +Brand#11 |SMALL POLISHED BRASS | 36| 4 +Brand#11 |SMALL POLISHED BRASS | 45| 4 +Brand#11 |SMALL POLISHED BRASS | 49| 4 +Brand#11 |SMALL POLISHED COPPER | 3| 4 +Brand#11 |SMALL POLISHED COPPER | 14| 4 +Brand#11 |SMALL POLISHED COPPER | 19| 4 +Brand#11 |SMALL POLISHED COPPER | 49| 4 +Brand#11 |SMALL POLISHED NICKEL | 3| 4 +Brand#11 |SMALL POLISHED NICKEL | 14| 4 +Brand#11 |SMALL POLISHED NICKEL | 19| 4 +Brand#11 |SMALL POLISHED STEEL | 9| 4 +Brand#11 |SMALL POLISHED STEEL | 49| 4 +Brand#11 |SMALL POLISHED TIN | 14| 4 +Brand#11 |SMALL POLISHED TIN | 19| 4 +Brand#11 |SMALL POLISHED TIN | 36| 4 +Brand#11 |SMALL POLISHED TIN | 45| 4 +Brand#11 |SMALL POLISHED TIN | 49| 4 +Brand#11 |STANDARD ANODIZED BRASS | 3| 4 +Brand#11 |STANDARD ANODIZED BRASS | 9| 4 +Brand#11 |STANDARD ANODIZED BRASS | 36| 4 +Brand#11 |STANDARD ANODIZED BRASS | 49| 4 +Brand#11 |STANDARD ANODIZED COPPER | 23| 4 +Brand#11 |STANDARD ANODIZED COPPER | 45| 4 +Brand#11 |STANDARD ANODIZED NICKEL | 3| 4 +Brand#11 |STANDARD ANODIZED NICKEL | 49| 4 +Brand#11 |STANDARD ANODIZED STEEL | 3| 4 +Brand#11 |STANDARD ANODIZED STEEL | 14| 4 +Brand#11 |STANDARD ANODIZED STEEL | 23| 4 +Brand#11 |STANDARD ANODIZED STEEL | 36| 4 +Brand#11 |STANDARD ANODIZED STEEL | 45| 4 +Brand#11 |STANDARD ANODIZED STEEL | 49| 4 +Brand#11 |STANDARD ANODIZED TIN | 3| 4 +Brand#11 |STANDARD ANODIZED TIN | 19| 4 +Brand#11 |STANDARD ANODIZED TIN | 36| 4 +Brand#11 |STANDARD ANODIZED TIN | 49| 4 +Brand#11 |STANDARD BRUSHED BRASS | 9| 4 +Brand#11 |STANDARD BRUSHED BRASS | 14| 4 +Brand#11 |STANDARD BRUSHED BRASS | 36| 4 +Brand#11 |STANDARD BRUSHED BRASS | 45| 4 +Brand#11 |STANDARD BRUSHED COPPER | 9| 4 +Brand#11 |STANDARD BRUSHED COPPER | 19| 4 +Brand#11 |STANDARD BRUSHED COPPER | 49| 4 +Brand#11 |STANDARD BRUSHED NICKEL | 19| 4 +Brand#11 |STANDARD BRUSHED NICKEL | 23| 4 +Brand#11 |STANDARD BRUSHED NICKEL | 36| 4 +Brand#11 |STANDARD BRUSHED NICKEL | 49| 4 +Brand#11 |STANDARD BRUSHED STEEL | 23| 4 +Brand#11 |STANDARD BRUSHED STEEL | 36| 4 +Brand#11 |STANDARD BRUSHED TIN | 14| 4 +Brand#11 |STANDARD BRUSHED TIN | 45| 4 +Brand#11 |STANDARD BURNISHED BRASS | 3| 4 +Brand#11 |STANDARD BURNISHED BRASS | 14| 4 +Brand#11 |STANDARD BURNISHED BRASS | 45| 4 +Brand#11 |STANDARD BURNISHED COPPER| 3| 4 +Brand#11 |STANDARD BURNISHED COPPER| 45| 4 +Brand#11 |STANDARD BURNISHED NICKEL| 3| 4 +Brand#11 |STANDARD BURNISHED NICKEL| 9| 4 +Brand#11 |STANDARD BURNISHED NICKEL| 14| 4 +Brand#11 |STANDARD BURNISHED NICKEL| 19| 4 +Brand#11 |STANDARD BURNISHED STEEL | 9| 4 +Brand#11 |STANDARD BURNISHED STEEL | 14| 4 +Brand#11 |STANDARD BURNISHED STEEL | 19| 4 +Brand#11 |STANDARD BURNISHED STEEL | 49| 4 +Brand#11 |STANDARD BURNISHED TIN | 9| 4 +Brand#11 |STANDARD BURNISHED TIN | 19| 4 +Brand#11 |STANDARD BURNISHED TIN | 23| 4 +Brand#11 |STANDARD BURNISHED TIN | 36| 4 +Brand#11 |STANDARD PLATED BRASS | 3| 4 +Brand#11 |STANDARD PLATED BRASS | 14| 4 +Brand#11 |STANDARD PLATED BRASS | 36| 4 +Brand#11 |STANDARD PLATED COPPER | 9| 4 +Brand#11 |STANDARD PLATED COPPER | 14| 4 +Brand#11 |STANDARD PLATED COPPER | 45| 4 +Brand#11 |STANDARD PLATED NICKEL | 3| 4 +Brand#11 |STANDARD PLATED NICKEL | 9| 4 +Brand#11 |STANDARD PLATED NICKEL | 23| 4 +Brand#11 |STANDARD PLATED NICKEL | 49| 4 +Brand#11 |STANDARD PLATED STEEL | 9| 4 +Brand#11 |STANDARD PLATED STEEL | 36| 4 +Brand#11 |STANDARD PLATED TIN | 19| 4 +Brand#11 |STANDARD POLISHED BRASS | 19| 4 +Brand#11 |STANDARD POLISHED BRASS | 36| 4 +Brand#11 |STANDARD POLISHED BRASS | 49| 4 +Brand#11 |STANDARD POLISHED COPPER | 3| 4 +Brand#11 |STANDARD POLISHED COPPER | 45| 4 +Brand#11 |STANDARD POLISHED COPPER | 49| 4 +Brand#11 |STANDARD POLISHED NICKEL | 14| 4 +Brand#11 |STANDARD POLISHED NICKEL | 36| 4 +Brand#11 |STANDARD POLISHED NICKEL | 45| 4 +Brand#11 |STANDARD POLISHED STEEL | 14| 4 +Brand#11 |STANDARD POLISHED STEEL | 23| 4 +Brand#11 |STANDARD POLISHED STEEL | 36| 4 +Brand#11 |STANDARD POLISHED STEEL | 45| 4 +Brand#11 |STANDARD POLISHED TIN | 3| 4 +Brand#11 |STANDARD POLISHED TIN | 19| 4 +Brand#11 |STANDARD POLISHED TIN | 36| 4 +Brand#11 |STANDARD POLISHED TIN | 45| 4 +Brand#12 |ECONOMY ANODIZED BRASS | 9| 4 +Brand#12 |ECONOMY ANODIZED BRASS | 19| 4 +Brand#12 |ECONOMY ANODIZED BRASS | 23| 4 +Brand#12 |ECONOMY ANODIZED COPPER | 9| 4 +Brand#12 |ECONOMY ANODIZED COPPER | 19| 4 +Brand#12 |ECONOMY ANODIZED COPPER | 23| 4 +Brand#12 |ECONOMY ANODIZED COPPER | 36| 4 +Brand#12 |ECONOMY ANODIZED COPPER | 45| 4 +Brand#12 |ECONOMY ANODIZED COPPER | 49| 4 +Brand#12 |ECONOMY ANODIZED NICKEL | 3| 4 +Brand#12 |ECONOMY ANODIZED NICKEL | 9| 4 +Brand#12 |ECONOMY ANODIZED NICKEL | 23| 4 +Brand#12 |ECONOMY ANODIZED NICKEL | 49| 4 +Brand#12 |ECONOMY ANODIZED STEEL | 9| 4 +Brand#12 |ECONOMY ANODIZED STEEL | 49| 4 +Brand#12 |ECONOMY ANODIZED TIN | 9| 4 +Brand#12 |ECONOMY ANODIZED TIN | 36| 4 +Brand#12 |ECONOMY ANODIZED TIN | 49| 4 +Brand#12 |ECONOMY BRUSHED BRASS | 9| 4 +Brand#12 |ECONOMY BRUSHED BRASS | 14| 4 +Brand#12 |ECONOMY BRUSHED BRASS | 45| 4 +Brand#12 |ECONOMY BRUSHED COPPER | 45| 4 +Brand#12 |ECONOMY BRUSHED NICKEL | 9| 4 +Brand#12 |ECONOMY BRUSHED NICKEL | 14| 4 +Brand#12 |ECONOMY BRUSHED NICKEL | 19| 4 +Brand#12 |ECONOMY BRUSHED NICKEL | 36| 4 +Brand#12 |ECONOMY BRUSHED NICKEL | 45| 4 +Brand#12 |ECONOMY BRUSHED NICKEL | 49| 4 +Brand#12 |ECONOMY BRUSHED STEEL | 14| 4 +Brand#12 |ECONOMY BRUSHED STEEL | 19| 4 +Brand#12 |ECONOMY BRUSHED TIN | 45| 4 +Brand#12 |ECONOMY BURNISHED BRASS | 3| 4 +Brand#12 |ECONOMY BURNISHED BRASS | 14| 4 +Brand#12 |ECONOMY BURNISHED BRASS | 36| 4 +Brand#12 |ECONOMY BURNISHED BRASS | 45| 4 +Brand#12 |ECONOMY BURNISHED COPPER | 9| 4 +Brand#12 |ECONOMY BURNISHED COPPER | 23| 4 +Brand#12 |ECONOMY BURNISHED COPPER | 36| 4 +Brand#12 |ECONOMY BURNISHED COPPER | 45| 4 +Brand#12 |ECONOMY BURNISHED NICKEL | 9| 4 +Brand#12 |ECONOMY BURNISHED NICKEL | 49| 4 +Brand#12 |ECONOMY BURNISHED STEEL | 14| 4 +Brand#12 |ECONOMY BURNISHED STEEL | 19| 4 +Brand#12 |ECONOMY BURNISHED STEEL | 23| 4 +Brand#12 |ECONOMY BURNISHED STEEL | 45| 4 +Brand#12 |ECONOMY BURNISHED TIN | 49| 4 +Brand#12 |ECONOMY PLATED BRASS | 9| 4 +Brand#12 |ECONOMY PLATED BRASS | 14| 4 +Brand#12 |ECONOMY PLATED BRASS | 23| 4 +Brand#12 |ECONOMY PLATED BRASS | 36| 4 +Brand#12 |ECONOMY PLATED COPPER | 49| 4 +Brand#12 |ECONOMY PLATED NICKEL | 14| 4 +Brand#12 |ECONOMY PLATED NICKEL | 23| 4 +Brand#12 |ECONOMY PLATED NICKEL | 36| 4 +Brand#12 |ECONOMY PLATED NICKEL | 45| 4 +Brand#12 |ECONOMY PLATED NICKEL | 49| 4 +Brand#12 |ECONOMY PLATED STEEL | 3| 4 +Brand#12 |ECONOMY PLATED STEEL | 9| 4 +Brand#12 |ECONOMY PLATED STEEL | 14| 4 +Brand#12 |ECONOMY PLATED STEEL | 19| 4 +Brand#12 |ECONOMY PLATED STEEL | 36| 4 +Brand#12 |ECONOMY PLATED STEEL | 49| 4 +Brand#12 |ECONOMY PLATED TIN | 9| 4 +Brand#12 |ECONOMY PLATED TIN | 14| 4 +Brand#12 |ECONOMY PLATED TIN | 19| 4 +Brand#12 |ECONOMY PLATED TIN | 23| 4 +Brand#12 |ECONOMY POLISHED BRASS | 36| 4 +Brand#12 |ECONOMY POLISHED BRASS | 49| 4 +Brand#12 |ECONOMY POLISHED COPPER | 23| 4 +Brand#12 |ECONOMY POLISHED COPPER | 45| 4 +Brand#12 |ECONOMY POLISHED NICKEL | 9| 4 +Brand#12 |ECONOMY POLISHED NICKEL | 23| 4 +Brand#12 |ECONOMY POLISHED STEEL | 14| 4 +Brand#12 |ECONOMY POLISHED STEEL | 36| 4 +Brand#12 |ECONOMY POLISHED STEEL | 45| 4 +Brand#12 |ECONOMY POLISHED TIN | 23| 4 +Brand#12 |ECONOMY POLISHED TIN | 45| 4 +Brand#12 |LARGE ANODIZED BRASS | 3| 4 +Brand#12 |LARGE ANODIZED BRASS | 9| 4 +Brand#12 |LARGE ANODIZED BRASS | 19| 4 +Brand#12 |LARGE ANODIZED BRASS | 49| 4 +Brand#12 |LARGE ANODIZED COPPER | 3| 4 +Brand#12 |LARGE ANODIZED COPPER | 23| 4 +Brand#12 |LARGE ANODIZED NICKEL | 3| 4 +Brand#12 |LARGE ANODIZED NICKEL | 14| 4 +Brand#12 |LARGE ANODIZED NICKEL | 19| 4 +Brand#12 |LARGE ANODIZED NICKEL | 23| 4 +Brand#12 |LARGE ANODIZED NICKEL | 45| 4 +Brand#12 |LARGE ANODIZED STEEL | 14| 4 +Brand#12 |LARGE ANODIZED STEEL | 19| 4 +Brand#12 |LARGE ANODIZED STEEL | 45| 4 +Brand#12 |LARGE ANODIZED TIN | 9| 4 +Brand#12 |LARGE ANODIZED TIN | 36| 4 +Brand#12 |LARGE ANODIZED TIN | 45| 4 +Brand#12 |LARGE BRUSHED BRASS | 3| 4 +Brand#12 |LARGE BRUSHED COPPER | 3| 4 +Brand#12 |LARGE BRUSHED COPPER | 9| 4 +Brand#12 |LARGE BRUSHED COPPER | 45| 4 +Brand#12 |LARGE BRUSHED NICKEL | 3| 4 +Brand#12 |LARGE BRUSHED NICKEL | 19| 4 +Brand#12 |LARGE BRUSHED NICKEL | 45| 4 +Brand#12 |LARGE BRUSHED STEEL | 14| 4 +Brand#12 |LARGE BRUSHED TIN | 36| 4 +Brand#12 |LARGE BRUSHED TIN | 49| 4 +Brand#12 |LARGE BURNISHED BRASS | 3| 4 +Brand#12 |LARGE BURNISHED BRASS | 19| 4 +Brand#12 |LARGE BURNISHED BRASS | 23| 4 +Brand#12 |LARGE BURNISHED BRASS | 36| 4 +Brand#12 |LARGE BURNISHED BRASS | 49| 4 +Brand#12 |LARGE BURNISHED COPPER | 9| 4 +Brand#12 |LARGE BURNISHED COPPER | 14| 4 +Brand#12 |LARGE BURNISHED COPPER | 23| 4 +Brand#12 |LARGE BURNISHED COPPER | 45| 4 +Brand#12 |LARGE BURNISHED NICKEL | 9| 4 +Brand#12 |LARGE BURNISHED NICKEL | 23| 4 +Brand#12 |LARGE BURNISHED NICKEL | 36| 4 +Brand#12 |LARGE BURNISHED NICKEL | 49| 4 +Brand#12 |LARGE BURNISHED STEEL | 14| 4 +Brand#12 |LARGE BURNISHED STEEL | 19| 4 +Brand#12 |LARGE BURNISHED STEEL | 23| 4 +Brand#12 |LARGE BURNISHED STEEL | 36| 4 +Brand#12 |LARGE BURNISHED TIN | 19| 4 +Brand#12 |LARGE PLATED BRASS | 14| 4 +Brand#12 |LARGE PLATED BRASS | 19| 4 +Brand#12 |LARGE PLATED BRASS | 23| 4 +Brand#12 |LARGE PLATED BRASS | 36| 4 +Brand#12 |LARGE PLATED BRASS | 45| 4 +Brand#12 |LARGE PLATED COPPER | 9| 4 +Brand#12 |LARGE PLATED COPPER | 19| 4 +Brand#12 |LARGE PLATED NICKEL | 14| 4 +Brand#12 |LARGE PLATED NICKEL | 19| 4 +Brand#12 |LARGE PLATED NICKEL | 23| 4 +Brand#12 |LARGE PLATED NICKEL | 45| 4 +Brand#12 |LARGE PLATED STEEL | 23| 4 +Brand#12 |LARGE PLATED STEEL | 45| 4 +Brand#12 |LARGE PLATED STEEL | 49| 4 +Brand#12 |LARGE PLATED TIN | 3| 4 +Brand#12 |LARGE PLATED TIN | 23| 4 +Brand#12 |LARGE POLISHED BRASS | 14| 4 +Brand#12 |LARGE POLISHED BRASS | 36| 4 +Brand#12 |LARGE POLISHED BRASS | 45| 4 +Brand#12 |LARGE POLISHED COPPER | 14| 4 +Brand#12 |LARGE POLISHED COPPER | 45| 4 +Brand#12 |LARGE POLISHED NICKEL | 3| 4 +Brand#12 |LARGE POLISHED NICKEL | 9| 4 +Brand#12 |LARGE POLISHED STEEL | 3| 4 +Brand#12 |LARGE POLISHED STEEL | 19| 4 +Brand#12 |LARGE POLISHED STEEL | 45| 4 +Brand#12 |LARGE POLISHED TIN | 14| 4 +Brand#12 |LARGE POLISHED TIN | 23| 4 +Brand#12 |LARGE POLISHED TIN | 49| 4 +Brand#12 |MEDIUM ANODIZED BRASS | 9| 4 +Brand#12 |MEDIUM ANODIZED BRASS | 19| 4 +Brand#12 |MEDIUM ANODIZED BRASS | 36| 4 +Brand#12 |MEDIUM ANODIZED COPPER | 14| 4 +Brand#12 |MEDIUM ANODIZED COPPER | 36| 4 +Brand#12 |MEDIUM ANODIZED COPPER | 45| 4 +Brand#12 |MEDIUM ANODIZED NICKEL | 14| 4 +Brand#12 |MEDIUM ANODIZED NICKEL | 23| 4 +Brand#12 |MEDIUM ANODIZED NICKEL | 45| 4 +Brand#12 |MEDIUM ANODIZED NICKEL | 49| 4 +Brand#12 |MEDIUM ANODIZED STEEL | 23| 4 +Brand#12 |MEDIUM ANODIZED STEEL | 36| 4 +Brand#12 |MEDIUM ANODIZED TIN | 14| 4 +Brand#12 |MEDIUM ANODIZED TIN | 36| 4 +Brand#12 |MEDIUM ANODIZED TIN | 45| 4 +Brand#12 |MEDIUM BRUSHED BRASS | 19| 4 +Brand#12 |MEDIUM BRUSHED BRASS | 36| 4 +Brand#12 |MEDIUM BRUSHED BRASS | 49| 4 +Brand#12 |MEDIUM BRUSHED COPPER | 14| 4 +Brand#12 |MEDIUM BRUSHED COPPER | 45| 4 +Brand#12 |MEDIUM BRUSHED COPPER | 49| 4 +Brand#12 |MEDIUM BRUSHED NICKEL | 3| 4 +Brand#12 |MEDIUM BRUSHED NICKEL | 9| 4 +Brand#12 |MEDIUM BRUSHED NICKEL | 19| 4 +Brand#12 |MEDIUM BRUSHED NICKEL | 23| 4 +Brand#12 |MEDIUM BRUSHED STEEL | 14| 4 +Brand#12 |MEDIUM BRUSHED STEEL | 45| 4 +Brand#12 |MEDIUM BRUSHED STEEL | 49| 4 +Brand#12 |MEDIUM BRUSHED TIN | 23| 4 +Brand#12 |MEDIUM BRUSHED TIN | 45| 4 +Brand#12 |MEDIUM BURNISHED BRASS | 3| 4 +Brand#12 |MEDIUM BURNISHED BRASS | 9| 4 +Brand#12 |MEDIUM BURNISHED BRASS | 14| 4 +Brand#12 |MEDIUM BURNISHED COPPER | 9| 4 +Brand#12 |MEDIUM BURNISHED COPPER | 14| 4 +Brand#12 |MEDIUM BURNISHED COPPER | 23| 4 +Brand#12 |MEDIUM BURNISHED COPPER | 36| 4 +Brand#12 |MEDIUM BURNISHED NICKEL | 14| 4 +Brand#12 |MEDIUM BURNISHED NICKEL | 19| 4 +Brand#12 |MEDIUM BURNISHED NICKEL | 23| 4 +Brand#12 |MEDIUM BURNISHED NICKEL | 36| 4 +Brand#12 |MEDIUM BURNISHED NICKEL | 45| 4 +Brand#12 |MEDIUM BURNISHED STEEL | 23| 4 +Brand#12 |MEDIUM BURNISHED STEEL | 36| 4 +Brand#12 |MEDIUM BURNISHED STEEL | 45| 4 +Brand#12 |MEDIUM BURNISHED TIN | 23| 4 +Brand#12 |MEDIUM BURNISHED TIN | 36| 4 +Brand#12 |MEDIUM BURNISHED TIN | 49| 4 +Brand#12 |MEDIUM PLATED BRASS | 19| 4 +Brand#12 |MEDIUM PLATED BRASS | 45| 4 +Brand#12 |MEDIUM PLATED COPPER | 3| 4 +Brand#12 |MEDIUM PLATED COPPER | 9| 4 +Brand#12 |MEDIUM PLATED COPPER | 14| 4 +Brand#12 |MEDIUM PLATED COPPER | 23| 4 +Brand#12 |MEDIUM PLATED COPPER | 36| 4 +Brand#12 |MEDIUM PLATED NICKEL | 14| 4 +Brand#12 |MEDIUM PLATED NICKEL | 19| 4 +Brand#12 |MEDIUM PLATED STEEL | 36| 4 +Brand#12 |MEDIUM PLATED STEEL | 49| 4 +Brand#12 |MEDIUM PLATED TIN | 49| 4 +Brand#12 |PROMO ANODIZED BRASS | 9| 4 +Brand#12 |PROMO ANODIZED BRASS | 23| 4 +Brand#12 |PROMO ANODIZED BRASS | 36| 4 +Brand#12 |PROMO ANODIZED COPPER | 9| 4 +Brand#12 |PROMO ANODIZED COPPER | 14| 4 +Brand#12 |PROMO ANODIZED COPPER | 23| 4 +Brand#12 |PROMO ANODIZED STEEL | 3| 4 +Brand#12 |PROMO ANODIZED STEEL | 9| 4 +Brand#12 |PROMO ANODIZED STEEL | 14| 4 +Brand#12 |PROMO ANODIZED STEEL | 45| 4 +Brand#12 |PROMO ANODIZED TIN | 3| 4 +Brand#12 |PROMO ANODIZED TIN | 45| 4 +Brand#12 |PROMO BRUSHED BRASS | 14| 4 +Brand#12 |PROMO BRUSHED COPPER | 14| 4 +Brand#12 |PROMO BRUSHED COPPER | 19| 4 +Brand#12 |PROMO BRUSHED COPPER | 45| 4 +Brand#12 |PROMO BRUSHED COPPER | 49| 4 +Brand#12 |PROMO BRUSHED NICKEL | 3| 4 +Brand#12 |PROMO BRUSHED NICKEL | 9| 4 +Brand#12 |PROMO BRUSHED NICKEL | 14| 4 +Brand#12 |PROMO BRUSHED NICKEL | 19| 4 +Brand#12 |PROMO BRUSHED NICKEL | 36| 4 +Brand#12 |PROMO BRUSHED NICKEL | 45| 4 +Brand#12 |PROMO BRUSHED NICKEL | 49| 4 +Brand#12 |PROMO BRUSHED STEEL | 36| 4 +Brand#12 |PROMO BRUSHED TIN | 19| 4 +Brand#12 |PROMO BRUSHED TIN | 23| 4 +Brand#12 |PROMO BRUSHED TIN | 49| 4 +Brand#12 |PROMO BURNISHED BRASS | 19| 4 +Brand#12 |PROMO BURNISHED BRASS | 23| 4 +Brand#12 |PROMO BURNISHED BRASS | 36| 4 +Brand#12 |PROMO BURNISHED BRASS | 49| 4 +Brand#12 |PROMO BURNISHED COPPER | 9| 4 +Brand#12 |PROMO BURNISHED COPPER | 14| 4 +Brand#12 |PROMO BURNISHED COPPER | 23| 4 +Brand#12 |PROMO BURNISHED COPPER | 36| 4 +Brand#12 |PROMO BURNISHED COPPER | 45| 4 +Brand#12 |PROMO BURNISHED COPPER | 49| 4 +Brand#12 |PROMO BURNISHED NICKEL | 3| 4 +Brand#12 |PROMO BURNISHED NICKEL | 19| 4 +Brand#12 |PROMO BURNISHED NICKEL | 23| 4 +Brand#12 |PROMO BURNISHED NICKEL | 36| 4 +Brand#12 |PROMO BURNISHED NICKEL | 45| 4 +Brand#12 |PROMO BURNISHED STEEL | 14| 4 +Brand#12 |PROMO BURNISHED STEEL | 19| 4 +Brand#12 |PROMO BURNISHED STEEL | 23| 4 +Brand#12 |PROMO BURNISHED STEEL | 45| 4 +Brand#12 |PROMO BURNISHED STEEL | 49| 4 +Brand#12 |PROMO BURNISHED TIN | 3| 4 +Brand#12 |PROMO BURNISHED TIN | 19| 4 +Brand#12 |PROMO PLATED BRASS | 14| 4 +Brand#12 |PROMO PLATED BRASS | 23| 4 +Brand#12 |PROMO PLATED COPPER | 3| 4 +Brand#12 |PROMO PLATED COPPER | 19| 4 +Brand#12 |PROMO PLATED COPPER | 49| 4 +Brand#12 |PROMO PLATED NICKEL | 9| 4 +Brand#12 |PROMO PLATED NICKEL | 19| 4 +Brand#12 |PROMO PLATED NICKEL | 49| 4 +Brand#12 |PROMO PLATED STEEL | 9| 4 +Brand#12 |PROMO PLATED STEEL | 14| 4 +Brand#12 |PROMO PLATED STEEL | 23| 4 +Brand#12 |PROMO PLATED STEEL | 45| 4 +Brand#12 |PROMO PLATED TIN | 14| 4 +Brand#12 |PROMO PLATED TIN | 19| 4 +Brand#12 |PROMO PLATED TIN | 49| 4 +Brand#12 |PROMO POLISHED BRASS | 14| 4 +Brand#12 |PROMO POLISHED BRASS | 45| 4 +Brand#12 |PROMO POLISHED COPPER | 3| 4 +Brand#12 |PROMO POLISHED COPPER | 9| 4 +Brand#12 |PROMO POLISHED COPPER | 36| 4 +Brand#12 |PROMO POLISHED COPPER | 49| 4 +Brand#12 |PROMO POLISHED NICKEL | 9| 4 +Brand#12 |PROMO POLISHED NICKEL | 23| 4 +Brand#12 |PROMO POLISHED NICKEL | 45| 4 +Brand#12 |PROMO POLISHED STEEL | 9| 4 +Brand#12 |PROMO POLISHED STEEL | 14| 4 +Brand#12 |PROMO POLISHED TIN | 9| 4 +Brand#12 |PROMO POLISHED TIN | 45| 4 +Brand#12 |SMALL ANODIZED BRASS | 3| 4 +Brand#12 |SMALL ANODIZED BRASS | 14| 4 +Brand#12 |SMALL ANODIZED BRASS | 19| 4 +Brand#12 |SMALL ANODIZED BRASS | 23| 4 +Brand#12 |SMALL ANODIZED COPPER | 19| 4 +Brand#12 |SMALL ANODIZED COPPER | 23| 4 +Brand#12 |SMALL ANODIZED COPPER | 45| 4 +Brand#12 |SMALL ANODIZED COPPER | 49| 4 +Brand#12 |SMALL ANODIZED NICKEL | 9| 4 +Brand#12 |SMALL ANODIZED NICKEL | 14| 4 +Brand#12 |SMALL ANODIZED STEEL | 19| 4 +Brand#12 |SMALL ANODIZED STEEL | 36| 4 +Brand#12 |SMALL ANODIZED TIN | 3| 4 +Brand#12 |SMALL ANODIZED TIN | 36| 4 +Brand#12 |SMALL BRUSHED BRASS | 9| 4 +Brand#12 |SMALL BRUSHED BRASS | 19| 4 +Brand#12 |SMALL BRUSHED COPPER | 9| 4 +Brand#12 |SMALL BRUSHED COPPER | 14| 4 +Brand#12 |SMALL BRUSHED COPPER | 19| 4 +Brand#12 |SMALL BRUSHED COPPER | 23| 4 +Brand#12 |SMALL BRUSHED COPPER | 45| 4 +Brand#12 |SMALL BRUSHED COPPER | 49| 4 +Brand#12 |SMALL BRUSHED STEEL | 3| 4 +Brand#12 |SMALL BRUSHED TIN | 14| 4 +Brand#12 |SMALL BRUSHED TIN | 19| 4 +Brand#12 |SMALL BRUSHED TIN | 23| 4 +Brand#12 |SMALL BRUSHED TIN | 36| 4 +Brand#12 |SMALL BURNISHED BRASS | 3| 4 +Brand#12 |SMALL BURNISHED COPPER | 3| 4 +Brand#12 |SMALL BURNISHED COPPER | 9| 4 +Brand#12 |SMALL BURNISHED COPPER | 19| 4 +Brand#12 |SMALL BURNISHED COPPER | 45| 4 +Brand#12 |SMALL BURNISHED NICKEL | 23| 4 +Brand#12 |SMALL BURNISHED NICKEL | 49| 4 +Brand#12 |SMALL BURNISHED STEEL | 14| 4 +Brand#12 |SMALL BURNISHED STEEL | 19| 4 +Brand#12 |SMALL BURNISHED STEEL | 36| 4 +Brand#12 |SMALL BURNISHED STEEL | 45| 4 +Brand#12 |SMALL BURNISHED STEEL | 49| 4 +Brand#12 |SMALL BURNISHED TIN | 9| 4 +Brand#12 |SMALL BURNISHED TIN | 36| 4 +Brand#12 |SMALL BURNISHED TIN | 49| 4 +Brand#12 |SMALL PLATED BRASS | 9| 4 +Brand#12 |SMALL PLATED BRASS | 36| 4 +Brand#12 |SMALL PLATED COPPER | 3| 4 +Brand#12 |SMALL PLATED COPPER | 9| 4 +Brand#12 |SMALL PLATED COPPER | 14| 4 +Brand#12 |SMALL PLATED COPPER | 36| 4 +Brand#12 |SMALL PLATED COPPER | 45| 4 +Brand#12 |SMALL PLATED COPPER | 49| 4 +Brand#12 |SMALL PLATED NICKEL | 9| 4 +Brand#12 |SMALL PLATED NICKEL | 36| 4 +Brand#12 |SMALL PLATED STEEL | 14| 4 +Brand#12 |SMALL PLATED TIN | 3| 4 +Brand#12 |SMALL PLATED TIN | 9| 4 +Brand#12 |SMALL PLATED TIN | 14| 4 +Brand#12 |SMALL PLATED TIN | 19| 4 +Brand#12 |SMALL PLATED TIN | 36| 4 +Brand#12 |SMALL PLATED TIN | 49| 4 +Brand#12 |SMALL POLISHED BRASS | 3| 4 +Brand#12 |SMALL POLISHED BRASS | 9| 4 +Brand#12 |SMALL POLISHED BRASS | 49| 4 +Brand#12 |SMALL POLISHED COPPER | 3| 4 +Brand#12 |SMALL POLISHED COPPER | 9| 4 +Brand#12 |SMALL POLISHED COPPER | 19| 4 +Brand#12 |SMALL POLISHED COPPER | 23| 4 +Brand#12 |SMALL POLISHED COPPER | 36| 4 +Brand#12 |SMALL POLISHED NICKEL | 3| 4 +Brand#12 |SMALL POLISHED NICKEL | 9| 4 +Brand#12 |SMALL POLISHED NICKEL | 19| 4 +Brand#12 |SMALL POLISHED NICKEL | 36| 4 +Brand#12 |SMALL POLISHED NICKEL | 45| 4 +Brand#12 |SMALL POLISHED STEEL | 3| 4 +Brand#12 |SMALL POLISHED STEEL | 9| 4 +Brand#12 |SMALL POLISHED STEEL | 14| 4 +Brand#12 |SMALL POLISHED STEEL | 23| 4 +Brand#12 |SMALL POLISHED STEEL | 36| 4 +Brand#12 |SMALL POLISHED STEEL | 49| 4 +Brand#12 |SMALL POLISHED TIN | 3| 4 +Brand#12 |SMALL POLISHED TIN | 9| 4 +Brand#12 |SMALL POLISHED TIN | 23| 4 +Brand#12 |SMALL POLISHED TIN | 49| 4 +Brand#12 |STANDARD ANODIZED BRASS | 9| 4 +Brand#12 |STANDARD ANODIZED BRASS | 19| 4 +Brand#12 |STANDARD ANODIZED BRASS | 45| 4 +Brand#12 |STANDARD ANODIZED COPPER | 9| 4 +Brand#12 |STANDARD ANODIZED COPPER | 19| 4 +Brand#12 |STANDARD ANODIZED COPPER | 36| 4 +Brand#12 |STANDARD ANODIZED COPPER | 49| 4 +Brand#12 |STANDARD ANODIZED STEEL | 3| 4 +Brand#12 |STANDARD ANODIZED STEEL | 45| 4 +Brand#12 |STANDARD ANODIZED TIN | 19| 4 +Brand#12 |STANDARD BRUSHED BRASS | 9| 4 +Brand#12 |STANDARD BRUSHED BRASS | 14| 4 +Brand#12 |STANDARD BRUSHED BRASS | 49| 4 +Brand#12 |STANDARD BRUSHED COPPER | 19| 4 +Brand#12 |STANDARD BRUSHED COPPER | 23| 4 +Brand#12 |STANDARD BRUSHED COPPER | 45| 4 +Brand#12 |STANDARD BRUSHED NICKEL | 49| 4 +Brand#12 |STANDARD BRUSHED STEEL | 14| 4 +Brand#12 |STANDARD BRUSHED STEEL | 19| 4 +Brand#12 |STANDARD BRUSHED STEEL | 23| 4 +Brand#12 |STANDARD BRUSHED STEEL | 49| 4 +Brand#12 |STANDARD BRUSHED TIN | 3| 4 +Brand#12 |STANDARD BRUSHED TIN | 49| 4 +Brand#12 |STANDARD BURNISHED BRASS | 9| 4 +Brand#12 |STANDARD BURNISHED BRASS | 45| 4 +Brand#12 |STANDARD BURNISHED COPPER| 19| 4 +Brand#12 |STANDARD BURNISHED COPPER| 23| 4 +Brand#12 |STANDARD BURNISHED COPPER| 36| 4 +Brand#12 |STANDARD BURNISHED COPPER| 49| 4 +Brand#12 |STANDARD BURNISHED NICKEL| 19| 4 +Brand#12 |STANDARD BURNISHED NICKEL| 36| 4 +Brand#12 |STANDARD BURNISHED NICKEL| 45| 4 +Brand#12 |STANDARD BURNISHED NICKEL| 49| 4 +Brand#12 |STANDARD BURNISHED STEEL | 3| 4 +Brand#12 |STANDARD BURNISHED STEEL | 19| 4 +Brand#12 |STANDARD BURNISHED STEEL | 23| 4 +Brand#12 |STANDARD BURNISHED STEEL | 36| 4 +Brand#12 |STANDARD BURNISHED STEEL | 45| 4 +Brand#12 |STANDARD BURNISHED TIN | 19| 4 +Brand#12 |STANDARD PLATED BRASS | 14| 4 +Brand#12 |STANDARD PLATED BRASS | 23| 4 +Brand#12 |STANDARD PLATED BRASS | 36| 4 +Brand#12 |STANDARD PLATED BRASS | 45| 4 +Brand#12 |STANDARD PLATED COPPER | 3| 4 +Brand#12 |STANDARD PLATED COPPER | 9| 4 +Brand#12 |STANDARD PLATED COPPER | 19| 4 +Brand#12 |STANDARD PLATED COPPER | 45| 4 +Brand#12 |STANDARD PLATED NICKEL | 23| 4 +Brand#12 |STANDARD PLATED NICKEL | 36| 4 +Brand#12 |STANDARD PLATED NICKEL | 49| 4 +Brand#12 |STANDARD PLATED STEEL | 9| 4 +Brand#12 |STANDARD PLATED TIN | 14| 4 +Brand#12 |STANDARD PLATED TIN | 23| 4 +Brand#12 |STANDARD PLATED TIN | 49| 4 +Brand#12 |STANDARD POLISHED BRASS | 9| 4 +Brand#12 |STANDARD POLISHED BRASS | 19| 4 +Brand#12 |STANDARD POLISHED BRASS | 49| 4 +Brand#12 |STANDARD POLISHED COPPER | 14| 4 +Brand#12 |STANDARD POLISHED COPPER | 45| 4 +Brand#12 |STANDARD POLISHED COPPER | 49| 4 +Brand#12 |STANDARD POLISHED NICKEL | 9| 4 +Brand#12 |STANDARD POLISHED NICKEL | 14| 4 +Brand#12 |STANDARD POLISHED NICKEL | 19| 4 +Brand#12 |STANDARD POLISHED NICKEL | 23| 4 +Brand#12 |STANDARD POLISHED NICKEL | 45| 4 +Brand#12 |STANDARD POLISHED STEEL | 36| 4 +Brand#12 |STANDARD POLISHED TIN | 14| 4 +Brand#12 |STANDARD POLISHED TIN | 19| 4 +Brand#12 |STANDARD POLISHED TIN | 49| 4 +Brand#13 |ECONOMY ANODIZED BRASS | 3| 4 +Brand#13 |ECONOMY ANODIZED BRASS | 9| 4 +Brand#13 |ECONOMY ANODIZED BRASS | 14| 4 +Brand#13 |ECONOMY ANODIZED BRASS | 23| 4 +Brand#13 |ECONOMY ANODIZED BRASS | 49| 4 +Brand#13 |ECONOMY ANODIZED COPPER | 3| 4 +Brand#13 |ECONOMY ANODIZED COPPER | 36| 4 +Brand#13 |ECONOMY ANODIZED COPPER | 49| 4 +Brand#13 |ECONOMY ANODIZED STEEL | 14| 4 +Brand#13 |ECONOMY ANODIZED STEEL | 19| 4 +Brand#13 |ECONOMY ANODIZED STEEL | 36| 4 +Brand#13 |ECONOMY ANODIZED STEEL | 49| 4 +Brand#13 |ECONOMY ANODIZED TIN | 3| 4 +Brand#13 |ECONOMY ANODIZED TIN | 14| 4 +Brand#13 |ECONOMY ANODIZED TIN | 36| 4 +Brand#13 |ECONOMY BRUSHED BRASS | 3| 4 +Brand#13 |ECONOMY BRUSHED BRASS | 14| 4 +Brand#13 |ECONOMY BRUSHED BRASS | 23| 4 +Brand#13 |ECONOMY BRUSHED BRASS | 36| 4 +Brand#13 |ECONOMY BRUSHED BRASS | 49| 4 +Brand#13 |ECONOMY BRUSHED COPPER | 19| 4 +Brand#13 |ECONOMY BRUSHED COPPER | 23| 4 +Brand#13 |ECONOMY BRUSHED COPPER | 45| 4 +Brand#13 |ECONOMY BRUSHED NICKEL | 3| 4 +Brand#13 |ECONOMY BRUSHED NICKEL | 9| 4 +Brand#13 |ECONOMY BRUSHED NICKEL | 14| 4 +Brand#13 |ECONOMY BRUSHED STEEL | 19| 4 +Brand#13 |ECONOMY BRUSHED STEEL | 23| 4 +Brand#13 |ECONOMY BRUSHED STEEL | 36| 4 +Brand#13 |ECONOMY BRUSHED TIN | 3| 4 +Brand#13 |ECONOMY BRUSHED TIN | 36| 4 +Brand#13 |ECONOMY BRUSHED TIN | 45| 4 +Brand#13 |ECONOMY BURNISHED BRASS | 9| 4 +Brand#13 |ECONOMY BURNISHED BRASS | 14| 4 +Brand#13 |ECONOMY BURNISHED BRASS | 19| 4 +Brand#13 |ECONOMY BURNISHED BRASS | 23| 4 +Brand#13 |ECONOMY BURNISHED BRASS | 36| 4 +Brand#13 |ECONOMY BURNISHED COPPER | 3| 4 +Brand#13 |ECONOMY BURNISHED COPPER | 9| 4 +Brand#13 |ECONOMY BURNISHED COPPER | 49| 4 +Brand#13 |ECONOMY BURNISHED NICKEL | 14| 4 +Brand#13 |ECONOMY BURNISHED NICKEL | 23| 4 +Brand#13 |ECONOMY BURNISHED NICKEL | 45| 4 +Brand#13 |ECONOMY BURNISHED NICKEL | 49| 4 +Brand#13 |ECONOMY BURNISHED STEEL | 9| 4 +Brand#13 |ECONOMY BURNISHED STEEL | 23| 4 +Brand#13 |ECONOMY BURNISHED STEEL | 49| 4 +Brand#13 |ECONOMY BURNISHED TIN | 3| 4 +Brand#13 |ECONOMY BURNISHED TIN | 9| 4 +Brand#13 |ECONOMY BURNISHED TIN | 19| 4 +Brand#13 |ECONOMY BURNISHED TIN | 45| 4 +Brand#13 |ECONOMY PLATED BRASS | 3| 4 +Brand#13 |ECONOMY PLATED BRASS | 19| 4 +Brand#13 |ECONOMY PLATED BRASS | 45| 4 +Brand#13 |ECONOMY PLATED COPPER | 23| 4 +Brand#13 |ECONOMY PLATED COPPER | 45| 4 +Brand#13 |ECONOMY PLATED NICKEL | 45| 4 +Brand#13 |ECONOMY PLATED STEEL | 9| 4 +Brand#13 |ECONOMY PLATED STEEL | 14| 4 +Brand#13 |ECONOMY PLATED STEEL | 49| 4 +Brand#13 |ECONOMY PLATED TIN | 19| 4 +Brand#13 |ECONOMY PLATED TIN | 36| 4 +Brand#13 |ECONOMY PLATED TIN | 49| 4 +Brand#13 |ECONOMY POLISHED BRASS | 19| 4 +Brand#13 |ECONOMY POLISHED COPPER | 3| 4 +Brand#13 |ECONOMY POLISHED COPPER | 14| 4 +Brand#13 |ECONOMY POLISHED COPPER | 23| 4 +Brand#13 |ECONOMY POLISHED NICKEL | 9| 4 +Brand#13 |ECONOMY POLISHED NICKEL | 14| 4 +Brand#13 |ECONOMY POLISHED NICKEL | 19| 4 +Brand#13 |ECONOMY POLISHED NICKEL | 36| 4 +Brand#13 |ECONOMY POLISHED NICKEL | 45| 4 +Brand#13 |ECONOMY POLISHED NICKEL | 49| 4 +Brand#13 |ECONOMY POLISHED STEEL | 14| 4 +Brand#13 |ECONOMY POLISHED TIN | 9| 4 +Brand#13 |ECONOMY POLISHED TIN | 14| 4 +Brand#13 |ECONOMY POLISHED TIN | 49| 4 +Brand#13 |LARGE ANODIZED BRASS | 3| 4 +Brand#13 |LARGE ANODIZED BRASS | 9| 4 +Brand#13 |LARGE ANODIZED BRASS | 14| 4 +Brand#13 |LARGE ANODIZED BRASS | 19| 4 +Brand#13 |LARGE ANODIZED BRASS | 23| 4 +Brand#13 |LARGE ANODIZED COPPER | 9| 4 +Brand#13 |LARGE ANODIZED COPPER | 14| 4 +Brand#13 |LARGE ANODIZED COPPER | 36| 4 +Brand#13 |LARGE ANODIZED COPPER | 45| 4 +Brand#13 |LARGE ANODIZED COPPER | 49| 4 +Brand#13 |LARGE ANODIZED NICKEL | 3| 4 +Brand#13 |LARGE ANODIZED NICKEL | 9| 4 +Brand#13 |LARGE ANODIZED NICKEL | 36| 4 +Brand#13 |LARGE ANODIZED STEEL | 23| 4 +Brand#13 |LARGE ANODIZED TIN | 3| 4 +Brand#13 |LARGE ANODIZED TIN | 23| 4 +Brand#13 |LARGE BRUSHED BRASS | 14| 4 +Brand#13 |LARGE BRUSHED BRASS | 23| 4 +Brand#13 |LARGE BRUSHED BRASS | 36| 4 +Brand#13 |LARGE BRUSHED COPPER | 3| 4 +Brand#13 |LARGE BRUSHED COPPER | 14| 4 +Brand#13 |LARGE BRUSHED COPPER | 23| 4 +Brand#13 |LARGE BRUSHED COPPER | 36| 4 +Brand#13 |LARGE BRUSHED NICKEL | 14| 4 +Brand#13 |LARGE BRUSHED NICKEL | 19| 4 +Brand#13 |LARGE BRUSHED STEEL | 9| 4 +Brand#13 |LARGE BRUSHED STEEL | 14| 4 +Brand#13 |LARGE BRUSHED STEEL | 45| 4 +Brand#13 |LARGE BRUSHED STEEL | 49| 4 +Brand#13 |LARGE BRUSHED TIN | 14| 4 +Brand#13 |LARGE BRUSHED TIN | 19| 4 +Brand#13 |LARGE BRUSHED TIN | 45| 4 +Brand#13 |LARGE BRUSHED TIN | 49| 4 +Brand#13 |LARGE BURNISHED BRASS | 9| 4 +Brand#13 |LARGE BURNISHED BRASS | 19| 4 +Brand#13 |LARGE BURNISHED BRASS | 36| 4 +Brand#13 |LARGE BURNISHED BRASS | 49| 4 +Brand#13 |LARGE BURNISHED COPPER | 9| 4 +Brand#13 |LARGE BURNISHED COPPER | 49| 4 +Brand#13 |LARGE BURNISHED NICKEL | 3| 4 +Brand#13 |LARGE BURNISHED NICKEL | 23| 4 +Brand#13 |LARGE BURNISHED NICKEL | 36| 4 +Brand#13 |LARGE BURNISHED STEEL | 36| 4 +Brand#13 |LARGE BURNISHED TIN | 14| 4 +Brand#13 |LARGE BURNISHED TIN | 19| 4 +Brand#13 |LARGE BURNISHED TIN | 36| 4 +Brand#13 |LARGE BURNISHED TIN | 49| 4 +Brand#13 |LARGE PLATED BRASS | 3| 4 +Brand#13 |LARGE PLATED BRASS | 14| 4 +Brand#13 |LARGE PLATED BRASS | 23| 4 +Brand#13 |LARGE PLATED BRASS | 36| 4 +Brand#13 |LARGE PLATED BRASS | 49| 4 +Brand#13 |LARGE PLATED COPPER | 45| 4 +Brand#13 |LARGE PLATED NICKEL | 3| 4 +Brand#13 |LARGE PLATED NICKEL | 14| 4 +Brand#13 |LARGE PLATED STEEL | 19| 4 +Brand#13 |LARGE PLATED STEEL | 23| 4 +Brand#13 |LARGE PLATED TIN | 3| 4 +Brand#13 |LARGE PLATED TIN | 19| 4 +Brand#13 |LARGE PLATED TIN | 49| 4 +Brand#13 |LARGE POLISHED BRASS | 3| 4 +Brand#13 |LARGE POLISHED BRASS | 45| 4 +Brand#13 |LARGE POLISHED COPPER | 3| 4 +Brand#13 |LARGE POLISHED COPPER | 9| 4 +Brand#13 |LARGE POLISHED COPPER | 19| 4 +Brand#13 |LARGE POLISHED COPPER | 23| 4 +Brand#13 |LARGE POLISHED COPPER | 36| 4 +Brand#13 |LARGE POLISHED COPPER | 49| 4 +Brand#13 |LARGE POLISHED NICKEL | 3| 4 +Brand#13 |LARGE POLISHED NICKEL | 19| 4 +Brand#13 |LARGE POLISHED NICKEL | 36| 4 +Brand#13 |LARGE POLISHED STEEL | 14| 4 +Brand#13 |LARGE POLISHED STEEL | 45| 4 +Brand#13 |LARGE POLISHED STEEL | 49| 4 +Brand#13 |LARGE POLISHED TIN | 49| 4 +Brand#13 |MEDIUM ANODIZED BRASS | 3| 4 +Brand#13 |MEDIUM ANODIZED BRASS | 9| 4 +Brand#13 |MEDIUM ANODIZED BRASS | 14| 4 +Brand#13 |MEDIUM ANODIZED BRASS | 36| 4 +Brand#13 |MEDIUM ANODIZED COPPER | 9| 4 +Brand#13 |MEDIUM ANODIZED COPPER | 14| 4 +Brand#13 |MEDIUM ANODIZED COPPER | 19| 4 +Brand#13 |MEDIUM ANODIZED NICKEL | 19| 4 +Brand#13 |MEDIUM ANODIZED NICKEL | 23| 4 +Brand#13 |MEDIUM ANODIZED NICKEL | 49| 4 +Brand#13 |MEDIUM ANODIZED STEEL | 19| 4 +Brand#13 |MEDIUM ANODIZED STEEL | 36| 4 +Brand#13 |MEDIUM ANODIZED STEEL | 45| 4 +Brand#13 |MEDIUM ANODIZED TIN | 14| 4 +Brand#13 |MEDIUM ANODIZED TIN | 19| 4 +Brand#13 |MEDIUM ANODIZED TIN | 49| 4 +Brand#13 |MEDIUM BRUSHED BRASS | 3| 4 +Brand#13 |MEDIUM BRUSHED BRASS | 19| 4 +Brand#13 |MEDIUM BRUSHED BRASS | 23| 4 +Brand#13 |MEDIUM BRUSHED COPPER | 9| 4 +Brand#13 |MEDIUM BRUSHED COPPER | 36| 4 +Brand#13 |MEDIUM BRUSHED COPPER | 45| 4 +Brand#13 |MEDIUM BRUSHED NICKEL | 23| 4 +Brand#13 |MEDIUM BRUSHED NICKEL | 36| 4 +Brand#13 |MEDIUM BRUSHED NICKEL | 45| 4 +Brand#13 |MEDIUM BRUSHED STEEL | 3| 4 +Brand#13 |MEDIUM BRUSHED STEEL | 23| 4 +Brand#13 |MEDIUM BRUSHED TIN | 3| 4 +Brand#13 |MEDIUM BRUSHED TIN | 14| 4 +Brand#13 |MEDIUM BRUSHED TIN | 36| 4 +Brand#13 |MEDIUM BRUSHED TIN | 49| 4 +Brand#13 |MEDIUM BURNISHED BRASS | 9| 4 +Brand#13 |MEDIUM BURNISHED BRASS | 23| 4 +Brand#13 |MEDIUM BURNISHED BRASS | 49| 4 +Brand#13 |MEDIUM BURNISHED COPPER | 14| 4 +Brand#13 |MEDIUM BURNISHED COPPER | 49| 4 +Brand#13 |MEDIUM BURNISHED NICKEL | 14| 4 +Brand#13 |MEDIUM BURNISHED NICKEL | 19| 4 +Brand#13 |MEDIUM BURNISHED NICKEL | 45| 4 +Brand#13 |MEDIUM BURNISHED STEEL | 9| 4 +Brand#13 |MEDIUM BURNISHED STEEL | 23| 4 +Brand#13 |MEDIUM BURNISHED STEEL | 36| 4 +Brand#13 |MEDIUM BURNISHED TIN | 9| 4 +Brand#13 |MEDIUM BURNISHED TIN | 14| 4 +Brand#13 |MEDIUM BURNISHED TIN | 23| 4 +Brand#13 |MEDIUM PLATED BRASS | 3| 4 +Brand#13 |MEDIUM PLATED BRASS | 14| 4 +Brand#13 |MEDIUM PLATED BRASS | 36| 4 +Brand#13 |MEDIUM PLATED BRASS | 45| 4 +Brand#13 |MEDIUM PLATED COPPER | 3| 4 +Brand#13 |MEDIUM PLATED COPPER | 9| 4 +Brand#13 |MEDIUM PLATED COPPER | 23| 4 +Brand#13 |MEDIUM PLATED NICKEL | 9| 4 +Brand#13 |MEDIUM PLATED NICKEL | 49| 4 +Brand#13 |MEDIUM PLATED STEEL | 14| 4 +Brand#13 |MEDIUM PLATED STEEL | 49| 4 +Brand#13 |MEDIUM PLATED TIN | 14| 4 +Brand#13 |MEDIUM PLATED TIN | 23| 4 +Brand#13 |MEDIUM PLATED TIN | 45| 4 +Brand#13 |MEDIUM PLATED TIN | 49| 4 +Brand#13 |PROMO ANODIZED BRASS | 9| 4 +Brand#13 |PROMO ANODIZED BRASS | 36| 4 +Brand#13 |PROMO ANODIZED BRASS | 49| 4 +Brand#13 |PROMO ANODIZED COPPER | 19| 4 +Brand#13 |PROMO ANODIZED COPPER | 36| 4 +Brand#13 |PROMO ANODIZED COPPER | 49| 4 +Brand#13 |PROMO ANODIZED NICKEL | 14| 4 +Brand#13 |PROMO ANODIZED NICKEL | 19| 4 +Brand#13 |PROMO ANODIZED NICKEL | 23| 4 +Brand#13 |PROMO ANODIZED NICKEL | 36| 4 +Brand#13 |PROMO ANODIZED STEEL | 3| 4 +Brand#13 |PROMO ANODIZED STEEL | 9| 4 +Brand#13 |PROMO ANODIZED STEEL | 14| 4 +Brand#13 |PROMO ANODIZED STEEL | 23| 4 +Brand#13 |PROMO ANODIZED STEEL | 45| 4 +Brand#13 |PROMO ANODIZED STEEL | 49| 4 +Brand#13 |PROMO ANODIZED TIN | 3| 4 +Brand#13 |PROMO ANODIZED TIN | 9| 4 +Brand#13 |PROMO ANODIZED TIN | 14| 4 +Brand#13 |PROMO ANODIZED TIN | 19| 4 +Brand#13 |PROMO ANODIZED TIN | 23| 4 +Brand#13 |PROMO ANODIZED TIN | 45| 4 +Brand#13 |PROMO BRUSHED BRASS | 9| 4 +Brand#13 |PROMO BRUSHED BRASS | 14| 4 +Brand#13 |PROMO BRUSHED BRASS | 19| 4 +Brand#13 |PROMO BRUSHED COPPER | 9| 4 +Brand#13 |PROMO BRUSHED COPPER | 23| 4 +Brand#13 |PROMO BRUSHED COPPER | 45| 4 +Brand#13 |PROMO BRUSHED NICKEL | 3| 4 +Brand#13 |PROMO BRUSHED NICKEL | 45| 4 +Brand#13 |PROMO BRUSHED STEEL | 14| 4 +Brand#13 |PROMO BRUSHED STEEL | 19| 4 +Brand#13 |PROMO BRUSHED STEEL | 36| 4 +Brand#13 |PROMO BRUSHED STEEL | 49| 4 +Brand#13 |PROMO BRUSHED TIN | 19| 4 +Brand#13 |PROMO BRUSHED TIN | 49| 4 +Brand#13 |PROMO BURNISHED BRASS | 3| 4 +Brand#13 |PROMO BURNISHED BRASS | 14| 4 +Brand#13 |PROMO BURNISHED BRASS | 49| 4 +Brand#13 |PROMO BURNISHED COPPER | 14| 4 +Brand#13 |PROMO BURNISHED COPPER | 36| 4 +Brand#13 |PROMO BURNISHED NICKEL | 19| 4 +Brand#13 |PROMO BURNISHED NICKEL | 23| 4 +Brand#13 |PROMO BURNISHED NICKEL | 45| 4 +Brand#13 |PROMO BURNISHED STEEL | 3| 4 +Brand#13 |PROMO BURNISHED STEEL | 36| 4 +Brand#13 |PROMO BURNISHED TIN | 36| 4 +Brand#13 |PROMO BURNISHED TIN | 49| 4 +Brand#13 |PROMO PLATED BRASS | 3| 4 +Brand#13 |PROMO PLATED BRASS | 9| 4 +Brand#13 |PROMO PLATED BRASS | 19| 4 +Brand#13 |PROMO PLATED BRASS | 23| 4 +Brand#13 |PROMO PLATED BRASS | 36| 4 +Brand#13 |PROMO PLATED BRASS | 45| 4 +Brand#13 |PROMO PLATED COPPER | 19| 4 +Brand#13 |PROMO PLATED COPPER | 23| 4 +Brand#13 |PROMO PLATED COPPER | 49| 4 +Brand#13 |PROMO PLATED NICKEL | 45| 4 +Brand#13 |PROMO PLATED STEEL | 3| 4 +Brand#13 |PROMO PLATED STEEL | 14| 4 +Brand#13 |PROMO PLATED STEEL | 23| 4 +Brand#13 |PROMO PLATED STEEL | 36| 4 +Brand#13 |PROMO PLATED STEEL | 49| 4 +Brand#13 |PROMO PLATED TIN | 3| 4 +Brand#13 |PROMO PLATED TIN | 9| 4 +Brand#13 |PROMO PLATED TIN | 19| 4 +Brand#13 |PROMO PLATED TIN | 36| 4 +Brand#13 |PROMO PLATED TIN | 45| 4 +Brand#13 |PROMO PLATED TIN | 49| 4 +Brand#13 |PROMO POLISHED BRASS | 9| 4 +Brand#13 |PROMO POLISHED BRASS | 14| 4 +Brand#13 |PROMO POLISHED BRASS | 23| 4 +Brand#13 |PROMO POLISHED COPPER | 3| 4 +Brand#13 |PROMO POLISHED COPPER | 23| 4 +Brand#13 |PROMO POLISHED COPPER | 49| 4 +Brand#13 |PROMO POLISHED NICKEL | 9| 4 +Brand#13 |PROMO POLISHED NICKEL | 19| 4 +Brand#13 |PROMO POLISHED STEEL | 3| 4 +Brand#13 |PROMO POLISHED STEEL | 9| 4 +Brand#13 |PROMO POLISHED STEEL | 19| 4 +Brand#13 |PROMO POLISHED STEEL | 49| 4 +Brand#13 |PROMO POLISHED TIN | 3| 4 +Brand#13 |PROMO POLISHED TIN | 14| 4 +Brand#13 |PROMO POLISHED TIN | 49| 4 +Brand#13 |SMALL ANODIZED BRASS | 3| 4 +Brand#13 |SMALL ANODIZED BRASS | 9| 4 +Brand#13 |SMALL ANODIZED BRASS | 23| 4 +Brand#13 |SMALL ANODIZED BRASS | 45| 4 +Brand#13 |SMALL ANODIZED COPPER | 3| 4 +Brand#13 |SMALL ANODIZED COPPER | 14| 4 +Brand#13 |SMALL ANODIZED COPPER | 45| 4 +Brand#13 |SMALL ANODIZED COPPER | 49| 4 +Brand#13 |SMALL ANODIZED NICKEL | 9| 4 +Brand#13 |SMALL ANODIZED NICKEL | 23| 4 +Brand#13 |SMALL ANODIZED NICKEL | 36| 4 +Brand#13 |SMALL ANODIZED STEEL | 19| 4 +Brand#13 |SMALL ANODIZED STEEL | 36| 4 +Brand#13 |SMALL ANODIZED STEEL | 49| 4 +Brand#13 |SMALL ANODIZED TIN | 3| 4 +Brand#13 |SMALL BRUSHED BRASS | 23| 4 +Brand#13 |SMALL BRUSHED BRASS | 45| 4 +Brand#13 |SMALL BRUSHED COPPER | 3| 4 +Brand#13 |SMALL BRUSHED COPPER | 49| 4 +Brand#13 |SMALL BRUSHED NICKEL | 45| 4 +Brand#13 |SMALL BRUSHED NICKEL | 49| 4 +Brand#13 |SMALL BRUSHED STEEL | 9| 4 +Brand#13 |SMALL BRUSHED STEEL | 14| 4 +Brand#13 |SMALL BRUSHED STEEL | 19| 4 +Brand#13 |SMALL BRUSHED TIN | 14| 4 +Brand#13 |SMALL BRUSHED TIN | 19| 4 +Brand#13 |SMALL BRUSHED TIN | 36| 4 +Brand#13 |SMALL BURNISHED BRASS | 9| 4 +Brand#13 |SMALL BURNISHED BRASS | 23| 4 +Brand#13 |SMALL BURNISHED BRASS | 36| 4 +Brand#13 |SMALL BURNISHED COPPER | 3| 4 +Brand#13 |SMALL BURNISHED COPPER | 14| 4 +Brand#13 |SMALL BURNISHED COPPER | 19| 4 +Brand#13 |SMALL BURNISHED COPPER | 36| 4 +Brand#13 |SMALL BURNISHED NICKEL | 14| 4 +Brand#13 |SMALL BURNISHED NICKEL | 36| 4 +Brand#13 |SMALL BURNISHED STEEL | 14| 4 +Brand#13 |SMALL BURNISHED TIN | 3| 4 +Brand#13 |SMALL BURNISHED TIN | 23| 4 +Brand#13 |SMALL BURNISHED TIN | 45| 4 +Brand#13 |SMALL PLATED BRASS | 3| 4 +Brand#13 |SMALL PLATED BRASS | 14| 4 +Brand#13 |SMALL PLATED COPPER | 9| 4 +Brand#13 |SMALL PLATED COPPER | 45| 4 +Brand#13 |SMALL PLATED NICKEL | 3| 4 +Brand#13 |SMALL PLATED NICKEL | 9| 4 +Brand#13 |SMALL PLATED NICKEL | 19| 4 +Brand#13 |SMALL PLATED STEEL | 3| 4 +Brand#13 |SMALL PLATED STEEL | 45| 4 +Brand#13 |SMALL PLATED STEEL | 49| 4 +Brand#13 |SMALL PLATED TIN | 9| 4 +Brand#13 |SMALL PLATED TIN | 23| 4 +Brand#13 |SMALL PLATED TIN | 45| 4 +Brand#13 |SMALL POLISHED BRASS | 3| 4 +Brand#13 |SMALL POLISHED BRASS | 19| 4 +Brand#13 |SMALL POLISHED BRASS | 36| 4 +Brand#13 |SMALL POLISHED COPPER | 14| 4 +Brand#13 |SMALL POLISHED COPPER | 23| 4 +Brand#13 |SMALL POLISHED COPPER | 36| 4 +Brand#13 |SMALL POLISHED NICKEL | 9| 4 +Brand#13 |SMALL POLISHED NICKEL | 23| 4 +Brand#13 |SMALL POLISHED NICKEL | 49| 4 +Brand#13 |SMALL POLISHED STEEL | 9| 4 +Brand#13 |SMALL POLISHED STEEL | 19| 4 +Brand#13 |SMALL POLISHED TIN | 3| 4 +Brand#13 |SMALL POLISHED TIN | 9| 4 +Brand#13 |SMALL POLISHED TIN | 19| 4 +Brand#13 |SMALL POLISHED TIN | 23| 4 +Brand#13 |SMALL POLISHED TIN | 36| 4 +Brand#13 |SMALL POLISHED TIN | 45| 4 +Brand#13 |SMALL POLISHED TIN | 49| 4 +Brand#13 |STANDARD ANODIZED BRASS | 3| 4 +Brand#13 |STANDARD ANODIZED BRASS | 19| 4 +Brand#13 |STANDARD ANODIZED BRASS | 36| 4 +Brand#13 |STANDARD ANODIZED BRASS | 45| 4 +Brand#13 |STANDARD ANODIZED COPPER | 9| 4 +Brand#13 |STANDARD ANODIZED COPPER | 45| 4 +Brand#13 |STANDARD ANODIZED NICKEL | 9| 4 +Brand#13 |STANDARD ANODIZED NICKEL | 36| 4 +Brand#13 |STANDARD ANODIZED STEEL | 49| 4 +Brand#13 |STANDARD ANODIZED TIN | 3| 4 +Brand#13 |STANDARD ANODIZED TIN | 14| 4 +Brand#13 |STANDARD ANODIZED TIN | 19| 4 +Brand#13 |STANDARD ANODIZED TIN | 45| 4 +Brand#13 |STANDARD ANODIZED TIN | 49| 4 +Brand#13 |STANDARD BRUSHED BRASS | 3| 4 +Brand#13 |STANDARD BRUSHED BRASS | 9| 4 +Brand#13 |STANDARD BRUSHED BRASS | 19| 4 +Brand#13 |STANDARD BRUSHED BRASS | 23| 4 +Brand#13 |STANDARD BRUSHED BRASS | 45| 4 +Brand#13 |STANDARD BRUSHED BRASS | 49| 4 +Brand#13 |STANDARD BRUSHED COPPER | 14| 4 +Brand#13 |STANDARD BRUSHED COPPER | 36| 4 +Brand#13 |STANDARD BRUSHED COPPER | 45| 4 +Brand#13 |STANDARD BRUSHED NICKEL | 3| 4 +Brand#13 |STANDARD BRUSHED NICKEL | 9| 4 +Brand#13 |STANDARD BRUSHED NICKEL | 19| 4 +Brand#13 |STANDARD BRUSHED NICKEL | 23| 4 +Brand#13 |STANDARD BRUSHED NICKEL | 45| 4 +Brand#13 |STANDARD BRUSHED STEEL | 3| 4 +Brand#13 |STANDARD BRUSHED STEEL | 14| 4 +Brand#13 |STANDARD BRUSHED STEEL | 19| 4 +Brand#13 |STANDARD BRUSHED STEEL | 23| 4 +Brand#13 |STANDARD BRUSHED TIN | 14| 4 +Brand#13 |STANDARD BRUSHED TIN | 36| 4 +Brand#13 |STANDARD BRUSHED TIN | 45| 4 +Brand#13 |STANDARD BURNISHED BRASS | 14| 4 +Brand#13 |STANDARD BURNISHED BRASS | 45| 4 +Brand#13 |STANDARD BURNISHED COPPER| 19| 4 +Brand#13 |STANDARD BURNISHED NICKEL| 36| 4 +Brand#13 |STANDARD BURNISHED NICKEL| 45| 4 +Brand#13 |STANDARD BURNISHED STEEL | 9| 4 +Brand#13 |STANDARD BURNISHED STEEL | 14| 4 +Brand#13 |STANDARD BURNISHED STEEL | 23| 4 +Brand#13 |STANDARD BURNISHED STEEL | 36| 4 +Brand#13 |STANDARD BURNISHED STEEL | 49| 4 +Brand#13 |STANDARD BURNISHED TIN | 14| 4 +Brand#13 |STANDARD BURNISHED TIN | 45| 4 +Brand#13 |STANDARD PLATED COPPER | 3| 4 +Brand#13 |STANDARD PLATED COPPER | 9| 4 +Brand#13 |STANDARD PLATED COPPER | 19| 4 +Brand#13 |STANDARD PLATED COPPER | 49| 4 +Brand#13 |STANDARD PLATED NICKEL | 19| 4 +Brand#13 |STANDARD PLATED STEEL | 3| 4 +Brand#13 |STANDARD PLATED STEEL | 23| 4 +Brand#13 |STANDARD PLATED STEEL | 45| 4 +Brand#13 |STANDARD PLATED TIN | 3| 4 +Brand#13 |STANDARD PLATED TIN | 9| 4 +Brand#13 |STANDARD POLISHED BRASS | 3| 4 +Brand#13 |STANDARD POLISHED BRASS | 9| 4 +Brand#13 |STANDARD POLISHED BRASS | 14| 4 +Brand#13 |STANDARD POLISHED BRASS | 23| 4 +Brand#13 |STANDARD POLISHED BRASS | 49| 4 +Brand#13 |STANDARD POLISHED COPPER | 9| 4 +Brand#13 |STANDARD POLISHED COPPER | 19| 4 +Brand#13 |STANDARD POLISHED COPPER | 49| 4 +Brand#13 |STANDARD POLISHED NICKEL | 14| 4 +Brand#13 |STANDARD POLISHED STEEL | 3| 4 +Brand#13 |STANDARD POLISHED TIN | 3| 4 +Brand#13 |STANDARD POLISHED TIN | 9| 4 +Brand#13 |STANDARD POLISHED TIN | 49| 4 +Brand#14 |ECONOMY ANODIZED BRASS | 9| 4 +Brand#14 |ECONOMY ANODIZED BRASS | 19| 4 +Brand#14 |ECONOMY ANODIZED COPPER | 19| 4 +Brand#14 |ECONOMY ANODIZED COPPER | 23| 4 +Brand#14 |ECONOMY ANODIZED COPPER | 49| 4 +Brand#14 |ECONOMY ANODIZED NICKEL | 3| 4 +Brand#14 |ECONOMY ANODIZED NICKEL | 19| 4 +Brand#14 |ECONOMY ANODIZED NICKEL | 36| 4 +Brand#14 |ECONOMY ANODIZED STEEL | 23| 4 +Brand#14 |ECONOMY ANODIZED STEEL | 36| 4 +Brand#14 |ECONOMY ANODIZED TIN | 14| 4 +Brand#14 |ECONOMY ANODIZED TIN | 36| 4 +Brand#14 |ECONOMY ANODIZED TIN | 49| 4 +Brand#14 |ECONOMY BRUSHED BRASS | 19| 4 +Brand#14 |ECONOMY BRUSHED BRASS | 36| 4 +Brand#14 |ECONOMY BRUSHED BRASS | 45| 4 +Brand#14 |ECONOMY BRUSHED COPPER | 9| 4 +Brand#14 |ECONOMY BRUSHED COPPER | 14| 4 +Brand#14 |ECONOMY BRUSHED COPPER | 23| 4 +Brand#14 |ECONOMY BRUSHED COPPER | 36| 4 +Brand#14 |ECONOMY BRUSHED NICKEL | 19| 4 +Brand#14 |ECONOMY BRUSHED NICKEL | 23| 4 +Brand#14 |ECONOMY BRUSHED NICKEL | 45| 4 +Brand#14 |ECONOMY BRUSHED NICKEL | 49| 4 +Brand#14 |ECONOMY BRUSHED STEEL | 9| 4 +Brand#14 |ECONOMY BRUSHED STEEL | 14| 4 +Brand#14 |ECONOMY BRUSHED STEEL | 19| 4 +Brand#14 |ECONOMY BRUSHED STEEL | 23| 4 +Brand#14 |ECONOMY BRUSHED TIN | 9| 4 +Brand#14 |ECONOMY BRUSHED TIN | 19| 4 +Brand#14 |ECONOMY BRUSHED TIN | 23| 4 +Brand#14 |ECONOMY BRUSHED TIN | 36| 4 +Brand#14 |ECONOMY BRUSHED TIN | 45| 4 +Brand#14 |ECONOMY BURNISHED BRASS | 3| 4 +Brand#14 |ECONOMY BURNISHED BRASS | 9| 4 +Brand#14 |ECONOMY BURNISHED BRASS | 19| 4 +Brand#14 |ECONOMY BURNISHED BRASS | 36| 4 +Brand#14 |ECONOMY BURNISHED COPPER | 3| 4 +Brand#14 |ECONOMY BURNISHED COPPER | 14| 4 +Brand#14 |ECONOMY BURNISHED COPPER | 19| 4 +Brand#14 |ECONOMY BURNISHED NICKEL | 14| 4 +Brand#14 |ECONOMY BURNISHED NICKEL | 19| 4 +Brand#14 |ECONOMY BURNISHED NICKEL | 49| 4 +Brand#14 |ECONOMY BURNISHED TIN | 3| 4 +Brand#14 |ECONOMY BURNISHED TIN | 45| 4 +Brand#14 |ECONOMY BURNISHED TIN | 49| 4 +Brand#14 |ECONOMY PLATED BRASS | 3| 4 +Brand#14 |ECONOMY PLATED BRASS | 19| 4 +Brand#14 |ECONOMY PLATED BRASS | 23| 4 +Brand#14 |ECONOMY PLATED BRASS | 49| 4 +Brand#14 |ECONOMY PLATED COPPER | 36| 4 +Brand#14 |ECONOMY PLATED COPPER | 45| 4 +Brand#14 |ECONOMY PLATED COPPER | 49| 4 +Brand#14 |ECONOMY PLATED NICKEL | 14| 4 +Brand#14 |ECONOMY PLATED NICKEL | 45| 4 +Brand#14 |ECONOMY PLATED STEEL | 14| 4 +Brand#14 |ECONOMY PLATED STEEL | 19| 4 +Brand#14 |ECONOMY PLATED STEEL | 23| 4 +Brand#14 |ECONOMY PLATED STEEL | 45| 4 +Brand#14 |ECONOMY PLATED STEEL | 49| 4 +Brand#14 |ECONOMY PLATED TIN | 3| 4 +Brand#14 |ECONOMY PLATED TIN | 14| 4 +Brand#14 |ECONOMY PLATED TIN | 23| 4 +Brand#14 |ECONOMY PLATED TIN | 49| 4 +Brand#14 |ECONOMY POLISHED BRASS | 9| 4 +Brand#14 |ECONOMY POLISHED BRASS | 14| 4 +Brand#14 |ECONOMY POLISHED BRASS | 45| 4 +Brand#14 |ECONOMY POLISHED COPPER | 3| 4 +Brand#14 |ECONOMY POLISHED COPPER | 9| 4 +Brand#14 |ECONOMY POLISHED COPPER | 19| 4 +Brand#14 |ECONOMY POLISHED COPPER | 36| 4 +Brand#14 |ECONOMY POLISHED COPPER | 45| 4 +Brand#14 |ECONOMY POLISHED NICKEL | 23| 4 +Brand#14 |ECONOMY POLISHED STEEL | 14| 4 +Brand#14 |ECONOMY POLISHED STEEL | 19| 4 +Brand#14 |ECONOMY POLISHED STEEL | 23| 4 +Brand#14 |ECONOMY POLISHED STEEL | 36| 4 +Brand#14 |ECONOMY POLISHED TIN | 9| 4 +Brand#14 |ECONOMY POLISHED TIN | 14| 4 +Brand#14 |ECONOMY POLISHED TIN | 36| 4 +Brand#14 |ECONOMY POLISHED TIN | 45| 4 +Brand#14 |LARGE ANODIZED BRASS | 23| 4 +Brand#14 |LARGE ANODIZED BRASS | 36| 4 +Brand#14 |LARGE ANODIZED BRASS | 45| 4 +Brand#14 |LARGE ANODIZED BRASS | 49| 4 +Brand#14 |LARGE ANODIZED COPPER | 9| 4 +Brand#14 |LARGE ANODIZED COPPER | 36| 4 +Brand#14 |LARGE ANODIZED NICKEL | 3| 4 +Brand#14 |LARGE ANODIZED NICKEL | 19| 4 +Brand#14 |LARGE ANODIZED STEEL | 14| 4 +Brand#14 |LARGE ANODIZED STEEL | 23| 4 +Brand#14 |LARGE ANODIZED STEEL | 36| 4 +Brand#14 |LARGE ANODIZED STEEL | 49| 4 +Brand#14 |LARGE ANODIZED TIN | 3| 4 +Brand#14 |LARGE ANODIZED TIN | 36| 4 +Brand#14 |LARGE ANODIZED TIN | 45| 4 +Brand#14 |LARGE ANODIZED TIN | 49| 4 +Brand#14 |LARGE BRUSHED BRASS | 3| 4 +Brand#14 |LARGE BRUSHED BRASS | 19| 4 +Brand#14 |LARGE BRUSHED BRASS | 36| 4 +Brand#14 |LARGE BRUSHED COPPER | 3| 4 +Brand#14 |LARGE BRUSHED COPPER | 45| 4 +Brand#14 |LARGE BRUSHED NICKEL | 9| 4 +Brand#14 |LARGE BRUSHED NICKEL | 36| 4 +Brand#14 |LARGE BRUSHED NICKEL | 49| 4 +Brand#14 |LARGE BRUSHED STEEL | 14| 4 +Brand#14 |LARGE BRUSHED STEEL | 23| 4 +Brand#14 |LARGE BRUSHED STEEL | 49| 4 +Brand#14 |LARGE BRUSHED TIN | 19| 4 +Brand#14 |LARGE BRUSHED TIN | 23| 4 +Brand#14 |LARGE BURNISHED BRASS | 3| 4 +Brand#14 |LARGE BURNISHED BRASS | 19| 4 +Brand#14 |LARGE BURNISHED BRASS | 36| 4 +Brand#14 |LARGE BURNISHED COPPER | 3| 4 +Brand#14 |LARGE BURNISHED COPPER | 23| 4 +Brand#14 |LARGE BURNISHED COPPER | 36| 4 +Brand#14 |LARGE BURNISHED COPPER | 45| 4 +Brand#14 |LARGE BURNISHED NICKEL | 14| 4 +Brand#14 |LARGE BURNISHED NICKEL | 19| 4 +Brand#14 |LARGE BURNISHED NICKEL | 45| 4 +Brand#14 |LARGE BURNISHED STEEL | 49| 4 +Brand#14 |LARGE BURNISHED TIN | 3| 4 +Brand#14 |LARGE BURNISHED TIN | 14| 4 +Brand#14 |LARGE BURNISHED TIN | 36| 4 +Brand#14 |LARGE BURNISHED TIN | 49| 4 +Brand#14 |LARGE PLATED BRASS | 3| 4 +Brand#14 |LARGE PLATED BRASS | 9| 4 +Brand#14 |LARGE PLATED COPPER | 9| 4 +Brand#14 |LARGE PLATED COPPER | 14| 4 +Brand#14 |LARGE PLATED COPPER | 19| 4 +Brand#14 |LARGE PLATED COPPER | 45| 4 +Brand#14 |LARGE PLATED NICKEL | 3| 4 +Brand#14 |LARGE PLATED NICKEL | 9| 4 +Brand#14 |LARGE PLATED NICKEL | 14| 4 +Brand#14 |LARGE PLATED STEEL | 14| 4 +Brand#14 |LARGE PLATED STEEL | 19| 4 +Brand#14 |LARGE PLATED TIN | 3| 4 +Brand#14 |LARGE PLATED TIN | 9| 4 +Brand#14 |LARGE PLATED TIN | 19| 4 +Brand#14 |LARGE PLATED TIN | 23| 4 +Brand#14 |LARGE PLATED TIN | 45| 4 +Brand#14 |LARGE PLATED TIN | 49| 4 +Brand#14 |LARGE POLISHED BRASS | 49| 4 +Brand#14 |LARGE POLISHED COPPER | 3| 4 +Brand#14 |LARGE POLISHED COPPER | 14| 4 +Brand#14 |LARGE POLISHED COPPER | 19| 4 +Brand#14 |LARGE POLISHED COPPER | 36| 4 +Brand#14 |LARGE POLISHED COPPER | 49| 4 +Brand#14 |LARGE POLISHED NICKEL | 3| 4 +Brand#14 |LARGE POLISHED NICKEL | 19| 4 +Brand#14 |LARGE POLISHED NICKEL | 45| 4 +Brand#14 |LARGE POLISHED NICKEL | 49| 4 +Brand#14 |LARGE POLISHED STEEL | 9| 4 +Brand#14 |LARGE POLISHED STEEL | 14| 4 +Brand#14 |LARGE POLISHED STEEL | 36| 4 +Brand#14 |LARGE POLISHED STEEL | 49| 4 +Brand#14 |LARGE POLISHED TIN | 3| 4 +Brand#14 |LARGE POLISHED TIN | 19| 4 +Brand#14 |MEDIUM ANODIZED BRASS | 9| 4 +Brand#14 |MEDIUM ANODIZED BRASS | 23| 4 +Brand#14 |MEDIUM ANODIZED BRASS | 36| 4 +Brand#14 |MEDIUM ANODIZED BRASS | 45| 4 +Brand#14 |MEDIUM ANODIZED BRASS | 49| 4 +Brand#14 |MEDIUM ANODIZED COPPER | 3| 4 +Brand#14 |MEDIUM ANODIZED COPPER | 14| 4 +Brand#14 |MEDIUM ANODIZED COPPER | 23| 4 +Brand#14 |MEDIUM ANODIZED NICKEL | 23| 4 +Brand#14 |MEDIUM ANODIZED NICKEL | 49| 4 +Brand#14 |MEDIUM ANODIZED STEEL | 3| 4 +Brand#14 |MEDIUM ANODIZED STEEL | 14| 4 +Brand#14 |MEDIUM ANODIZED STEEL | 23| 4 +Brand#14 |MEDIUM ANODIZED STEEL | 45| 4 +Brand#14 |MEDIUM ANODIZED STEEL | 49| 4 +Brand#14 |MEDIUM ANODIZED TIN | 3| 4 +Brand#14 |MEDIUM ANODIZED TIN | 19| 4 +Brand#14 |MEDIUM ANODIZED TIN | 23| 4 +Brand#14 |MEDIUM ANODIZED TIN | 45| 4 +Brand#14 |MEDIUM BRUSHED BRASS | 3| 4 +Brand#14 |MEDIUM BRUSHED BRASS | 14| 4 +Brand#14 |MEDIUM BRUSHED BRASS | 36| 4 +Brand#14 |MEDIUM BRUSHED BRASS | 45| 4 +Brand#14 |MEDIUM BRUSHED COPPER | 3| 4 +Brand#14 |MEDIUM BRUSHED COPPER | 14| 4 +Brand#14 |MEDIUM BRUSHED COPPER | 19| 4 +Brand#14 |MEDIUM BRUSHED COPPER | 49| 4 +Brand#14 |MEDIUM BRUSHED NICKEL | 3| 4 +Brand#14 |MEDIUM BRUSHED NICKEL | 19| 4 +Brand#14 |MEDIUM BRUSHED NICKEL | 23| 4 +Brand#14 |MEDIUM BRUSHED STEEL | 3| 4 +Brand#14 |MEDIUM BRUSHED STEEL | 14| 4 +Brand#14 |MEDIUM BRUSHED STEEL | 45| 4 +Brand#14 |MEDIUM BRUSHED TIN | 36| 4 +Brand#14 |MEDIUM BRUSHED TIN | 49| 4 +Brand#14 |MEDIUM BURNISHED BRASS | 9| 4 +Brand#14 |MEDIUM BURNISHED BRASS | 14| 4 +Brand#14 |MEDIUM BURNISHED BRASS | 45| 4 +Brand#14 |MEDIUM BURNISHED COPPER | 19| 4 +Brand#14 |MEDIUM BURNISHED COPPER | 23| 4 +Brand#14 |MEDIUM BURNISHED COPPER | 36| 4 +Brand#14 |MEDIUM BURNISHED COPPER | 49| 4 +Brand#14 |MEDIUM BURNISHED NICKEL | 45| 4 +Brand#14 |MEDIUM BURNISHED STEEL | 9| 4 +Brand#14 |MEDIUM BURNISHED TIN | 9| 4 +Brand#14 |MEDIUM BURNISHED TIN | 23| 4 +Brand#14 |MEDIUM PLATED BRASS | 14| 4 +Brand#14 |MEDIUM PLATED COPPER | 49| 4 +Brand#14 |MEDIUM PLATED NICKEL | 3| 4 +Brand#14 |MEDIUM PLATED NICKEL | 14| 4 +Brand#14 |MEDIUM PLATED NICKEL | 19| 4 +Brand#14 |MEDIUM PLATED NICKEL | 36| 4 +Brand#14 |MEDIUM PLATED NICKEL | 45| 4 +Brand#14 |MEDIUM PLATED STEEL | 3| 4 +Brand#14 |MEDIUM PLATED STEEL | 14| 4 +Brand#14 |MEDIUM PLATED STEEL | 23| 4 +Brand#14 |PROMO ANODIZED BRASS | 3| 4 +Brand#14 |PROMO ANODIZED BRASS | 9| 4 +Brand#14 |PROMO ANODIZED BRASS | 14| 4 +Brand#14 |PROMO ANODIZED BRASS | 49| 4 +Brand#14 |PROMO ANODIZED COPPER | 23| 4 +Brand#14 |PROMO ANODIZED COPPER | 49| 4 +Brand#14 |PROMO ANODIZED NICKEL | 3| 4 +Brand#14 |PROMO ANODIZED NICKEL | 23| 4 +Brand#14 |PROMO ANODIZED STEEL | 9| 4 +Brand#14 |PROMO ANODIZED STEEL | 49| 4 +Brand#14 |PROMO ANODIZED TIN | 3| 4 +Brand#14 |PROMO ANODIZED TIN | 23| 4 +Brand#14 |PROMO ANODIZED TIN | 36| 4 +Brand#14 |PROMO ANODIZED TIN | 45| 4 +Brand#14 |PROMO ANODIZED TIN | 49| 4 +Brand#14 |PROMO BRUSHED BRASS | 3| 4 +Brand#14 |PROMO BRUSHED BRASS | 9| 4 +Brand#14 |PROMO BRUSHED COPPER | 3| 4 +Brand#14 |PROMO BRUSHED COPPER | 19| 4 +Brand#14 |PROMO BRUSHED NICKEL | 3| 4 +Brand#14 |PROMO BRUSHED NICKEL | 9| 4 +Brand#14 |PROMO BRUSHED NICKEL | 14| 4 +Brand#14 |PROMO BRUSHED STEEL | 14| 4 +Brand#14 |PROMO BRUSHED STEEL | 19| 4 +Brand#14 |PROMO BRUSHED STEEL | 23| 4 +Brand#14 |PROMO BRUSHED STEEL | 45| 4 +Brand#14 |PROMO BRUSHED TIN | 14| 4 +Brand#14 |PROMO BRUSHED TIN | 19| 4 +Brand#14 |PROMO BRUSHED TIN | 23| 4 +Brand#14 |PROMO BRUSHED TIN | 45| 4 +Brand#14 |PROMO BRUSHED TIN | 49| 4 +Brand#14 |PROMO BURNISHED BRASS | 3| 4 +Brand#14 |PROMO BURNISHED BRASS | 14| 4 +Brand#14 |PROMO BURNISHED COPPER | 3| 4 +Brand#14 |PROMO BURNISHED COPPER | 9| 4 +Brand#14 |PROMO BURNISHED COPPER | 14| 4 +Brand#14 |PROMO BURNISHED COPPER | 19| 4 +Brand#14 |PROMO BURNISHED COPPER | 36| 4 +Brand#14 |PROMO BURNISHED NICKEL | 23| 4 +Brand#14 |PROMO BURNISHED NICKEL | 45| 4 +Brand#14 |PROMO BURNISHED NICKEL | 49| 4 +Brand#14 |PROMO BURNISHED STEEL | 3| 4 +Brand#14 |PROMO BURNISHED STEEL | 19| 4 +Brand#14 |PROMO BURNISHED STEEL | 49| 4 +Brand#14 |PROMO BURNISHED TIN | 3| 4 +Brand#14 |PROMO BURNISHED TIN | 9| 4 +Brand#14 |PROMO BURNISHED TIN | 23| 4 +Brand#14 |PROMO PLATED BRASS | 3| 4 +Brand#14 |PROMO PLATED BRASS | 23| 4 +Brand#14 |PROMO PLATED BRASS | 49| 4 +Brand#14 |PROMO PLATED COPPER | 3| 4 +Brand#14 |PROMO PLATED COPPER | 9| 4 +Brand#14 |PROMO PLATED COPPER | 36| 4 +Brand#14 |PROMO PLATED COPPER | 49| 4 +Brand#14 |PROMO PLATED NICKEL | 14| 4 +Brand#14 |PROMO PLATED NICKEL | 19| 4 +Brand#14 |PROMO PLATED STEEL | 36| 4 +Brand#14 |PROMO PLATED STEEL | 45| 4 +Brand#14 |PROMO PLATED TIN | 23| 4 +Brand#14 |PROMO POLISHED BRASS | 3| 4 +Brand#14 |PROMO POLISHED BRASS | 45| 4 +Brand#14 |PROMO POLISHED COPPER | 9| 4 +Brand#14 |PROMO POLISHED COPPER | 23| 4 +Brand#14 |PROMO POLISHED COPPER | 36| 4 +Brand#14 |PROMO POLISHED COPPER | 45| 4 +Brand#14 |PROMO POLISHED COPPER | 49| 4 +Brand#14 |PROMO POLISHED NICKEL | 19| 4 +Brand#14 |PROMO POLISHED NICKEL | 23| 4 +Brand#14 |PROMO POLISHED NICKEL | 36| 4 +Brand#14 |PROMO POLISHED NICKEL | 49| 4 +Brand#14 |PROMO POLISHED STEEL | 9| 4 +Brand#14 |PROMO POLISHED STEEL | 45| 4 +Brand#14 |PROMO POLISHED TIN | 23| 4 +Brand#14 |PROMO POLISHED TIN | 36| 4 +Brand#14 |SMALL ANODIZED BRASS | 3| 4 +Brand#14 |SMALL ANODIZED BRASS | 19| 4 +Brand#14 |SMALL ANODIZED BRASS | 23| 4 +Brand#14 |SMALL ANODIZED BRASS | 36| 4 +Brand#14 |SMALL ANODIZED BRASS | 45| 4 +Brand#14 |SMALL ANODIZED BRASS | 49| 4 +Brand#14 |SMALL ANODIZED COPPER | 9| 4 +Brand#14 |SMALL ANODIZED COPPER | 19| 4 +Brand#14 |SMALL ANODIZED COPPER | 23| 4 +Brand#14 |SMALL ANODIZED COPPER | 36| 4 +Brand#14 |SMALL ANODIZED COPPER | 45| 4 +Brand#14 |SMALL ANODIZED NICKEL | 14| 4 +Brand#14 |SMALL ANODIZED NICKEL | 23| 4 +Brand#14 |SMALL ANODIZED STEEL | 45| 4 +Brand#14 |SMALL ANODIZED TIN | 9| 4 +Brand#14 |SMALL ANODIZED TIN | 14| 4 +Brand#14 |SMALL ANODIZED TIN | 23| 4 +Brand#14 |SMALL ANODIZED TIN | 36| 4 +Brand#14 |SMALL ANODIZED TIN | 49| 4 +Brand#14 |SMALL BRUSHED BRASS | 3| 4 +Brand#14 |SMALL BRUSHED BRASS | 36| 4 +Brand#14 |SMALL BRUSHED COPPER | 9| 4 +Brand#14 |SMALL BRUSHED COPPER | 14| 4 +Brand#14 |SMALL BRUSHED COPPER | 19| 4 +Brand#14 |SMALL BRUSHED COPPER | 23| 4 +Brand#14 |SMALL BRUSHED COPPER | 45| 4 +Brand#14 |SMALL BRUSHED NICKEL | 3| 4 +Brand#14 |SMALL BRUSHED NICKEL | 14| 4 +Brand#14 |SMALL BRUSHED NICKEL | 23| 4 +Brand#14 |SMALL BRUSHED NICKEL | 45| 4 +Brand#14 |SMALL BRUSHED STEEL | 9| 4 +Brand#14 |SMALL BRUSHED STEEL | 19| 4 +Brand#14 |SMALL BRUSHED STEEL | 49| 4 +Brand#14 |SMALL BRUSHED TIN | 3| 4 +Brand#14 |SMALL BRUSHED TIN | 23| 4 +Brand#14 |SMALL BRUSHED TIN | 45| 4 +Brand#14 |SMALL BURNISHED BRASS | 9| 4 +Brand#14 |SMALL BURNISHED COPPER | 3| 4 +Brand#14 |SMALL BURNISHED COPPER | 9| 4 +Brand#14 |SMALL BURNISHED COPPER | 19| 4 +Brand#14 |SMALL BURNISHED COPPER | 23| 4 +Brand#14 |SMALL BURNISHED COPPER | 49| 4 +Brand#14 |SMALL BURNISHED NICKEL | 3| 4 +Brand#14 |SMALL BURNISHED NICKEL | 23| 4 +Brand#14 |SMALL BURNISHED STEEL | 3| 4 +Brand#14 |SMALL BURNISHED TIN | 3| 4 +Brand#14 |SMALL BURNISHED TIN | 9| 4 +Brand#14 |SMALL BURNISHED TIN | 14| 4 +Brand#14 |SMALL BURNISHED TIN | 36| 4 +Brand#14 |SMALL BURNISHED TIN | 45| 4 +Brand#14 |SMALL PLATED BRASS | 3| 4 +Brand#14 |SMALL PLATED BRASS | 19| 4 +Brand#14 |SMALL PLATED COPPER | 14| 4 +Brand#14 |SMALL PLATED COPPER | 36| 4 +Brand#14 |SMALL PLATED COPPER | 45| 4 +Brand#14 |SMALL PLATED NICKEL | 3| 4 +Brand#14 |SMALL PLATED NICKEL | 9| 4 +Brand#14 |SMALL PLATED NICKEL | 45| 4 +Brand#14 |SMALL PLATED NICKEL | 49| 4 +Brand#14 |SMALL PLATED STEEL | 3| 4 +Brand#14 |SMALL PLATED STEEL | 45| 4 +Brand#14 |SMALL PLATED TIN | 3| 4 +Brand#14 |SMALL PLATED TIN | 23| 4 +Brand#14 |SMALL PLATED TIN | 36| 4 +Brand#14 |SMALL POLISHED COPPER | 9| 4 +Brand#14 |SMALL POLISHED COPPER | 19| 4 +Brand#14 |SMALL POLISHED COPPER | 23| 4 +Brand#14 |SMALL POLISHED COPPER | 45| 4 +Brand#14 |SMALL POLISHED NICKEL | 14| 4 +Brand#14 |SMALL POLISHED NICKEL | 23| 4 +Brand#14 |SMALL POLISHED TIN | 23| 4 +Brand#14 |SMALL POLISHED TIN | 45| 4 +Brand#14 |STANDARD ANODIZED BRASS | 19| 4 +Brand#14 |STANDARD ANODIZED BRASS | 23| 4 +Brand#14 |STANDARD ANODIZED BRASS | 45| 4 +Brand#14 |STANDARD ANODIZED BRASS | 49| 4 +Brand#14 |STANDARD ANODIZED COPPER | 36| 4 +Brand#14 |STANDARD ANODIZED NICKEL | 9| 4 +Brand#14 |STANDARD ANODIZED NICKEL | 14| 4 +Brand#14 |STANDARD ANODIZED NICKEL | 23| 4 +Brand#14 |STANDARD ANODIZED NICKEL | 36| 4 +Brand#14 |STANDARD ANODIZED NICKEL | 45| 4 +Brand#14 |STANDARD ANODIZED NICKEL | 49| 4 +Brand#14 |STANDARD ANODIZED STEEL | 3| 4 +Brand#14 |STANDARD ANODIZED STEEL | 14| 4 +Brand#14 |STANDARD ANODIZED STEEL | 19| 4 +Brand#14 |STANDARD ANODIZED TIN | 9| 4 +Brand#14 |STANDARD ANODIZED TIN | 14| 4 +Brand#14 |STANDARD ANODIZED TIN | 19| 4 +Brand#14 |STANDARD ANODIZED TIN | 23| 4 +Brand#14 |STANDARD BRUSHED BRASS | 14| 4 +Brand#14 |STANDARD BRUSHED BRASS | 36| 4 +Brand#14 |STANDARD BRUSHED COPPER | 14| 4 +Brand#14 |STANDARD BRUSHED COPPER | 19| 4 +Brand#14 |STANDARD BRUSHED COPPER | 23| 4 +Brand#14 |STANDARD BRUSHED COPPER | 45| 4 +Brand#14 |STANDARD BRUSHED COPPER | 49| 4 +Brand#14 |STANDARD BRUSHED NICKEL | 9| 4 +Brand#14 |STANDARD BRUSHED NICKEL | 19| 4 +Brand#14 |STANDARD BRUSHED NICKEL | 36| 4 +Brand#14 |STANDARD BRUSHED NICKEL | 45| 4 +Brand#14 |STANDARD BRUSHED STEEL | 3| 4 +Brand#14 |STANDARD BRUSHED STEEL | 9| 4 +Brand#14 |STANDARD BRUSHED STEEL | 19| 4 +Brand#14 |STANDARD BRUSHED STEEL | 36| 4 +Brand#14 |STANDARD BRUSHED TIN | 3| 4 +Brand#14 |STANDARD BRUSHED TIN | 14| 4 +Brand#14 |STANDARD BRUSHED TIN | 36| 4 +Brand#14 |STANDARD BURNISHED COPPER| 36| 4 +Brand#14 |STANDARD BURNISHED COPPER| 45| 4 +Brand#14 |STANDARD BURNISHED COPPER| 49| 4 +Brand#14 |STANDARD BURNISHED NICKEL| 9| 4 +Brand#14 |STANDARD BURNISHED NICKEL| 14| 4 +Brand#14 |STANDARD BURNISHED NICKEL| 36| 4 +Brand#14 |STANDARD BURNISHED STEEL | 3| 4 +Brand#14 |STANDARD BURNISHED STEEL | 9| 4 +Brand#14 |STANDARD BURNISHED STEEL | 36| 4 +Brand#14 |STANDARD BURNISHED STEEL | 49| 4 +Brand#14 |STANDARD BURNISHED TIN | 23| 4 +Brand#14 |STANDARD BURNISHED TIN | 36| 4 +Brand#14 |STANDARD BURNISHED TIN | 45| 4 +Brand#14 |STANDARD PLATED BRASS | 23| 4 +Brand#14 |STANDARD PLATED BRASS | 36| 4 +Brand#14 |STANDARD PLATED COPPER | 3| 4 +Brand#14 |STANDARD PLATED COPPER | 9| 4 +Brand#14 |STANDARD PLATED COPPER | 19| 4 +Brand#14 |STANDARD PLATED NICKEL | 36| 4 +Brand#14 |STANDARD PLATED NICKEL | 45| 4 +Brand#14 |STANDARD PLATED STEEL | 14| 4 +Brand#14 |STANDARD PLATED STEEL | 19| 4 +Brand#14 |STANDARD PLATED STEEL | 45| 4 +Brand#14 |STANDARD PLATED STEEL | 49| 4 +Brand#14 |STANDARD PLATED TIN | 14| 4 +Brand#14 |STANDARD PLATED TIN | 23| 4 +Brand#14 |STANDARD PLATED TIN | 36| 4 +Brand#14 |STANDARD PLATED TIN | 45| 4 +Brand#14 |STANDARD POLISHED BRASS | 3| 4 +Brand#14 |STANDARD POLISHED BRASS | 36| 4 +Brand#14 |STANDARD POLISHED COPPER | 9| 4 +Brand#14 |STANDARD POLISHED COPPER | 23| 4 +Brand#14 |STANDARD POLISHED NICKEL | 14| 4 +Brand#14 |STANDARD POLISHED NICKEL | 23| 4 +Brand#14 |STANDARD POLISHED NICKEL | 45| 4 +Brand#14 |STANDARD POLISHED NICKEL | 49| 4 +Brand#14 |STANDARD POLISHED STEEL | 3| 4 +Brand#14 |STANDARD POLISHED STEEL | 9| 4 +Brand#14 |STANDARD POLISHED STEEL | 14| 4 +Brand#14 |STANDARD POLISHED STEEL | 19| 4 +Brand#14 |STANDARD POLISHED TIN | 19| 4 +Brand#14 |STANDARD POLISHED TIN | 23| 4 +Brand#14 |STANDARD POLISHED TIN | 36| 4 +Brand#15 |ECONOMY ANODIZED BRASS | 14| 4 +Brand#15 |ECONOMY ANODIZED BRASS | 19| 4 +Brand#15 |ECONOMY ANODIZED BRASS | 45| 4 +Brand#15 |ECONOMY ANODIZED BRASS | 49| 4 +Brand#15 |ECONOMY ANODIZED COPPER | 3| 4 +Brand#15 |ECONOMY ANODIZED COPPER | 14| 4 +Brand#15 |ECONOMY ANODIZED COPPER | 23| 4 +Brand#15 |ECONOMY ANODIZED COPPER | 36| 4 +Brand#15 |ECONOMY ANODIZED NICKEL | 14| 4 +Brand#15 |ECONOMY ANODIZED NICKEL | 45| 4 +Brand#15 |ECONOMY ANODIZED NICKEL | 49| 4 +Brand#15 |ECONOMY ANODIZED STEEL | 9| 4 +Brand#15 |ECONOMY ANODIZED STEEL | 19| 4 +Brand#15 |ECONOMY ANODIZED STEEL | 45| 4 +Brand#15 |ECONOMY ANODIZED STEEL | 49| 4 +Brand#15 |ECONOMY ANODIZED TIN | 3| 4 +Brand#15 |ECONOMY ANODIZED TIN | 14| 4 +Brand#15 |ECONOMY ANODIZED TIN | 23| 4 +Brand#15 |ECONOMY ANODIZED TIN | 45| 4 +Brand#15 |ECONOMY ANODIZED TIN | 49| 4 +Brand#15 |ECONOMY BRUSHED BRASS | 9| 4 +Brand#15 |ECONOMY BRUSHED BRASS | 14| 4 +Brand#15 |ECONOMY BRUSHED BRASS | 36| 4 +Brand#15 |ECONOMY BRUSHED BRASS | 45| 4 +Brand#15 |ECONOMY BRUSHED BRASS | 49| 4 +Brand#15 |ECONOMY BRUSHED COPPER | 14| 4 +Brand#15 |ECONOMY BRUSHED COPPER | 19| 4 +Brand#15 |ECONOMY BRUSHED COPPER | 45| 4 +Brand#15 |ECONOMY BRUSHED COPPER | 49| 4 +Brand#15 |ECONOMY BRUSHED NICKEL | 19| 4 +Brand#15 |ECONOMY BRUSHED STEEL | 3| 4 +Brand#15 |ECONOMY BRUSHED STEEL | 14| 4 +Brand#15 |ECONOMY BRUSHED TIN | 3| 4 +Brand#15 |ECONOMY BRUSHED TIN | 19| 4 +Brand#15 |ECONOMY BRUSHED TIN | 23| 4 +Brand#15 |ECONOMY BRUSHED TIN | 45| 4 +Brand#15 |ECONOMY BURNISHED BRASS | 23| 4 +Brand#15 |ECONOMY BURNISHED COPPER | 3| 4 +Brand#15 |ECONOMY BURNISHED NICKEL | 3| 4 +Brand#15 |ECONOMY BURNISHED NICKEL | 45| 4 +Brand#15 |ECONOMY BURNISHED STEEL | 14| 4 +Brand#15 |ECONOMY BURNISHED STEEL | 23| 4 +Brand#15 |ECONOMY BURNISHED STEEL | 36| 4 +Brand#15 |ECONOMY BURNISHED TIN | 3| 4 +Brand#15 |ECONOMY BURNISHED TIN | 14| 4 +Brand#15 |ECONOMY BURNISHED TIN | 19| 4 +Brand#15 |ECONOMY BURNISHED TIN | 36| 4 +Brand#15 |ECONOMY PLATED BRASS | 9| 4 +Brand#15 |ECONOMY PLATED BRASS | 19| 4 +Brand#15 |ECONOMY PLATED BRASS | 23| 4 +Brand#15 |ECONOMY PLATED BRASS | 45| 4 +Brand#15 |ECONOMY PLATED BRASS | 49| 4 +Brand#15 |ECONOMY PLATED COPPER | 14| 4 +Brand#15 |ECONOMY PLATED COPPER | 19| 4 +Brand#15 |ECONOMY PLATED NICKEL | 3| 4 +Brand#15 |ECONOMY PLATED NICKEL | 23| 4 +Brand#15 |ECONOMY PLATED NICKEL | 49| 4 +Brand#15 |ECONOMY PLATED STEEL | 9| 4 +Brand#15 |ECONOMY PLATED STEEL | 23| 4 +Brand#15 |ECONOMY PLATED STEEL | 36| 4 +Brand#15 |ECONOMY PLATED STEEL | 45| 4 +Brand#15 |ECONOMY PLATED STEEL | 49| 4 +Brand#15 |ECONOMY PLATED TIN | 3| 4 +Brand#15 |ECONOMY PLATED TIN | 19| 4 +Brand#15 |ECONOMY PLATED TIN | 23| 4 +Brand#15 |ECONOMY PLATED TIN | 36| 4 +Brand#15 |ECONOMY PLATED TIN | 45| 4 +Brand#15 |ECONOMY PLATED TIN | 49| 4 +Brand#15 |ECONOMY POLISHED BRASS | 9| 4 +Brand#15 |ECONOMY POLISHED BRASS | 23| 4 +Brand#15 |ECONOMY POLISHED BRASS | 45| 4 +Brand#15 |ECONOMY POLISHED BRASS | 49| 4 +Brand#15 |ECONOMY POLISHED COPPER | 14| 4 +Brand#15 |ECONOMY POLISHED COPPER | 19| 4 +Brand#15 |ECONOMY POLISHED COPPER | 23| 4 +Brand#15 |ECONOMY POLISHED NICKEL | 23| 4 +Brand#15 |ECONOMY POLISHED STEEL | 14| 4 +Brand#15 |ECONOMY POLISHED STEEL | 45| 4 +Brand#15 |ECONOMY POLISHED TIN | 19| 4 +Brand#15 |ECONOMY POLISHED TIN | 45| 4 +Brand#15 |ECONOMY POLISHED TIN | 49| 4 +Brand#15 |LARGE ANODIZED BRASS | 23| 4 +Brand#15 |LARGE ANODIZED BRASS | 45| 4 +Brand#15 |LARGE ANODIZED BRASS | 49| 4 +Brand#15 |LARGE ANODIZED COPPER | 3| 4 +Brand#15 |LARGE ANODIZED COPPER | 9| 4 +Brand#15 |LARGE ANODIZED NICKEL | 9| 4 +Brand#15 |LARGE ANODIZED NICKEL | 45| 4 +Brand#15 |LARGE ANODIZED STEEL | 9| 4 +Brand#15 |LARGE ANODIZED STEEL | 36| 4 +Brand#15 |LARGE ANODIZED STEEL | 49| 4 +Brand#15 |LARGE ANODIZED TIN | 3| 4 +Brand#15 |LARGE ANODIZED TIN | 9| 4 +Brand#15 |LARGE ANODIZED TIN | 19| 4 +Brand#15 |LARGE ANODIZED TIN | 45| 4 +Brand#15 |LARGE ANODIZED TIN | 49| 4 +Brand#15 |LARGE BRUSHED BRASS | 3| 4 +Brand#15 |LARGE BRUSHED COPPER | 23| 4 +Brand#15 |LARGE BRUSHED COPPER | 49| 4 +Brand#15 |LARGE BRUSHED NICKEL | 3| 4 +Brand#15 |LARGE BRUSHED NICKEL | 14| 4 +Brand#15 |LARGE BRUSHED NICKEL | 23| 4 +Brand#15 |LARGE BRUSHED NICKEL | 36| 4 +Brand#15 |LARGE BRUSHED STEEL | 3| 4 +Brand#15 |LARGE BRUSHED STEEL | 9| 4 +Brand#15 |LARGE BRUSHED STEEL | 36| 4 +Brand#15 |LARGE BRUSHED STEEL | 49| 4 +Brand#15 |LARGE BRUSHED TIN | 14| 4 +Brand#15 |LARGE BRUSHED TIN | 45| 4 +Brand#15 |LARGE BURNISHED BRASS | 49| 4 +Brand#15 |LARGE BURNISHED COPPER | 3| 4 +Brand#15 |LARGE BURNISHED COPPER | 14| 4 +Brand#15 |LARGE BURNISHED NICKEL | 14| 4 +Brand#15 |LARGE BURNISHED NICKEL | 23| 4 +Brand#15 |LARGE BURNISHED NICKEL | 45| 4 +Brand#15 |LARGE BURNISHED STEEL | 3| 4 +Brand#15 |LARGE BURNISHED TIN | 3| 4 +Brand#15 |LARGE BURNISHED TIN | 9| 4 +Brand#15 |LARGE BURNISHED TIN | 19| 4 +Brand#15 |LARGE BURNISHED TIN | 23| 4 +Brand#15 |LARGE BURNISHED TIN | 36| 4 +Brand#15 |LARGE BURNISHED TIN | 45| 4 +Brand#15 |LARGE PLATED BRASS | 3| 4 +Brand#15 |LARGE PLATED BRASS | 14| 4 +Brand#15 |LARGE PLATED BRASS | 19| 4 +Brand#15 |LARGE PLATED BRASS | 23| 4 +Brand#15 |LARGE PLATED BRASS | 49| 4 +Brand#15 |LARGE PLATED COPPER | 3| 4 +Brand#15 |LARGE PLATED COPPER | 14| 4 +Brand#15 |LARGE PLATED COPPER | 23| 4 +Brand#15 |LARGE PLATED NICKEL | 36| 4 +Brand#15 |LARGE PLATED STEEL | 3| 4 +Brand#15 |LARGE PLATED STEEL | 45| 4 +Brand#15 |LARGE PLATED STEEL | 49| 4 +Brand#15 |LARGE PLATED TIN | 9| 4 +Brand#15 |LARGE PLATED TIN | 19| 4 +Brand#15 |LARGE PLATED TIN | 36| 4 +Brand#15 |LARGE PLATED TIN | 45| 4 +Brand#15 |LARGE POLISHED BRASS | 3| 4 +Brand#15 |LARGE POLISHED BRASS | 9| 4 +Brand#15 |LARGE POLISHED BRASS | 14| 4 +Brand#15 |LARGE POLISHED COPPER | 9| 4 +Brand#15 |LARGE POLISHED COPPER | 14| 4 +Brand#15 |LARGE POLISHED COPPER | 19| 4 +Brand#15 |LARGE POLISHED COPPER | 45| 4 +Brand#15 |LARGE POLISHED NICKEL | 3| 4 +Brand#15 |LARGE POLISHED NICKEL | 14| 4 +Brand#15 |LARGE POLISHED NICKEL | 19| 4 +Brand#15 |LARGE POLISHED NICKEL | 23| 4 +Brand#15 |LARGE POLISHED NICKEL | 36| 4 +Brand#15 |LARGE POLISHED NICKEL | 49| 4 +Brand#15 |LARGE POLISHED STEEL | 3| 4 +Brand#15 |MEDIUM ANODIZED BRASS | 14| 4 +Brand#15 |MEDIUM ANODIZED BRASS | 45| 4 +Brand#15 |MEDIUM ANODIZED BRASS | 49| 4 +Brand#15 |MEDIUM ANODIZED COPPER | 3| 4 +Brand#15 |MEDIUM ANODIZED COPPER | 14| 4 +Brand#15 |MEDIUM ANODIZED COPPER | 23| 4 +Brand#15 |MEDIUM ANODIZED COPPER | 45| 4 +Brand#15 |MEDIUM ANODIZED COPPER | 49| 4 +Brand#15 |MEDIUM ANODIZED NICKEL | 14| 4 +Brand#15 |MEDIUM ANODIZED NICKEL | 19| 4 +Brand#15 |MEDIUM ANODIZED NICKEL | 23| 4 +Brand#15 |MEDIUM ANODIZED NICKEL | 49| 4 +Brand#15 |MEDIUM ANODIZED STEEL | 3| 4 +Brand#15 |MEDIUM ANODIZED STEEL | 14| 4 +Brand#15 |MEDIUM ANODIZED STEEL | 36| 4 +Brand#15 |MEDIUM ANODIZED TIN | 9| 4 +Brand#15 |MEDIUM ANODIZED TIN | 36| 4 +Brand#15 |MEDIUM ANODIZED TIN | 45| 4 +Brand#15 |MEDIUM BRUSHED BRASS | 9| 4 +Brand#15 |MEDIUM BRUSHED BRASS | 36| 4 +Brand#15 |MEDIUM BRUSHED COPPER | 19| 4 +Brand#15 |MEDIUM BRUSHED NICKEL | 36| 4 +Brand#15 |MEDIUM BRUSHED NICKEL | 45| 4 +Brand#15 |MEDIUM BRUSHED STEEL | 9| 4 +Brand#15 |MEDIUM BRUSHED STEEL | 14| 4 +Brand#15 |MEDIUM BRUSHED STEEL | 23| 4 +Brand#15 |MEDIUM BRUSHED TIN | 3| 4 +Brand#15 |MEDIUM BRUSHED TIN | 36| 4 +Brand#15 |MEDIUM BRUSHED TIN | 45| 4 +Brand#15 |MEDIUM BRUSHED TIN | 49| 4 +Brand#15 |MEDIUM BURNISHED BRASS | 3| 4 +Brand#15 |MEDIUM BURNISHED BRASS | 14| 4 +Brand#15 |MEDIUM BURNISHED BRASS | 19| 4 +Brand#15 |MEDIUM BURNISHED BRASS | 23| 4 +Brand#15 |MEDIUM BURNISHED BRASS | 49| 4 +Brand#15 |MEDIUM BURNISHED COPPER | 9| 4 +Brand#15 |MEDIUM BURNISHED COPPER | 19| 4 +Brand#15 |MEDIUM BURNISHED COPPER | 36| 4 +Brand#15 |MEDIUM BURNISHED NICKEL | 3| 4 +Brand#15 |MEDIUM BURNISHED NICKEL | 23| 4 +Brand#15 |MEDIUM BURNISHED STEEL | 9| 4 +Brand#15 |MEDIUM BURNISHED STEEL | 36| 4 +Brand#15 |MEDIUM BURNISHED STEEL | 45| 4 +Brand#15 |MEDIUM BURNISHED TIN | 3| 4 +Brand#15 |MEDIUM BURNISHED TIN | 19| 4 +Brand#15 |MEDIUM BURNISHED TIN | 23| 4 +Brand#15 |MEDIUM BURNISHED TIN | 36| 4 +Brand#15 |MEDIUM PLATED BRASS | 3| 4 +Brand#15 |MEDIUM PLATED BRASS | 9| 4 +Brand#15 |MEDIUM PLATED BRASS | 19| 4 +Brand#15 |MEDIUM PLATED BRASS | 23| 4 +Brand#15 |MEDIUM PLATED BRASS | 49| 4 +Brand#15 |MEDIUM PLATED COPPER | 9| 4 +Brand#15 |MEDIUM PLATED COPPER | 19| 4 +Brand#15 |MEDIUM PLATED COPPER | 36| 4 +Brand#15 |MEDIUM PLATED COPPER | 45| 4 +Brand#15 |MEDIUM PLATED COPPER | 49| 4 +Brand#15 |MEDIUM PLATED NICKEL | 3| 4 +Brand#15 |MEDIUM PLATED NICKEL | 9| 4 +Brand#15 |MEDIUM PLATED NICKEL | 14| 4 +Brand#15 |MEDIUM PLATED NICKEL | 19| 4 +Brand#15 |MEDIUM PLATED NICKEL | 36| 4 +Brand#15 |MEDIUM PLATED NICKEL | 45| 4 +Brand#15 |MEDIUM PLATED STEEL | 3| 4 +Brand#15 |MEDIUM PLATED STEEL | 14| 4 +Brand#15 |MEDIUM PLATED STEEL | 23| 4 +Brand#15 |MEDIUM PLATED STEEL | 36| 4 +Brand#15 |MEDIUM PLATED TIN | 14| 4 +Brand#15 |PROMO ANODIZED BRASS | 3| 4 +Brand#15 |PROMO ANODIZED BRASS | 9| 4 +Brand#15 |PROMO ANODIZED BRASS | 19| 4 +Brand#15 |PROMO ANODIZED BRASS | 49| 4 +Brand#15 |PROMO ANODIZED COPPER | 3| 4 +Brand#15 |PROMO ANODIZED COPPER | 19| 4 +Brand#15 |PROMO ANODIZED COPPER | 23| 4 +Brand#15 |PROMO ANODIZED COPPER | 49| 4 +Brand#15 |PROMO ANODIZED NICKEL | 19| 4 +Brand#15 |PROMO ANODIZED STEEL | 23| 4 +Brand#15 |PROMO ANODIZED STEEL | 45| 4 +Brand#15 |PROMO ANODIZED TIN | 23| 4 +Brand#15 |PROMO ANODIZED TIN | 36| 4 +Brand#15 |PROMO ANODIZED TIN | 45| 4 +Brand#15 |PROMO BRUSHED BRASS | 3| 4 +Brand#15 |PROMO BRUSHED BRASS | 23| 4 +Brand#15 |PROMO BRUSHED BRASS | 45| 4 +Brand#15 |PROMO BRUSHED COPPER | 14| 4 +Brand#15 |PROMO BRUSHED COPPER | 49| 4 +Brand#15 |PROMO BRUSHED NICKEL | 3| 4 +Brand#15 |PROMO BRUSHED NICKEL | 14| 4 +Brand#15 |PROMO BRUSHED NICKEL | 45| 4 +Brand#15 |PROMO BRUSHED STEEL | 3| 4 +Brand#15 |PROMO BRUSHED STEEL | 19| 4 +Brand#15 |PROMO BRUSHED TIN | 9| 4 +Brand#15 |PROMO BRUSHED TIN | 14| 4 +Brand#15 |PROMO BRUSHED TIN | 45| 4 +Brand#15 |PROMO BURNISHED BRASS | 3| 4 +Brand#15 |PROMO BURNISHED BRASS | 19| 4 +Brand#15 |PROMO BURNISHED BRASS | 45| 4 +Brand#15 |PROMO BURNISHED COPPER | 23| 4 +Brand#15 |PROMO BURNISHED COPPER | 49| 4 +Brand#15 |PROMO BURNISHED NICKEL | 45| 4 +Brand#15 |PROMO BURNISHED STEEL | 14| 4 +Brand#15 |PROMO BURNISHED STEEL | 45| 4 +Brand#15 |PROMO BURNISHED STEEL | 49| 4 +Brand#15 |PROMO BURNISHED TIN | 3| 4 +Brand#15 |PROMO BURNISHED TIN | 23| 4 +Brand#15 |PROMO PLATED BRASS | 3| 4 +Brand#15 |PROMO PLATED BRASS | 9| 4 +Brand#15 |PROMO PLATED BRASS | 45| 4 +Brand#15 |PROMO PLATED COPPER | 19| 4 +Brand#15 |PROMO PLATED COPPER | 49| 4 +Brand#15 |PROMO PLATED NICKEL | 3| 4 +Brand#15 |PROMO PLATED NICKEL | 49| 4 +Brand#15 |PROMO PLATED STEEL | 9| 4 +Brand#15 |PROMO PLATED STEEL | 19| 4 +Brand#15 |PROMO PLATED STEEL | 45| 4 +Brand#15 |PROMO PLATED STEEL | 49| 4 +Brand#15 |PROMO PLATED TIN | 14| 4 +Brand#15 |PROMO PLATED TIN | 36| 4 +Brand#15 |PROMO PLATED TIN | 45| 4 +Brand#15 |PROMO PLATED TIN | 49| 4 +Brand#15 |PROMO POLISHED BRASS | 19| 4 +Brand#15 |PROMO POLISHED BRASS | 23| 4 +Brand#15 |PROMO POLISHED BRASS | 36| 4 +Brand#15 |PROMO POLISHED BRASS | 45| 4 +Brand#15 |PROMO POLISHED BRASS | 49| 4 +Brand#15 |PROMO POLISHED COPPER | 23| 4 +Brand#15 |PROMO POLISHED NICKEL | 3| 4 +Brand#15 |PROMO POLISHED NICKEL | 9| 4 +Brand#15 |PROMO POLISHED NICKEL | 14| 4 +Brand#15 |PROMO POLISHED NICKEL | 45| 4 +Brand#15 |PROMO POLISHED STEEL | 23| 4 +Brand#15 |PROMO POLISHED STEEL | 36| 4 +Brand#15 |PROMO POLISHED STEEL | 45| 4 +Brand#15 |PROMO POLISHED TIN | 14| 4 +Brand#15 |PROMO POLISHED TIN | 19| 4 +Brand#15 |PROMO POLISHED TIN | 36| 4 +Brand#15 |SMALL ANODIZED BRASS | 3| 4 +Brand#15 |SMALL ANODIZED BRASS | 36| 4 +Brand#15 |SMALL ANODIZED COPPER | 3| 4 +Brand#15 |SMALL ANODIZED COPPER | 9| 4 +Brand#15 |SMALL ANODIZED COPPER | 14| 4 +Brand#15 |SMALL ANODIZED COPPER | 19| 4 +Brand#15 |SMALL ANODIZED COPPER | 36| 4 +Brand#15 |SMALL ANODIZED COPPER | 49| 4 +Brand#15 |SMALL ANODIZED NICKEL | 45| 4 +Brand#15 |SMALL ANODIZED NICKEL | 49| 4 +Brand#15 |SMALL ANODIZED STEEL | 19| 4 +Brand#15 |SMALL ANODIZED STEEL | 36| 4 +Brand#15 |SMALL ANODIZED TIN | 3| 4 +Brand#15 |SMALL ANODIZED TIN | 9| 4 +Brand#15 |SMALL ANODIZED TIN | 49| 4 +Brand#15 |SMALL BRUSHED COPPER | 3| 4 +Brand#15 |SMALL BRUSHED COPPER | 36| 4 +Brand#15 |SMALL BRUSHED COPPER | 49| 4 +Brand#15 |SMALL BRUSHED NICKEL | 3| 4 +Brand#15 |SMALL BRUSHED NICKEL | 45| 4 +Brand#15 |SMALL BRUSHED STEEL | 3| 4 +Brand#15 |SMALL BRUSHED STEEL | 45| 4 +Brand#15 |SMALL BRUSHED STEEL | 49| 4 +Brand#15 |SMALL BRUSHED TIN | 3| 4 +Brand#15 |SMALL BRUSHED TIN | 14| 4 +Brand#15 |SMALL BRUSHED TIN | 49| 4 +Brand#15 |SMALL BURNISHED BRASS | 36| 4 +Brand#15 |SMALL BURNISHED BRASS | 45| 4 +Brand#15 |SMALL BURNISHED BRASS | 49| 4 +Brand#15 |SMALL BURNISHED COPPER | 23| 4 +Brand#15 |SMALL BURNISHED COPPER | 36| 4 +Brand#15 |SMALL BURNISHED COPPER | 45| 4 +Brand#15 |SMALL BURNISHED NICKEL | 14| 4 +Brand#15 |SMALL BURNISHED NICKEL | 23| 4 +Brand#15 |SMALL BURNISHED NICKEL | 49| 4 +Brand#15 |SMALL BURNISHED STEEL | 3| 4 +Brand#15 |SMALL BURNISHED STEEL | 14| 4 +Brand#15 |SMALL BURNISHED STEEL | 23| 4 +Brand#15 |SMALL BURNISHED STEEL | 36| 4 +Brand#15 |SMALL BURNISHED STEEL | 45| 4 +Brand#15 |SMALL BURNISHED STEEL | 49| 4 +Brand#15 |SMALL BURNISHED TIN | 36| 4 +Brand#15 |SMALL BURNISHED TIN | 49| 4 +Brand#15 |SMALL PLATED BRASS | 3| 4 +Brand#15 |SMALL PLATED BRASS | 9| 4 +Brand#15 |SMALL PLATED BRASS | 14| 4 +Brand#15 |SMALL PLATED NICKEL | 14| 4 +Brand#15 |SMALL PLATED NICKEL | 36| 4 +Brand#15 |SMALL PLATED NICKEL | 49| 4 +Brand#15 |SMALL PLATED TIN | 3| 4 +Brand#15 |SMALL PLATED TIN | 23| 4 +Brand#15 |SMALL PLATED TIN | 49| 4 +Brand#15 |SMALL POLISHED BRASS | 14| 4 +Brand#15 |SMALL POLISHED BRASS | 36| 4 +Brand#15 |SMALL POLISHED COPPER | 14| 4 +Brand#15 |SMALL POLISHED COPPER | 19| 4 +Brand#15 |SMALL POLISHED COPPER | 23| 4 +Brand#15 |SMALL POLISHED NICKEL | 3| 4 +Brand#15 |SMALL POLISHED NICKEL | 9| 4 +Brand#15 |SMALL POLISHED NICKEL | 36| 4 +Brand#15 |SMALL POLISHED NICKEL | 49| 4 +Brand#15 |SMALL POLISHED STEEL | 14| 4 +Brand#15 |SMALL POLISHED STEEL | 19| 4 +Brand#15 |SMALL POLISHED TIN | 14| 4 +Brand#15 |SMALL POLISHED TIN | 23| 4 +Brand#15 |STANDARD ANODIZED BRASS | 3| 4 +Brand#15 |STANDARD ANODIZED BRASS | 36| 4 +Brand#15 |STANDARD ANODIZED BRASS | 49| 4 +Brand#15 |STANDARD ANODIZED COPPER | 9| 4 +Brand#15 |STANDARD ANODIZED COPPER | 19| 4 +Brand#15 |STANDARD ANODIZED COPPER | 49| 4 +Brand#15 |STANDARD ANODIZED NICKEL | 14| 4 +Brand#15 |STANDARD ANODIZED NICKEL | 19| 4 +Brand#15 |STANDARD ANODIZED NICKEL | 49| 4 +Brand#15 |STANDARD ANODIZED STEEL | 23| 4 +Brand#15 |STANDARD ANODIZED STEEL | 49| 4 +Brand#15 |STANDARD ANODIZED TIN | 9| 4 +Brand#15 |STANDARD ANODIZED TIN | 14| 4 +Brand#15 |STANDARD ANODIZED TIN | 23| 4 +Brand#15 |STANDARD ANODIZED TIN | 49| 4 +Brand#15 |STANDARD BRUSHED BRASS | 9| 4 +Brand#15 |STANDARD BRUSHED BRASS | 14| 4 +Brand#15 |STANDARD BRUSHED BRASS | 23| 4 +Brand#15 |STANDARD BRUSHED COPPER | 3| 4 +Brand#15 |STANDARD BRUSHED COPPER | 19| 4 +Brand#15 |STANDARD BRUSHED COPPER | 36| 4 +Brand#15 |STANDARD BRUSHED NICKEL | 36| 4 +Brand#15 |STANDARD BRUSHED NICKEL | 45| 4 +Brand#15 |STANDARD BRUSHED NICKEL | 49| 4 +Brand#15 |STANDARD BRUSHED STEEL | 3| 4 +Brand#15 |STANDARD BRUSHED STEEL | 23| 4 +Brand#15 |STANDARD BRUSHED STEEL | 36| 4 +Brand#15 |STANDARD BRUSHED STEEL | 45| 4 +Brand#15 |STANDARD BRUSHED TIN | 3| 4 +Brand#15 |STANDARD BRUSHED TIN | 9| 4 +Brand#15 |STANDARD BRUSHED TIN | 14| 4 +Brand#15 |STANDARD BRUSHED TIN | 19| 4 +Brand#15 |STANDARD BRUSHED TIN | 36| 4 +Brand#15 |STANDARD BRUSHED TIN | 49| 4 +Brand#15 |STANDARD BURNISHED BRASS | 14| 4 +Brand#15 |STANDARD BURNISHED BRASS | 36| 4 +Brand#15 |STANDARD BURNISHED COPPER| 3| 4 +Brand#15 |STANDARD BURNISHED COPPER| 9| 4 +Brand#15 |STANDARD BURNISHED COPPER| 23| 4 +Brand#15 |STANDARD BURNISHED NICKEL| 3| 4 +Brand#15 |STANDARD BURNISHED NICKEL| 19| 4 +Brand#15 |STANDARD BURNISHED STEEL | 3| 4 +Brand#15 |STANDARD BURNISHED STEEL | 9| 4 +Brand#15 |STANDARD BURNISHED STEEL | 14| 4 +Brand#15 |STANDARD BURNISHED STEEL | 36| 4 +Brand#15 |STANDARD BURNISHED STEEL | 49| 4 +Brand#15 |STANDARD BURNISHED TIN | 19| 4 +Brand#15 |STANDARD BURNISHED TIN | 23| 4 +Brand#15 |STANDARD BURNISHED TIN | 36| 4 +Brand#15 |STANDARD PLATED BRASS | 19| 4 +Brand#15 |STANDARD PLATED BRASS | 49| 4 +Brand#15 |STANDARD PLATED COPPER | 3| 4 +Brand#15 |STANDARD PLATED COPPER | 19| 4 +Brand#15 |STANDARD PLATED COPPER | 23| 4 +Brand#15 |STANDARD PLATED NICKEL | 19| 4 +Brand#15 |STANDARD PLATED NICKEL | 36| 4 +Brand#15 |STANDARD PLATED NICKEL | 45| 4 +Brand#15 |STANDARD PLATED STEEL | 9| 4 +Brand#15 |STANDARD PLATED STEEL | 45| 4 +Brand#15 |STANDARD PLATED TIN | 19| 4 +Brand#15 |STANDARD PLATED TIN | 49| 4 +Brand#15 |STANDARD POLISHED BRASS | 36| 4 +Brand#15 |STANDARD POLISHED BRASS | 49| 4 +Brand#15 |STANDARD POLISHED COPPER | 14| 4 +Brand#15 |STANDARD POLISHED COPPER | 19| 4 +Brand#15 |STANDARD POLISHED COPPER | 45| 4 +Brand#15 |STANDARD POLISHED COPPER | 49| 4 +Brand#15 |STANDARD POLISHED NICKEL | 9| 4 +Brand#15 |STANDARD POLISHED NICKEL | 19| 4 +Brand#15 |STANDARD POLISHED NICKEL | 49| 4 +Brand#15 |STANDARD POLISHED STEEL | 9| 4 +Brand#15 |STANDARD POLISHED STEEL | 14| 4 +Brand#15 |STANDARD POLISHED STEEL | 19| 4 +Brand#15 |STANDARD POLISHED STEEL | 36| 4 +Brand#15 |STANDARD POLISHED STEEL | 45| 4 +Brand#15 |STANDARD POLISHED TIN | 9| 4 +Brand#15 |STANDARD POLISHED TIN | 19| 4 +Brand#15 |STANDARD POLISHED TIN | 45| 4 +Brand#21 |ECONOMY ANODIZED BRASS | 3| 4 +Brand#21 |ECONOMY ANODIZED BRASS | 9| 4 +Brand#21 |ECONOMY ANODIZED BRASS | 19| 4 +Brand#21 |ECONOMY ANODIZED COPPER | 9| 4 +Brand#21 |ECONOMY ANODIZED COPPER | 23| 4 +Brand#21 |ECONOMY ANODIZED NICKEL | 3| 4 +Brand#21 |ECONOMY ANODIZED NICKEL | 19| 4 +Brand#21 |ECONOMY ANODIZED NICKEL | 49| 4 +Brand#21 |ECONOMY ANODIZED STEEL | 9| 4 +Brand#21 |ECONOMY ANODIZED STEEL | 14| 4 +Brand#21 |ECONOMY ANODIZED STEEL | 23| 4 +Brand#21 |ECONOMY ANODIZED TIN | 14| 4 +Brand#21 |ECONOMY ANODIZED TIN | 19| 4 +Brand#21 |ECONOMY ANODIZED TIN | 23| 4 +Brand#21 |ECONOMY ANODIZED TIN | 45| 4 +Brand#21 |ECONOMY ANODIZED TIN | 49| 4 +Brand#21 |ECONOMY BRUSHED BRASS | 9| 4 +Brand#21 |ECONOMY BRUSHED BRASS | 23| 4 +Brand#21 |ECONOMY BRUSHED BRASS | 45| 4 +Brand#21 |ECONOMY BRUSHED BRASS | 49| 4 +Brand#21 |ECONOMY BRUSHED COPPER | 3| 4 +Brand#21 |ECONOMY BRUSHED COPPER | 23| 4 +Brand#21 |ECONOMY BRUSHED COPPER | 36| 4 +Brand#21 |ECONOMY BRUSHED COPPER | 49| 4 +Brand#21 |ECONOMY BRUSHED NICKEL | 3| 4 +Brand#21 |ECONOMY BRUSHED NICKEL | 45| 4 +Brand#21 |ECONOMY BRUSHED NICKEL | 49| 4 +Brand#21 |ECONOMY BRUSHED STEEL | 9| 4 +Brand#21 |ECONOMY BRUSHED STEEL | 14| 4 +Brand#21 |ECONOMY BRUSHED STEEL | 19| 4 +Brand#21 |ECONOMY BRUSHED STEEL | 23| 4 +Brand#21 |ECONOMY BRUSHED STEEL | 36| 4 +Brand#21 |ECONOMY BRUSHED TIN | 3| 4 +Brand#21 |ECONOMY BRUSHED TIN | 45| 4 +Brand#21 |ECONOMY BRUSHED TIN | 49| 4 +Brand#21 |ECONOMY BURNISHED BRASS | 23| 4 +Brand#21 |ECONOMY BURNISHED COPPER | 9| 4 +Brand#21 |ECONOMY BURNISHED COPPER | 14| 4 +Brand#21 |ECONOMY BURNISHED COPPER | 36| 4 +Brand#21 |ECONOMY BURNISHED NICKEL | 14| 4 +Brand#21 |ECONOMY BURNISHED NICKEL | 19| 4 +Brand#21 |ECONOMY BURNISHED NICKEL | 23| 4 +Brand#21 |ECONOMY BURNISHED NICKEL | 36| 4 +Brand#21 |ECONOMY BURNISHED STEEL | 3| 4 +Brand#21 |ECONOMY BURNISHED STEEL | 19| 4 +Brand#21 |ECONOMY BURNISHED STEEL | 49| 4 +Brand#21 |ECONOMY BURNISHED TIN | 23| 4 +Brand#21 |ECONOMY BURNISHED TIN | 36| 4 +Brand#21 |ECONOMY PLATED BRASS | 14| 4 +Brand#21 |ECONOMY PLATED BRASS | 19| 4 +Brand#21 |ECONOMY PLATED BRASS | 36| 4 +Brand#21 |ECONOMY PLATED BRASS | 45| 4 +Brand#21 |ECONOMY PLATED COPPER | 9| 4 +Brand#21 |ECONOMY PLATED COPPER | 19| 4 +Brand#21 |ECONOMY PLATED COPPER | 23| 4 +Brand#21 |ECONOMY PLATED NICKEL | 9| 4 +Brand#21 |ECONOMY PLATED NICKEL | 14| 4 +Brand#21 |ECONOMY PLATED NICKEL | 19| 4 +Brand#21 |ECONOMY PLATED STEEL | 36| 4 +Brand#21 |ECONOMY PLATED STEEL | 49| 4 +Brand#21 |ECONOMY PLATED TIN | 9| 4 +Brand#21 |ECONOMY PLATED TIN | 14| 4 +Brand#21 |ECONOMY PLATED TIN | 45| 4 +Brand#21 |ECONOMY PLATED TIN | 49| 4 +Brand#21 |ECONOMY POLISHED BRASS | 3| 4 +Brand#21 |ECONOMY POLISHED BRASS | 19| 4 +Brand#21 |ECONOMY POLISHED BRASS | 36| 4 +Brand#21 |ECONOMY POLISHED BRASS | 45| 4 +Brand#21 |ECONOMY POLISHED COPPER | 45| 4 +Brand#21 |ECONOMY POLISHED COPPER | 49| 4 +Brand#21 |ECONOMY POLISHED NICKEL | 9| 4 +Brand#21 |ECONOMY POLISHED NICKEL | 23| 4 +Brand#21 |ECONOMY POLISHED STEEL | 3| 4 +Brand#21 |ECONOMY POLISHED STEEL | 9| 4 +Brand#21 |ECONOMY POLISHED TIN | 14| 4 +Brand#21 |ECONOMY POLISHED TIN | 36| 4 +Brand#21 |LARGE ANODIZED BRASS | 36| 4 +Brand#21 |LARGE ANODIZED BRASS | 49| 4 +Brand#21 |LARGE ANODIZED COPPER | 9| 4 +Brand#21 |LARGE ANODIZED COPPER | 14| 4 +Brand#21 |LARGE ANODIZED COPPER | 23| 4 +Brand#21 |LARGE ANODIZED COPPER | 45| 4 +Brand#21 |LARGE ANODIZED COPPER | 49| 4 +Brand#21 |LARGE ANODIZED NICKEL | 23| 4 +Brand#21 |LARGE ANODIZED STEEL | 19| 4 +Brand#21 |LARGE ANODIZED TIN | 9| 4 +Brand#21 |LARGE ANODIZED TIN | 23| 4 +Brand#21 |LARGE ANODIZED TIN | 36| 4 +Brand#21 |LARGE BRUSHED BRASS | 3| 4 +Brand#21 |LARGE BRUSHED BRASS | 14| 4 +Brand#21 |LARGE BRUSHED BRASS | 36| 4 +Brand#21 |LARGE BRUSHED COPPER | 14| 4 +Brand#21 |LARGE BRUSHED COPPER | 45| 4 +Brand#21 |LARGE BRUSHED NICKEL | 9| 4 +Brand#21 |LARGE BRUSHED NICKEL | 19| 4 +Brand#21 |LARGE BRUSHED NICKEL | 49| 4 +Brand#21 |LARGE BRUSHED STEEL | 3| 4 +Brand#21 |LARGE BRUSHED STEEL | 19| 4 +Brand#21 |LARGE BRUSHED STEEL | 36| 4 +Brand#21 |LARGE BURNISHED BRASS | 3| 4 +Brand#21 |LARGE BURNISHED BRASS | 9| 4 +Brand#21 |LARGE BURNISHED BRASS | 23| 4 +Brand#21 |LARGE BURNISHED BRASS | 49| 4 +Brand#21 |LARGE BURNISHED COPPER | 36| 4 +Brand#21 |LARGE BURNISHED COPPER | 45| 4 +Brand#21 |LARGE BURNISHED COPPER | 49| 4 +Brand#21 |LARGE BURNISHED NICKEL | 19| 4 +Brand#21 |LARGE BURNISHED NICKEL | 23| 4 +Brand#21 |LARGE BURNISHED NICKEL | 45| 4 +Brand#21 |LARGE BURNISHED NICKEL | 49| 4 +Brand#21 |LARGE BURNISHED STEEL | 9| 4 +Brand#21 |LARGE BURNISHED STEEL | 23| 4 +Brand#21 |LARGE BURNISHED TIN | 19| 4 +Brand#21 |LARGE BURNISHED TIN | 36| 4 +Brand#21 |LARGE PLATED BRASS | 3| 4 +Brand#21 |LARGE PLATED BRASS | 49| 4 +Brand#21 |LARGE PLATED NICKEL | 3| 4 +Brand#21 |LARGE PLATED NICKEL | 14| 4 +Brand#21 |LARGE PLATED NICKEL | 19| 4 +Brand#21 |LARGE PLATED NICKEL | 36| 4 +Brand#21 |LARGE PLATED NICKEL | 49| 4 +Brand#21 |LARGE PLATED STEEL | 9| 4 +Brand#21 |LARGE PLATED STEEL | 23| 4 +Brand#21 |LARGE PLATED TIN | 19| 4 +Brand#21 |LARGE POLISHED BRASS | 3| 4 +Brand#21 |LARGE POLISHED BRASS | 9| 4 +Brand#21 |LARGE POLISHED BRASS | 45| 4 +Brand#21 |LARGE POLISHED COPPER | 3| 4 +Brand#21 |LARGE POLISHED COPPER | 36| 4 +Brand#21 |LARGE POLISHED COPPER | 45| 4 +Brand#21 |LARGE POLISHED NICKEL | 9| 4 +Brand#21 |LARGE POLISHED NICKEL | 14| 4 +Brand#21 |LARGE POLISHED NICKEL | 19| 4 +Brand#21 |LARGE POLISHED STEEL | 14| 4 +Brand#21 |LARGE POLISHED STEEL | 19| 4 +Brand#21 |LARGE POLISHED STEEL | 36| 4 +Brand#21 |LARGE POLISHED STEEL | 49| 4 +Brand#21 |LARGE POLISHED TIN | 9| 4 +Brand#21 |LARGE POLISHED TIN | 14| 4 +Brand#21 |LARGE POLISHED TIN | 23| 4 +Brand#21 |LARGE POLISHED TIN | 49| 4 +Brand#21 |MEDIUM ANODIZED BRASS | 19| 4 +Brand#21 |MEDIUM ANODIZED BRASS | 45| 4 +Brand#21 |MEDIUM ANODIZED COPPER | 36| 4 +Brand#21 |MEDIUM ANODIZED COPPER | 49| 4 +Brand#21 |MEDIUM ANODIZED NICKEL | 9| 4 +Brand#21 |MEDIUM ANODIZED NICKEL | 19| 4 +Brand#21 |MEDIUM ANODIZED NICKEL | 49| 4 +Brand#21 |MEDIUM ANODIZED STEEL | 3| 4 +Brand#21 |MEDIUM ANODIZED STEEL | 9| 4 +Brand#21 |MEDIUM ANODIZED STEEL | 19| 4 +Brand#21 |MEDIUM ANODIZED STEEL | 23| 4 +Brand#21 |MEDIUM ANODIZED STEEL | 49| 4 +Brand#21 |MEDIUM ANODIZED TIN | 3| 4 +Brand#21 |MEDIUM ANODIZED TIN | 19| 4 +Brand#21 |MEDIUM ANODIZED TIN | 36| 4 +Brand#21 |MEDIUM BRUSHED BRASS | 36| 4 +Brand#21 |MEDIUM BRUSHED COPPER | 9| 4 +Brand#21 |MEDIUM BRUSHED COPPER | 36| 4 +Brand#21 |MEDIUM BRUSHED COPPER | 49| 4 +Brand#21 |MEDIUM BRUSHED NICKEL | 3| 4 +Brand#21 |MEDIUM BRUSHED NICKEL | 9| 4 +Brand#21 |MEDIUM BRUSHED NICKEL | 23| 4 +Brand#21 |MEDIUM BRUSHED NICKEL | 36| 4 +Brand#21 |MEDIUM BRUSHED NICKEL | 45| 4 +Brand#21 |MEDIUM BRUSHED STEEL | 3| 4 +Brand#21 |MEDIUM BRUSHED STEEL | 9| 4 +Brand#21 |MEDIUM BRUSHED STEEL | 14| 4 +Brand#21 |MEDIUM BRUSHED STEEL | 36| 4 +Brand#21 |MEDIUM BRUSHED STEEL | 49| 4 +Brand#21 |MEDIUM BRUSHED TIN | 3| 4 +Brand#21 |MEDIUM BRUSHED TIN | 14| 4 +Brand#21 |MEDIUM BRUSHED TIN | 49| 4 +Brand#21 |MEDIUM BURNISHED BRASS | 23| 4 +Brand#21 |MEDIUM BURNISHED BRASS | 45| 4 +Brand#21 |MEDIUM BURNISHED COPPER | 3| 4 +Brand#21 |MEDIUM BURNISHED COPPER | 9| 4 +Brand#21 |MEDIUM BURNISHED COPPER | 14| 4 +Brand#21 |MEDIUM BURNISHED COPPER | 45| 4 +Brand#21 |MEDIUM BURNISHED NICKEL | 3| 4 +Brand#21 |MEDIUM BURNISHED NICKEL | 19| 4 +Brand#21 |MEDIUM BURNISHED NICKEL | 45| 4 +Brand#21 |MEDIUM BURNISHED NICKEL | 49| 4 +Brand#21 |MEDIUM BURNISHED STEEL | 49| 4 +Brand#21 |MEDIUM BURNISHED TIN | 3| 4 +Brand#21 |MEDIUM BURNISHED TIN | 19| 4 +Brand#21 |MEDIUM BURNISHED TIN | 23| 4 +Brand#21 |MEDIUM BURNISHED TIN | 36| 4 +Brand#21 |MEDIUM PLATED BRASS | 3| 4 +Brand#21 |MEDIUM PLATED BRASS | 19| 4 +Brand#21 |MEDIUM PLATED BRASS | 23| 4 +Brand#21 |MEDIUM PLATED BRASS | 49| 4 +Brand#21 |MEDIUM PLATED COPPER | 3| 4 +Brand#21 |MEDIUM PLATED COPPER | 19| 4 +Brand#21 |MEDIUM PLATED COPPER | 36| 4 +Brand#21 |MEDIUM PLATED COPPER | 45| 4 +Brand#21 |MEDIUM PLATED NICKEL | 3| 4 +Brand#21 |MEDIUM PLATED NICKEL | 9| 4 +Brand#21 |MEDIUM PLATED NICKEL | 14| 4 +Brand#21 |MEDIUM PLATED NICKEL | 45| 4 +Brand#21 |MEDIUM PLATED NICKEL | 49| 4 +Brand#21 |MEDIUM PLATED TIN | 19| 4 +Brand#21 |MEDIUM PLATED TIN | 45| 4 +Brand#21 |MEDIUM PLATED TIN | 49| 4 +Brand#21 |PROMO ANODIZED BRASS | 3| 4 +Brand#21 |PROMO ANODIZED BRASS | 23| 4 +Brand#21 |PROMO ANODIZED BRASS | 45| 4 +Brand#21 |PROMO ANODIZED BRASS | 49| 4 +Brand#21 |PROMO ANODIZED COPPER | 3| 4 +Brand#21 |PROMO ANODIZED COPPER | 19| 4 +Brand#21 |PROMO ANODIZED COPPER | 49| 4 +Brand#21 |PROMO ANODIZED NICKEL | 36| 4 +Brand#21 |PROMO ANODIZED NICKEL | 45| 4 +Brand#21 |PROMO ANODIZED STEEL | 14| 4 +Brand#21 |PROMO ANODIZED STEEL | 23| 4 +Brand#21 |PROMO ANODIZED TIN | 3| 4 +Brand#21 |PROMO ANODIZED TIN | 9| 4 +Brand#21 |PROMO ANODIZED TIN | 36| 4 +Brand#21 |PROMO ANODIZED TIN | 49| 4 +Brand#21 |PROMO BRUSHED BRASS | 9| 4 +Brand#21 |PROMO BRUSHED BRASS | 14| 4 +Brand#21 |PROMO BRUSHED BRASS | 36| 4 +Brand#21 |PROMO BRUSHED COPPER | 9| 4 +Brand#21 |PROMO BRUSHED COPPER | 14| 4 +Brand#21 |PROMO BRUSHED COPPER | 19| 4 +Brand#21 |PROMO BRUSHED COPPER | 23| 4 +Brand#21 |PROMO BRUSHED NICKEL | 3| 4 +Brand#21 |PROMO BRUSHED NICKEL | 14| 4 +Brand#21 |PROMO BRUSHED NICKEL | 23| 4 +Brand#21 |PROMO BRUSHED STEEL | 9| 4 +Brand#21 |PROMO BRUSHED STEEL | 49| 4 +Brand#21 |PROMO BRUSHED TIN | 49| 4 +Brand#21 |PROMO BURNISHED BRASS | 3| 4 +Brand#21 |PROMO BURNISHED BRASS | 14| 4 +Brand#21 |PROMO BURNISHED BRASS | 36| 4 +Brand#21 |PROMO BURNISHED COPPER | 14| 4 +Brand#21 |PROMO BURNISHED COPPER | 19| 4 +Brand#21 |PROMO BURNISHED COPPER | 23| 4 +Brand#21 |PROMO BURNISHED COPPER | 36| 4 +Brand#21 |PROMO BURNISHED COPPER | 45| 4 +Brand#21 |PROMO BURNISHED NICKEL | 9| 4 +Brand#21 |PROMO BURNISHED NICKEL | 14| 4 +Brand#21 |PROMO BURNISHED NICKEL | 45| 4 +Brand#21 |PROMO BURNISHED NICKEL | 49| 4 +Brand#21 |PROMO BURNISHED STEEL | 3| 4 +Brand#21 |PROMO BURNISHED STEEL | 19| 4 +Brand#21 |PROMO BURNISHED TIN | 3| 4 +Brand#21 |PROMO BURNISHED TIN | 9| 4 +Brand#21 |PROMO BURNISHED TIN | 14| 4 +Brand#21 |PROMO BURNISHED TIN | 19| 4 +Brand#21 |PROMO BURNISHED TIN | 23| 4 +Brand#21 |PROMO PLATED BRASS | 9| 4 +Brand#21 |PROMO PLATED BRASS | 45| 4 +Brand#21 |PROMO PLATED COPPER | 36| 4 +Brand#21 |PROMO PLATED COPPER | 45| 4 +Brand#21 |PROMO PLATED NICKEL | 9| 4 +Brand#21 |PROMO PLATED NICKEL | 36| 4 +Brand#21 |PROMO PLATED STEEL | 19| 4 +Brand#21 |PROMO PLATED STEEL | 45| 4 +Brand#21 |PROMO PLATED TIN | 9| 4 +Brand#21 |PROMO PLATED TIN | 19| 4 +Brand#21 |PROMO PLATED TIN | 49| 4 +Brand#21 |PROMO POLISHED BRASS | 36| 4 +Brand#21 |PROMO POLISHED BRASS | 49| 4 +Brand#21 |PROMO POLISHED COPPER | 23| 4 +Brand#21 |PROMO POLISHED COPPER | 49| 4 +Brand#21 |PROMO POLISHED NICKEL | 3| 4 +Brand#21 |PROMO POLISHED NICKEL | 9| 4 +Brand#21 |PROMO POLISHED NICKEL | 19| 4 +Brand#21 |PROMO POLISHED NICKEL | 49| 4 +Brand#21 |PROMO POLISHED TIN | 3| 4 +Brand#21 |PROMO POLISHED TIN | 23| 4 +Brand#21 |PROMO POLISHED TIN | 36| 4 +Brand#21 |SMALL ANODIZED BRASS | 9| 4 +Brand#21 |SMALL ANODIZED BRASS | 14| 4 +Brand#21 |SMALL ANODIZED BRASS | 36| 4 +Brand#21 |SMALL ANODIZED BRASS | 49| 4 +Brand#21 |SMALL ANODIZED COPPER | 3| 4 +Brand#21 |SMALL ANODIZED COPPER | 14| 4 +Brand#21 |SMALL ANODIZED COPPER | 23| 4 +Brand#21 |SMALL ANODIZED COPPER | 36| 4 +Brand#21 |SMALL ANODIZED STEEL | 9| 4 +Brand#21 |SMALL ANODIZED STEEL | 19| 4 +Brand#21 |SMALL ANODIZED TIN | 3| 4 +Brand#21 |SMALL ANODIZED TIN | 45| 4 +Brand#21 |SMALL BRUSHED BRASS | 3| 4 +Brand#21 |SMALL BRUSHED BRASS | 9| 4 +Brand#21 |SMALL BRUSHED BRASS | 23| 4 +Brand#21 |SMALL BRUSHED BRASS | 49| 4 +Brand#21 |SMALL BRUSHED COPPER | 19| 4 +Brand#21 |SMALL BRUSHED COPPER | 23| 4 +Brand#21 |SMALL BRUSHED COPPER | 49| 4 +Brand#21 |SMALL BRUSHED NICKEL | 3| 4 +Brand#21 |SMALL BRUSHED NICKEL | 49| 4 +Brand#21 |SMALL BRUSHED STEEL | 19| 4 +Brand#21 |SMALL BRUSHED STEEL | 23| 4 +Brand#21 |SMALL BRUSHED STEEL | 45| 4 +Brand#21 |SMALL BRUSHED STEEL | 49| 4 +Brand#21 |SMALL BRUSHED TIN | 36| 4 +Brand#21 |SMALL BRUSHED TIN | 49| 4 +Brand#21 |SMALL BURNISHED BRASS | 3| 4 +Brand#21 |SMALL BURNISHED BRASS | 9| 4 +Brand#21 |SMALL BURNISHED BRASS | 19| 4 +Brand#21 |SMALL BURNISHED BRASS | 23| 4 +Brand#21 |SMALL BURNISHED BRASS | 45| 4 +Brand#21 |SMALL BURNISHED COPPER | 9| 4 +Brand#21 |SMALL BURNISHED COPPER | 23| 4 +Brand#21 |SMALL BURNISHED NICKEL | 3| 4 +Brand#21 |SMALL BURNISHED NICKEL | 19| 4 +Brand#21 |SMALL BURNISHED NICKEL | 23| 4 +Brand#21 |SMALL BURNISHED STEEL | 3| 4 +Brand#21 |SMALL BURNISHED STEEL | 14| 4 +Brand#21 |SMALL BURNISHED STEEL | 19| 4 +Brand#21 |SMALL BURNISHED STEEL | 36| 4 +Brand#21 |SMALL BURNISHED STEEL | 45| 4 +Brand#21 |SMALL BURNISHED TIN | 14| 4 +Brand#21 |SMALL BURNISHED TIN | 19| 4 +Brand#21 |SMALL BURNISHED TIN | 36| 4 +Brand#21 |SMALL BURNISHED TIN | 45| 4 +Brand#21 |SMALL BURNISHED TIN | 49| 4 +Brand#21 |SMALL PLATED BRASS | 19| 4 +Brand#21 |SMALL PLATED BRASS | 45| 4 +Brand#21 |SMALL PLATED BRASS | 49| 4 +Brand#21 |SMALL PLATED COPPER | 19| 4 +Brand#21 |SMALL PLATED COPPER | 49| 4 +Brand#21 |SMALL PLATED NICKEL | 19| 4 +Brand#21 |SMALL PLATED NICKEL | 49| 4 +Brand#21 |SMALL PLATED STEEL | 14| 4 +Brand#21 |SMALL PLATED STEEL | 36| 4 +Brand#21 |SMALL PLATED TIN | 3| 4 +Brand#21 |SMALL PLATED TIN | 9| 4 +Brand#21 |SMALL PLATED TIN | 14| 4 +Brand#21 |SMALL PLATED TIN | 23| 4 +Brand#21 |SMALL POLISHED BRASS | 3| 4 +Brand#21 |SMALL POLISHED BRASS | 9| 4 +Brand#21 |SMALL POLISHED BRASS | 23| 4 +Brand#21 |SMALL POLISHED BRASS | 45| 4 +Brand#21 |SMALL POLISHED COPPER | 3| 4 +Brand#21 |SMALL POLISHED COPPER | 9| 4 +Brand#21 |SMALL POLISHED COPPER | 19| 4 +Brand#21 |SMALL POLISHED COPPER | 45| 4 +Brand#21 |SMALL POLISHED NICKEL | 3| 4 +Brand#21 |SMALL POLISHED NICKEL | 14| 4 +Brand#21 |SMALL POLISHED NICKEL | 45| 4 +Brand#21 |SMALL POLISHED STEEL | 14| 4 +Brand#21 |SMALL POLISHED STEEL | 19| 4 +Brand#21 |SMALL POLISHED STEEL | 49| 4 +Brand#21 |SMALL POLISHED TIN | 3| 4 +Brand#21 |SMALL POLISHED TIN | 9| 4 +Brand#21 |SMALL POLISHED TIN | 23| 4 +Brand#21 |SMALL POLISHED TIN | 36| 4 +Brand#21 |SMALL POLISHED TIN | 45| 4 +Brand#21 |SMALL POLISHED TIN | 49| 4 +Brand#21 |STANDARD ANODIZED BRASS | 9| 4 +Brand#21 |STANDARD ANODIZED BRASS | 14| 4 +Brand#21 |STANDARD ANODIZED BRASS | 49| 4 +Brand#21 |STANDARD ANODIZED COPPER | 9| 4 +Brand#21 |STANDARD ANODIZED COPPER | 19| 4 +Brand#21 |STANDARD ANODIZED COPPER | 49| 4 +Brand#21 |STANDARD ANODIZED NICKEL | 14| 4 +Brand#21 |STANDARD ANODIZED NICKEL | 23| 4 +Brand#21 |STANDARD ANODIZED STEEL | 9| 4 +Brand#21 |STANDARD ANODIZED STEEL | 14| 4 +Brand#21 |STANDARD ANODIZED STEEL | 45| 4 +Brand#21 |STANDARD ANODIZED TIN | 14| 4 +Brand#21 |STANDARD ANODIZED TIN | 19| 4 +Brand#21 |STANDARD ANODIZED TIN | 23| 4 +Brand#21 |STANDARD ANODIZED TIN | 45| 4 +Brand#21 |STANDARD BRUSHED BRASS | 3| 4 +Brand#21 |STANDARD BRUSHED BRASS | 23| 4 +Brand#21 |STANDARD BRUSHED COPPER | 9| 4 +Brand#21 |STANDARD BRUSHED COPPER | 14| 4 +Brand#21 |STANDARD BRUSHED COPPER | 19| 4 +Brand#21 |STANDARD BRUSHED COPPER | 45| 4 +Brand#21 |STANDARD BRUSHED COPPER | 49| 4 +Brand#21 |STANDARD BRUSHED NICKEL | 3| 4 +Brand#21 |STANDARD BRUSHED NICKEL | 9| 4 +Brand#21 |STANDARD BRUSHED NICKEL | 36| 4 +Brand#21 |STANDARD BRUSHED NICKEL | 49| 4 +Brand#21 |STANDARD BRUSHED TIN | 3| 4 +Brand#21 |STANDARD BRUSHED TIN | 9| 4 +Brand#21 |STANDARD BRUSHED TIN | 14| 4 +Brand#21 |STANDARD BRUSHED TIN | 19| 4 +Brand#21 |STANDARD BRUSHED TIN | 49| 4 +Brand#21 |STANDARD BURNISHED BRASS | 9| 4 +Brand#21 |STANDARD BURNISHED BRASS | 23| 4 +Brand#21 |STANDARD BURNISHED COPPER| 23| 4 +Brand#21 |STANDARD BURNISHED COPPER| 36| 4 +Brand#21 |STANDARD BURNISHED COPPER| 45| 4 +Brand#21 |STANDARD BURNISHED COPPER| 49| 4 +Brand#21 |STANDARD BURNISHED NICKEL| 14| 4 +Brand#21 |STANDARD BURNISHED NICKEL| 19| 4 +Brand#21 |STANDARD BURNISHED NICKEL| 49| 4 +Brand#21 |STANDARD BURNISHED STEEL | 9| 4 +Brand#21 |STANDARD BURNISHED STEEL | 23| 4 +Brand#21 |STANDARD BURNISHED TIN | 3| 4 +Brand#21 |STANDARD BURNISHED TIN | 9| 4 +Brand#21 |STANDARD PLATED BRASS | 3| 4 +Brand#21 |STANDARD PLATED BRASS | 9| 4 +Brand#21 |STANDARD PLATED BRASS | 45| 4 +Brand#21 |STANDARD PLATED COPPER | 9| 4 +Brand#21 |STANDARD PLATED NICKEL | 9| 4 +Brand#21 |STANDARD PLATED NICKEL | 14| 4 +Brand#21 |STANDARD PLATED NICKEL | 23| 4 +Brand#21 |STANDARD PLATED STEEL | 3| 4 +Brand#21 |STANDARD PLATED STEEL | 9| 4 +Brand#21 |STANDARD PLATED STEEL | 19| 4 +Brand#21 |STANDARD PLATED STEEL | 23| 4 +Brand#21 |STANDARD PLATED STEEL | 45| 4 +Brand#21 |STANDARD PLATED TIN | 19| 4 +Brand#21 |STANDARD PLATED TIN | 23| 4 +Brand#21 |STANDARD PLATED TIN | 36| 4 +Brand#21 |STANDARD POLISHED BRASS | 3| 4 +Brand#21 |STANDARD POLISHED BRASS | 23| 4 +Brand#21 |STANDARD POLISHED BRASS | 36| 4 +Brand#21 |STANDARD POLISHED COPPER | 3| 4 +Brand#21 |STANDARD POLISHED COPPER | 36| 4 +Brand#21 |STANDARD POLISHED NICKEL | 3| 4 +Brand#21 |STANDARD POLISHED NICKEL | 36| 4 +Brand#21 |STANDARD POLISHED NICKEL | 45| 4 +Brand#21 |STANDARD POLISHED NICKEL | 49| 4 +Brand#21 |STANDARD POLISHED STEEL | 9| 4 +Brand#21 |STANDARD POLISHED STEEL | 23| 4 +Brand#21 |STANDARD POLISHED STEEL | 45| 4 +Brand#21 |STANDARD POLISHED STEEL | 49| 4 +Brand#21 |STANDARD POLISHED TIN | 3| 4 +Brand#21 |STANDARD POLISHED TIN | 19| 4 +Brand#21 |STANDARD POLISHED TIN | 23| 4 +Brand#21 |STANDARD POLISHED TIN | 45| 4 +Brand#21 |STANDARD POLISHED TIN | 49| 4 +Brand#22 |ECONOMY ANODIZED BRASS | 14| 4 +Brand#22 |ECONOMY ANODIZED BRASS | 23| 4 +Brand#22 |ECONOMY ANODIZED BRASS | 45| 4 +Brand#22 |ECONOMY ANODIZED BRASS | 49| 4 +Brand#22 |ECONOMY ANODIZED COPPER | 3| 4 +Brand#22 |ECONOMY ANODIZED COPPER | 9| 4 +Brand#22 |ECONOMY ANODIZED COPPER | 19| 4 +Brand#22 |ECONOMY ANODIZED NICKEL | 9| 4 +Brand#22 |ECONOMY ANODIZED NICKEL | 14| 4 +Brand#22 |ECONOMY ANODIZED NICKEL | 49| 4 +Brand#22 |ECONOMY ANODIZED STEEL | 3| 4 +Brand#22 |ECONOMY ANODIZED STEEL | 9| 4 +Brand#22 |ECONOMY ANODIZED STEEL | 14| 4 +Brand#22 |ECONOMY ANODIZED STEEL | 19| 4 +Brand#22 |ECONOMY ANODIZED STEEL | 36| 4 +Brand#22 |ECONOMY ANODIZED STEEL | 49| 4 +Brand#22 |ECONOMY ANODIZED TIN | 3| 4 +Brand#22 |ECONOMY ANODIZED TIN | 9| 4 +Brand#22 |ECONOMY ANODIZED TIN | 19| 4 +Brand#22 |ECONOMY BRUSHED BRASS | 3| 4 +Brand#22 |ECONOMY BRUSHED BRASS | 36| 4 +Brand#22 |ECONOMY BRUSHED COPPER | 14| 4 +Brand#22 |ECONOMY BRUSHED COPPER | 36| 4 +Brand#22 |ECONOMY BRUSHED COPPER | 45| 4 +Brand#22 |ECONOMY BRUSHED COPPER | 49| 4 +Brand#22 |ECONOMY BRUSHED NICKEL | 19| 4 +Brand#22 |ECONOMY BRUSHED NICKEL | 23| 4 +Brand#22 |ECONOMY BRUSHED NICKEL | 49| 4 +Brand#22 |ECONOMY BRUSHED STEEL | 9| 4 +Brand#22 |ECONOMY BRUSHED STEEL | 14| 4 +Brand#22 |ECONOMY BRUSHED STEEL | 23| 4 +Brand#22 |ECONOMY BRUSHED STEEL | 36| 4 +Brand#22 |ECONOMY BRUSHED TIN | 9| 4 +Brand#22 |ECONOMY BRUSHED TIN | 14| 4 +Brand#22 |ECONOMY BRUSHED TIN | 19| 4 +Brand#22 |ECONOMY BURNISHED BRASS | 3| 4 +Brand#22 |ECONOMY BURNISHED BRASS | 9| 4 +Brand#22 |ECONOMY BURNISHED BRASS | 49| 4 +Brand#22 |ECONOMY BURNISHED COPPER | 19| 4 +Brand#22 |ECONOMY BURNISHED COPPER | 23| 4 +Brand#22 |ECONOMY BURNISHED COPPER | 36| 4 +Brand#22 |ECONOMY BURNISHED NICKEL | 19| 4 +Brand#22 |ECONOMY BURNISHED NICKEL | 45| 4 +Brand#22 |ECONOMY BURNISHED STEEL | 3| 4 +Brand#22 |ECONOMY BURNISHED STEEL | 14| 4 +Brand#22 |ECONOMY BURNISHED TIN | 3| 4 +Brand#22 |ECONOMY BURNISHED TIN | 14| 4 +Brand#22 |ECONOMY BURNISHED TIN | 36| 4 +Brand#22 |ECONOMY BURNISHED TIN | 45| 4 +Brand#22 |ECONOMY BURNISHED TIN | 49| 4 +Brand#22 |ECONOMY PLATED BRASS | 9| 4 +Brand#22 |ECONOMY PLATED BRASS | 14| 4 +Brand#22 |ECONOMY PLATED BRASS | 23| 4 +Brand#22 |ECONOMY PLATED COPPER | 14| 4 +Brand#22 |ECONOMY PLATED COPPER | 23| 4 +Brand#22 |ECONOMY PLATED COPPER | 36| 4 +Brand#22 |ECONOMY PLATED COPPER | 45| 4 +Brand#22 |ECONOMY PLATED COPPER | 49| 4 +Brand#22 |ECONOMY PLATED NICKEL | 19| 4 +Brand#22 |ECONOMY PLATED NICKEL | 23| 4 +Brand#22 |ECONOMY PLATED STEEL | 9| 4 +Brand#22 |ECONOMY PLATED STEEL | 36| 4 +Brand#22 |ECONOMY PLATED STEEL | 49| 4 +Brand#22 |ECONOMY PLATED TIN | 3| 4 +Brand#22 |ECONOMY PLATED TIN | 14| 4 +Brand#22 |ECONOMY PLATED TIN | 23| 4 +Brand#22 |ECONOMY PLATED TIN | 36| 4 +Brand#22 |ECONOMY PLATED TIN | 45| 4 +Brand#22 |ECONOMY POLISHED BRASS | 3| 4 +Brand#22 |ECONOMY POLISHED BRASS | 9| 4 +Brand#22 |ECONOMY POLISHED BRASS | 14| 4 +Brand#22 |ECONOMY POLISHED BRASS | 19| 4 +Brand#22 |ECONOMY POLISHED BRASS | 49| 4 +Brand#22 |ECONOMY POLISHED COPPER | 3| 4 +Brand#22 |ECONOMY POLISHED COPPER | 36| 4 +Brand#22 |ECONOMY POLISHED NICKEL | 3| 4 +Brand#22 |ECONOMY POLISHED NICKEL | 14| 4 +Brand#22 |ECONOMY POLISHED NICKEL | 19| 4 +Brand#22 |ECONOMY POLISHED NICKEL | 23| 4 +Brand#22 |ECONOMY POLISHED NICKEL | 36| 4 +Brand#22 |ECONOMY POLISHED NICKEL | 49| 4 +Brand#22 |ECONOMY POLISHED STEEL | 3| 4 +Brand#22 |ECONOMY POLISHED TIN | 3| 4 +Brand#22 |ECONOMY POLISHED TIN | 23| 4 +Brand#22 |LARGE ANODIZED BRASS | 3| 4 +Brand#22 |LARGE ANODIZED BRASS | 9| 4 +Brand#22 |LARGE ANODIZED BRASS | 19| 4 +Brand#22 |LARGE ANODIZED BRASS | 23| 4 +Brand#22 |LARGE ANODIZED BRASS | 36| 4 +Brand#22 |LARGE ANODIZED BRASS | 45| 4 +Brand#22 |LARGE ANODIZED COPPER | 14| 4 +Brand#22 |LARGE ANODIZED COPPER | 45| 4 +Brand#22 |LARGE ANODIZED COPPER | 49| 4 +Brand#22 |LARGE ANODIZED NICKEL | 3| 4 +Brand#22 |LARGE ANODIZED NICKEL | 9| 4 +Brand#22 |LARGE ANODIZED NICKEL | 36| 4 +Brand#22 |LARGE ANODIZED NICKEL | 49| 4 +Brand#22 |LARGE ANODIZED STEEL | 3| 4 +Brand#22 |LARGE ANODIZED STEEL | 14| 4 +Brand#22 |LARGE ANODIZED STEEL | 23| 4 +Brand#22 |LARGE ANODIZED STEEL | 49| 4 +Brand#22 |LARGE ANODIZED TIN | 36| 4 +Brand#22 |LARGE BRUSHED BRASS | 3| 4 +Brand#22 |LARGE BRUSHED COPPER | 3| 4 +Brand#22 |LARGE BRUSHED NICKEL | 3| 4 +Brand#22 |LARGE BRUSHED NICKEL | 19| 4 +Brand#22 |LARGE BRUSHED NICKEL | 36| 4 +Brand#22 |LARGE BRUSHED STEEL | 9| 4 +Brand#22 |LARGE BRUSHED STEEL | 45| 4 +Brand#22 |LARGE BRUSHED STEEL | 49| 4 +Brand#22 |LARGE BRUSHED TIN | 3| 4 +Brand#22 |LARGE BRUSHED TIN | 9| 4 +Brand#22 |LARGE BRUSHED TIN | 19| 4 +Brand#22 |LARGE BRUSHED TIN | 45| 4 +Brand#22 |LARGE BRUSHED TIN | 49| 4 +Brand#22 |LARGE BURNISHED BRASS | 19| 4 +Brand#22 |LARGE BURNISHED BRASS | 45| 4 +Brand#22 |LARGE BURNISHED BRASS | 49| 4 +Brand#22 |LARGE BURNISHED COPPER | 3| 4 +Brand#22 |LARGE BURNISHED COPPER | 14| 4 +Brand#22 |LARGE BURNISHED COPPER | 36| 4 +Brand#22 |LARGE BURNISHED COPPER | 45| 4 +Brand#22 |LARGE BURNISHED COPPER | 49| 4 +Brand#22 |LARGE BURNISHED NICKEL | 14| 4 +Brand#22 |LARGE BURNISHED STEEL | 3| 4 +Brand#22 |LARGE BURNISHED STEEL | 19| 4 +Brand#22 |LARGE BURNISHED STEEL | 23| 4 +Brand#22 |LARGE BURNISHED STEEL | 45| 4 +Brand#22 |LARGE BURNISHED TIN | 9| 4 +Brand#22 |LARGE BURNISHED TIN | 14| 4 +Brand#22 |LARGE BURNISHED TIN | 49| 4 +Brand#22 |LARGE PLATED BRASS | 9| 4 +Brand#22 |LARGE PLATED BRASS | 14| 4 +Brand#22 |LARGE PLATED BRASS | 36| 4 +Brand#22 |LARGE PLATED BRASS | 49| 4 +Brand#22 |LARGE PLATED COPPER | 9| 4 +Brand#22 |LARGE PLATED COPPER | 14| 4 +Brand#22 |LARGE PLATED COPPER | 49| 4 +Brand#22 |LARGE PLATED NICKEL | 14| 4 +Brand#22 |LARGE PLATED NICKEL | 49| 4 +Brand#22 |LARGE PLATED STEEL | 3| 4 +Brand#22 |LARGE PLATED STEEL | 36| 4 +Brand#22 |LARGE PLATED STEEL | 45| 4 +Brand#22 |LARGE PLATED STEEL | 49| 4 +Brand#22 |LARGE PLATED TIN | 9| 4 +Brand#22 |LARGE PLATED TIN | 19| 4 +Brand#22 |LARGE POLISHED BRASS | 9| 4 +Brand#22 |LARGE POLISHED BRASS | 19| 4 +Brand#22 |LARGE POLISHED COPPER | 14| 4 +Brand#22 |LARGE POLISHED COPPER | 45| 4 +Brand#22 |LARGE POLISHED NICKEL | 9| 4 +Brand#22 |LARGE POLISHED NICKEL | 36| 4 +Brand#22 |LARGE POLISHED STEEL | 14| 4 +Brand#22 |LARGE POLISHED STEEL | 19| 4 +Brand#22 |LARGE POLISHED STEEL | 23| 4 +Brand#22 |LARGE POLISHED STEEL | 36| 4 +Brand#22 |LARGE POLISHED TIN | 3| 4 +Brand#22 |LARGE POLISHED TIN | 19| 4 +Brand#22 |LARGE POLISHED TIN | 23| 4 +Brand#22 |MEDIUM ANODIZED BRASS | 3| 4 +Brand#22 |MEDIUM ANODIZED BRASS | 19| 4 +Brand#22 |MEDIUM ANODIZED BRASS | 36| 4 +Brand#22 |MEDIUM ANODIZED BRASS | 45| 4 +Brand#22 |MEDIUM ANODIZED COPPER | 49| 4 +Brand#22 |MEDIUM ANODIZED NICKEL | 14| 4 +Brand#22 |MEDIUM ANODIZED STEEL | 3| 4 +Brand#22 |MEDIUM ANODIZED STEEL | 14| 4 +Brand#22 |MEDIUM ANODIZED STEEL | 45| 4 +Brand#22 |MEDIUM ANODIZED STEEL | 49| 4 +Brand#22 |MEDIUM ANODIZED TIN | 3| 4 +Brand#22 |MEDIUM ANODIZED TIN | 9| 4 +Brand#22 |MEDIUM ANODIZED TIN | 14| 4 +Brand#22 |MEDIUM ANODIZED TIN | 36| 4 +Brand#22 |MEDIUM ANODIZED TIN | 49| 4 +Brand#22 |MEDIUM BRUSHED BRASS | 3| 4 +Brand#22 |MEDIUM BRUSHED BRASS | 9| 4 +Brand#22 |MEDIUM BRUSHED BRASS | 14| 4 +Brand#22 |MEDIUM BRUSHED BRASS | 19| 4 +Brand#22 |MEDIUM BRUSHED BRASS | 23| 4 +Brand#22 |MEDIUM BRUSHED COPPER | 23| 4 +Brand#22 |MEDIUM BRUSHED NICKEL | 3| 4 +Brand#22 |MEDIUM BRUSHED NICKEL | 19| 4 +Brand#22 |MEDIUM BRUSHED NICKEL | 23| 4 +Brand#22 |MEDIUM BRUSHED NICKEL | 36| 4 +Brand#22 |MEDIUM BRUSHED NICKEL | 45| 4 +Brand#22 |MEDIUM BRUSHED STEEL | 9| 4 +Brand#22 |MEDIUM BRUSHED TIN | 9| 4 +Brand#22 |MEDIUM BRUSHED TIN | 14| 4 +Brand#22 |MEDIUM BRUSHED TIN | 19| 4 +Brand#22 |MEDIUM BRUSHED TIN | 23| 4 +Brand#22 |MEDIUM BRUSHED TIN | 45| 4 +Brand#22 |MEDIUM BURNISHED BRASS | 3| 4 +Brand#22 |MEDIUM BURNISHED BRASS | 19| 4 +Brand#22 |MEDIUM BURNISHED BRASS | 23| 4 +Brand#22 |MEDIUM BURNISHED COPPER | 3| 4 +Brand#22 |MEDIUM BURNISHED COPPER | 19| 4 +Brand#22 |MEDIUM BURNISHED NICKEL | 19| 4 +Brand#22 |MEDIUM BURNISHED NICKEL | 45| 4 +Brand#22 |MEDIUM BURNISHED NICKEL | 49| 4 +Brand#22 |MEDIUM BURNISHED STEEL | 23| 4 +Brand#22 |MEDIUM BURNISHED STEEL | 49| 4 +Brand#22 |MEDIUM BURNISHED TIN | 23| 4 +Brand#22 |MEDIUM BURNISHED TIN | 45| 4 +Brand#22 |MEDIUM PLATED BRASS | 3| 4 +Brand#22 |MEDIUM PLATED BRASS | 19| 4 +Brand#22 |MEDIUM PLATED BRASS | 45| 4 +Brand#22 |MEDIUM PLATED BRASS | 49| 4 +Brand#22 |MEDIUM PLATED COPPER | 9| 4 +Brand#22 |MEDIUM PLATED COPPER | 14| 4 +Brand#22 |MEDIUM PLATED COPPER | 23| 4 +Brand#22 |MEDIUM PLATED COPPER | 49| 4 +Brand#22 |MEDIUM PLATED NICKEL | 19| 4 +Brand#22 |MEDIUM PLATED STEEL | 14| 4 +Brand#22 |MEDIUM PLATED STEEL | 36| 4 +Brand#22 |MEDIUM PLATED STEEL | 49| 4 +Brand#22 |MEDIUM PLATED TIN | 3| 4 +Brand#22 |MEDIUM PLATED TIN | 9| 4 +Brand#22 |MEDIUM PLATED TIN | 14| 4 +Brand#22 |PROMO ANODIZED BRASS | 14| 4 +Brand#22 |PROMO ANODIZED COPPER | 14| 4 +Brand#22 |PROMO ANODIZED COPPER | 36| 4 +Brand#22 |PROMO ANODIZED COPPER | 49| 4 +Brand#22 |PROMO ANODIZED NICKEL | 3| 4 +Brand#22 |PROMO ANODIZED NICKEL | 14| 4 +Brand#22 |PROMO ANODIZED NICKEL | 19| 4 +Brand#22 |PROMO ANODIZED NICKEL | 49| 4 +Brand#22 |PROMO ANODIZED STEEL | 3| 4 +Brand#22 |PROMO ANODIZED STEEL | 23| 4 +Brand#22 |PROMO ANODIZED STEEL | 45| 4 +Brand#22 |PROMO ANODIZED TIN | 3| 4 +Brand#22 |PROMO ANODIZED TIN | 9| 4 +Brand#22 |PROMO BRUSHED BRASS | 9| 4 +Brand#22 |PROMO BRUSHED COPPER | 3| 4 +Brand#22 |PROMO BRUSHED COPPER | 9| 4 +Brand#22 |PROMO BRUSHED COPPER | 14| 4 +Brand#22 |PROMO BRUSHED COPPER | 19| 4 +Brand#22 |PROMO BRUSHED NICKEL | 3| 4 +Brand#22 |PROMO BRUSHED NICKEL | 23| 4 +Brand#22 |PROMO BRUSHED STEEL | 9| 4 +Brand#22 |PROMO BRUSHED STEEL | 14| 4 +Brand#22 |PROMO BRUSHED STEEL | 19| 4 +Brand#22 |PROMO BRUSHED STEEL | 23| 4 +Brand#22 |PROMO BRUSHED STEEL | 49| 4 +Brand#22 |PROMO BRUSHED TIN | 14| 4 +Brand#22 |PROMO BRUSHED TIN | 23| 4 +Brand#22 |PROMO BRUSHED TIN | 45| 4 +Brand#22 |PROMO BRUSHED TIN | 49| 4 +Brand#22 |PROMO BURNISHED BRASS | 9| 4 +Brand#22 |PROMO BURNISHED BRASS | 19| 4 +Brand#22 |PROMO BURNISHED BRASS | 45| 4 +Brand#22 |PROMO BURNISHED COPPER | 3| 4 +Brand#22 |PROMO BURNISHED COPPER | 9| 4 +Brand#22 |PROMO BURNISHED COPPER | 19| 4 +Brand#22 |PROMO BURNISHED COPPER | 45| 4 +Brand#22 |PROMO BURNISHED NICKEL | 9| 4 +Brand#22 |PROMO BURNISHED NICKEL | 23| 4 +Brand#22 |PROMO BURNISHED NICKEL | 36| 4 +Brand#22 |PROMO BURNISHED NICKEL | 49| 4 +Brand#22 |PROMO BURNISHED STEEL | 9| 4 +Brand#22 |PROMO BURNISHED TIN | 9| 4 +Brand#22 |PROMO BURNISHED TIN | 19| 4 +Brand#22 |PROMO BURNISHED TIN | 23| 4 +Brand#22 |PROMO BURNISHED TIN | 36| 4 +Brand#22 |PROMO BURNISHED TIN | 45| 4 +Brand#22 |PROMO BURNISHED TIN | 49| 4 +Brand#22 |PROMO PLATED BRASS | 49| 4 +Brand#22 |PROMO PLATED COPPER | 9| 4 +Brand#22 |PROMO PLATED COPPER | 23| 4 +Brand#22 |PROMO PLATED COPPER | 49| 4 +Brand#22 |PROMO PLATED NICKEL | 3| 4 +Brand#22 |PROMO PLATED NICKEL | 14| 4 +Brand#22 |PROMO PLATED NICKEL | 36| 4 +Brand#22 |PROMO PLATED STEEL | 14| 4 +Brand#22 |PROMO PLATED STEEL | 19| 4 +Brand#22 |PROMO PLATED STEEL | 49| 4 +Brand#22 |PROMO PLATED TIN | 9| 4 +Brand#22 |PROMO PLATED TIN | 14| 4 +Brand#22 |PROMO PLATED TIN | 45| 4 +Brand#22 |PROMO PLATED TIN | 49| 4 +Brand#22 |PROMO POLISHED BRASS | 19| 4 +Brand#22 |PROMO POLISHED BRASS | 23| 4 +Brand#22 |PROMO POLISHED COPPER | 9| 4 +Brand#22 |PROMO POLISHED COPPER | 14| 4 +Brand#22 |PROMO POLISHED COPPER | 36| 4 +Brand#22 |PROMO POLISHED COPPER | 49| 4 +Brand#22 |PROMO POLISHED NICKEL | 3| 4 +Brand#22 |PROMO POLISHED NICKEL | 14| 4 +Brand#22 |PROMO POLISHED STEEL | 3| 4 +Brand#22 |PROMO POLISHED STEEL | 9| 4 +Brand#22 |PROMO POLISHED STEEL | 23| 4 +Brand#22 |PROMO POLISHED STEEL | 45| 4 +Brand#22 |PROMO POLISHED TIN | 9| 4 +Brand#22 |PROMO POLISHED TIN | 36| 4 +Brand#22 |PROMO POLISHED TIN | 45| 4 +Brand#22 |SMALL ANODIZED BRASS | 3| 4 +Brand#22 |SMALL ANODIZED BRASS | 9| 4 +Brand#22 |SMALL ANODIZED BRASS | 23| 4 +Brand#22 |SMALL ANODIZED BRASS | 45| 4 +Brand#22 |SMALL ANODIZED COPPER | 14| 4 +Brand#22 |SMALL ANODIZED COPPER | 36| 4 +Brand#22 |SMALL ANODIZED NICKEL | 9| 4 +Brand#22 |SMALL ANODIZED NICKEL | 14| 4 +Brand#22 |SMALL ANODIZED NICKEL | 19| 4 +Brand#22 |SMALL ANODIZED NICKEL | 49| 4 +Brand#22 |SMALL ANODIZED STEEL | 3| 4 +Brand#22 |SMALL ANODIZED STEEL | 9| 4 +Brand#22 |SMALL ANODIZED STEEL | 36| 4 +Brand#22 |SMALL ANODIZED STEEL | 49| 4 +Brand#22 |SMALL ANODIZED TIN | 3| 4 +Brand#22 |SMALL ANODIZED TIN | 14| 4 +Brand#22 |SMALL ANODIZED TIN | 36| 4 +Brand#22 |SMALL BRUSHED BRASS | 23| 4 +Brand#22 |SMALL BRUSHED BRASS | 49| 4 +Brand#22 |SMALL BRUSHED COPPER | 3| 4 +Brand#22 |SMALL BRUSHED COPPER | 14| 4 +Brand#22 |SMALL BRUSHED COPPER | 19| 4 +Brand#22 |SMALL BRUSHED COPPER | 23| 4 +Brand#22 |SMALL BRUSHED COPPER | 49| 4 +Brand#22 |SMALL BRUSHED NICKEL | 14| 4 +Brand#22 |SMALL BRUSHED NICKEL | 19| 4 +Brand#22 |SMALL BRUSHED NICKEL | 36| 4 +Brand#22 |SMALL BRUSHED STEEL | 3| 4 +Brand#22 |SMALL BRUSHED STEEL | 9| 4 +Brand#22 |SMALL BRUSHED STEEL | 14| 4 +Brand#22 |SMALL BRUSHED STEEL | 19| 4 +Brand#22 |SMALL BRUSHED STEEL | 36| 4 +Brand#22 |SMALL BRUSHED STEEL | 49| 4 +Brand#22 |SMALL BRUSHED TIN | 3| 4 +Brand#22 |SMALL BRUSHED TIN | 9| 4 +Brand#22 |SMALL BRUSHED TIN | 36| 4 +Brand#22 |SMALL BURNISHED BRASS | 45| 4 +Brand#22 |SMALL BURNISHED BRASS | 49| 4 +Brand#22 |SMALL BURNISHED COPPER | 9| 4 +Brand#22 |SMALL BURNISHED COPPER | 23| 4 +Brand#22 |SMALL BURNISHED COPPER | 36| 4 +Brand#22 |SMALL BURNISHED NICKEL | 14| 4 +Brand#22 |SMALL BURNISHED NICKEL | 19| 4 +Brand#22 |SMALL BURNISHED NICKEL | 23| 4 +Brand#22 |SMALL BURNISHED NICKEL | 36| 4 +Brand#22 |SMALL BURNISHED NICKEL | 45| 4 +Brand#22 |SMALL BURNISHED STEEL | 3| 4 +Brand#22 |SMALL BURNISHED STEEL | 19| 4 +Brand#22 |SMALL BURNISHED TIN | 9| 4 +Brand#22 |SMALL BURNISHED TIN | 14| 4 +Brand#22 |SMALL PLATED BRASS | 3| 4 +Brand#22 |SMALL PLATED BRASS | 19| 4 +Brand#22 |SMALL PLATED BRASS | 36| 4 +Brand#22 |SMALL PLATED BRASS | 45| 4 +Brand#22 |SMALL PLATED COPPER | 9| 4 +Brand#22 |SMALL PLATED COPPER | 19| 4 +Brand#22 |SMALL PLATED COPPER | 23| 4 +Brand#22 |SMALL PLATED COPPER | 45| 4 +Brand#22 |SMALL PLATED NICKEL | 14| 4 +Brand#22 |SMALL PLATED NICKEL | 23| 4 +Brand#22 |SMALL PLATED NICKEL | 36| 4 +Brand#22 |SMALL PLATED NICKEL | 49| 4 +Brand#22 |SMALL PLATED STEEL | 9| 4 +Brand#22 |SMALL PLATED TIN | 3| 4 +Brand#22 |SMALL PLATED TIN | 9| 4 +Brand#22 |SMALL PLATED TIN | 14| 4 +Brand#22 |SMALL PLATED TIN | 19| 4 +Brand#22 |SMALL PLATED TIN | 36| 4 +Brand#22 |SMALL PLATED TIN | 49| 4 +Brand#22 |SMALL POLISHED BRASS | 9| 4 +Brand#22 |SMALL POLISHED BRASS | 23| 4 +Brand#22 |SMALL POLISHED BRASS | 49| 4 +Brand#22 |SMALL POLISHED COPPER | 14| 4 +Brand#22 |SMALL POLISHED COPPER | 36| 4 +Brand#22 |SMALL POLISHED NICKEL | 36| 4 +Brand#22 |SMALL POLISHED STEEL | 3| 4 +Brand#22 |SMALL POLISHED STEEL | 19| 4 +Brand#22 |SMALL POLISHED STEEL | 23| 4 +Brand#22 |SMALL POLISHED STEEL | 36| 4 +Brand#22 |SMALL POLISHED TIN | 3| 4 +Brand#22 |SMALL POLISHED TIN | 9| 4 +Brand#22 |SMALL POLISHED TIN | 36| 4 +Brand#22 |STANDARD ANODIZED BRASS | 9| 4 +Brand#22 |STANDARD ANODIZED BRASS | 45| 4 +Brand#22 |STANDARD ANODIZED BRASS | 49| 4 +Brand#22 |STANDARD ANODIZED COPPER | 3| 4 +Brand#22 |STANDARD ANODIZED COPPER | 19| 4 +Brand#22 |STANDARD ANODIZED NICKEL | 19| 4 +Brand#22 |STANDARD ANODIZED NICKEL | 45| 4 +Brand#22 |STANDARD ANODIZED STEEL | 3| 4 +Brand#22 |STANDARD ANODIZED STEEL | 9| 4 +Brand#22 |STANDARD ANODIZED STEEL | 36| 4 +Brand#22 |STANDARD ANODIZED STEEL | 45| 4 +Brand#22 |STANDARD ANODIZED TIN | 19| 4 +Brand#22 |STANDARD ANODIZED TIN | 23| 4 +Brand#22 |STANDARD ANODIZED TIN | 36| 4 +Brand#22 |STANDARD BRUSHED BRASS | 23| 4 +Brand#22 |STANDARD BRUSHED BRASS | 45| 4 +Brand#22 |STANDARD BRUSHED BRASS | 49| 4 +Brand#22 |STANDARD BRUSHED COPPER | 3| 4 +Brand#22 |STANDARD BRUSHED COPPER | 9| 4 +Brand#22 |STANDARD BRUSHED COPPER | 14| 4 +Brand#22 |STANDARD BRUSHED COPPER | 23| 4 +Brand#22 |STANDARD BRUSHED COPPER | 45| 4 +Brand#22 |STANDARD BRUSHED COPPER | 49| 4 +Brand#22 |STANDARD BRUSHED NICKEL | 3| 4 +Brand#22 |STANDARD BRUSHED NICKEL | 36| 4 +Brand#22 |STANDARD BRUSHED STEEL | 3| 4 +Brand#22 |STANDARD BRUSHED STEEL | 23| 4 +Brand#22 |STANDARD BURNISHED BRASS | 3| 4 +Brand#22 |STANDARD BURNISHED BRASS | 9| 4 +Brand#22 |STANDARD BURNISHED BRASS | 19| 4 +Brand#22 |STANDARD BURNISHED COPPER| 3| 4 +Brand#22 |STANDARD BURNISHED COPPER| 14| 4 +Brand#22 |STANDARD BURNISHED COPPER| 19| 4 +Brand#22 |STANDARD BURNISHED COPPER| 23| 4 +Brand#22 |STANDARD BURNISHED COPPER| 45| 4 +Brand#22 |STANDARD BURNISHED NICKEL| 9| 4 +Brand#22 |STANDARD BURNISHED NICKEL| 49| 4 +Brand#22 |STANDARD BURNISHED STEEL | 3| 4 +Brand#22 |STANDARD BURNISHED STEEL | 14| 4 +Brand#22 |STANDARD BURNISHED STEEL | 19| 4 +Brand#22 |STANDARD BURNISHED STEEL | 23| 4 +Brand#22 |STANDARD BURNISHED STEEL | 49| 4 +Brand#22 |STANDARD BURNISHED TIN | 36| 4 +Brand#22 |STANDARD BURNISHED TIN | 49| 4 +Brand#22 |STANDARD PLATED COPPER | 9| 4 +Brand#22 |STANDARD PLATED COPPER | 45| 4 +Brand#22 |STANDARD PLATED COPPER | 49| 4 +Brand#22 |STANDARD PLATED NICKEL | 3| 4 +Brand#22 |STANDARD PLATED NICKEL | 14| 4 +Brand#22 |STANDARD PLATED NICKEL | 45| 4 +Brand#22 |STANDARD PLATED NICKEL | 49| 4 +Brand#22 |STANDARD PLATED STEEL | 3| 4 +Brand#22 |STANDARD PLATED TIN | 9| 4 +Brand#22 |STANDARD PLATED TIN | 14| 4 +Brand#22 |STANDARD PLATED TIN | 19| 4 +Brand#22 |STANDARD PLATED TIN | 45| 4 +Brand#22 |STANDARD POLISHED BRASS | 23| 4 +Brand#22 |STANDARD POLISHED COPPER | 3| 4 +Brand#22 |STANDARD POLISHED COPPER | 14| 4 +Brand#22 |STANDARD POLISHED COPPER | 23| 4 +Brand#22 |STANDARD POLISHED COPPER | 36| 4 +Brand#22 |STANDARD POLISHED COPPER | 45| 4 +Brand#22 |STANDARD POLISHED COPPER | 49| 4 +Brand#22 |STANDARD POLISHED NICKEL | 9| 4 +Brand#22 |STANDARD POLISHED NICKEL | 36| 4 +Brand#22 |STANDARD POLISHED NICKEL | 49| 4 +Brand#22 |STANDARD POLISHED STEEL | 3| 4 +Brand#22 |STANDARD POLISHED STEEL | 23| 4 +Brand#22 |STANDARD POLISHED TIN | 14| 4 +Brand#22 |STANDARD POLISHED TIN | 23| 4 +Brand#22 |STANDARD POLISHED TIN | 36| 4 +Brand#22 |STANDARD POLISHED TIN | 49| 4 +Brand#23 |ECONOMY ANODIZED BRASS | 14| 4 +Brand#23 |ECONOMY ANODIZED BRASS | 19| 4 +Brand#23 |ECONOMY ANODIZED BRASS | 23| 4 +Brand#23 |ECONOMY ANODIZED BRASS | 45| 4 +Brand#23 |ECONOMY ANODIZED COPPER | 9| 4 +Brand#23 |ECONOMY ANODIZED COPPER | 14| 4 +Brand#23 |ECONOMY ANODIZED COPPER | 19| 4 +Brand#23 |ECONOMY ANODIZED COPPER | 36| 4 +Brand#23 |ECONOMY ANODIZED COPPER | 45| 4 +Brand#23 |ECONOMY ANODIZED NICKEL | 14| 4 +Brand#23 |ECONOMY ANODIZED NICKEL | 45| 4 +Brand#23 |ECONOMY ANODIZED STEEL | 3| 4 +Brand#23 |ECONOMY ANODIZED STEEL | 19| 4 +Brand#23 |ECONOMY ANODIZED TIN | 3| 4 +Brand#23 |ECONOMY ANODIZED TIN | 9| 4 +Brand#23 |ECONOMY BRUSHED BRASS | 23| 4 +Brand#23 |ECONOMY BRUSHED BRASS | 45| 4 +Brand#23 |ECONOMY BRUSHED BRASS | 49| 4 +Brand#23 |ECONOMY BRUSHED COPPER | 45| 4 +Brand#23 |ECONOMY BRUSHED NICKEL | 3| 4 +Brand#23 |ECONOMY BRUSHED NICKEL | 9| 4 +Brand#23 |ECONOMY BRUSHED STEEL | 14| 4 +Brand#23 |ECONOMY BRUSHED STEEL | 36| 4 +Brand#23 |ECONOMY BRUSHED STEEL | 45| 4 +Brand#23 |ECONOMY BRUSHED TIN | 3| 4 +Brand#23 |ECONOMY BRUSHED TIN | 36| 4 +Brand#23 |ECONOMY BURNISHED BRASS | 3| 4 +Brand#23 |ECONOMY BURNISHED BRASS | 19| 4 +Brand#23 |ECONOMY BURNISHED BRASS | 36| 4 +Brand#23 |ECONOMY BURNISHED COPPER | 19| 4 +Brand#23 |ECONOMY BURNISHED COPPER | 36| 4 +Brand#23 |ECONOMY BURNISHED NICKEL | 14| 4 +Brand#23 |ECONOMY BURNISHED NICKEL | 49| 4 +Brand#23 |ECONOMY BURNISHED STEEL | 19| 4 +Brand#23 |ECONOMY BURNISHED STEEL | 36| 4 +Brand#23 |ECONOMY BURNISHED TIN | 14| 4 +Brand#23 |ECONOMY BURNISHED TIN | 23| 4 +Brand#23 |ECONOMY PLATED BRASS | 3| 4 +Brand#23 |ECONOMY PLATED BRASS | 36| 4 +Brand#23 |ECONOMY PLATED COPPER | 3| 4 +Brand#23 |ECONOMY PLATED COPPER | 45| 4 +Brand#23 |ECONOMY PLATED NICKEL | 14| 4 +Brand#23 |ECONOMY PLATED NICKEL | 36| 4 +Brand#23 |ECONOMY PLATED STEEL | 9| 4 +Brand#23 |ECONOMY PLATED STEEL | 23| 4 +Brand#23 |ECONOMY PLATED STEEL | 45| 4 +Brand#23 |ECONOMY POLISHED BRASS | 3| 4 +Brand#23 |ECONOMY POLISHED BRASS | 14| 4 +Brand#23 |ECONOMY POLISHED BRASS | 23| 4 +Brand#23 |ECONOMY POLISHED BRASS | 45| 4 +Brand#23 |ECONOMY POLISHED COPPER | 36| 4 +Brand#23 |ECONOMY POLISHED NICKEL | 9| 4 +Brand#23 |ECONOMY POLISHED NICKEL | 14| 4 +Brand#23 |ECONOMY POLISHED NICKEL | 49| 4 +Brand#23 |ECONOMY POLISHED STEEL | 9| 4 +Brand#23 |ECONOMY POLISHED STEEL | 19| 4 +Brand#23 |ECONOMY POLISHED TIN | 9| 4 +Brand#23 |ECONOMY POLISHED TIN | 14| 4 +Brand#23 |ECONOMY POLISHED TIN | 19| 4 +Brand#23 |ECONOMY POLISHED TIN | 23| 4 +Brand#23 |ECONOMY POLISHED TIN | 36| 4 +Brand#23 |LARGE ANODIZED BRASS | 3| 4 +Brand#23 |LARGE ANODIZED BRASS | 23| 4 +Brand#23 |LARGE ANODIZED COPPER | 14| 4 +Brand#23 |LARGE ANODIZED COPPER | 23| 4 +Brand#23 |LARGE ANODIZED NICKEL | 3| 4 +Brand#23 |LARGE ANODIZED NICKEL | 45| 4 +Brand#23 |LARGE ANODIZED NICKEL | 49| 4 +Brand#23 |LARGE ANODIZED STEEL | 3| 4 +Brand#23 |LARGE ANODIZED TIN | 3| 4 +Brand#23 |LARGE ANODIZED TIN | 9| 4 +Brand#23 |LARGE ANODIZED TIN | 23| 4 +Brand#23 |LARGE BRUSHED BRASS | 3| 4 +Brand#23 |LARGE BRUSHED BRASS | 19| 4 +Brand#23 |LARGE BRUSHED BRASS | 23| 4 +Brand#23 |LARGE BRUSHED BRASS | 49| 4 +Brand#23 |LARGE BRUSHED COPPER | 36| 4 +Brand#23 |LARGE BRUSHED COPPER | 45| 4 +Brand#23 |LARGE BRUSHED COPPER | 49| 4 +Brand#23 |LARGE BRUSHED NICKEL | 9| 4 +Brand#23 |LARGE BRUSHED NICKEL | 19| 4 +Brand#23 |LARGE BRUSHED NICKEL | 49| 4 +Brand#23 |LARGE BRUSHED STEEL | 45| 4 +Brand#23 |LARGE BRUSHED TIN | 14| 4 +Brand#23 |LARGE BRUSHED TIN | 23| 4 +Brand#23 |LARGE BRUSHED TIN | 36| 4 +Brand#23 |LARGE BRUSHED TIN | 45| 4 +Brand#23 |LARGE BURNISHED BRASS | 3| 4 +Brand#23 |LARGE BURNISHED BRASS | 9| 4 +Brand#23 |LARGE BURNISHED BRASS | 14| 4 +Brand#23 |LARGE BURNISHED BRASS | 19| 4 +Brand#23 |LARGE BURNISHED BRASS | 36| 4 +Brand#23 |LARGE BURNISHED BRASS | 45| 4 +Brand#23 |LARGE BURNISHED NICKEL | 23| 4 +Brand#23 |LARGE BURNISHED STEEL | 36| 4 +Brand#23 |LARGE BURNISHED TIN | 3| 4 +Brand#23 |LARGE BURNISHED TIN | 9| 4 +Brand#23 |LARGE BURNISHED TIN | 36| 4 +Brand#23 |LARGE BURNISHED TIN | 45| 4 +Brand#23 |LARGE PLATED BRASS | 19| 4 +Brand#23 |LARGE PLATED BRASS | 23| 4 +Brand#23 |LARGE PLATED BRASS | 49| 4 +Brand#23 |LARGE PLATED COPPER | 3| 4 +Brand#23 |LARGE PLATED COPPER | 36| 4 +Brand#23 |LARGE PLATED COPPER | 49| 4 +Brand#23 |LARGE PLATED NICKEL | 3| 4 +Brand#23 |LARGE PLATED NICKEL | 14| 4 +Brand#23 |LARGE PLATED NICKEL | 19| 4 +Brand#23 |LARGE PLATED STEEL | 19| 4 +Brand#23 |LARGE PLATED STEEL | 36| 4 +Brand#23 |LARGE PLATED TIN | 9| 4 +Brand#23 |LARGE PLATED TIN | 14| 4 +Brand#23 |LARGE PLATED TIN | 19| 4 +Brand#23 |LARGE PLATED TIN | 23| 4 +Brand#23 |LARGE PLATED TIN | 36| 4 +Brand#23 |LARGE PLATED TIN | 45| 4 +Brand#23 |LARGE POLISHED BRASS | 3| 4 +Brand#23 |LARGE POLISHED BRASS | 14| 4 +Brand#23 |LARGE POLISHED BRASS | 23| 4 +Brand#23 |LARGE POLISHED BRASS | 36| 4 +Brand#23 |LARGE POLISHED BRASS | 45| 4 +Brand#23 |LARGE POLISHED BRASS | 49| 4 +Brand#23 |LARGE POLISHED COPPER | 19| 4 +Brand#23 |LARGE POLISHED NICKEL | 14| 4 +Brand#23 |LARGE POLISHED NICKEL | 19| 4 +Brand#23 |LARGE POLISHED NICKEL | 23| 4 +Brand#23 |LARGE POLISHED NICKEL | 45| 4 +Brand#23 |LARGE POLISHED STEEL | 9| 4 +Brand#23 |LARGE POLISHED STEEL | 14| 4 +Brand#23 |LARGE POLISHED STEEL | 19| 4 +Brand#23 |LARGE POLISHED STEEL | 36| 4 +Brand#23 |LARGE POLISHED TIN | 19| 4 +Brand#23 |LARGE POLISHED TIN | 23| 4 +Brand#23 |MEDIUM ANODIZED BRASS | 14| 4 +Brand#23 |MEDIUM ANODIZED BRASS | 19| 4 +Brand#23 |MEDIUM ANODIZED BRASS | 36| 4 +Brand#23 |MEDIUM ANODIZED BRASS | 49| 4 +Brand#23 |MEDIUM ANODIZED COPPER | 3| 4 +Brand#23 |MEDIUM ANODIZED COPPER | 9| 4 +Brand#23 |MEDIUM ANODIZED NICKEL | 36| 4 +Brand#23 |MEDIUM ANODIZED NICKEL | 49| 4 +Brand#23 |MEDIUM ANODIZED STEEL | 9| 4 +Brand#23 |MEDIUM ANODIZED STEEL | 14| 4 +Brand#23 |MEDIUM ANODIZED TIN | 3| 4 +Brand#23 |MEDIUM ANODIZED TIN | 9| 4 +Brand#23 |MEDIUM ANODIZED TIN | 19| 4 +Brand#23 |MEDIUM ANODIZED TIN | 36| 4 +Brand#23 |MEDIUM ANODIZED TIN | 49| 4 +Brand#23 |MEDIUM BRUSHED BRASS | 23| 4 +Brand#23 |MEDIUM BRUSHED BRASS | 36| 4 +Brand#23 |MEDIUM BRUSHED COPPER | 9| 4 +Brand#23 |MEDIUM BRUSHED COPPER | 36| 4 +Brand#23 |MEDIUM BRUSHED NICKEL | 9| 4 +Brand#23 |MEDIUM BRUSHED STEEL | 9| 4 +Brand#23 |MEDIUM BRUSHED STEEL | 14| 4 +Brand#23 |MEDIUM BRUSHED STEEL | 19| 4 +Brand#23 |MEDIUM BRUSHED STEEL | 23| 4 +Brand#23 |MEDIUM BRUSHED STEEL | 49| 4 +Brand#23 |MEDIUM BRUSHED TIN | 3| 4 +Brand#23 |MEDIUM BRUSHED TIN | 9| 4 +Brand#23 |MEDIUM BRUSHED TIN | 19| 4 +Brand#23 |MEDIUM BRUSHED TIN | 36| 4 +Brand#23 |MEDIUM BURNISHED BRASS | 19| 4 +Brand#23 |MEDIUM BURNISHED BRASS | 23| 4 +Brand#23 |MEDIUM BURNISHED BRASS | 45| 4 +Brand#23 |MEDIUM BURNISHED BRASS | 49| 4 +Brand#23 |MEDIUM BURNISHED COPPER | 49| 4 +Brand#23 |MEDIUM BURNISHED NICKEL | 14| 4 +Brand#23 |MEDIUM BURNISHED NICKEL | 23| 4 +Brand#23 |MEDIUM BURNISHED NICKEL | 36| 4 +Brand#23 |MEDIUM BURNISHED STEEL | 19| 4 +Brand#23 |MEDIUM BURNISHED STEEL | 36| 4 +Brand#23 |MEDIUM BURNISHED STEEL | 49| 4 +Brand#23 |MEDIUM BURNISHED TIN | 3| 4 +Brand#23 |MEDIUM BURNISHED TIN | 19| 4 +Brand#23 |MEDIUM BURNISHED TIN | 23| 4 +Brand#23 |MEDIUM BURNISHED TIN | 49| 4 +Brand#23 |MEDIUM PLATED BRASS | 3| 4 +Brand#23 |MEDIUM PLATED BRASS | 23| 4 +Brand#23 |MEDIUM PLATED BRASS | 36| 4 +Brand#23 |MEDIUM PLATED BRASS | 49| 4 +Brand#23 |MEDIUM PLATED COPPER | 3| 4 +Brand#23 |MEDIUM PLATED COPPER | 14| 4 +Brand#23 |MEDIUM PLATED COPPER | 36| 4 +Brand#23 |MEDIUM PLATED COPPER | 45| 4 +Brand#23 |MEDIUM PLATED COPPER | 49| 4 +Brand#23 |MEDIUM PLATED NICKEL | 14| 4 +Brand#23 |MEDIUM PLATED NICKEL | 45| 4 +Brand#23 |MEDIUM PLATED STEEL | 3| 4 +Brand#23 |MEDIUM PLATED STEEL | 9| 4 +Brand#23 |MEDIUM PLATED STEEL | 45| 4 +Brand#23 |MEDIUM PLATED STEEL | 49| 4 +Brand#23 |MEDIUM PLATED TIN | 9| 4 +Brand#23 |MEDIUM PLATED TIN | 14| 4 +Brand#23 |MEDIUM PLATED TIN | 36| 4 +Brand#23 |PROMO ANODIZED BRASS | 14| 4 +Brand#23 |PROMO ANODIZED BRASS | 36| 4 +Brand#23 |PROMO ANODIZED BRASS | 45| 4 +Brand#23 |PROMO ANODIZED BRASS | 49| 4 +Brand#23 |PROMO ANODIZED COPPER | 9| 4 +Brand#23 |PROMO ANODIZED COPPER | 14| 4 +Brand#23 |PROMO ANODIZED NICKEL | 9| 4 +Brand#23 |PROMO ANODIZED NICKEL | 19| 4 +Brand#23 |PROMO ANODIZED NICKEL | 49| 4 +Brand#23 |PROMO ANODIZED STEEL | 14| 4 +Brand#23 |PROMO ANODIZED STEEL | 45| 4 +Brand#23 |PROMO ANODIZED STEEL | 49| 4 +Brand#23 |PROMO ANODIZED TIN | 36| 4 +Brand#23 |PROMO ANODIZED TIN | 45| 4 +Brand#23 |PROMO BRUSHED BRASS | 3| 4 +Brand#23 |PROMO BRUSHED BRASS | 9| 4 +Brand#23 |PROMO BRUSHED BRASS | 14| 4 +Brand#23 |PROMO BRUSHED BRASS | 45| 4 +Brand#23 |PROMO BRUSHED BRASS | 49| 4 +Brand#23 |PROMO BRUSHED COPPER | 3| 4 +Brand#23 |PROMO BRUSHED COPPER | 9| 4 +Brand#23 |PROMO BRUSHED COPPER | 49| 4 +Brand#23 |PROMO BRUSHED NICKEL | 9| 4 +Brand#23 |PROMO BRUSHED NICKEL | 36| 4 +Brand#23 |PROMO BRUSHED STEEL | 14| 4 +Brand#23 |PROMO BRUSHED STEEL | 19| 4 +Brand#23 |PROMO BRUSHED STEEL | 23| 4 +Brand#23 |PROMO BRUSHED STEEL | 36| 4 +Brand#23 |PROMO BRUSHED STEEL | 45| 4 +Brand#23 |PROMO BRUSHED STEEL | 49| 4 +Brand#23 |PROMO BRUSHED TIN | 14| 4 +Brand#23 |PROMO BRUSHED TIN | 36| 4 +Brand#23 |PROMO BURNISHED BRASS | 3| 4 +Brand#23 |PROMO BURNISHED BRASS | 19| 4 +Brand#23 |PROMO BURNISHED BRASS | 23| 4 +Brand#23 |PROMO BURNISHED BRASS | 36| 4 +Brand#23 |PROMO BURNISHED COPPER | 45| 4 +Brand#23 |PROMO BURNISHED NICKEL | 3| 4 +Brand#23 |PROMO BURNISHED NICKEL | 14| 4 +Brand#23 |PROMO BURNISHED NICKEL | 36| 4 +Brand#23 |PROMO BURNISHED NICKEL | 45| 4 +Brand#23 |PROMO BURNISHED STEEL | 19| 4 +Brand#23 |PROMO BURNISHED STEEL | 36| 4 +Brand#23 |PROMO BURNISHED STEEL | 49| 4 +Brand#23 |PROMO BURNISHED TIN | 19| 4 +Brand#23 |PROMO BURNISHED TIN | 23| 4 +Brand#23 |PROMO PLATED BRASS | 9| 4 +Brand#23 |PROMO PLATED BRASS | 36| 4 +Brand#23 |PROMO PLATED BRASS | 45| 4 +Brand#23 |PROMO PLATED COPPER | 3| 4 +Brand#23 |PROMO PLATED COPPER | 9| 4 +Brand#23 |PROMO PLATED COPPER | 19| 4 +Brand#23 |PROMO PLATED COPPER | 49| 4 +Brand#23 |PROMO PLATED NICKEL | 14| 4 +Brand#23 |PROMO PLATED NICKEL | 19| 4 +Brand#23 |PROMO PLATED NICKEL | 49| 4 +Brand#23 |PROMO PLATED STEEL | 36| 4 +Brand#23 |PROMO PLATED TIN | 49| 4 +Brand#23 |PROMO POLISHED BRASS | 3| 4 +Brand#23 |PROMO POLISHED BRASS | 23| 4 +Brand#23 |PROMO POLISHED BRASS | 36| 4 +Brand#23 |PROMO POLISHED BRASS | 49| 4 +Brand#23 |PROMO POLISHED COPPER | 3| 4 +Brand#23 |PROMO POLISHED COPPER | 14| 4 +Brand#23 |PROMO POLISHED COPPER | 19| 4 +Brand#23 |PROMO POLISHED COPPER | 49| 4 +Brand#23 |PROMO POLISHED NICKEL | 14| 4 +Brand#23 |PROMO POLISHED NICKEL | 49| 4 +Brand#23 |PROMO POLISHED STEEL | 9| 4 +Brand#23 |PROMO POLISHED STEEL | 36| 4 +Brand#23 |PROMO POLISHED STEEL | 45| 4 +Brand#23 |PROMO POLISHED TIN | 3| 4 +Brand#23 |PROMO POLISHED TIN | 9| 4 +Brand#23 |PROMO POLISHED TIN | 19| 4 +Brand#23 |SMALL ANODIZED BRASS | 3| 4 +Brand#23 |SMALL ANODIZED BRASS | 9| 4 +Brand#23 |SMALL ANODIZED COPPER | 3| 4 +Brand#23 |SMALL ANODIZED COPPER | 9| 4 +Brand#23 |SMALL ANODIZED COPPER | 23| 4 +Brand#23 |SMALL ANODIZED COPPER | 49| 4 +Brand#23 |SMALL ANODIZED NICKEL | 3| 4 +Brand#23 |SMALL ANODIZED NICKEL | 9| 4 +Brand#23 |SMALL ANODIZED NICKEL | 19| 4 +Brand#23 |SMALL ANODIZED STEEL | 9| 4 +Brand#23 |SMALL ANODIZED STEEL | 19| 4 +Brand#23 |SMALL ANODIZED STEEL | 36| 4 +Brand#23 |SMALL ANODIZED TIN | 14| 4 +Brand#23 |SMALL ANODIZED TIN | 19| 4 +Brand#23 |SMALL ANODIZED TIN | 23| 4 +Brand#23 |SMALL ANODIZED TIN | 49| 4 +Brand#23 |SMALL BRUSHED BRASS | 3| 4 +Brand#23 |SMALL BRUSHED BRASS | 14| 4 +Brand#23 |SMALL BRUSHED BRASS | 36| 4 +Brand#23 |SMALL BRUSHED COPPER | 3| 4 +Brand#23 |SMALL BRUSHED COPPER | 14| 4 +Brand#23 |SMALL BRUSHED COPPER | 36| 4 +Brand#23 |SMALL BRUSHED COPPER | 49| 4 +Brand#23 |SMALL BRUSHED NICKEL | 19| 4 +Brand#23 |SMALL BRUSHED NICKEL | 36| 4 +Brand#23 |SMALL BRUSHED NICKEL | 45| 4 +Brand#23 |SMALL BRUSHED STEEL | 9| 4 +Brand#23 |SMALL BRUSHED STEEL | 14| 4 +Brand#23 |SMALL BRUSHED STEEL | 19| 4 +Brand#23 |SMALL BRUSHED TIN | 9| 4 +Brand#23 |SMALL BRUSHED TIN | 19| 4 +Brand#23 |SMALL BRUSHED TIN | 23| 4 +Brand#23 |SMALL BRUSHED TIN | 36| 4 +Brand#23 |SMALL BRUSHED TIN | 49| 4 +Brand#23 |SMALL BURNISHED BRASS | 36| 4 +Brand#23 |SMALL BURNISHED COPPER | 3| 4 +Brand#23 |SMALL BURNISHED COPPER | 9| 4 +Brand#23 |SMALL BURNISHED COPPER | 19| 4 +Brand#23 |SMALL BURNISHED COPPER | 49| 4 +Brand#23 |SMALL BURNISHED NICKEL | 19| 4 +Brand#23 |SMALL BURNISHED NICKEL | 23| 4 +Brand#23 |SMALL BURNISHED STEEL | 3| 4 +Brand#23 |SMALL BURNISHED STEEL | 36| 4 +Brand#23 |SMALL BURNISHED TIN | 9| 4 +Brand#23 |SMALL BURNISHED TIN | 19| 4 +Brand#23 |SMALL BURNISHED TIN | 23| 4 +Brand#23 |SMALL BURNISHED TIN | 49| 4 +Brand#23 |SMALL PLATED BRASS | 14| 4 +Brand#23 |SMALL PLATED BRASS | 19| 4 +Brand#23 |SMALL PLATED BRASS | 23| 4 +Brand#23 |SMALL PLATED BRASS | 36| 4 +Brand#23 |SMALL PLATED COPPER | 9| 4 +Brand#23 |SMALL PLATED COPPER | 19| 4 +Brand#23 |SMALL PLATED COPPER | 23| 4 +Brand#23 |SMALL PLATED NICKEL | 14| 4 +Brand#23 |SMALL PLATED NICKEL | 19| 4 +Brand#23 |SMALL PLATED NICKEL | 49| 4 +Brand#23 |SMALL PLATED STEEL | 3| 4 +Brand#23 |SMALL PLATED STEEL | 45| 4 +Brand#23 |SMALL PLATED TIN | 36| 4 +Brand#23 |SMALL POLISHED BRASS | 9| 4 +Brand#23 |SMALL POLISHED BRASS | 14| 4 +Brand#23 |SMALL POLISHED BRASS | 23| 4 +Brand#23 |SMALL POLISHED COPPER | 14| 4 +Brand#23 |SMALL POLISHED COPPER | 23| 4 +Brand#23 |SMALL POLISHED COPPER | 36| 4 +Brand#23 |SMALL POLISHED COPPER | 45| 4 +Brand#23 |SMALL POLISHED STEEL | 3| 4 +Brand#23 |SMALL POLISHED STEEL | 9| 4 +Brand#23 |SMALL POLISHED STEEL | 14| 4 +Brand#23 |SMALL POLISHED STEEL | 45| 4 +Brand#23 |SMALL POLISHED STEEL | 49| 4 +Brand#23 |SMALL POLISHED TIN | 9| 4 +Brand#23 |SMALL POLISHED TIN | 14| 4 +Brand#23 |SMALL POLISHED TIN | 36| 4 +Brand#23 |SMALL POLISHED TIN | 45| 4 +Brand#23 |STANDARD ANODIZED BRASS | 3| 4 +Brand#23 |STANDARD ANODIZED BRASS | 9| 4 +Brand#23 |STANDARD ANODIZED BRASS | 14| 4 +Brand#23 |STANDARD ANODIZED BRASS | 45| 4 +Brand#23 |STANDARD ANODIZED COPPER | 9| 4 +Brand#23 |STANDARD ANODIZED COPPER | 49| 4 +Brand#23 |STANDARD ANODIZED NICKEL | 3| 4 +Brand#23 |STANDARD ANODIZED NICKEL | 36| 4 +Brand#23 |STANDARD ANODIZED NICKEL | 45| 4 +Brand#23 |STANDARD ANODIZED NICKEL | 49| 4 +Brand#23 |STANDARD ANODIZED STEEL | 3| 4 +Brand#23 |STANDARD ANODIZED STEEL | 36| 4 +Brand#23 |STANDARD ANODIZED TIN | 36| 4 +Brand#23 |STANDARD BRUSHED BRASS | 14| 4 +Brand#23 |STANDARD BRUSHED BRASS | 23| 4 +Brand#23 |STANDARD BRUSHED BRASS | 45| 4 +Brand#23 |STANDARD BRUSHED BRASS | 49| 4 +Brand#23 |STANDARD BRUSHED COPPER | 3| 4 +Brand#23 |STANDARD BRUSHED COPPER | 19| 4 +Brand#23 |STANDARD BRUSHED COPPER | 23| 4 +Brand#23 |STANDARD BRUSHED COPPER | 45| 4 +Brand#23 |STANDARD BRUSHED STEEL | 3| 4 +Brand#23 |STANDARD BRUSHED STEEL | 23| 4 +Brand#23 |STANDARD BRUSHED TIN | 9| 4 +Brand#23 |STANDARD BRUSHED TIN | 23| 4 +Brand#23 |STANDARD BURNISHED BRASS | 14| 4 +Brand#23 |STANDARD BURNISHED BRASS | 19| 4 +Brand#23 |STANDARD BURNISHED BRASS | 23| 4 +Brand#23 |STANDARD BURNISHED BRASS | 49| 4 +Brand#23 |STANDARD BURNISHED COPPER| 9| 4 +Brand#23 |STANDARD BURNISHED COPPER| 14| 4 +Brand#23 |STANDARD BURNISHED COPPER| 23| 4 +Brand#23 |STANDARD BURNISHED NICKEL| 3| 4 +Brand#23 |STANDARD BURNISHED NICKEL| 14| 4 +Brand#23 |STANDARD BURNISHED NICKEL| 19| 4 +Brand#23 |STANDARD BURNISHED STEEL | 3| 4 +Brand#23 |STANDARD BURNISHED STEEL | 14| 4 +Brand#23 |STANDARD BURNISHED STEEL | 19| 4 +Brand#23 |STANDARD BURNISHED TIN | 3| 4 +Brand#23 |STANDARD BURNISHED TIN | 23| 4 +Brand#23 |STANDARD PLATED BRASS | 14| 4 +Brand#23 |STANDARD PLATED BRASS | 45| 4 +Brand#23 |STANDARD PLATED COPPER | 9| 4 +Brand#23 |STANDARD PLATED COPPER | 19| 4 +Brand#23 |STANDARD PLATED NICKEL | 9| 4 +Brand#23 |STANDARD PLATED NICKEL | 45| 4 +Brand#23 |STANDARD PLATED STEEL | 23| 4 +Brand#23 |STANDARD PLATED TIN | 49| 4 +Brand#23 |STANDARD POLISHED BRASS | 3| 4 +Brand#23 |STANDARD POLISHED BRASS | 14| 4 +Brand#23 |STANDARD POLISHED BRASS | 23| 4 +Brand#23 |STANDARD POLISHED COPPER | 3| 4 +Brand#23 |STANDARD POLISHED COPPER | 9| 4 +Brand#23 |STANDARD POLISHED COPPER | 49| 4 +Brand#23 |STANDARD POLISHED NICKEL | 19| 4 +Brand#23 |STANDARD POLISHED NICKEL | 23| 4 +Brand#23 |STANDARD POLISHED NICKEL | 45| 4 +Brand#23 |STANDARD POLISHED NICKEL | 49| 4 +Brand#23 |STANDARD POLISHED STEEL | 3| 4 +Brand#23 |STANDARD POLISHED STEEL | 9| 4 +Brand#23 |STANDARD POLISHED STEEL | 19| 4 +Brand#23 |STANDARD POLISHED STEEL | 36| 4 +Brand#23 |STANDARD POLISHED STEEL | 45| 4 +Brand#23 |STANDARD POLISHED STEEL | 49| 4 +Brand#23 |STANDARD POLISHED TIN | 9| 4 +Brand#23 |STANDARD POLISHED TIN | 14| 4 +Brand#23 |STANDARD POLISHED TIN | 49| 4 +Brand#24 |ECONOMY ANODIZED BRASS | 9| 4 +Brand#24 |ECONOMY ANODIZED BRASS | 14| 4 +Brand#24 |ECONOMY ANODIZED BRASS | 36| 4 +Brand#24 |ECONOMY ANODIZED BRASS | 45| 4 +Brand#24 |ECONOMY ANODIZED BRASS | 49| 4 +Brand#24 |ECONOMY ANODIZED COPPER | 19| 4 +Brand#24 |ECONOMY ANODIZED COPPER | 45| 4 +Brand#24 |ECONOMY ANODIZED NICKEL | 23| 4 +Brand#24 |ECONOMY ANODIZED NICKEL | 45| 4 +Brand#24 |ECONOMY ANODIZED NICKEL | 49| 4 +Brand#24 |ECONOMY ANODIZED STEEL | 9| 4 +Brand#24 |ECONOMY ANODIZED TIN | 9| 4 +Brand#24 |ECONOMY ANODIZED TIN | 49| 4 +Brand#24 |ECONOMY BRUSHED BRASS | 36| 4 +Brand#24 |ECONOMY BRUSHED BRASS | 45| 4 +Brand#24 |ECONOMY BRUSHED BRASS | 49| 4 +Brand#24 |ECONOMY BRUSHED COPPER | 9| 4 +Brand#24 |ECONOMY BRUSHED COPPER | 19| 4 +Brand#24 |ECONOMY BRUSHED COPPER | 45| 4 +Brand#24 |ECONOMY BRUSHED COPPER | 49| 4 +Brand#24 |ECONOMY BRUSHED NICKEL | 14| 4 +Brand#24 |ECONOMY BRUSHED NICKEL | 19| 4 +Brand#24 |ECONOMY BRUSHED STEEL | 3| 4 +Brand#24 |ECONOMY BRUSHED STEEL | 19| 4 +Brand#24 |ECONOMY BRUSHED STEEL | 45| 4 +Brand#24 |ECONOMY BRUSHED TIN | 3| 4 +Brand#24 |ECONOMY BRUSHED TIN | 19| 4 +Brand#24 |ECONOMY BRUSHED TIN | 23| 4 +Brand#24 |ECONOMY BRUSHED TIN | 45| 4 +Brand#24 |ECONOMY BURNISHED BRASS | 3| 4 +Brand#24 |ECONOMY BURNISHED BRASS | 9| 4 +Brand#24 |ECONOMY BURNISHED BRASS | 36| 4 +Brand#24 |ECONOMY BURNISHED BRASS | 45| 4 +Brand#24 |ECONOMY BURNISHED COPPER | 9| 4 +Brand#24 |ECONOMY BURNISHED COPPER | 36| 4 +Brand#24 |ECONOMY BURNISHED NICKEL | 23| 4 +Brand#24 |ECONOMY BURNISHED NICKEL | 36| 4 +Brand#24 |ECONOMY BURNISHED NICKEL | 45| 4 +Brand#24 |ECONOMY BURNISHED NICKEL | 49| 4 +Brand#24 |ECONOMY BURNISHED STEEL | 14| 4 +Brand#24 |ECONOMY BURNISHED STEEL | 23| 4 +Brand#24 |ECONOMY BURNISHED TIN | 3| 4 +Brand#24 |ECONOMY BURNISHED TIN | 9| 4 +Brand#24 |ECONOMY BURNISHED TIN | 19| 4 +Brand#24 |ECONOMY BURNISHED TIN | 45| 4 +Brand#24 |ECONOMY PLATED BRASS | 3| 4 +Brand#24 |ECONOMY PLATED BRASS | 9| 4 +Brand#24 |ECONOMY PLATED BRASS | 23| 4 +Brand#24 |ECONOMY PLATED BRASS | 45| 4 +Brand#24 |ECONOMY PLATED COPPER | 3| 4 +Brand#24 |ECONOMY PLATED COPPER | 14| 4 +Brand#24 |ECONOMY PLATED COPPER | 23| 4 +Brand#24 |ECONOMY PLATED NICKEL | 45| 4 +Brand#24 |ECONOMY PLATED NICKEL | 49| 4 +Brand#24 |ECONOMY PLATED STEEL | 3| 4 +Brand#24 |ECONOMY PLATED STEEL | 23| 4 +Brand#24 |ECONOMY PLATED TIN | 14| 4 +Brand#24 |ECONOMY PLATED TIN | 19| 4 +Brand#24 |ECONOMY PLATED TIN | 23| 4 +Brand#24 |ECONOMY PLATED TIN | 45| 4 +Brand#24 |ECONOMY POLISHED BRASS | 19| 4 +Brand#24 |ECONOMY POLISHED BRASS | 49| 4 +Brand#24 |ECONOMY POLISHED COPPER | 9| 4 +Brand#24 |ECONOMY POLISHED COPPER | 14| 4 +Brand#24 |ECONOMY POLISHED COPPER | 45| 4 +Brand#24 |ECONOMY POLISHED NICKEL | 9| 4 +Brand#24 |ECONOMY POLISHED NICKEL | 19| 4 +Brand#24 |ECONOMY POLISHED NICKEL | 45| 4 +Brand#24 |ECONOMY POLISHED NICKEL | 49| 4 +Brand#24 |ECONOMY POLISHED STEEL | 19| 4 +Brand#24 |ECONOMY POLISHED STEEL | 45| 4 +Brand#24 |ECONOMY POLISHED STEEL | 49| 4 +Brand#24 |ECONOMY POLISHED TIN | 3| 4 +Brand#24 |LARGE ANODIZED BRASS | 14| 4 +Brand#24 |LARGE ANODIZED BRASS | 19| 4 +Brand#24 |LARGE ANODIZED BRASS | 49| 4 +Brand#24 |LARGE ANODIZED COPPER | 3| 4 +Brand#24 |LARGE ANODIZED COPPER | 9| 4 +Brand#24 |LARGE ANODIZED COPPER | 36| 4 +Brand#24 |LARGE ANODIZED COPPER | 49| 4 +Brand#24 |LARGE ANODIZED NICKEL | 9| 4 +Brand#24 |LARGE ANODIZED NICKEL | 19| 4 +Brand#24 |LARGE ANODIZED NICKEL | 36| 4 +Brand#24 |LARGE ANODIZED NICKEL | 45| 4 +Brand#24 |LARGE ANODIZED STEEL | 9| 4 +Brand#24 |LARGE ANODIZED STEEL | 36| 4 +Brand#24 |LARGE ANODIZED TIN | 14| 4 +Brand#24 |LARGE ANODIZED TIN | 36| 4 +Brand#24 |LARGE ANODIZED TIN | 45| 4 +Brand#24 |LARGE BRUSHED BRASS | 3| 4 +Brand#24 |LARGE BRUSHED BRASS | 23| 4 +Brand#24 |LARGE BRUSHED COPPER | 23| 4 +Brand#24 |LARGE BRUSHED COPPER | 36| 4 +Brand#24 |LARGE BRUSHED COPPER | 45| 4 +Brand#24 |LARGE BRUSHED NICKEL | 9| 4 +Brand#24 |LARGE BRUSHED NICKEL | 19| 4 +Brand#24 |LARGE BRUSHED NICKEL | 23| 4 +Brand#24 |LARGE BRUSHED STEEL | 14| 4 +Brand#24 |LARGE BRUSHED STEEL | 36| 4 +Brand#24 |LARGE BRUSHED TIN | 3| 4 +Brand#24 |LARGE BRUSHED TIN | 14| 4 +Brand#24 |LARGE BRUSHED TIN | 19| 4 +Brand#24 |LARGE BURNISHED BRASS | 19| 4 +Brand#24 |LARGE BURNISHED BRASS | 49| 4 +Brand#24 |LARGE BURNISHED COPPER | 9| 4 +Brand#24 |LARGE BURNISHED COPPER | 14| 4 +Brand#24 |LARGE BURNISHED COPPER | 19| 4 +Brand#24 |LARGE BURNISHED COPPER | 23| 4 +Brand#24 |LARGE BURNISHED COPPER | 45| 4 +Brand#24 |LARGE BURNISHED NICKEL | 3| 4 +Brand#24 |LARGE BURNISHED NICKEL | 9| 4 +Brand#24 |LARGE BURNISHED NICKEL | 23| 4 +Brand#24 |LARGE BURNISHED NICKEL | 45| 4 +Brand#24 |LARGE BURNISHED STEEL | 9| 4 +Brand#24 |LARGE BURNISHED STEEL | 49| 4 +Brand#24 |LARGE BURNISHED TIN | 3| 4 +Brand#24 |LARGE BURNISHED TIN | 19| 4 +Brand#24 |LARGE BURNISHED TIN | 36| 4 +Brand#24 |LARGE PLATED BRASS | 3| 4 +Brand#24 |LARGE PLATED BRASS | 14| 4 +Brand#24 |LARGE PLATED BRASS | 36| 4 +Brand#24 |LARGE PLATED BRASS | 45| 4 +Brand#24 |LARGE PLATED COPPER | 36| 4 +Brand#24 |LARGE PLATED NICKEL | 3| 4 +Brand#24 |LARGE PLATED NICKEL | 9| 4 +Brand#24 |LARGE PLATED NICKEL | 23| 4 +Brand#24 |LARGE PLATED NICKEL | 36| 4 +Brand#24 |LARGE PLATED NICKEL | 45| 4 +Brand#24 |LARGE PLATED STEEL | 9| 4 +Brand#24 |LARGE PLATED STEEL | 14| 4 +Brand#24 |LARGE PLATED STEEL | 23| 4 +Brand#24 |LARGE PLATED STEEL | 49| 4 +Brand#24 |LARGE PLATED TIN | 36| 4 +Brand#24 |LARGE PLATED TIN | 49| 4 +Brand#24 |LARGE POLISHED BRASS | 9| 4 +Brand#24 |LARGE POLISHED BRASS | 19| 4 +Brand#24 |LARGE POLISHED BRASS | 23| 4 +Brand#24 |LARGE POLISHED BRASS | 49| 4 +Brand#24 |LARGE POLISHED COPPER | 3| 4 +Brand#24 |LARGE POLISHED COPPER | 19| 4 +Brand#24 |LARGE POLISHED COPPER | 36| 4 +Brand#24 |LARGE POLISHED COPPER | 49| 4 +Brand#24 |LARGE POLISHED NICKEL | 3| 4 +Brand#24 |LARGE POLISHED NICKEL | 14| 4 +Brand#24 |LARGE POLISHED STEEL | 14| 4 +Brand#24 |LARGE POLISHED TIN | 3| 4 +Brand#24 |LARGE POLISHED TIN | 9| 4 +Brand#24 |LARGE POLISHED TIN | 19| 4 +Brand#24 |LARGE POLISHED TIN | 36| 4 +Brand#24 |LARGE POLISHED TIN | 45| 4 +Brand#24 |MEDIUM ANODIZED BRASS | 3| 4 +Brand#24 |MEDIUM ANODIZED BRASS | 9| 4 +Brand#24 |MEDIUM ANODIZED BRASS | 19| 4 +Brand#24 |MEDIUM ANODIZED BRASS | 23| 4 +Brand#24 |MEDIUM ANODIZED BRASS | 36| 4 +Brand#24 |MEDIUM ANODIZED COPPER | 36| 4 +Brand#24 |MEDIUM ANODIZED NICKEL | 19| 4 +Brand#24 |MEDIUM ANODIZED NICKEL | 45| 4 +Brand#24 |MEDIUM ANODIZED NICKEL | 49| 4 +Brand#24 |MEDIUM ANODIZED STEEL | 3| 4 +Brand#24 |MEDIUM ANODIZED STEEL | 14| 4 +Brand#24 |MEDIUM ANODIZED STEEL | 36| 4 +Brand#24 |MEDIUM ANODIZED STEEL | 45| 4 +Brand#24 |MEDIUM ANODIZED TIN | 9| 4 +Brand#24 |MEDIUM ANODIZED TIN | 19| 4 +Brand#24 |MEDIUM ANODIZED TIN | 23| 4 +Brand#24 |MEDIUM ANODIZED TIN | 36| 4 +Brand#24 |MEDIUM ANODIZED TIN | 45| 4 +Brand#24 |MEDIUM ANODIZED TIN | 49| 4 +Brand#24 |MEDIUM BRUSHED BRASS | 9| 4 +Brand#24 |MEDIUM BRUSHED BRASS | 14| 4 +Brand#24 |MEDIUM BRUSHED BRASS | 23| 4 +Brand#24 |MEDIUM BRUSHED BRASS | 36| 4 +Brand#24 |MEDIUM BRUSHED COPPER | 9| 4 +Brand#24 |MEDIUM BRUSHED COPPER | 45| 4 +Brand#24 |MEDIUM BRUSHED NICKEL | 3| 4 +Brand#24 |MEDIUM BRUSHED NICKEL | 23| 4 +Brand#24 |MEDIUM BRUSHED STEEL | 3| 4 +Brand#24 |MEDIUM BRUSHED STEEL | 9| 4 +Brand#24 |MEDIUM BRUSHED STEEL | 14| 4 +Brand#24 |MEDIUM BRUSHED STEEL | 45| 4 +Brand#24 |MEDIUM BRUSHED TIN | 19| 4 +Brand#24 |MEDIUM BRUSHED TIN | 36| 4 +Brand#24 |MEDIUM BRUSHED TIN | 45| 4 +Brand#24 |MEDIUM BURNISHED BRASS | 3| 4 +Brand#24 |MEDIUM BURNISHED BRASS | 14| 4 +Brand#24 |MEDIUM BURNISHED BRASS | 19| 4 +Brand#24 |MEDIUM BURNISHED BRASS | 45| 4 +Brand#24 |MEDIUM BURNISHED COPPER | 36| 4 +Brand#24 |MEDIUM BURNISHED COPPER | 45| 4 +Brand#24 |MEDIUM BURNISHED NICKEL | 3| 4 +Brand#24 |MEDIUM BURNISHED NICKEL | 9| 4 +Brand#24 |MEDIUM BURNISHED NICKEL | 14| 4 +Brand#24 |MEDIUM BURNISHED NICKEL | 19| 4 +Brand#24 |MEDIUM BURNISHED STEEL | 9| 4 +Brand#24 |MEDIUM BURNISHED STEEL | 14| 4 +Brand#24 |MEDIUM BURNISHED STEEL | 45| 4 +Brand#24 |MEDIUM BURNISHED TIN | 3| 4 +Brand#24 |MEDIUM BURNISHED TIN | 19| 4 +Brand#24 |MEDIUM BURNISHED TIN | 45| 4 +Brand#24 |MEDIUM BURNISHED TIN | 49| 4 +Brand#24 |MEDIUM PLATED BRASS | 9| 4 +Brand#24 |MEDIUM PLATED BRASS | 14| 4 +Brand#24 |MEDIUM PLATED COPPER | 14| 4 +Brand#24 |MEDIUM PLATED COPPER | 36| 4 +Brand#24 |MEDIUM PLATED NICKEL | 14| 4 +Brand#24 |MEDIUM PLATED NICKEL | 23| 4 +Brand#24 |MEDIUM PLATED NICKEL | 49| 4 +Brand#24 |MEDIUM PLATED STEEL | 3| 4 +Brand#24 |MEDIUM PLATED STEEL | 23| 4 +Brand#24 |MEDIUM PLATED TIN | 3| 4 +Brand#24 |MEDIUM PLATED TIN | 9| 4 +Brand#24 |MEDIUM PLATED TIN | 14| 4 +Brand#24 |MEDIUM PLATED TIN | 19| 4 +Brand#24 |MEDIUM PLATED TIN | 23| 4 +Brand#24 |MEDIUM PLATED TIN | 36| 4 +Brand#24 |MEDIUM PLATED TIN | 45| 4 +Brand#24 |MEDIUM PLATED TIN | 49| 4 +Brand#24 |PROMO ANODIZED BRASS | 9| 4 +Brand#24 |PROMO ANODIZED BRASS | 14| 4 +Brand#24 |PROMO ANODIZED BRASS | 19| 4 +Brand#24 |PROMO ANODIZED BRASS | 23| 4 +Brand#24 |PROMO ANODIZED BRASS | 36| 4 +Brand#24 |PROMO ANODIZED BRASS | 45| 4 +Brand#24 |PROMO ANODIZED BRASS | 49| 4 +Brand#24 |PROMO ANODIZED COPPER | 14| 4 +Brand#24 |PROMO ANODIZED COPPER | 23| 4 +Brand#24 |PROMO ANODIZED COPPER | 49| 4 +Brand#24 |PROMO ANODIZED NICKEL | 9| 4 +Brand#24 |PROMO ANODIZED NICKEL | 23| 4 +Brand#24 |PROMO ANODIZED NICKEL | 49| 4 +Brand#24 |PROMO ANODIZED STEEL | 3| 4 +Brand#24 |PROMO ANODIZED STEEL | 14| 4 +Brand#24 |PROMO ANODIZED STEEL | 49| 4 +Brand#24 |PROMO ANODIZED TIN | 36| 4 +Brand#24 |PROMO ANODIZED TIN | 45| 4 +Brand#24 |PROMO BRUSHED BRASS | 3| 4 +Brand#24 |PROMO BRUSHED BRASS | 9| 4 +Brand#24 |PROMO BRUSHED BRASS | 36| 4 +Brand#24 |PROMO BRUSHED BRASS | 45| 4 +Brand#24 |PROMO BRUSHED BRASS | 49| 4 +Brand#24 |PROMO BRUSHED COPPER | 9| 4 +Brand#24 |PROMO BRUSHED COPPER | 36| 4 +Brand#24 |PROMO BRUSHED NICKEL | 23| 4 +Brand#24 |PROMO BRUSHED STEEL | 9| 4 +Brand#24 |PROMO BRUSHED STEEL | 14| 4 +Brand#24 |PROMO BRUSHED STEEL | 36| 4 +Brand#24 |PROMO BRUSHED STEEL | 45| 4 +Brand#24 |PROMO BRUSHED STEEL | 49| 4 +Brand#24 |PROMO BRUSHED TIN | 19| 4 +Brand#24 |PROMO BRUSHED TIN | 23| 4 +Brand#24 |PROMO BRUSHED TIN | 45| 4 +Brand#24 |PROMO BRUSHED TIN | 49| 4 +Brand#24 |PROMO BURNISHED BRASS | 3| 4 +Brand#24 |PROMO BURNISHED BRASS | 9| 4 +Brand#24 |PROMO BURNISHED BRASS | 19| 4 +Brand#24 |PROMO BURNISHED BRASS | 45| 4 +Brand#24 |PROMO BURNISHED COPPER | 3| 4 +Brand#24 |PROMO BURNISHED COPPER | 9| 4 +Brand#24 |PROMO BURNISHED COPPER | 14| 4 +Brand#24 |PROMO BURNISHED COPPER | 19| 4 +Brand#24 |PROMO BURNISHED COPPER | 23| 4 +Brand#24 |PROMO BURNISHED COPPER | 36| 4 +Brand#24 |PROMO BURNISHED NICKEL | 9| 4 +Brand#24 |PROMO BURNISHED NICKEL | 49| 4 +Brand#24 |PROMO BURNISHED TIN | 3| 4 +Brand#24 |PROMO BURNISHED TIN | 9| 4 +Brand#24 |PROMO BURNISHED TIN | 36| 4 +Brand#24 |PROMO PLATED BRASS | 14| 4 +Brand#24 |PROMO PLATED COPPER | 19| 4 +Brand#24 |PROMO PLATED COPPER | 23| 4 +Brand#24 |PROMO PLATED NICKEL | 3| 4 +Brand#24 |PROMO PLATED NICKEL | 19| 4 +Brand#24 |PROMO PLATED NICKEL | 45| 4 +Brand#24 |PROMO PLATED NICKEL | 49| 4 +Brand#24 |PROMO PLATED STEEL | 19| 4 +Brand#24 |PROMO PLATED STEEL | 45| 4 +Brand#24 |PROMO PLATED TIN | 3| 4 +Brand#24 |PROMO PLATED TIN | 9| 4 +Brand#24 |PROMO PLATED TIN | 45| 4 +Brand#24 |PROMO POLISHED BRASS | 23| 4 +Brand#24 |PROMO POLISHED BRASS | 49| 4 +Brand#24 |PROMO POLISHED COPPER | 36| 4 +Brand#24 |PROMO POLISHED NICKEL | 3| 4 +Brand#24 |PROMO POLISHED NICKEL | 14| 4 +Brand#24 |PROMO POLISHED NICKEL | 19| 4 +Brand#24 |PROMO POLISHED NICKEL | 23| 4 +Brand#24 |PROMO POLISHED STEEL | 3| 4 +Brand#24 |PROMO POLISHED STEEL | 19| 4 +Brand#24 |PROMO POLISHED STEEL | 45| 4 +Brand#24 |PROMO POLISHED STEEL | 49| 4 +Brand#24 |PROMO POLISHED TIN | 19| 4 +Brand#24 |PROMO POLISHED TIN | 23| 4 +Brand#24 |PROMO POLISHED TIN | 36| 4 +Brand#24 |PROMO POLISHED TIN | 49| 4 +Brand#24 |SMALL ANODIZED BRASS | 3| 4 +Brand#24 |SMALL ANODIZED BRASS | 9| 4 +Brand#24 |SMALL ANODIZED BRASS | 36| 4 +Brand#24 |SMALL ANODIZED BRASS | 45| 4 +Brand#24 |SMALL ANODIZED BRASS | 49| 4 +Brand#24 |SMALL ANODIZED COPPER | 14| 4 +Brand#24 |SMALL ANODIZED COPPER | 23| 4 +Brand#24 |SMALL ANODIZED COPPER | 49| 4 +Brand#24 |SMALL ANODIZED NICKEL | 3| 4 +Brand#24 |SMALL ANODIZED NICKEL | 14| 4 +Brand#24 |SMALL ANODIZED NICKEL | 36| 4 +Brand#24 |SMALL ANODIZED STEEL | 14| 4 +Brand#24 |SMALL ANODIZED STEEL | 36| 4 +Brand#24 |SMALL ANODIZED TIN | 3| 4 +Brand#24 |SMALL ANODIZED TIN | 19| 4 +Brand#24 |SMALL ANODIZED TIN | 49| 4 +Brand#24 |SMALL BRUSHED BRASS | 14| 4 +Brand#24 |SMALL BRUSHED BRASS | 49| 4 +Brand#24 |SMALL BRUSHED COPPER | 36| 4 +Brand#24 |SMALL BRUSHED COPPER | 45| 4 +Brand#24 |SMALL BRUSHED COPPER | 49| 4 +Brand#24 |SMALL BRUSHED NICKEL | 3| 4 +Brand#24 |SMALL BRUSHED NICKEL | 9| 4 +Brand#24 |SMALL BRUSHED NICKEL | 14| 4 +Brand#24 |SMALL BRUSHED NICKEL | 23| 4 +Brand#24 |SMALL BRUSHED NICKEL | 45| 4 +Brand#24 |SMALL BRUSHED STEEL | 3| 4 +Brand#24 |SMALL BRUSHED STEEL | 49| 4 +Brand#24 |SMALL BRUSHED TIN | 23| 4 +Brand#24 |SMALL BRUSHED TIN | 45| 4 +Brand#24 |SMALL BURNISHED BRASS | 9| 4 +Brand#24 |SMALL BURNISHED BRASS | 23| 4 +Brand#24 |SMALL BURNISHED BRASS | 45| 4 +Brand#24 |SMALL BURNISHED COPPER | 3| 4 +Brand#24 |SMALL BURNISHED COPPER | 9| 4 +Brand#24 |SMALL BURNISHED COPPER | 14| 4 +Brand#24 |SMALL BURNISHED NICKEL | 49| 4 +Brand#24 |SMALL BURNISHED STEEL | 3| 4 +Brand#24 |SMALL BURNISHED STEEL | 9| 4 +Brand#24 |SMALL BURNISHED STEEL | 14| 4 +Brand#24 |SMALL BURNISHED STEEL | 19| 4 +Brand#24 |SMALL BURNISHED STEEL | 45| 4 +Brand#24 |SMALL BURNISHED TIN | 3| 4 +Brand#24 |SMALL BURNISHED TIN | 19| 4 +Brand#24 |SMALL BURNISHED TIN | 36| 4 +Brand#24 |SMALL BURNISHED TIN | 49| 4 +Brand#24 |SMALL PLATED BRASS | 49| 4 +Brand#24 |SMALL PLATED COPPER | 9| 4 +Brand#24 |SMALL PLATED COPPER | 14| 4 +Brand#24 |SMALL PLATED COPPER | 36| 4 +Brand#24 |SMALL PLATED COPPER | 45| 4 +Brand#24 |SMALL PLATED COPPER | 49| 4 +Brand#24 |SMALL PLATED NICKEL | 9| 4 +Brand#24 |SMALL PLATED NICKEL | 19| 4 +Brand#24 |SMALL PLATED NICKEL | 23| 4 +Brand#24 |SMALL PLATED NICKEL | 36| 4 +Brand#24 |SMALL PLATED NICKEL | 45| 4 +Brand#24 |SMALL PLATED STEEL | 9| 4 +Brand#24 |SMALL PLATED STEEL | 45| 4 +Brand#24 |SMALL PLATED TIN | 19| 4 +Brand#24 |SMALL PLATED TIN | 36| 4 +Brand#24 |SMALL PLATED TIN | 49| 4 +Brand#24 |SMALL POLISHED BRASS | 19| 4 +Brand#24 |SMALL POLISHED BRASS | 36| 4 +Brand#24 |SMALL POLISHED BRASS | 45| 4 +Brand#24 |SMALL POLISHED BRASS | 49| 4 +Brand#24 |SMALL POLISHED COPPER | 9| 4 +Brand#24 |SMALL POLISHED COPPER | 14| 4 +Brand#24 |SMALL POLISHED COPPER | 19| 4 +Brand#24 |SMALL POLISHED COPPER | 49| 4 +Brand#24 |SMALL POLISHED NICKEL | 14| 4 +Brand#24 |SMALL POLISHED NICKEL | 23| 4 +Brand#24 |SMALL POLISHED STEEL | 23| 4 +Brand#24 |SMALL POLISHED STEEL | 36| 4 +Brand#24 |SMALL POLISHED TIN | 14| 4 +Brand#24 |SMALL POLISHED TIN | 23| 4 +Brand#24 |STANDARD ANODIZED BRASS | 9| 4 +Brand#24 |STANDARD ANODIZED BRASS | 19| 4 +Brand#24 |STANDARD ANODIZED BRASS | 45| 4 +Brand#24 |STANDARD ANODIZED COPPER | 3| 4 +Brand#24 |STANDARD ANODIZED COPPER | 9| 4 +Brand#24 |STANDARD ANODIZED COPPER | 23| 4 +Brand#24 |STANDARD ANODIZED COPPER | 36| 4 +Brand#24 |STANDARD ANODIZED COPPER | 45| 4 +Brand#24 |STANDARD ANODIZED COPPER | 49| 4 +Brand#24 |STANDARD ANODIZED NICKEL | 19| 4 +Brand#24 |STANDARD ANODIZED NICKEL | 23| 4 +Brand#24 |STANDARD ANODIZED NICKEL | 45| 4 +Brand#24 |STANDARD ANODIZED STEEL | 9| 4 +Brand#24 |STANDARD ANODIZED STEEL | 19| 4 +Brand#24 |STANDARD ANODIZED STEEL | 45| 4 +Brand#24 |STANDARD ANODIZED STEEL | 49| 4 +Brand#24 |STANDARD ANODIZED TIN | 9| 4 +Brand#24 |STANDARD ANODIZED TIN | 23| 4 +Brand#24 |STANDARD BRUSHED BRASS | 45| 4 +Brand#24 |STANDARD BRUSHED COPPER | 3| 4 +Brand#24 |STANDARD BRUSHED NICKEL | 9| 4 +Brand#24 |STANDARD BRUSHED NICKEL | 36| 4 +Brand#24 |STANDARD BRUSHED STEEL | 3| 4 +Brand#24 |STANDARD BRUSHED STEEL | 9| 4 +Brand#24 |STANDARD BRUSHED STEEL | 14| 4 +Brand#24 |STANDARD BRUSHED STEEL | 36| 4 +Brand#24 |STANDARD BRUSHED STEEL | 49| 4 +Brand#24 |STANDARD BRUSHED TIN | 9| 4 +Brand#24 |STANDARD BRUSHED TIN | 19| 4 +Brand#24 |STANDARD BRUSHED TIN | 45| 4 +Brand#24 |STANDARD BURNISHED BRASS | 3| 4 +Brand#24 |STANDARD BURNISHED BRASS | 9| 4 +Brand#24 |STANDARD BURNISHED BRASS | 19| 4 +Brand#24 |STANDARD BURNISHED BRASS | 23| 4 +Brand#24 |STANDARD BURNISHED BRASS | 49| 4 +Brand#24 |STANDARD BURNISHED COPPER| 9| 4 +Brand#24 |STANDARD BURNISHED COPPER| 14| 4 +Brand#24 |STANDARD BURNISHED COPPER| 36| 4 +Brand#24 |STANDARD BURNISHED NICKEL| 14| 4 +Brand#24 |STANDARD BURNISHED NICKEL| 45| 4 +Brand#24 |STANDARD BURNISHED NICKEL| 49| 4 +Brand#24 |STANDARD BURNISHED STEEL | 3| 4 +Brand#24 |STANDARD BURNISHED STEEL | 14| 4 +Brand#24 |STANDARD BURNISHED STEEL | 19| 4 +Brand#24 |STANDARD BURNISHED STEEL | 23| 4 +Brand#24 |STANDARD BURNISHED STEEL | 49| 4 +Brand#24 |STANDARD BURNISHED TIN | 9| 4 +Brand#24 |STANDARD BURNISHED TIN | 19| 4 +Brand#24 |STANDARD BURNISHED TIN | 36| 4 +Brand#24 |STANDARD BURNISHED TIN | 49| 4 +Brand#24 |STANDARD PLATED BRASS | 3| 4 +Brand#24 |STANDARD PLATED BRASS | 19| 4 +Brand#24 |STANDARD PLATED BRASS | 45| 4 +Brand#24 |STANDARD PLATED BRASS | 49| 4 +Brand#24 |STANDARD PLATED COPPER | 19| 4 +Brand#24 |STANDARD PLATED COPPER | 45| 4 +Brand#24 |STANDARD PLATED NICKEL | 49| 4 +Brand#24 |STANDARD PLATED STEEL | 23| 4 +Brand#24 |STANDARD PLATED STEEL | 36| 4 +Brand#24 |STANDARD PLATED TIN | 3| 4 +Brand#24 |STANDARD PLATED TIN | 14| 4 +Brand#24 |STANDARD PLATED TIN | 19| 4 +Brand#24 |STANDARD PLATED TIN | 23| 4 +Brand#24 |STANDARD POLISHED BRASS | 3| 4 +Brand#24 |STANDARD POLISHED BRASS | 19| 4 +Brand#24 |STANDARD POLISHED BRASS | 36| 4 +Brand#24 |STANDARD POLISHED COPPER | 19| 4 +Brand#24 |STANDARD POLISHED NICKEL | 19| 4 +Brand#24 |STANDARD POLISHED NICKEL | 36| 4 +Brand#24 |STANDARD POLISHED STEEL | 36| 4 +Brand#24 |STANDARD POLISHED STEEL | 45| 4 +Brand#24 |STANDARD POLISHED STEEL | 49| 4 +Brand#24 |STANDARD POLISHED TIN | 3| 4 +Brand#24 |STANDARD POLISHED TIN | 9| 4 +Brand#24 |STANDARD POLISHED TIN | 14| 4 +Brand#24 |STANDARD POLISHED TIN | 19| 4 +Brand#24 |STANDARD POLISHED TIN | 36| 4 +Brand#24 |STANDARD POLISHED TIN | 49| 4 +Brand#25 |ECONOMY ANODIZED BRASS | 9| 4 +Brand#25 |ECONOMY ANODIZED BRASS | 14| 4 +Brand#25 |ECONOMY ANODIZED BRASS | 23| 4 +Brand#25 |ECONOMY ANODIZED BRASS | 45| 4 +Brand#25 |ECONOMY ANODIZED COPPER | 3| 4 +Brand#25 |ECONOMY ANODIZED COPPER | 36| 4 +Brand#25 |ECONOMY ANODIZED COPPER | 45| 4 +Brand#25 |ECONOMY ANODIZED COPPER | 49| 4 +Brand#25 |ECONOMY ANODIZED NICKEL | 23| 4 +Brand#25 |ECONOMY ANODIZED NICKEL | 36| 4 +Brand#25 |ECONOMY ANODIZED NICKEL | 49| 4 +Brand#25 |ECONOMY ANODIZED STEEL | 9| 4 +Brand#25 |ECONOMY ANODIZED STEEL | 23| 4 +Brand#25 |ECONOMY ANODIZED TIN | 3| 4 +Brand#25 |ECONOMY ANODIZED TIN | 9| 4 +Brand#25 |ECONOMY ANODIZED TIN | 14| 4 +Brand#25 |ECONOMY ANODIZED TIN | 19| 4 +Brand#25 |ECONOMY ANODIZED TIN | 23| 4 +Brand#25 |ECONOMY ANODIZED TIN | 45| 4 +Brand#25 |ECONOMY BRUSHED BRASS | 9| 4 +Brand#25 |ECONOMY BRUSHED BRASS | 23| 4 +Brand#25 |ECONOMY BRUSHED BRASS | 49| 4 +Brand#25 |ECONOMY BRUSHED COPPER | 19| 4 +Brand#25 |ECONOMY BRUSHED COPPER | 23| 4 +Brand#25 |ECONOMY BRUSHED COPPER | 36| 4 +Brand#25 |ECONOMY BRUSHED COPPER | 49| 4 +Brand#25 |ECONOMY BRUSHED NICKEL | 19| 4 +Brand#25 |ECONOMY BRUSHED STEEL | 14| 4 +Brand#25 |ECONOMY BRUSHED STEEL | 23| 4 +Brand#25 |ECONOMY BRUSHED TIN | 19| 4 +Brand#25 |ECONOMY BRUSHED TIN | 36| 4 +Brand#25 |ECONOMY BURNISHED BRASS | 3| 4 +Brand#25 |ECONOMY BURNISHED BRASS | 23| 4 +Brand#25 |ECONOMY BURNISHED BRASS | 36| 4 +Brand#25 |ECONOMY BURNISHED BRASS | 45| 4 +Brand#25 |ECONOMY BURNISHED BRASS | 49| 4 +Brand#25 |ECONOMY BURNISHED COPPER | 3| 4 +Brand#25 |ECONOMY BURNISHED COPPER | 36| 4 +Brand#25 |ECONOMY BURNISHED NICKEL | 19| 4 +Brand#25 |ECONOMY BURNISHED NICKEL | 49| 4 +Brand#25 |ECONOMY BURNISHED STEEL | 14| 4 +Brand#25 |ECONOMY BURNISHED STEEL | 19| 4 +Brand#25 |ECONOMY BURNISHED STEEL | 23| 4 +Brand#25 |ECONOMY BURNISHED STEEL | 45| 4 +Brand#25 |ECONOMY BURNISHED TIN | 3| 4 +Brand#25 |ECONOMY BURNISHED TIN | 9| 4 +Brand#25 |ECONOMY BURNISHED TIN | 19| 4 +Brand#25 |ECONOMY BURNISHED TIN | 49| 4 +Brand#25 |ECONOMY PLATED BRASS | 9| 4 +Brand#25 |ECONOMY PLATED BRASS | 19| 4 +Brand#25 |ECONOMY PLATED BRASS | 36| 4 +Brand#25 |ECONOMY PLATED BRASS | 45| 4 +Brand#25 |ECONOMY PLATED BRASS | 49| 4 +Brand#25 |ECONOMY PLATED COPPER | 14| 4 +Brand#25 |ECONOMY PLATED COPPER | 23| 4 +Brand#25 |ECONOMY PLATED COPPER | 36| 4 +Brand#25 |ECONOMY PLATED COPPER | 49| 4 +Brand#25 |ECONOMY PLATED NICKEL | 3| 4 +Brand#25 |ECONOMY PLATED NICKEL | 9| 4 +Brand#25 |ECONOMY PLATED NICKEL | 23| 4 +Brand#25 |ECONOMY PLATED NICKEL | 49| 4 +Brand#25 |ECONOMY PLATED STEEL | 3| 4 +Brand#25 |ECONOMY PLATED STEEL | 14| 4 +Brand#25 |ECONOMY PLATED STEEL | 36| 4 +Brand#25 |ECONOMY PLATED STEEL | 45| 4 +Brand#25 |ECONOMY PLATED TIN | 9| 4 +Brand#25 |ECONOMY PLATED TIN | 23| 4 +Brand#25 |ECONOMY PLATED TIN | 45| 4 +Brand#25 |ECONOMY PLATED TIN | 49| 4 +Brand#25 |ECONOMY POLISHED BRASS | 14| 4 +Brand#25 |ECONOMY POLISHED BRASS | 23| 4 +Brand#25 |ECONOMY POLISHED BRASS | 49| 4 +Brand#25 |ECONOMY POLISHED COPPER | 19| 4 +Brand#25 |ECONOMY POLISHED COPPER | 45| 4 +Brand#25 |ECONOMY POLISHED COPPER | 49| 4 +Brand#25 |ECONOMY POLISHED NICKEL | 19| 4 +Brand#25 |ECONOMY POLISHED NICKEL | 36| 4 +Brand#25 |ECONOMY POLISHED NICKEL | 49| 4 +Brand#25 |ECONOMY POLISHED STEEL | 3| 4 +Brand#25 |ECONOMY POLISHED STEEL | 19| 4 +Brand#25 |ECONOMY POLISHED STEEL | 23| 4 +Brand#25 |ECONOMY POLISHED STEEL | 45| 4 +Brand#25 |ECONOMY POLISHED STEEL | 49| 4 +Brand#25 |ECONOMY POLISHED TIN | 3| 4 +Brand#25 |ECONOMY POLISHED TIN | 9| 4 +Brand#25 |ECONOMY POLISHED TIN | 14| 4 +Brand#25 |LARGE ANODIZED BRASS | 9| 4 +Brand#25 |LARGE ANODIZED BRASS | 19| 4 +Brand#25 |LARGE ANODIZED BRASS | 36| 4 +Brand#25 |LARGE ANODIZED BRASS | 49| 4 +Brand#25 |LARGE ANODIZED COPPER | 49| 4 +Brand#25 |LARGE ANODIZED NICKEL | 9| 4 +Brand#25 |LARGE ANODIZED NICKEL | 19| 4 +Brand#25 |LARGE ANODIZED NICKEL | 23| 4 +Brand#25 |LARGE ANODIZED NICKEL | 49| 4 +Brand#25 |LARGE ANODIZED STEEL | 19| 4 +Brand#25 |LARGE ANODIZED STEEL | 23| 4 +Brand#25 |LARGE ANODIZED STEEL | 36| 4 +Brand#25 |LARGE ANODIZED STEEL | 49| 4 +Brand#25 |LARGE ANODIZED TIN | 14| 4 +Brand#25 |LARGE ANODIZED TIN | 49| 4 +Brand#25 |LARGE BRUSHED BRASS | 14| 4 +Brand#25 |LARGE BRUSHED BRASS | 36| 4 +Brand#25 |LARGE BRUSHED BRASS | 45| 4 +Brand#25 |LARGE BRUSHED COPPER | 3| 4 +Brand#25 |LARGE BRUSHED COPPER | 9| 4 +Brand#25 |LARGE BRUSHED COPPER | 19| 4 +Brand#25 |LARGE BRUSHED COPPER | 23| 4 +Brand#25 |LARGE BRUSHED COPPER | 45| 4 +Brand#25 |LARGE BRUSHED COPPER | 49| 4 +Brand#25 |LARGE BRUSHED NICKEL | 3| 4 +Brand#25 |LARGE BRUSHED NICKEL | 23| 4 +Brand#25 |LARGE BRUSHED NICKEL | 45| 4 +Brand#25 |LARGE BRUSHED STEEL | 3| 4 +Brand#25 |LARGE BRUSHED STEEL | 9| 4 +Brand#25 |LARGE BRUSHED STEEL | 14| 4 +Brand#25 |LARGE BRUSHED TIN | 14| 4 +Brand#25 |LARGE BRUSHED TIN | 19| 4 +Brand#25 |LARGE BRUSHED TIN | 23| 4 +Brand#25 |LARGE BRUSHED TIN | 36| 4 +Brand#25 |LARGE BRUSHED TIN | 45| 4 +Brand#25 |LARGE BURNISHED BRASS | 19| 4 +Brand#25 |LARGE BURNISHED COPPER | 9| 4 +Brand#25 |LARGE BURNISHED COPPER | 49| 4 +Brand#25 |LARGE BURNISHED NICKEL | 3| 4 +Brand#25 |LARGE BURNISHED STEEL | 3| 4 +Brand#25 |LARGE BURNISHED STEEL | 9| 4 +Brand#25 |LARGE BURNISHED STEEL | 19| 4 +Brand#25 |LARGE BURNISHED STEEL | 49| 4 +Brand#25 |LARGE BURNISHED TIN | 19| 4 +Brand#25 |LARGE BURNISHED TIN | 45| 4 +Brand#25 |LARGE BURNISHED TIN | 49| 4 +Brand#25 |LARGE PLATED BRASS | 14| 4 +Brand#25 |LARGE PLATED BRASS | 45| 4 +Brand#25 |LARGE PLATED COPPER | 19| 4 +Brand#25 |LARGE PLATED COPPER | 23| 4 +Brand#25 |LARGE PLATED NICKEL | 3| 4 +Brand#25 |LARGE PLATED NICKEL | 9| 4 +Brand#25 |LARGE PLATED NICKEL | 14| 4 +Brand#25 |LARGE PLATED NICKEL | 19| 4 +Brand#25 |LARGE PLATED NICKEL | 23| 4 +Brand#25 |LARGE PLATED STEEL | 14| 4 +Brand#25 |LARGE PLATED STEEL | 36| 4 +Brand#25 |LARGE PLATED TIN | 14| 4 +Brand#25 |LARGE POLISHED BRASS | 3| 4 +Brand#25 |LARGE POLISHED BRASS | 45| 4 +Brand#25 |LARGE POLISHED BRASS | 49| 4 +Brand#25 |LARGE POLISHED COPPER | 3| 4 +Brand#25 |LARGE POLISHED COPPER | 9| 4 +Brand#25 |LARGE POLISHED COPPER | 19| 4 +Brand#25 |LARGE POLISHED COPPER | 49| 4 +Brand#25 |LARGE POLISHED NICKEL | 3| 4 +Brand#25 |LARGE POLISHED NICKEL | 9| 4 +Brand#25 |LARGE POLISHED NICKEL | 23| 4 +Brand#25 |LARGE POLISHED STEEL | 3| 4 +Brand#25 |LARGE POLISHED STEEL | 14| 4 +Brand#25 |LARGE POLISHED TIN | 9| 4 +Brand#25 |LARGE POLISHED TIN | 19| 4 +Brand#25 |LARGE POLISHED TIN | 36| 4 +Brand#25 |MEDIUM ANODIZED BRASS | 23| 4 +Brand#25 |MEDIUM ANODIZED BRASS | 45| 4 +Brand#25 |MEDIUM ANODIZED COPPER | 3| 4 +Brand#25 |MEDIUM ANODIZED COPPER | 14| 4 +Brand#25 |MEDIUM ANODIZED COPPER | 19| 4 +Brand#25 |MEDIUM ANODIZED COPPER | 23| 4 +Brand#25 |MEDIUM ANODIZED NICKEL | 14| 4 +Brand#25 |MEDIUM ANODIZED NICKEL | 19| 4 +Brand#25 |MEDIUM ANODIZED STEEL | 9| 4 +Brand#25 |MEDIUM ANODIZED STEEL | 45| 4 +Brand#25 |MEDIUM ANODIZED STEEL | 49| 4 +Brand#25 |MEDIUM ANODIZED TIN | 14| 4 +Brand#25 |MEDIUM ANODIZED TIN | 19| 4 +Brand#25 |MEDIUM ANODIZED TIN | 23| 4 +Brand#25 |MEDIUM ANODIZED TIN | 36| 4 +Brand#25 |MEDIUM BRUSHED BRASS | 19| 4 +Brand#25 |MEDIUM BRUSHED BRASS | 23| 4 +Brand#25 |MEDIUM BRUSHED BRASS | 45| 4 +Brand#25 |MEDIUM BRUSHED COPPER | 19| 4 +Brand#25 |MEDIUM BRUSHED COPPER | 36| 4 +Brand#25 |MEDIUM BRUSHED NICKEL | 9| 4 +Brand#25 |MEDIUM BRUSHED NICKEL | 19| 4 +Brand#25 |MEDIUM BRUSHED NICKEL | 45| 4 +Brand#25 |MEDIUM BRUSHED STEEL | 14| 4 +Brand#25 |MEDIUM BRUSHED STEEL | 19| 4 +Brand#25 |MEDIUM BRUSHED STEEL | 23| 4 +Brand#25 |MEDIUM BRUSHED STEEL | 36| 4 +Brand#25 |MEDIUM BRUSHED STEEL | 49| 4 +Brand#25 |MEDIUM BRUSHED TIN | 3| 4 +Brand#25 |MEDIUM BRUSHED TIN | 36| 4 +Brand#25 |MEDIUM BRUSHED TIN | 45| 4 +Brand#25 |MEDIUM BURNISHED BRASS | 9| 4 +Brand#25 |MEDIUM BURNISHED BRASS | 14| 4 +Brand#25 |MEDIUM BURNISHED BRASS | 23| 4 +Brand#25 |MEDIUM BURNISHED COPPER | 9| 4 +Brand#25 |MEDIUM BURNISHED COPPER | 23| 4 +Brand#25 |MEDIUM BURNISHED NICKEL | 23| 4 +Brand#25 |MEDIUM BURNISHED NICKEL | 36| 4 +Brand#25 |MEDIUM BURNISHED NICKEL | 45| 4 +Brand#25 |MEDIUM BURNISHED NICKEL | 49| 4 +Brand#25 |MEDIUM BURNISHED STEEL | 9| 4 +Brand#25 |MEDIUM BURNISHED STEEL | 14| 4 +Brand#25 |MEDIUM BURNISHED STEEL | 23| 4 +Brand#25 |MEDIUM BURNISHED TIN | 23| 4 +Brand#25 |MEDIUM BURNISHED TIN | 36| 4 +Brand#25 |MEDIUM BURNISHED TIN | 45| 4 +Brand#25 |MEDIUM PLATED BRASS | 3| 4 +Brand#25 |MEDIUM PLATED BRASS | 9| 4 +Brand#25 |MEDIUM PLATED BRASS | 19| 4 +Brand#25 |MEDIUM PLATED BRASS | 23| 4 +Brand#25 |MEDIUM PLATED BRASS | 36| 4 +Brand#25 |MEDIUM PLATED COPPER | 3| 4 +Brand#25 |MEDIUM PLATED COPPER | 19| 4 +Brand#25 |MEDIUM PLATED COPPER | 36| 4 +Brand#25 |MEDIUM PLATED NICKEL | 3| 4 +Brand#25 |MEDIUM PLATED NICKEL | 14| 4 +Brand#25 |MEDIUM PLATED STEEL | 14| 4 +Brand#25 |MEDIUM PLATED STEEL | 19| 4 +Brand#25 |MEDIUM PLATED STEEL | 49| 4 +Brand#25 |MEDIUM PLATED TIN | 9| 4 +Brand#25 |MEDIUM PLATED TIN | 19| 4 +Brand#25 |MEDIUM PLATED TIN | 23| 4 +Brand#25 |PROMO ANODIZED BRASS | 3| 4 +Brand#25 |PROMO ANODIZED BRASS | 19| 4 +Brand#25 |PROMO ANODIZED COPPER | 9| 4 +Brand#25 |PROMO ANODIZED COPPER | 14| 4 +Brand#25 |PROMO ANODIZED NICKEL | 9| 4 +Brand#25 |PROMO ANODIZED NICKEL | 19| 4 +Brand#25 |PROMO ANODIZED STEEL | 3| 4 +Brand#25 |PROMO ANODIZED STEEL | 14| 4 +Brand#25 |PROMO ANODIZED STEEL | 36| 4 +Brand#25 |PROMO ANODIZED TIN | 45| 4 +Brand#25 |PROMO BRUSHED BRASS | 3| 4 +Brand#25 |PROMO BRUSHED BRASS | 9| 4 +Brand#25 |PROMO BRUSHED COPPER | 3| 4 +Brand#25 |PROMO BRUSHED COPPER | 36| 4 +Brand#25 |PROMO BRUSHED NICKEL | 23| 4 +Brand#25 |PROMO BRUSHED NICKEL | 49| 4 +Brand#25 |PROMO BRUSHED STEEL | 19| 4 +Brand#25 |PROMO BRUSHED STEEL | 36| 4 +Brand#25 |PROMO BRUSHED TIN | 3| 4 +Brand#25 |PROMO BRUSHED TIN | 23| 4 +Brand#25 |PROMO BRUSHED TIN | 36| 4 +Brand#25 |PROMO BURNISHED BRASS | 9| 4 +Brand#25 |PROMO BURNISHED COPPER | 3| 4 +Brand#25 |PROMO BURNISHED COPPER | 9| 4 +Brand#25 |PROMO BURNISHED NICKEL | 14| 4 +Brand#25 |PROMO BURNISHED NICKEL | 19| 4 +Brand#25 |PROMO BURNISHED NICKEL | 23| 4 +Brand#25 |PROMO BURNISHED STEEL | 3| 4 +Brand#25 |PROMO BURNISHED STEEL | 49| 4 +Brand#25 |PROMO BURNISHED TIN | 9| 4 +Brand#25 |PROMO BURNISHED TIN | 23| 4 +Brand#25 |PROMO BURNISHED TIN | 45| 4 +Brand#25 |PROMO BURNISHED TIN | 49| 4 +Brand#25 |PROMO PLATED BRASS | 36| 4 +Brand#25 |PROMO PLATED BRASS | 45| 4 +Brand#25 |PROMO PLATED COPPER | 3| 4 +Brand#25 |PROMO PLATED COPPER | 14| 4 +Brand#25 |PROMO PLATED COPPER | 19| 4 +Brand#25 |PROMO PLATED COPPER | 45| 4 +Brand#25 |PROMO PLATED NICKEL | 14| 4 +Brand#25 |PROMO PLATED NICKEL | 19| 4 +Brand#25 |PROMO PLATED NICKEL | 23| 4 +Brand#25 |PROMO PLATED NICKEL | 45| 4 +Brand#25 |PROMO PLATED STEEL | 14| 4 +Brand#25 |PROMO PLATED STEEL | 19| 4 +Brand#25 |PROMO PLATED TIN | 3| 4 +Brand#25 |PROMO PLATED TIN | 19| 4 +Brand#25 |PROMO PLATED TIN | 23| 4 +Brand#25 |PROMO PLATED TIN | 36| 4 +Brand#25 |PROMO PLATED TIN | 49| 4 +Brand#25 |PROMO POLISHED BRASS | 9| 4 +Brand#25 |PROMO POLISHED BRASS | 23| 4 +Brand#25 |PROMO POLISHED BRASS | 45| 4 +Brand#25 |PROMO POLISHED COPPER | 3| 4 +Brand#25 |PROMO POLISHED COPPER | 45| 4 +Brand#25 |PROMO POLISHED NICKEL | 3| 4 +Brand#25 |PROMO POLISHED NICKEL | 9| 4 +Brand#25 |PROMO POLISHED STEEL | 19| 4 +Brand#25 |PROMO POLISHED TIN | 3| 4 +Brand#25 |PROMO POLISHED TIN | 23| 4 +Brand#25 |PROMO POLISHED TIN | 45| 4 +Brand#25 |PROMO POLISHED TIN | 49| 4 +Brand#25 |SMALL ANODIZED BRASS | 45| 4 +Brand#25 |SMALL ANODIZED COPPER | 3| 4 +Brand#25 |SMALL ANODIZED COPPER | 9| 4 +Brand#25 |SMALL ANODIZED COPPER | 14| 4 +Brand#25 |SMALL ANODIZED COPPER | 19| 4 +Brand#25 |SMALL ANODIZED COPPER | 49| 4 +Brand#25 |SMALL ANODIZED NICKEL | 3| 4 +Brand#25 |SMALL ANODIZED NICKEL | 9| 4 +Brand#25 |SMALL ANODIZED NICKEL | 23| 4 +Brand#25 |SMALL ANODIZED NICKEL | 45| 4 +Brand#25 |SMALL ANODIZED STEEL | 3| 4 +Brand#25 |SMALL ANODIZED STEEL | 9| 4 +Brand#25 |SMALL ANODIZED STEEL | 14| 4 +Brand#25 |SMALL ANODIZED STEEL | 19| 4 +Brand#25 |SMALL ANODIZED STEEL | 45| 4 +Brand#25 |SMALL ANODIZED STEEL | 49| 4 +Brand#25 |SMALL ANODIZED TIN | 9| 4 +Brand#25 |SMALL ANODIZED TIN | 19| 4 +Brand#25 |SMALL BRUSHED BRASS | 9| 4 +Brand#25 |SMALL BRUSHED BRASS | 14| 4 +Brand#25 |SMALL BRUSHED BRASS | 19| 4 +Brand#25 |SMALL BRUSHED BRASS | 45| 4 +Brand#25 |SMALL BRUSHED COPPER | 3| 4 +Brand#25 |SMALL BRUSHED COPPER | 9| 4 +Brand#25 |SMALL BRUSHED COPPER | 45| 4 +Brand#25 |SMALL BRUSHED COPPER | 49| 4 +Brand#25 |SMALL BRUSHED NICKEL | 19| 4 +Brand#25 |SMALL BRUSHED NICKEL | 23| 4 +Brand#25 |SMALL BRUSHED NICKEL | 36| 4 +Brand#25 |SMALL BRUSHED NICKEL | 45| 4 +Brand#25 |SMALL BRUSHED STEEL | 19| 4 +Brand#25 |SMALL BRUSHED STEEL | 36| 4 +Brand#25 |SMALL BRUSHED STEEL | 45| 4 +Brand#25 |SMALL BRUSHED STEEL | 49| 4 +Brand#25 |SMALL BRUSHED TIN | 9| 4 +Brand#25 |SMALL BRUSHED TIN | 14| 4 +Brand#25 |SMALL BRUSHED TIN | 19| 4 +Brand#25 |SMALL BURNISHED BRASS | 14| 4 +Brand#25 |SMALL BURNISHED BRASS | 19| 4 +Brand#25 |SMALL BURNISHED BRASS | 45| 4 +Brand#25 |SMALL BURNISHED BRASS | 49| 4 +Brand#25 |SMALL BURNISHED COPPER | 3| 4 +Brand#25 |SMALL BURNISHED COPPER | 14| 4 +Brand#25 |SMALL BURNISHED COPPER | 19| 4 +Brand#25 |SMALL BURNISHED COPPER | 23| 4 +Brand#25 |SMALL BURNISHED NICKEL | 14| 4 +Brand#25 |SMALL BURNISHED NICKEL | 19| 4 +Brand#25 |SMALL BURNISHED STEEL | 9| 4 +Brand#25 |SMALL BURNISHED STEEL | 19| 4 +Brand#25 |SMALL BURNISHED STEEL | 23| 4 +Brand#25 |SMALL BURNISHED STEEL | 36| 4 +Brand#25 |SMALL BURNISHED TIN | 9| 4 +Brand#25 |SMALL BURNISHED TIN | 14| 4 +Brand#25 |SMALL BURNISHED TIN | 23| 4 +Brand#25 |SMALL BURNISHED TIN | 36| 4 +Brand#25 |SMALL BURNISHED TIN | 49| 4 +Brand#25 |SMALL PLATED BRASS | 3| 4 +Brand#25 |SMALL PLATED BRASS | 23| 4 +Brand#25 |SMALL PLATED BRASS | 45| 4 +Brand#25 |SMALL PLATED COPPER | 3| 4 +Brand#25 |SMALL PLATED COPPER | 14| 4 +Brand#25 |SMALL PLATED NICKEL | 3| 4 +Brand#25 |SMALL PLATED NICKEL | 19| 4 +Brand#25 |SMALL PLATED NICKEL | 23| 4 +Brand#25 |SMALL PLATED NICKEL | 49| 4 +Brand#25 |SMALL PLATED STEEL | 3| 4 +Brand#25 |SMALL PLATED STEEL | 14| 4 +Brand#25 |SMALL PLATED TIN | 9| 4 +Brand#25 |SMALL PLATED TIN | 14| 4 +Brand#25 |SMALL PLATED TIN | 19| 4 +Brand#25 |SMALL PLATED TIN | 36| 4 +Brand#25 |SMALL PLATED TIN | 45| 4 +Brand#25 |SMALL POLISHED BRASS | 14| 4 +Brand#25 |SMALL POLISHED BRASS | 36| 4 +Brand#25 |SMALL POLISHED NICKEL | 36| 4 +Brand#25 |SMALL POLISHED NICKEL | 49| 4 +Brand#25 |SMALL POLISHED STEEL | 9| 4 +Brand#25 |SMALL POLISHED STEEL | 49| 4 +Brand#25 |SMALL POLISHED TIN | 14| 4 +Brand#25 |STANDARD ANODIZED BRASS | 14| 4 +Brand#25 |STANDARD ANODIZED BRASS | 23| 4 +Brand#25 |STANDARD ANODIZED BRASS | 36| 4 +Brand#25 |STANDARD ANODIZED COPPER | 9| 4 +Brand#25 |STANDARD ANODIZED COPPER | 14| 4 +Brand#25 |STANDARD ANODIZED COPPER | 19| 4 +Brand#25 |STANDARD ANODIZED COPPER | 36| 4 +Brand#25 |STANDARD ANODIZED COPPER | 49| 4 +Brand#25 |STANDARD ANODIZED NICKEL | 9| 4 +Brand#25 |STANDARD ANODIZED NICKEL | 19| 4 +Brand#25 |STANDARD ANODIZED NICKEL | 36| 4 +Brand#25 |STANDARD ANODIZED STEEL | 19| 4 +Brand#25 |STANDARD ANODIZED STEEL | 36| 4 +Brand#25 |STANDARD ANODIZED STEEL | 45| 4 +Brand#25 |STANDARD ANODIZED STEEL | 49| 4 +Brand#25 |STANDARD ANODIZED TIN | 36| 4 +Brand#25 |STANDARD ANODIZED TIN | 45| 4 +Brand#25 |STANDARD BRUSHED BRASS | 14| 4 +Brand#25 |STANDARD BRUSHED BRASS | 19| 4 +Brand#25 |STANDARD BRUSHED BRASS | 23| 4 +Brand#25 |STANDARD BRUSHED COPPER | 45| 4 +Brand#25 |STANDARD BRUSHED NICKEL | 3| 4 +Brand#25 |STANDARD BRUSHED NICKEL | 9| 4 +Brand#25 |STANDARD BRUSHED NICKEL | 45| 4 +Brand#25 |STANDARD BRUSHED STEEL | 14| 4 +Brand#25 |STANDARD BRUSHED STEEL | 23| 4 +Brand#25 |STANDARD BRUSHED STEEL | 45| 4 +Brand#25 |STANDARD BRUSHED TIN | 3| 4 +Brand#25 |STANDARD BRUSHED TIN | 9| 4 +Brand#25 |STANDARD BRUSHED TIN | 14| 4 +Brand#25 |STANDARD BURNISHED BRASS | 19| 4 +Brand#25 |STANDARD BURNISHED BRASS | 36| 4 +Brand#25 |STANDARD BURNISHED BRASS | 45| 4 +Brand#25 |STANDARD BURNISHED BRASS | 49| 4 +Brand#25 |STANDARD BURNISHED COPPER| 3| 4 +Brand#25 |STANDARD BURNISHED COPPER| 14| 4 +Brand#25 |STANDARD BURNISHED COPPER| 36| 4 +Brand#25 |STANDARD BURNISHED COPPER| 45| 4 +Brand#25 |STANDARD BURNISHED COPPER| 49| 4 +Brand#25 |STANDARD BURNISHED NICKEL| 14| 4 +Brand#25 |STANDARD BURNISHED NICKEL| 45| 4 +Brand#25 |STANDARD BURNISHED NICKEL| 49| 4 +Brand#25 |STANDARD BURNISHED STEEL | 3| 4 +Brand#25 |STANDARD BURNISHED STEEL | 9| 4 +Brand#25 |STANDARD BURNISHED STEEL | 19| 4 +Brand#25 |STANDARD BURNISHED STEEL | 49| 4 +Brand#25 |STANDARD BURNISHED TIN | 9| 4 +Brand#25 |STANDARD BURNISHED TIN | 45| 4 +Brand#25 |STANDARD PLATED BRASS | 3| 4 +Brand#25 |STANDARD PLATED BRASS | 36| 4 +Brand#25 |STANDARD PLATED COPPER | 3| 4 +Brand#25 |STANDARD PLATED COPPER | 19| 4 +Brand#25 |STANDARD PLATED NICKEL | 9| 4 +Brand#25 |STANDARD PLATED NICKEL | 19| 4 +Brand#25 |STANDARD PLATED STEEL | 23| 4 +Brand#25 |STANDARD PLATED TIN | 3| 4 +Brand#25 |STANDARD PLATED TIN | 9| 4 +Brand#25 |STANDARD PLATED TIN | 14| 4 +Brand#25 |STANDARD PLATED TIN | 19| 4 +Brand#25 |STANDARD PLATED TIN | 45| 4 +Brand#25 |STANDARD POLISHED BRASS | 3| 4 +Brand#25 |STANDARD POLISHED BRASS | 14| 4 +Brand#25 |STANDARD POLISHED BRASS | 23| 4 +Brand#25 |STANDARD POLISHED BRASS | 45| 4 +Brand#25 |STANDARD POLISHED COPPER | 9| 4 +Brand#25 |STANDARD POLISHED COPPER | 19| 4 +Brand#25 |STANDARD POLISHED COPPER | 45| 4 +Brand#25 |STANDARD POLISHED COPPER | 49| 4 +Brand#25 |STANDARD POLISHED NICKEL | 14| 4 +Brand#25 |STANDARD POLISHED NICKEL | 23| 4 +Brand#25 |STANDARD POLISHED NICKEL | 49| 4 +Brand#25 |STANDARD POLISHED STEEL | 49| 4 +Brand#25 |STANDARD POLISHED TIN | 9| 4 +Brand#25 |STANDARD POLISHED TIN | 23| 4 +Brand#31 |ECONOMY ANODIZED BRASS | 3| 4 +Brand#31 |ECONOMY ANODIZED BRASS | 9| 4 +Brand#31 |ECONOMY ANODIZED BRASS | 23| 4 +Brand#31 |ECONOMY ANODIZED COPPER | 3| 4 +Brand#31 |ECONOMY ANODIZED COPPER | 9| 4 +Brand#31 |ECONOMY ANODIZED COPPER | 14| 4 +Brand#31 |ECONOMY ANODIZED COPPER | 36| 4 +Brand#31 |ECONOMY ANODIZED COPPER | 45| 4 +Brand#31 |ECONOMY ANODIZED NICKEL | 19| 4 +Brand#31 |ECONOMY ANODIZED NICKEL | 23| 4 +Brand#31 |ECONOMY ANODIZED NICKEL | 36| 4 +Brand#31 |ECONOMY ANODIZED STEEL | 9| 4 +Brand#31 |ECONOMY ANODIZED STEEL | 19| 4 +Brand#31 |ECONOMY ANODIZED STEEL | 23| 4 +Brand#31 |ECONOMY ANODIZED STEEL | 49| 4 +Brand#31 |ECONOMY ANODIZED TIN | 14| 4 +Brand#31 |ECONOMY ANODIZED TIN | 45| 4 +Brand#31 |ECONOMY BRUSHED BRASS | 14| 4 +Brand#31 |ECONOMY BRUSHED BRASS | 23| 4 +Brand#31 |ECONOMY BRUSHED BRASS | 45| 4 +Brand#31 |ECONOMY BRUSHED BRASS | 49| 4 +Brand#31 |ECONOMY BRUSHED COPPER | 19| 4 +Brand#31 |ECONOMY BRUSHED COPPER | 23| 4 +Brand#31 |ECONOMY BRUSHED COPPER | 45| 4 +Brand#31 |ECONOMY BRUSHED COPPER | 49| 4 +Brand#31 |ECONOMY BRUSHED NICKEL | 23| 4 +Brand#31 |ECONOMY BRUSHED NICKEL | 36| 4 +Brand#31 |ECONOMY BRUSHED NICKEL | 45| 4 +Brand#31 |ECONOMY BRUSHED NICKEL | 49| 4 +Brand#31 |ECONOMY BRUSHED STEEL | 45| 4 +Brand#31 |ECONOMY BRUSHED TIN | 3| 4 +Brand#31 |ECONOMY BRUSHED TIN | 9| 4 +Brand#31 |ECONOMY BRUSHED TIN | 45| 4 +Brand#31 |ECONOMY BURNISHED BRASS | 9| 4 +Brand#31 |ECONOMY BURNISHED BRASS | 19| 4 +Brand#31 |ECONOMY BURNISHED BRASS | 36| 4 +Brand#31 |ECONOMY BURNISHED BRASS | 49| 4 +Brand#31 |ECONOMY BURNISHED COPPER | 3| 4 +Brand#31 |ECONOMY BURNISHED COPPER | 23| 4 +Brand#31 |ECONOMY BURNISHED COPPER | 36| 4 +Brand#31 |ECONOMY BURNISHED NICKEL | 3| 4 +Brand#31 |ECONOMY BURNISHED NICKEL | 9| 4 +Brand#31 |ECONOMY BURNISHED NICKEL | 14| 4 +Brand#31 |ECONOMY BURNISHED NICKEL | 23| 4 +Brand#31 |ECONOMY BURNISHED NICKEL | 49| 4 +Brand#31 |ECONOMY BURNISHED STEEL | 9| 4 +Brand#31 |ECONOMY BURNISHED STEEL | 23| 4 +Brand#31 |ECONOMY BURNISHED STEEL | 36| 4 +Brand#31 |ECONOMY BURNISHED STEEL | 45| 4 +Brand#31 |ECONOMY BURNISHED TIN | 36| 4 +Brand#31 |ECONOMY PLATED BRASS | 3| 4 +Brand#31 |ECONOMY PLATED BRASS | 9| 4 +Brand#31 |ECONOMY PLATED BRASS | 14| 4 +Brand#31 |ECONOMY PLATED BRASS | 19| 4 +Brand#31 |ECONOMY PLATED BRASS | 49| 4 +Brand#31 |ECONOMY PLATED COPPER | 9| 4 +Brand#31 |ECONOMY PLATED COPPER | 14| 4 +Brand#31 |ECONOMY PLATED COPPER | 23| 4 +Brand#31 |ECONOMY PLATED COPPER | 36| 4 +Brand#31 |ECONOMY PLATED COPPER | 45| 4 +Brand#31 |ECONOMY PLATED NICKEL | 3| 4 +Brand#31 |ECONOMY PLATED NICKEL | 14| 4 +Brand#31 |ECONOMY PLATED NICKEL | 19| 4 +Brand#31 |ECONOMY PLATED NICKEL | 23| 4 +Brand#31 |ECONOMY PLATED NICKEL | 45| 4 +Brand#31 |ECONOMY PLATED NICKEL | 49| 4 +Brand#31 |ECONOMY PLATED STEEL | 9| 4 +Brand#31 |ECONOMY PLATED STEEL | 14| 4 +Brand#31 |ECONOMY PLATED STEEL | 19| 4 +Brand#31 |ECONOMY PLATED STEEL | 36| 4 +Brand#31 |ECONOMY PLATED TIN | 9| 4 +Brand#31 |ECONOMY PLATED TIN | 14| 4 +Brand#31 |ECONOMY PLATED TIN | 49| 4 +Brand#31 |ECONOMY POLISHED BRASS | 19| 4 +Brand#31 |ECONOMY POLISHED BRASS | 49| 4 +Brand#31 |ECONOMY POLISHED COPPER | 9| 4 +Brand#31 |ECONOMY POLISHED COPPER | 23| 4 +Brand#31 |ECONOMY POLISHED COPPER | 36| 4 +Brand#31 |ECONOMY POLISHED COPPER | 45| 4 +Brand#31 |ECONOMY POLISHED NICKEL | 19| 4 +Brand#31 |ECONOMY POLISHED NICKEL | 23| 4 +Brand#31 |ECONOMY POLISHED NICKEL | 49| 4 +Brand#31 |ECONOMY POLISHED STEEL | 14| 4 +Brand#31 |ECONOMY POLISHED STEEL | 19| 4 +Brand#31 |ECONOMY POLISHED STEEL | 23| 4 +Brand#31 |ECONOMY POLISHED STEEL | 36| 4 +Brand#31 |ECONOMY POLISHED TIN | 9| 4 +Brand#31 |ECONOMY POLISHED TIN | 14| 4 +Brand#31 |ECONOMY POLISHED TIN | 19| 4 +Brand#31 |ECONOMY POLISHED TIN | 23| 4 +Brand#31 |ECONOMY POLISHED TIN | 49| 4 +Brand#31 |LARGE ANODIZED BRASS | 9| 4 +Brand#31 |LARGE ANODIZED BRASS | 19| 4 +Brand#31 |LARGE ANODIZED BRASS | 23| 4 +Brand#31 |LARGE ANODIZED BRASS | 49| 4 +Brand#31 |LARGE ANODIZED COPPER | 9| 4 +Brand#31 |LARGE ANODIZED NICKEL | 3| 4 +Brand#31 |LARGE ANODIZED NICKEL | 9| 4 +Brand#31 |LARGE ANODIZED NICKEL | 23| 4 +Brand#31 |LARGE ANODIZED STEEL | 14| 4 +Brand#31 |LARGE ANODIZED STEEL | 19| 4 +Brand#31 |LARGE ANODIZED STEEL | 23| 4 +Brand#31 |LARGE ANODIZED TIN | 23| 4 +Brand#31 |LARGE BRUSHED BRASS | 3| 4 +Brand#31 |LARGE BRUSHED BRASS | 14| 4 +Brand#31 |LARGE BRUSHED BRASS | 19| 4 +Brand#31 |LARGE BRUSHED COPPER | 14| 4 +Brand#31 |LARGE BRUSHED COPPER | 23| 4 +Brand#31 |LARGE BRUSHED COPPER | 36| 4 +Brand#31 |LARGE BRUSHED COPPER | 49| 4 +Brand#31 |LARGE BRUSHED NICKEL | 9| 4 +Brand#31 |LARGE BRUSHED NICKEL | 49| 4 +Brand#31 |LARGE BRUSHED STEEL | 3| 4 +Brand#31 |LARGE BRUSHED STEEL | 9| 4 +Brand#31 |LARGE BRUSHED STEEL | 19| 4 +Brand#31 |LARGE BRUSHED STEEL | 49| 4 +Brand#31 |LARGE BRUSHED TIN | 3| 4 +Brand#31 |LARGE BRUSHED TIN | 9| 4 +Brand#31 |LARGE BRUSHED TIN | 19| 4 +Brand#31 |LARGE BRUSHED TIN | 23| 4 +Brand#31 |LARGE BURNISHED BRASS | 9| 4 +Brand#31 |LARGE BURNISHED BRASS | 14| 4 +Brand#31 |LARGE BURNISHED COPPER | 3| 4 +Brand#31 |LARGE BURNISHED COPPER | 14| 4 +Brand#31 |LARGE BURNISHED COPPER | 19| 4 +Brand#31 |LARGE BURNISHED COPPER | 49| 4 +Brand#31 |LARGE BURNISHED NICKEL | 3| 4 +Brand#31 |LARGE BURNISHED NICKEL | 23| 4 +Brand#31 |LARGE BURNISHED STEEL | 14| 4 +Brand#31 |LARGE BURNISHED STEEL | 19| 4 +Brand#31 |LARGE BURNISHED STEEL | 45| 4 +Brand#31 |LARGE BURNISHED TIN | 3| 4 +Brand#31 |LARGE BURNISHED TIN | 9| 4 +Brand#31 |LARGE BURNISHED TIN | 36| 4 +Brand#31 |LARGE BURNISHED TIN | 45| 4 +Brand#31 |LARGE PLATED BRASS | 19| 4 +Brand#31 |LARGE PLATED BRASS | 36| 4 +Brand#31 |LARGE PLATED COPPER | 9| 4 +Brand#31 |LARGE PLATED COPPER | 14| 4 +Brand#31 |LARGE PLATED COPPER | 36| 4 +Brand#31 |LARGE PLATED COPPER | 49| 4 +Brand#31 |LARGE PLATED NICKEL | 3| 4 +Brand#31 |LARGE PLATED NICKEL | 9| 4 +Brand#31 |LARGE PLATED NICKEL | 14| 4 +Brand#31 |LARGE PLATED STEEL | 3| 4 +Brand#31 |LARGE PLATED STEEL | 19| 4 +Brand#31 |LARGE PLATED STEEL | 36| 4 +Brand#31 |LARGE PLATED STEEL | 49| 4 +Brand#31 |LARGE PLATED TIN | 3| 4 +Brand#31 |LARGE PLATED TIN | 19| 4 +Brand#31 |LARGE PLATED TIN | 45| 4 +Brand#31 |LARGE PLATED TIN | 49| 4 +Brand#31 |LARGE POLISHED BRASS | 3| 4 +Brand#31 |LARGE POLISHED BRASS | 14| 4 +Brand#31 |LARGE POLISHED BRASS | 19| 4 +Brand#31 |LARGE POLISHED BRASS | 36| 4 +Brand#31 |LARGE POLISHED BRASS | 49| 4 +Brand#31 |LARGE POLISHED COPPER | 36| 4 +Brand#31 |LARGE POLISHED COPPER | 45| 4 +Brand#31 |LARGE POLISHED NICKEL | 9| 4 +Brand#31 |LARGE POLISHED NICKEL | 23| 4 +Brand#31 |LARGE POLISHED STEEL | 3| 4 +Brand#31 |LARGE POLISHED STEEL | 9| 4 +Brand#31 |LARGE POLISHED STEEL | 14| 4 +Brand#31 |LARGE POLISHED STEEL | 19| 4 +Brand#31 |LARGE POLISHED TIN | 36| 4 +Brand#31 |LARGE POLISHED TIN | 45| 4 +Brand#31 |MEDIUM ANODIZED BRASS | 3| 4 +Brand#31 |MEDIUM ANODIZED BRASS | 9| 4 +Brand#31 |MEDIUM ANODIZED BRASS | 36| 4 +Brand#31 |MEDIUM ANODIZED BRASS | 49| 4 +Brand#31 |MEDIUM ANODIZED COPPER | 36| 4 +Brand#31 |MEDIUM ANODIZED NICKEL | 23| 4 +Brand#31 |MEDIUM ANODIZED NICKEL | 36| 4 +Brand#31 |MEDIUM ANODIZED NICKEL | 45| 4 +Brand#31 |MEDIUM ANODIZED STEEL | 36| 4 +Brand#31 |MEDIUM ANODIZED TIN | 36| 4 +Brand#31 |MEDIUM ANODIZED TIN | 45| 4 +Brand#31 |MEDIUM ANODIZED TIN | 49| 4 +Brand#31 |MEDIUM BRUSHED BRASS | 49| 4 +Brand#31 |MEDIUM BRUSHED COPPER | 3| 4 +Brand#31 |MEDIUM BRUSHED COPPER | 45| 4 +Brand#31 |MEDIUM BRUSHED NICKEL | 3| 4 +Brand#31 |MEDIUM BRUSHED NICKEL | 23| 4 +Brand#31 |MEDIUM BRUSHED NICKEL | 45| 4 +Brand#31 |MEDIUM BRUSHED STEEL | 9| 4 +Brand#31 |MEDIUM BRUSHED STEEL | 14| 4 +Brand#31 |MEDIUM BRUSHED STEEL | 36| 4 +Brand#31 |MEDIUM BRUSHED STEEL | 45| 4 +Brand#31 |MEDIUM BRUSHED TIN | 19| 4 +Brand#31 |MEDIUM BRUSHED TIN | 36| 4 +Brand#31 |MEDIUM BRUSHED TIN | 45| 4 +Brand#31 |MEDIUM BURNISHED BRASS | 9| 4 +Brand#31 |MEDIUM BURNISHED BRASS | 36| 4 +Brand#31 |MEDIUM BURNISHED COPPER | 3| 4 +Brand#31 |MEDIUM BURNISHED COPPER | 9| 4 +Brand#31 |MEDIUM BURNISHED COPPER | 14| 4 +Brand#31 |MEDIUM BURNISHED COPPER | 23| 4 +Brand#31 |MEDIUM BURNISHED NICKEL | 36| 4 +Brand#31 |MEDIUM BURNISHED NICKEL | 49| 4 +Brand#31 |MEDIUM BURNISHED STEEL | 14| 4 +Brand#31 |MEDIUM BURNISHED STEEL | 49| 4 +Brand#31 |MEDIUM BURNISHED TIN | 9| 4 +Brand#31 |MEDIUM BURNISHED TIN | 45| 4 +Brand#31 |MEDIUM BURNISHED TIN | 49| 4 +Brand#31 |MEDIUM PLATED BRASS | 14| 4 +Brand#31 |MEDIUM PLATED BRASS | 36| 4 +Brand#31 |MEDIUM PLATED BRASS | 45| 4 +Brand#31 |MEDIUM PLATED COPPER | 45| 4 +Brand#31 |MEDIUM PLATED NICKEL | 14| 4 +Brand#31 |MEDIUM PLATED NICKEL | 19| 4 +Brand#31 |MEDIUM PLATED NICKEL | 45| 4 +Brand#31 |MEDIUM PLATED STEEL | 14| 4 +Brand#31 |MEDIUM PLATED STEEL | 49| 4 +Brand#31 |MEDIUM PLATED TIN | 3| 4 +Brand#31 |MEDIUM PLATED TIN | 9| 4 +Brand#31 |MEDIUM PLATED TIN | 14| 4 +Brand#31 |MEDIUM PLATED TIN | 36| 4 +Brand#31 |MEDIUM PLATED TIN | 49| 4 +Brand#31 |PROMO ANODIZED BRASS | 19| 4 +Brand#31 |PROMO ANODIZED BRASS | 45| 4 +Brand#31 |PROMO ANODIZED COPPER | 19| 4 +Brand#31 |PROMO ANODIZED COPPER | 36| 4 +Brand#31 |PROMO ANODIZED COPPER | 45| 4 +Brand#31 |PROMO ANODIZED NICKEL | 9| 4 +Brand#31 |PROMO ANODIZED NICKEL | 49| 4 +Brand#31 |PROMO ANODIZED STEEL | 3| 4 +Brand#31 |PROMO ANODIZED STEEL | 23| 4 +Brand#31 |PROMO ANODIZED STEEL | 45| 4 +Brand#31 |PROMO ANODIZED TIN | 9| 4 +Brand#31 |PROMO ANODIZED TIN | 45| 4 +Brand#31 |PROMO ANODIZED TIN | 49| 4 +Brand#31 |PROMO BRUSHED BRASS | 9| 4 +Brand#31 |PROMO BRUSHED BRASS | 14| 4 +Brand#31 |PROMO BRUSHED BRASS | 45| 4 +Brand#31 |PROMO BRUSHED COPPER | 9| 4 +Brand#31 |PROMO BRUSHED COPPER | 36| 4 +Brand#31 |PROMO BRUSHED COPPER | 49| 4 +Brand#31 |PROMO BRUSHED NICKEL | 19| 4 +Brand#31 |PROMO BRUSHED NICKEL | 36| 4 +Brand#31 |PROMO BRUSHED NICKEL | 45| 4 +Brand#31 |PROMO BRUSHED STEEL | 14| 4 +Brand#31 |PROMO BRUSHED STEEL | 19| 4 +Brand#31 |PROMO BRUSHED STEEL | 36| 4 +Brand#31 |PROMO BRUSHED TIN | 14| 4 +Brand#31 |PROMO BRUSHED TIN | 19| 4 +Brand#31 |PROMO BRUSHED TIN | 23| 4 +Brand#31 |PROMO BRUSHED TIN | 49| 4 +Brand#31 |PROMO BURNISHED BRASS | 23| 4 +Brand#31 |PROMO BURNISHED BRASS | 45| 4 +Brand#31 |PROMO BURNISHED COPPER | 23| 4 +Brand#31 |PROMO BURNISHED COPPER | 49| 4 +Brand#31 |PROMO BURNISHED NICKEL | 23| 4 +Brand#31 |PROMO BURNISHED NICKEL | 36| 4 +Brand#31 |PROMO BURNISHED STEEL | 9| 4 +Brand#31 |PROMO BURNISHED TIN | 3| 4 +Brand#31 |PROMO BURNISHED TIN | 9| 4 +Brand#31 |PROMO BURNISHED TIN | 14| 4 +Brand#31 |PROMO BURNISHED TIN | 19| 4 +Brand#31 |PROMO BURNISHED TIN | 36| 4 +Brand#31 |PROMO BURNISHED TIN | 45| 4 +Brand#31 |PROMO PLATED BRASS | 9| 4 +Brand#31 |PROMO PLATED BRASS | 14| 4 +Brand#31 |PROMO PLATED BRASS | 19| 4 +Brand#31 |PROMO PLATED BRASS | 49| 4 +Brand#31 |PROMO PLATED COPPER | 3| 4 +Brand#31 |PROMO PLATED COPPER | 9| 4 +Brand#31 |PROMO PLATED COPPER | 23| 4 +Brand#31 |PROMO PLATED COPPER | 45| 4 +Brand#31 |PROMO PLATED NICKEL | 3| 4 +Brand#31 |PROMO PLATED NICKEL | 9| 4 +Brand#31 |PROMO PLATED NICKEL | 14| 4 +Brand#31 |PROMO PLATED NICKEL | 19| 4 +Brand#31 |PROMO PLATED NICKEL | 23| 4 +Brand#31 |PROMO PLATED NICKEL | 49| 4 +Brand#31 |PROMO PLATED STEEL | 3| 4 +Brand#31 |PROMO PLATED STEEL | 9| 4 +Brand#31 |PROMO PLATED STEEL | 14| 4 +Brand#31 |PROMO PLATED TIN | 9| 4 +Brand#31 |PROMO PLATED TIN | 36| 4 +Brand#31 |PROMO POLISHED BRASS | 14| 4 +Brand#31 |PROMO POLISHED BRASS | 36| 4 +Brand#31 |PROMO POLISHED COPPER | 14| 4 +Brand#31 |PROMO POLISHED NICKEL | 9| 4 +Brand#31 |PROMO POLISHED NICKEL | 36| 4 +Brand#31 |PROMO POLISHED STEEL | 19| 4 +Brand#31 |PROMO POLISHED STEEL | 45| 4 +Brand#31 |PROMO POLISHED STEEL | 49| 4 +Brand#31 |PROMO POLISHED TIN | 3| 4 +Brand#31 |PROMO POLISHED TIN | 14| 4 +Brand#31 |PROMO POLISHED TIN | 19| 4 +Brand#31 |PROMO POLISHED TIN | 23| 4 +Brand#31 |PROMO POLISHED TIN | 36| 4 +Brand#31 |SMALL ANODIZED BRASS | 3| 4 +Brand#31 |SMALL ANODIZED BRASS | 14| 4 +Brand#31 |SMALL ANODIZED BRASS | 23| 4 +Brand#31 |SMALL ANODIZED BRASS | 45| 4 +Brand#31 |SMALL ANODIZED BRASS | 49| 4 +Brand#31 |SMALL ANODIZED COPPER | 9| 4 +Brand#31 |SMALL ANODIZED COPPER | 19| 4 +Brand#31 |SMALL ANODIZED COPPER | 23| 4 +Brand#31 |SMALL ANODIZED NICKEL | 19| 4 +Brand#31 |SMALL ANODIZED NICKEL | 36| 4 +Brand#31 |SMALL ANODIZED NICKEL | 45| 4 +Brand#31 |SMALL ANODIZED STEEL | 19| 4 +Brand#31 |SMALL ANODIZED STEEL | 23| 4 +Brand#31 |SMALL ANODIZED STEEL | 36| 4 +Brand#31 |SMALL ANODIZED STEEL | 49| 4 +Brand#31 |SMALL ANODIZED TIN | 9| 4 +Brand#31 |SMALL ANODIZED TIN | 19| 4 +Brand#31 |SMALL ANODIZED TIN | 45| 4 +Brand#31 |SMALL ANODIZED TIN | 49| 4 +Brand#31 |SMALL BRUSHED BRASS | 9| 4 +Brand#31 |SMALL BRUSHED BRASS | 14| 4 +Brand#31 |SMALL BRUSHED BRASS | 19| 4 +Brand#31 |SMALL BRUSHED BRASS | 36| 4 +Brand#31 |SMALL BRUSHED COPPER | 36| 4 +Brand#31 |SMALL BRUSHED COPPER | 45| 4 +Brand#31 |SMALL BRUSHED COPPER | 49| 4 +Brand#31 |SMALL BRUSHED NICKEL | 9| 4 +Brand#31 |SMALL BRUSHED NICKEL | 45| 4 +Brand#31 |SMALL BRUSHED STEEL | 19| 4 +Brand#31 |SMALL BRUSHED STEEL | 45| 4 +Brand#31 |SMALL BRUSHED TIN | 23| 4 +Brand#31 |SMALL BRUSHED TIN | 36| 4 +Brand#31 |SMALL BURNISHED BRASS | 19| 4 +Brand#31 |SMALL BURNISHED BRASS | 23| 4 +Brand#31 |SMALL BURNISHED BRASS | 45| 4 +Brand#31 |SMALL BURNISHED COPPER | 9| 4 +Brand#31 |SMALL BURNISHED COPPER | 14| 4 +Brand#31 |SMALL BURNISHED COPPER | 23| 4 +Brand#31 |SMALL BURNISHED COPPER | 36| 4 +Brand#31 |SMALL BURNISHED COPPER | 45| 4 +Brand#31 |SMALL BURNISHED COPPER | 49| 4 +Brand#31 |SMALL BURNISHED NICKEL | 19| 4 +Brand#31 |SMALL BURNISHED NICKEL | 36| 4 +Brand#31 |SMALL BURNISHED NICKEL | 45| 4 +Brand#31 |SMALL BURNISHED TIN | 3| 4 +Brand#31 |SMALL BURNISHED TIN | 9| 4 +Brand#31 |SMALL BURNISHED TIN | 19| 4 +Brand#31 |SMALL PLATED BRASS | 9| 4 +Brand#31 |SMALL PLATED BRASS | 19| 4 +Brand#31 |SMALL PLATED BRASS | 36| 4 +Brand#31 |SMALL PLATED BRASS | 45| 4 +Brand#31 |SMALL PLATED COPPER | 3| 4 +Brand#31 |SMALL PLATED COPPER | 36| 4 +Brand#31 |SMALL PLATED COPPER | 45| 4 +Brand#31 |SMALL PLATED NICKEL | 3| 4 +Brand#31 |SMALL PLATED NICKEL | 9| 4 +Brand#31 |SMALL PLATED NICKEL | 14| 4 +Brand#31 |SMALL PLATED NICKEL | 45| 4 +Brand#31 |SMALL PLATED NICKEL | 49| 4 +Brand#31 |SMALL PLATED STEEL | 3| 4 +Brand#31 |SMALL PLATED STEEL | 49| 4 +Brand#31 |SMALL PLATED TIN | 14| 4 +Brand#31 |SMALL PLATED TIN | 19| 4 +Brand#31 |SMALL PLATED TIN | 23| 4 +Brand#31 |SMALL PLATED TIN | 49| 4 +Brand#31 |SMALL POLISHED BRASS | 9| 4 +Brand#31 |SMALL POLISHED BRASS | 36| 4 +Brand#31 |SMALL POLISHED BRASS | 45| 4 +Brand#31 |SMALL POLISHED COPPER | 14| 4 +Brand#31 |SMALL POLISHED COPPER | 23| 4 +Brand#31 |SMALL POLISHED COPPER | 45| 4 +Brand#31 |SMALL POLISHED COPPER | 49| 4 +Brand#31 |SMALL POLISHED NICKEL | 9| 4 +Brand#31 |SMALL POLISHED NICKEL | 23| 4 +Brand#31 |SMALL POLISHED NICKEL | 45| 4 +Brand#31 |SMALL POLISHED NICKEL | 49| 4 +Brand#31 |SMALL POLISHED STEEL | 36| 4 +Brand#31 |SMALL POLISHED STEEL | 45| 4 +Brand#31 |SMALL POLISHED TIN | 3| 4 +Brand#31 |SMALL POLISHED TIN | 19| 4 +Brand#31 |STANDARD ANODIZED BRASS | 3| 4 +Brand#31 |STANDARD ANODIZED BRASS | 14| 4 +Brand#31 |STANDARD ANODIZED BRASS | 23| 4 +Brand#31 |STANDARD ANODIZED BRASS | 49| 4 +Brand#31 |STANDARD ANODIZED COPPER | 3| 4 +Brand#31 |STANDARD ANODIZED COPPER | 9| 4 +Brand#31 |STANDARD ANODIZED COPPER | 19| 4 +Brand#31 |STANDARD ANODIZED COPPER | 36| 4 +Brand#31 |STANDARD ANODIZED COPPER | 49| 4 +Brand#31 |STANDARD ANODIZED NICKEL | 36| 4 +Brand#31 |STANDARD ANODIZED NICKEL | 49| 4 +Brand#31 |STANDARD ANODIZED STEEL | 3| 4 +Brand#31 |STANDARD ANODIZED STEEL | 14| 4 +Brand#31 |STANDARD ANODIZED STEEL | 23| 4 +Brand#31 |STANDARD ANODIZED TIN | 14| 4 +Brand#31 |STANDARD ANODIZED TIN | 23| 4 +Brand#31 |STANDARD BRUSHED BRASS | 3| 4 +Brand#31 |STANDARD BRUSHED BRASS | 14| 4 +Brand#31 |STANDARD BRUSHED BRASS | 19| 4 +Brand#31 |STANDARD BRUSHED BRASS | 23| 4 +Brand#31 |STANDARD BRUSHED BRASS | 49| 4 +Brand#31 |STANDARD BRUSHED COPPER | 9| 4 +Brand#31 |STANDARD BRUSHED COPPER | 14| 4 +Brand#31 |STANDARD BRUSHED COPPER | 19| 4 +Brand#31 |STANDARD BRUSHED COPPER | 23| 4 +Brand#31 |STANDARD BRUSHED COPPER | 49| 4 +Brand#31 |STANDARD BRUSHED NICKEL | 14| 4 +Brand#31 |STANDARD BRUSHED NICKEL | 19| 4 +Brand#31 |STANDARD BRUSHED NICKEL | 23| 4 +Brand#31 |STANDARD BRUSHED NICKEL | 49| 4 +Brand#31 |STANDARD BRUSHED STEEL | 3| 4 +Brand#31 |STANDARD BRUSHED STEEL | 23| 4 +Brand#31 |STANDARD BRUSHED STEEL | 49| 4 +Brand#31 |STANDARD BRUSHED TIN | 49| 4 +Brand#31 |STANDARD BURNISHED BRASS | 3| 4 +Brand#31 |STANDARD BURNISHED BRASS | 14| 4 +Brand#31 |STANDARD BURNISHED BRASS | 19| 4 +Brand#31 |STANDARD BURNISHED COPPER| 19| 4 +Brand#31 |STANDARD BURNISHED COPPER| 36| 4 +Brand#31 |STANDARD BURNISHED COPPER| 45| 4 +Brand#31 |STANDARD BURNISHED NICKEL| 3| 4 +Brand#31 |STANDARD BURNISHED NICKEL| 36| 4 +Brand#31 |STANDARD BURNISHED TIN | 14| 4 +Brand#31 |STANDARD BURNISHED TIN | 23| 4 +Brand#31 |STANDARD BURNISHED TIN | 45| 4 +Brand#31 |STANDARD BURNISHED TIN | 49| 4 +Brand#31 |STANDARD PLATED BRASS | 14| 4 +Brand#31 |STANDARD PLATED BRASS | 23| 4 +Brand#31 |STANDARD PLATED BRASS | 45| 4 +Brand#31 |STANDARD PLATED BRASS | 49| 4 +Brand#31 |STANDARD PLATED COPPER | 9| 4 +Brand#31 |STANDARD PLATED COPPER | 19| 4 +Brand#31 |STANDARD PLATED COPPER | 45| 4 +Brand#31 |STANDARD PLATED NICKEL | 14| 4 +Brand#31 |STANDARD PLATED NICKEL | 19| 4 +Brand#31 |STANDARD PLATED NICKEL | 45| 4 +Brand#31 |STANDARD PLATED NICKEL | 49| 4 +Brand#31 |STANDARD PLATED STEEL | 3| 4 +Brand#31 |STANDARD PLATED STEEL | 14| 4 +Brand#31 |STANDARD PLATED STEEL | 36| 4 +Brand#31 |STANDARD PLATED STEEL | 45| 4 +Brand#31 |STANDARD PLATED STEEL | 49| 4 +Brand#31 |STANDARD PLATED TIN | 3| 4 +Brand#31 |STANDARD PLATED TIN | 45| 4 +Brand#31 |STANDARD PLATED TIN | 49| 4 +Brand#31 |STANDARD POLISHED BRASS | 3| 4 +Brand#31 |STANDARD POLISHED BRASS | 9| 4 +Brand#31 |STANDARD POLISHED BRASS | 45| 4 +Brand#31 |STANDARD POLISHED COPPER | 9| 4 +Brand#31 |STANDARD POLISHED COPPER | 36| 4 +Brand#31 |STANDARD POLISHED COPPER | 49| 4 +Brand#31 |STANDARD POLISHED NICKEL | 3| 4 +Brand#31 |STANDARD POLISHED NICKEL | 14| 4 +Brand#31 |STANDARD POLISHED NICKEL | 36| 4 +Brand#31 |STANDARD POLISHED NICKEL | 49| 4 +Brand#31 |STANDARD POLISHED STEEL | 9| 4 +Brand#31 |STANDARD POLISHED STEEL | 49| 4 +Brand#31 |STANDARD POLISHED TIN | 3| 4 +Brand#31 |STANDARD POLISHED TIN | 9| 4 +Brand#31 |STANDARD POLISHED TIN | 14| 4 +Brand#32 |ECONOMY ANODIZED BRASS | 36| 4 +Brand#32 |ECONOMY ANODIZED NICKEL | 9| 4 +Brand#32 |ECONOMY ANODIZED NICKEL | 23| 4 +Brand#32 |ECONOMY ANODIZED NICKEL | 36| 4 +Brand#32 |ECONOMY ANODIZED STEEL | 9| 4 +Brand#32 |ECONOMY ANODIZED STEEL | 14| 4 +Brand#32 |ECONOMY ANODIZED STEEL | 36| 4 +Brand#32 |ECONOMY ANODIZED TIN | 3| 4 +Brand#32 |ECONOMY ANODIZED TIN | 9| 4 +Brand#32 |ECONOMY ANODIZED TIN | 14| 4 +Brand#32 |ECONOMY ANODIZED TIN | 19| 4 +Brand#32 |ECONOMY ANODIZED TIN | 36| 4 +Brand#32 |ECONOMY ANODIZED TIN | 45| 4 +Brand#32 |ECONOMY BRUSHED BRASS | 14| 4 +Brand#32 |ECONOMY BRUSHED BRASS | 19| 4 +Brand#32 |ECONOMY BRUSHED BRASS | 23| 4 +Brand#32 |ECONOMY BRUSHED BRASS | 36| 4 +Brand#32 |ECONOMY BRUSHED COPPER | 9| 4 +Brand#32 |ECONOMY BRUSHED COPPER | 19| 4 +Brand#32 |ECONOMY BRUSHED COPPER | 45| 4 +Brand#32 |ECONOMY BRUSHED NICKEL | 9| 4 +Brand#32 |ECONOMY BRUSHED NICKEL | 14| 4 +Brand#32 |ECONOMY BRUSHED NICKEL | 23| 4 +Brand#32 |ECONOMY BRUSHED NICKEL | 45| 4 +Brand#32 |ECONOMY BRUSHED STEEL | 19| 4 +Brand#32 |ECONOMY BRUSHED STEEL | 23| 4 +Brand#32 |ECONOMY BRUSHED STEEL | 45| 4 +Brand#32 |ECONOMY BRUSHED STEEL | 49| 4 +Brand#32 |ECONOMY BRUSHED TIN | 9| 4 +Brand#32 |ECONOMY BRUSHED TIN | 36| 4 +Brand#32 |ECONOMY BURNISHED BRASS | 3| 4 +Brand#32 |ECONOMY BURNISHED BRASS | 9| 4 +Brand#32 |ECONOMY BURNISHED BRASS | 14| 4 +Brand#32 |ECONOMY BURNISHED BRASS | 19| 4 +Brand#32 |ECONOMY BURNISHED BRASS | 23| 4 +Brand#32 |ECONOMY BURNISHED BRASS | 36| 4 +Brand#32 |ECONOMY BURNISHED BRASS | 49| 4 +Brand#32 |ECONOMY BURNISHED COPPER | 19| 4 +Brand#32 |ECONOMY BURNISHED COPPER | 23| 4 +Brand#32 |ECONOMY BURNISHED COPPER | 36| 4 +Brand#32 |ECONOMY BURNISHED COPPER | 45| 4 +Brand#32 |ECONOMY BURNISHED COPPER | 49| 4 +Brand#32 |ECONOMY BURNISHED NICKEL | 45| 4 +Brand#32 |ECONOMY BURNISHED NICKEL | 49| 4 +Brand#32 |ECONOMY BURNISHED STEEL | 23| 4 +Brand#32 |ECONOMY BURNISHED STEEL | 45| 4 +Brand#32 |ECONOMY BURNISHED STEEL | 49| 4 +Brand#32 |ECONOMY BURNISHED TIN | 14| 4 +Brand#32 |ECONOMY PLATED BRASS | 23| 4 +Brand#32 |ECONOMY PLATED BRASS | 36| 4 +Brand#32 |ECONOMY PLATED COPPER | 3| 4 +Brand#32 |ECONOMY PLATED COPPER | 9| 4 +Brand#32 |ECONOMY PLATED COPPER | 14| 4 +Brand#32 |ECONOMY PLATED COPPER | 23| 4 +Brand#32 |ECONOMY PLATED COPPER | 36| 4 +Brand#32 |ECONOMY PLATED COPPER | 45| 4 +Brand#32 |ECONOMY PLATED COPPER | 49| 4 +Brand#32 |ECONOMY PLATED NICKEL | 9| 4 +Brand#32 |ECONOMY PLATED NICKEL | 45| 4 +Brand#32 |ECONOMY PLATED STEEL | 9| 4 +Brand#32 |ECONOMY PLATED STEEL | 45| 4 +Brand#32 |ECONOMY PLATED TIN | 3| 4 +Brand#32 |ECONOMY PLATED TIN | 14| 4 +Brand#32 |ECONOMY PLATED TIN | 36| 4 +Brand#32 |ECONOMY POLISHED BRASS | 9| 4 +Brand#32 |ECONOMY POLISHED COPPER | 14| 4 +Brand#32 |ECONOMY POLISHED COPPER | 19| 4 +Brand#32 |ECONOMY POLISHED NICKEL | 36| 4 +Brand#32 |ECONOMY POLISHED NICKEL | 45| 4 +Brand#32 |ECONOMY POLISHED STEEL | 3| 4 +Brand#32 |ECONOMY POLISHED STEEL | 14| 4 +Brand#32 |ECONOMY POLISHED STEEL | 45| 4 +Brand#32 |ECONOMY POLISHED STEEL | 49| 4 +Brand#32 |ECONOMY POLISHED TIN | 14| 4 +Brand#32 |ECONOMY POLISHED TIN | 36| 4 +Brand#32 |ECONOMY POLISHED TIN | 45| 4 +Brand#32 |ECONOMY POLISHED TIN | 49| 4 +Brand#32 |LARGE ANODIZED BRASS | 14| 4 +Brand#32 |LARGE ANODIZED BRASS | 23| 4 +Brand#32 |LARGE ANODIZED COPPER | 9| 4 +Brand#32 |LARGE ANODIZED COPPER | 14| 4 +Brand#32 |LARGE ANODIZED COPPER | 36| 4 +Brand#32 |LARGE ANODIZED COPPER | 45| 4 +Brand#32 |LARGE ANODIZED NICKEL | 14| 4 +Brand#32 |LARGE ANODIZED NICKEL | 23| 4 +Brand#32 |LARGE ANODIZED STEEL | 19| 4 +Brand#32 |LARGE ANODIZED STEEL | 23| 4 +Brand#32 |LARGE ANODIZED STEEL | 45| 4 +Brand#32 |LARGE ANODIZED TIN | 3| 4 +Brand#32 |LARGE ANODIZED TIN | 45| 4 +Brand#32 |LARGE BRUSHED BRASS | 9| 4 +Brand#32 |LARGE BRUSHED BRASS | 36| 4 +Brand#32 |LARGE BRUSHED BRASS | 49| 4 +Brand#32 |LARGE BRUSHED COPPER | 19| 4 +Brand#32 |LARGE BRUSHED COPPER | 23| 4 +Brand#32 |LARGE BRUSHED COPPER | 36| 4 +Brand#32 |LARGE BRUSHED NICKEL | 23| 4 +Brand#32 |LARGE BRUSHED NICKEL | 36| 4 +Brand#32 |LARGE BRUSHED STEEL | 3| 4 +Brand#32 |LARGE BRUSHED STEEL | 14| 4 +Brand#32 |LARGE BRUSHED STEEL | 19| 4 +Brand#32 |LARGE BRUSHED STEEL | 36| 4 +Brand#32 |LARGE BRUSHED STEEL | 49| 4 +Brand#32 |LARGE BRUSHED TIN | 3| 4 +Brand#32 |LARGE BRUSHED TIN | 45| 4 +Brand#32 |LARGE BRUSHED TIN | 49| 4 +Brand#32 |LARGE BURNISHED BRASS | 19| 4 +Brand#32 |LARGE BURNISHED COPPER | 3| 4 +Brand#32 |LARGE BURNISHED COPPER | 9| 4 +Brand#32 |LARGE BURNISHED COPPER | 19| 4 +Brand#32 |LARGE BURNISHED COPPER | 45| 4 +Brand#32 |LARGE BURNISHED NICKEL | 14| 4 +Brand#32 |LARGE BURNISHED NICKEL | 23| 4 +Brand#32 |LARGE BURNISHED NICKEL | 49| 4 +Brand#32 |LARGE BURNISHED STEEL | 3| 4 +Brand#32 |LARGE BURNISHED STEEL | 36| 4 +Brand#32 |LARGE BURNISHED STEEL | 45| 4 +Brand#32 |LARGE BURNISHED TIN | 19| 4 +Brand#32 |LARGE PLATED COPPER | 3| 4 +Brand#32 |LARGE PLATED COPPER | 9| 4 +Brand#32 |LARGE PLATED COPPER | 23| 4 +Brand#32 |LARGE PLATED COPPER | 45| 4 +Brand#32 |LARGE PLATED NICKEL | 9| 4 +Brand#32 |LARGE PLATED NICKEL | 49| 4 +Brand#32 |LARGE PLATED STEEL | 3| 4 +Brand#32 |LARGE PLATED STEEL | 9| 4 +Brand#32 |LARGE PLATED STEEL | 14| 4 +Brand#32 |LARGE PLATED STEEL | 36| 4 +Brand#32 |LARGE PLATED STEEL | 49| 4 +Brand#32 |LARGE PLATED TIN | 19| 4 +Brand#32 |LARGE PLATED TIN | 23| 4 +Brand#32 |LARGE PLATED TIN | 45| 4 +Brand#32 |LARGE PLATED TIN | 49| 4 +Brand#32 |LARGE POLISHED BRASS | 3| 4 +Brand#32 |LARGE POLISHED BRASS | 14| 4 +Brand#32 |LARGE POLISHED BRASS | 49| 4 +Brand#32 |LARGE POLISHED COPPER | 14| 4 +Brand#32 |LARGE POLISHED COPPER | 36| 4 +Brand#32 |LARGE POLISHED COPPER | 45| 4 +Brand#32 |LARGE POLISHED COPPER | 49| 4 +Brand#32 |LARGE POLISHED NICKEL | 14| 4 +Brand#32 |LARGE POLISHED NICKEL | 19| 4 +Brand#32 |LARGE POLISHED NICKEL | 36| 4 +Brand#32 |LARGE POLISHED NICKEL | 45| 4 +Brand#32 |LARGE POLISHED NICKEL | 49| 4 +Brand#32 |LARGE POLISHED STEEL | 3| 4 +Brand#32 |LARGE POLISHED STEEL | 9| 4 +Brand#32 |LARGE POLISHED TIN | 23| 4 +Brand#32 |LARGE POLISHED TIN | 36| 4 +Brand#32 |MEDIUM ANODIZED BRASS | 9| 4 +Brand#32 |MEDIUM ANODIZED BRASS | 14| 4 +Brand#32 |MEDIUM ANODIZED BRASS | 19| 4 +Brand#32 |MEDIUM ANODIZED BRASS | 49| 4 +Brand#32 |MEDIUM ANODIZED COPPER | 9| 4 +Brand#32 |MEDIUM ANODIZED COPPER | 19| 4 +Brand#32 |MEDIUM ANODIZED COPPER | 23| 4 +Brand#32 |MEDIUM ANODIZED COPPER | 36| 4 +Brand#32 |MEDIUM ANODIZED NICKEL | 3| 4 +Brand#32 |MEDIUM ANODIZED NICKEL | 9| 4 +Brand#32 |MEDIUM ANODIZED NICKEL | 14| 4 +Brand#32 |MEDIUM ANODIZED NICKEL | 19| 4 +Brand#32 |MEDIUM ANODIZED NICKEL | 23| 4 +Brand#32 |MEDIUM ANODIZED STEEL | 14| 4 +Brand#32 |MEDIUM ANODIZED STEEL | 36| 4 +Brand#32 |MEDIUM ANODIZED STEEL | 45| 4 +Brand#32 |MEDIUM ANODIZED TIN | 14| 4 +Brand#32 |MEDIUM ANODIZED TIN | 23| 4 +Brand#32 |MEDIUM BRUSHED BRASS | 23| 4 +Brand#32 |MEDIUM BRUSHED BRASS | 45| 4 +Brand#32 |MEDIUM BRUSHED COPPER | 3| 4 +Brand#32 |MEDIUM BRUSHED COPPER | 9| 4 +Brand#32 |MEDIUM BRUSHED COPPER | 19| 4 +Brand#32 |MEDIUM BRUSHED COPPER | 45| 4 +Brand#32 |MEDIUM BRUSHED NICKEL | 14| 4 +Brand#32 |MEDIUM BRUSHED NICKEL | 23| 4 +Brand#32 |MEDIUM BRUSHED NICKEL | 49| 4 +Brand#32 |MEDIUM BRUSHED STEEL | 9| 4 +Brand#32 |MEDIUM BRUSHED STEEL | 14| 4 +Brand#32 |MEDIUM BRUSHED STEEL | 19| 4 +Brand#32 |MEDIUM BRUSHED STEEL | 36| 4 +Brand#32 |MEDIUM BRUSHED STEEL | 45| 4 +Brand#32 |MEDIUM BRUSHED STEEL | 49| 4 +Brand#32 |MEDIUM BRUSHED TIN | 14| 4 +Brand#32 |MEDIUM BRUSHED TIN | 49| 4 +Brand#32 |MEDIUM BURNISHED BRASS | 9| 4 +Brand#32 |MEDIUM BURNISHED BRASS | 36| 4 +Brand#32 |MEDIUM BURNISHED COPPER | 3| 4 +Brand#32 |MEDIUM BURNISHED COPPER | 14| 4 +Brand#32 |MEDIUM BURNISHED COPPER | 45| 4 +Brand#32 |MEDIUM BURNISHED NICKEL | 3| 4 +Brand#32 |MEDIUM BURNISHED NICKEL | 9| 4 +Brand#32 |MEDIUM BURNISHED NICKEL | 36| 4 +Brand#32 |MEDIUM BURNISHED STEEL | 19| 4 +Brand#32 |MEDIUM BURNISHED STEEL | 36| 4 +Brand#32 |MEDIUM BURNISHED TIN | 19| 4 +Brand#32 |MEDIUM BURNISHED TIN | 36| 4 +Brand#32 |MEDIUM BURNISHED TIN | 45| 4 +Brand#32 |MEDIUM BURNISHED TIN | 49| 4 +Brand#32 |MEDIUM PLATED BRASS | 19| 4 +Brand#32 |MEDIUM PLATED BRASS | 36| 4 +Brand#32 |MEDIUM PLATED COPPER | 14| 4 +Brand#32 |MEDIUM PLATED COPPER | 45| 4 +Brand#32 |MEDIUM PLATED COPPER | 49| 4 +Brand#32 |MEDIUM PLATED NICKEL | 3| 4 +Brand#32 |MEDIUM PLATED NICKEL | 14| 4 +Brand#32 |MEDIUM PLATED NICKEL | 19| 4 +Brand#32 |MEDIUM PLATED NICKEL | 36| 4 +Brand#32 |MEDIUM PLATED NICKEL | 45| 4 +Brand#32 |MEDIUM PLATED NICKEL | 49| 4 +Brand#32 |MEDIUM PLATED STEEL | 19| 4 +Brand#32 |MEDIUM PLATED STEEL | 36| 4 +Brand#32 |MEDIUM PLATED TIN | 9| 4 +Brand#32 |MEDIUM PLATED TIN | 45| 4 +Brand#32 |MEDIUM PLATED TIN | 49| 4 +Brand#32 |PROMO ANODIZED BRASS | 19| 4 +Brand#32 |PROMO ANODIZED BRASS | 23| 4 +Brand#32 |PROMO ANODIZED BRASS | 49| 4 +Brand#32 |PROMO ANODIZED COPPER | 14| 4 +Brand#32 |PROMO ANODIZED COPPER | 36| 4 +Brand#32 |PROMO ANODIZED NICKEL | 23| 4 +Brand#32 |PROMO ANODIZED NICKEL | 45| 4 +Brand#32 |PROMO ANODIZED STEEL | 14| 4 +Brand#32 |PROMO ANODIZED STEEL | 45| 4 +Brand#32 |PROMO ANODIZED TIN | 9| 4 +Brand#32 |PROMO ANODIZED TIN | 19| 4 +Brand#32 |PROMO ANODIZED TIN | 23| 4 +Brand#32 |PROMO BRUSHED BRASS | 23| 4 +Brand#32 |PROMO BRUSHED BRASS | 45| 4 +Brand#32 |PROMO BRUSHED COPPER | 9| 4 +Brand#32 |PROMO BRUSHED COPPER | 19| 4 +Brand#32 |PROMO BRUSHED COPPER | 36| 4 +Brand#32 |PROMO BRUSHED NICKEL | 14| 4 +Brand#32 |PROMO BRUSHED NICKEL | 19| 4 +Brand#32 |PROMO BRUSHED NICKEL | 49| 4 +Brand#32 |PROMO BRUSHED STEEL | 14| 4 +Brand#32 |PROMO BRUSHED STEEL | 19| 4 +Brand#32 |PROMO BRUSHED STEEL | 36| 4 +Brand#32 |PROMO BRUSHED TIN | 3| 4 +Brand#32 |PROMO BRUSHED TIN | 19| 4 +Brand#32 |PROMO BURNISHED BRASS | 9| 4 +Brand#32 |PROMO BURNISHED BRASS | 23| 4 +Brand#32 |PROMO BURNISHED BRASS | 36| 4 +Brand#32 |PROMO BURNISHED BRASS | 49| 4 +Brand#32 |PROMO BURNISHED COPPER | 14| 4 +Brand#32 |PROMO BURNISHED COPPER | 23| 4 +Brand#32 |PROMO BURNISHED COPPER | 45| 4 +Brand#32 |PROMO BURNISHED STEEL | 3| 4 +Brand#32 |PROMO BURNISHED STEEL | 19| 4 +Brand#32 |PROMO BURNISHED STEEL | 49| 4 +Brand#32 |PROMO BURNISHED TIN | 19| 4 +Brand#32 |PROMO PLATED BRASS | 14| 4 +Brand#32 |PROMO PLATED BRASS | 19| 4 +Brand#32 |PROMO PLATED BRASS | 45| 4 +Brand#32 |PROMO PLATED BRASS | 49| 4 +Brand#32 |PROMO PLATED COPPER | 9| 4 +Brand#32 |PROMO PLATED COPPER | 14| 4 +Brand#32 |PROMO PLATED COPPER | 36| 4 +Brand#32 |PROMO PLATED NICKEL | 3| 4 +Brand#32 |PROMO PLATED NICKEL | 14| 4 +Brand#32 |PROMO PLATED NICKEL | 19| 4 +Brand#32 |PROMO PLATED NICKEL | 23| 4 +Brand#32 |PROMO PLATED NICKEL | 45| 4 +Brand#32 |PROMO PLATED STEEL | 9| 4 +Brand#32 |PROMO PLATED STEEL | 19| 4 +Brand#32 |PROMO PLATED TIN | 14| 4 +Brand#32 |PROMO PLATED TIN | 23| 4 +Brand#32 |PROMO POLISHED BRASS | 9| 4 +Brand#32 |PROMO POLISHED BRASS | 19| 4 +Brand#32 |PROMO POLISHED BRASS | 36| 4 +Brand#32 |PROMO POLISHED COPPER | 3| 4 +Brand#32 |PROMO POLISHED COPPER | 9| 4 +Brand#32 |PROMO POLISHED COPPER | 14| 4 +Brand#32 |PROMO POLISHED COPPER | 19| 4 +Brand#32 |PROMO POLISHED COPPER | 23| 4 +Brand#32 |PROMO POLISHED COPPER | 36| 4 +Brand#32 |PROMO POLISHED NICKEL | 14| 4 +Brand#32 |PROMO POLISHED NICKEL | 19| 4 +Brand#32 |PROMO POLISHED NICKEL | 45| 4 +Brand#32 |PROMO POLISHED STEEL | 9| 4 +Brand#32 |PROMO POLISHED STEEL | 23| 4 +Brand#32 |PROMO POLISHED STEEL | 45| 4 +Brand#32 |SMALL ANODIZED BRASS | 9| 4 +Brand#32 |SMALL ANODIZED COPPER | 3| 4 +Brand#32 |SMALL ANODIZED COPPER | 19| 4 +Brand#32 |SMALL ANODIZED COPPER | 45| 4 +Brand#32 |SMALL ANODIZED NICKEL | 3| 4 +Brand#32 |SMALL ANODIZED NICKEL | 14| 4 +Brand#32 |SMALL ANODIZED NICKEL | 19| 4 +Brand#32 |SMALL ANODIZED NICKEL | 36| 4 +Brand#32 |SMALL ANODIZED NICKEL | 45| 4 +Brand#32 |SMALL ANODIZED STEEL | 9| 4 +Brand#32 |SMALL ANODIZED STEEL | 14| 4 +Brand#32 |SMALL ANODIZED STEEL | 19| 4 +Brand#32 |SMALL ANODIZED TIN | 9| 4 +Brand#32 |SMALL ANODIZED TIN | 19| 4 +Brand#32 |SMALL ANODIZED TIN | 23| 4 +Brand#32 |SMALL ANODIZED TIN | 45| 4 +Brand#32 |SMALL BRUSHED BRASS | 3| 4 +Brand#32 |SMALL BRUSHED BRASS | 9| 4 +Brand#32 |SMALL BRUSHED BRASS | 19| 4 +Brand#32 |SMALL BRUSHED BRASS | 23| 4 +Brand#32 |SMALL BRUSHED BRASS | 45| 4 +Brand#32 |SMALL BRUSHED COPPER | 3| 4 +Brand#32 |SMALL BRUSHED COPPER | 9| 4 +Brand#32 |SMALL BRUSHED COPPER | 45| 4 +Brand#32 |SMALL BRUSHED NICKEL | 9| 4 +Brand#32 |SMALL BRUSHED NICKEL | 14| 4 +Brand#32 |SMALL BRUSHED NICKEL | 23| 4 +Brand#32 |SMALL BRUSHED NICKEL | 45| 4 +Brand#32 |SMALL BRUSHED STEEL | 3| 4 +Brand#32 |SMALL BRUSHED STEEL | 19| 4 +Brand#32 |SMALL BRUSHED STEEL | 23| 4 +Brand#32 |SMALL BRUSHED STEEL | 45| 4 +Brand#32 |SMALL BRUSHED STEEL | 49| 4 +Brand#32 |SMALL BRUSHED TIN | 19| 4 +Brand#32 |SMALL BRUSHED TIN | 23| 4 +Brand#32 |SMALL BRUSHED TIN | 36| 4 +Brand#32 |SMALL BRUSHED TIN | 45| 4 +Brand#32 |SMALL BRUSHED TIN | 49| 4 +Brand#32 |SMALL BURNISHED BRASS | 3| 4 +Brand#32 |SMALL BURNISHED BRASS | 14| 4 +Brand#32 |SMALL BURNISHED BRASS | 19| 4 +Brand#32 |SMALL BURNISHED BRASS | 23| 4 +Brand#32 |SMALL BURNISHED COPPER | 9| 4 +Brand#32 |SMALL BURNISHED COPPER | 14| 4 +Brand#32 |SMALL BURNISHED COPPER | 23| 4 +Brand#32 |SMALL BURNISHED COPPER | 36| 4 +Brand#32 |SMALL BURNISHED COPPER | 49| 4 +Brand#32 |SMALL BURNISHED NICKEL | 14| 4 +Brand#32 |SMALL BURNISHED NICKEL | 19| 4 +Brand#32 |SMALL BURNISHED NICKEL | 36| 4 +Brand#32 |SMALL BURNISHED NICKEL | 45| 4 +Brand#32 |SMALL BURNISHED NICKEL | 49| 4 +Brand#32 |SMALL BURNISHED STEEL | 36| 4 +Brand#32 |SMALL BURNISHED STEEL | 45| 4 +Brand#32 |SMALL BURNISHED STEEL | 49| 4 +Brand#32 |SMALL BURNISHED TIN | 9| 4 +Brand#32 |SMALL PLATED BRASS | 14| 4 +Brand#32 |SMALL PLATED BRASS | 23| 4 +Brand#32 |SMALL PLATED BRASS | 49| 4 +Brand#32 |SMALL PLATED NICKEL | 19| 4 +Brand#32 |SMALL PLATED NICKEL | 23| 4 +Brand#32 |SMALL PLATED STEEL | 45| 4 +Brand#32 |SMALL PLATED STEEL | 49| 4 +Brand#32 |SMALL PLATED TIN | 9| 4 +Brand#32 |SMALL PLATED TIN | 19| 4 +Brand#32 |SMALL POLISHED BRASS | 9| 4 +Brand#32 |SMALL POLISHED BRASS | 19| 4 +Brand#32 |SMALL POLISHED BRASS | 36| 4 +Brand#32 |SMALL POLISHED BRASS | 49| 4 +Brand#32 |SMALL POLISHED NICKEL | 3| 4 +Brand#32 |SMALL POLISHED NICKEL | 14| 4 +Brand#32 |SMALL POLISHED NICKEL | 19| 4 +Brand#32 |SMALL POLISHED STEEL | 49| 4 +Brand#32 |SMALL POLISHED TIN | 3| 4 +Brand#32 |SMALL POLISHED TIN | 14| 4 +Brand#32 |SMALL POLISHED TIN | 23| 4 +Brand#32 |SMALL POLISHED TIN | 49| 4 +Brand#32 |STANDARD ANODIZED BRASS | 9| 4 +Brand#32 |STANDARD ANODIZED BRASS | 23| 4 +Brand#32 |STANDARD ANODIZED BRASS | 36| 4 +Brand#32 |STANDARD ANODIZED BRASS | 45| 4 +Brand#32 |STANDARD ANODIZED COPPER | 3| 4 +Brand#32 |STANDARD ANODIZED COPPER | 9| 4 +Brand#32 |STANDARD ANODIZED NICKEL | 3| 4 +Brand#32 |STANDARD ANODIZED NICKEL | 14| 4 +Brand#32 |STANDARD ANODIZED NICKEL | 23| 4 +Brand#32 |STANDARD ANODIZED STEEL | 23| 4 +Brand#32 |STANDARD ANODIZED TIN | 3| 4 +Brand#32 |STANDARD ANODIZED TIN | 23| 4 +Brand#32 |STANDARD ANODIZED TIN | 49| 4 +Brand#32 |STANDARD BRUSHED BRASS | 3| 4 +Brand#32 |STANDARD BRUSHED BRASS | 9| 4 +Brand#32 |STANDARD BRUSHED BRASS | 19| 4 +Brand#32 |STANDARD BRUSHED BRASS | 23| 4 +Brand#32 |STANDARD BRUSHED COPPER | 14| 4 +Brand#32 |STANDARD BRUSHED COPPER | 19| 4 +Brand#32 |STANDARD BRUSHED COPPER | 23| 4 +Brand#32 |STANDARD BRUSHED COPPER | 49| 4 +Brand#32 |STANDARD BRUSHED NICKEL | 9| 4 +Brand#32 |STANDARD BRUSHED NICKEL | 23| 4 +Brand#32 |STANDARD BRUSHED NICKEL | 45| 4 +Brand#32 |STANDARD BRUSHED NICKEL | 49| 4 +Brand#32 |STANDARD BRUSHED TIN | 14| 4 +Brand#32 |STANDARD BRUSHED TIN | 19| 4 +Brand#32 |STANDARD BRUSHED TIN | 23| 4 +Brand#32 |STANDARD BURNISHED BRASS | 3| 4 +Brand#32 |STANDARD BURNISHED BRASS | 19| 4 +Brand#32 |STANDARD BURNISHED BRASS | 23| 4 +Brand#32 |STANDARD BURNISHED BRASS | 36| 4 +Brand#32 |STANDARD BURNISHED COPPER| 14| 4 +Brand#32 |STANDARD BURNISHED COPPER| 23| 4 +Brand#32 |STANDARD BURNISHED COPPER| 36| 4 +Brand#32 |STANDARD BURNISHED NICKEL| 36| 4 +Brand#32 |STANDARD BURNISHED NICKEL| 45| 4 +Brand#32 |STANDARD BURNISHED STEEL | 9| 4 +Brand#32 |STANDARD BURNISHED STEEL | 23| 4 +Brand#32 |STANDARD BURNISHED TIN | 3| 4 +Brand#32 |STANDARD BURNISHED TIN | 9| 4 +Brand#32 |STANDARD BURNISHED TIN | 19| 4 +Brand#32 |STANDARD BURNISHED TIN | 36| 4 +Brand#32 |STANDARD PLATED BRASS | 23| 4 +Brand#32 |STANDARD PLATED BRASS | 45| 4 +Brand#32 |STANDARD PLATED COPPER | 3| 4 +Brand#32 |STANDARD PLATED COPPER | 36| 4 +Brand#32 |STANDARD PLATED NICKEL | 49| 4 +Brand#32 |STANDARD PLATED STEEL | 3| 4 +Brand#32 |STANDARD PLATED STEEL | 19| 4 +Brand#32 |STANDARD PLATED STEEL | 36| 4 +Brand#32 |STANDARD PLATED TIN | 14| 4 +Brand#32 |STANDARD PLATED TIN | 23| 4 +Brand#32 |STANDARD PLATED TIN | 36| 4 +Brand#32 |STANDARD PLATED TIN | 49| 4 +Brand#32 |STANDARD POLISHED BRASS | 19| 4 +Brand#32 |STANDARD POLISHED BRASS | 23| 4 +Brand#32 |STANDARD POLISHED BRASS | 45| 4 +Brand#32 |STANDARD POLISHED BRASS | 49| 4 +Brand#32 |STANDARD POLISHED COPPER | 19| 4 +Brand#32 |STANDARD POLISHED COPPER | 45| 4 +Brand#32 |STANDARD POLISHED COPPER | 49| 4 +Brand#32 |STANDARD POLISHED NICKEL | 19| 4 +Brand#32 |STANDARD POLISHED NICKEL | 36| 4 +Brand#32 |STANDARD POLISHED NICKEL | 45| 4 +Brand#32 |STANDARD POLISHED STEEL | 3| 4 +Brand#32 |STANDARD POLISHED STEEL | 19| 4 +Brand#32 |STANDARD POLISHED STEEL | 45| 4 +Brand#32 |STANDARD POLISHED STEEL | 49| 4 +Brand#32 |STANDARD POLISHED TIN | 9| 4 +Brand#32 |STANDARD POLISHED TIN | 36| 4 +Brand#33 |ECONOMY ANODIZED BRASS | 9| 4 +Brand#33 |ECONOMY ANODIZED BRASS | 14| 4 +Brand#33 |ECONOMY ANODIZED BRASS | 23| 4 +Brand#33 |ECONOMY ANODIZED BRASS | 36| 4 +Brand#33 |ECONOMY ANODIZED COPPER | 14| 4 +Brand#33 |ECONOMY ANODIZED NICKEL | 9| 4 +Brand#33 |ECONOMY ANODIZED NICKEL | 49| 4 +Brand#33 |ECONOMY ANODIZED STEEL | 3| 4 +Brand#33 |ECONOMY ANODIZED STEEL | 14| 4 +Brand#33 |ECONOMY ANODIZED TIN | 9| 4 +Brand#33 |ECONOMY ANODIZED TIN | 14| 4 +Brand#33 |ECONOMY ANODIZED TIN | 36| 4 +Brand#33 |ECONOMY BRUSHED BRASS | 23| 4 +Brand#33 |ECONOMY BRUSHED COPPER | 49| 4 +Brand#33 |ECONOMY BRUSHED NICKEL | 3| 4 +Brand#33 |ECONOMY BRUSHED NICKEL | 36| 4 +Brand#33 |ECONOMY BRUSHED STEEL | 3| 4 +Brand#33 |ECONOMY BRUSHED STEEL | 9| 4 +Brand#33 |ECONOMY BRUSHED STEEL | 45| 4 +Brand#33 |ECONOMY BRUSHED TIN | 9| 4 +Brand#33 |ECONOMY BRUSHED TIN | 14| 4 +Brand#33 |ECONOMY BRUSHED TIN | 19| 4 +Brand#33 |ECONOMY BRUSHED TIN | 23| 4 +Brand#33 |ECONOMY BRUSHED TIN | 49| 4 +Brand#33 |ECONOMY BURNISHED BRASS | 23| 4 +Brand#33 |ECONOMY BURNISHED BRASS | 36| 4 +Brand#33 |ECONOMY BURNISHED COPPER | 9| 4 +Brand#33 |ECONOMY BURNISHED COPPER | 23| 4 +Brand#33 |ECONOMY BURNISHED COPPER | 36| 4 +Brand#33 |ECONOMY BURNISHED COPPER | 45| 4 +Brand#33 |ECONOMY BURNISHED COPPER | 49| 4 +Brand#33 |ECONOMY BURNISHED NICKEL | 3| 4 +Brand#33 |ECONOMY BURNISHED NICKEL | 23| 4 +Brand#33 |ECONOMY BURNISHED NICKEL | 36| 4 +Brand#33 |ECONOMY BURNISHED NICKEL | 49| 4 +Brand#33 |ECONOMY BURNISHED STEEL | 3| 4 +Brand#33 |ECONOMY BURNISHED STEEL | 14| 4 +Brand#33 |ECONOMY BURNISHED STEEL | 23| 4 +Brand#33 |ECONOMY BURNISHED STEEL | 45| 4 +Brand#33 |ECONOMY BURNISHED TIN | 14| 4 +Brand#33 |ECONOMY BURNISHED TIN | 19| 4 +Brand#33 |ECONOMY BURNISHED TIN | 36| 4 +Brand#33 |ECONOMY PLATED BRASS | 23| 4 +Brand#33 |ECONOMY PLATED COPPER | 3| 4 +Brand#33 |ECONOMY PLATED COPPER | 36| 4 +Brand#33 |ECONOMY PLATED COPPER | 45| 4 +Brand#33 |ECONOMY PLATED NICKEL | 9| 4 +Brand#33 |ECONOMY PLATED NICKEL | 14| 4 +Brand#33 |ECONOMY PLATED NICKEL | 23| 4 +Brand#33 |ECONOMY PLATED NICKEL | 45| 4 +Brand#33 |ECONOMY PLATED NICKEL | 49| 4 +Brand#33 |ECONOMY PLATED STEEL | 36| 4 +Brand#33 |ECONOMY PLATED STEEL | 49| 4 +Brand#33 |ECONOMY PLATED TIN | 14| 4 +Brand#33 |ECONOMY PLATED TIN | 19| 4 +Brand#33 |ECONOMY PLATED TIN | 23| 4 +Brand#33 |ECONOMY PLATED TIN | 36| 4 +Brand#33 |ECONOMY POLISHED BRASS | 9| 4 +Brand#33 |ECONOMY POLISHED COPPER | 9| 4 +Brand#33 |ECONOMY POLISHED COPPER | 23| 4 +Brand#33 |ECONOMY POLISHED NICKEL | 3| 4 +Brand#33 |ECONOMY POLISHED NICKEL | 23| 4 +Brand#33 |ECONOMY POLISHED NICKEL | 36| 4 +Brand#33 |ECONOMY POLISHED NICKEL | 49| 4 +Brand#33 |ECONOMY POLISHED STEEL | 14| 4 +Brand#33 |ECONOMY POLISHED STEEL | 19| 4 +Brand#33 |ECONOMY POLISHED STEEL | 23| 4 +Brand#33 |ECONOMY POLISHED STEEL | 45| 4 +Brand#33 |ECONOMY POLISHED TIN | 3| 4 +Brand#33 |ECONOMY POLISHED TIN | 9| 4 +Brand#33 |ECONOMY POLISHED TIN | 14| 4 +Brand#33 |ECONOMY POLISHED TIN | 23| 4 +Brand#33 |ECONOMY POLISHED TIN | 45| 4 +Brand#33 |LARGE ANODIZED BRASS | 14| 4 +Brand#33 |LARGE ANODIZED COPPER | 14| 4 +Brand#33 |LARGE ANODIZED COPPER | 45| 4 +Brand#33 |LARGE ANODIZED COPPER | 49| 4 +Brand#33 |LARGE ANODIZED NICKEL | 3| 4 +Brand#33 |LARGE ANODIZED NICKEL | 9| 4 +Brand#33 |LARGE ANODIZED NICKEL | 14| 4 +Brand#33 |LARGE ANODIZED NICKEL | 36| 4 +Brand#33 |LARGE ANODIZED STEEL | 14| 4 +Brand#33 |LARGE ANODIZED STEEL | 19| 4 +Brand#33 |LARGE ANODIZED STEEL | 36| 4 +Brand#33 |LARGE ANODIZED TIN | 14| 4 +Brand#33 |LARGE ANODIZED TIN | 19| 4 +Brand#33 |LARGE BRUSHED BRASS | 45| 4 +Brand#33 |LARGE BRUSHED COPPER | 3| 4 +Brand#33 |LARGE BRUSHED COPPER | 14| 4 +Brand#33 |LARGE BRUSHED COPPER | 23| 4 +Brand#33 |LARGE BRUSHED COPPER | 36| 4 +Brand#33 |LARGE BRUSHED COPPER | 45| 4 +Brand#33 |LARGE BRUSHED NICKEL | 3| 4 +Brand#33 |LARGE BRUSHED STEEL | 9| 4 +Brand#33 |LARGE BRUSHED STEEL | 14| 4 +Brand#33 |LARGE BRUSHED STEEL | 19| 4 +Brand#33 |LARGE BRUSHED TIN | 9| 4 +Brand#33 |LARGE BURNISHED COPPER | 3| 4 +Brand#33 |LARGE BURNISHED COPPER | 23| 4 +Brand#33 |LARGE BURNISHED COPPER | 36| 4 +Brand#33 |LARGE BURNISHED COPPER | 49| 4 +Brand#33 |LARGE BURNISHED NICKEL | 23| 4 +Brand#33 |LARGE BURNISHED NICKEL | 45| 4 +Brand#33 |LARGE BURNISHED STEEL | 19| 4 +Brand#33 |LARGE BURNISHED STEEL | 36| 4 +Brand#33 |LARGE BURNISHED TIN | 19| 4 +Brand#33 |LARGE BURNISHED TIN | 36| 4 +Brand#33 |LARGE PLATED BRASS | 9| 4 +Brand#33 |LARGE PLATED BRASS | 23| 4 +Brand#33 |LARGE PLATED BRASS | 36| 4 +Brand#33 |LARGE PLATED BRASS | 49| 4 +Brand#33 |LARGE PLATED COPPER | 9| 4 +Brand#33 |LARGE PLATED COPPER | 14| 4 +Brand#33 |LARGE PLATED COPPER | 19| 4 +Brand#33 |LARGE PLATED COPPER | 23| 4 +Brand#33 |LARGE PLATED COPPER | 45| 4 +Brand#33 |LARGE PLATED COPPER | 49| 4 +Brand#33 |LARGE PLATED NICKEL | 14| 4 +Brand#33 |LARGE PLATED NICKEL | 45| 4 +Brand#33 |LARGE PLATED STEEL | 9| 4 +Brand#33 |LARGE PLATED STEEL | 19| 4 +Brand#33 |LARGE PLATED STEEL | 23| 4 +Brand#33 |LARGE PLATED STEEL | 36| 4 +Brand#33 |LARGE PLATED STEEL | 45| 4 +Brand#33 |LARGE PLATED STEEL | 49| 4 +Brand#33 |LARGE PLATED TIN | 9| 4 +Brand#33 |LARGE PLATED TIN | 14| 4 +Brand#33 |LARGE PLATED TIN | 23| 4 +Brand#33 |LARGE PLATED TIN | 45| 4 +Brand#33 |LARGE PLATED TIN | 49| 4 +Brand#33 |LARGE POLISHED BRASS | 19| 4 +Brand#33 |LARGE POLISHED BRASS | 45| 4 +Brand#33 |LARGE POLISHED BRASS | 49| 4 +Brand#33 |LARGE POLISHED COPPER | 14| 4 +Brand#33 |LARGE POLISHED COPPER | 19| 4 +Brand#33 |LARGE POLISHED COPPER | 23| 4 +Brand#33 |LARGE POLISHED COPPER | 49| 4 +Brand#33 |LARGE POLISHED NICKEL | 23| 4 +Brand#33 |LARGE POLISHED NICKEL | 36| 4 +Brand#33 |LARGE POLISHED STEEL | 9| 4 +Brand#33 |LARGE POLISHED STEEL | 14| 4 +Brand#33 |LARGE POLISHED STEEL | 19| 4 +Brand#33 |LARGE POLISHED STEEL | 23| 4 +Brand#33 |LARGE POLISHED STEEL | 49| 4 +Brand#33 |LARGE POLISHED TIN | 45| 4 +Brand#33 |MEDIUM ANODIZED BRASS | 9| 4 +Brand#33 |MEDIUM ANODIZED BRASS | 36| 4 +Brand#33 |MEDIUM ANODIZED BRASS | 45| 4 +Brand#33 |MEDIUM ANODIZED BRASS | 49| 4 +Brand#33 |MEDIUM ANODIZED COPPER | 14| 4 +Brand#33 |MEDIUM ANODIZED COPPER | 36| 4 +Brand#33 |MEDIUM ANODIZED COPPER | 49| 4 +Brand#33 |MEDIUM ANODIZED NICKEL | 9| 4 +Brand#33 |MEDIUM ANODIZED NICKEL | 14| 4 +Brand#33 |MEDIUM ANODIZED NICKEL | 23| 4 +Brand#33 |MEDIUM ANODIZED STEEL | 19| 4 +Brand#33 |MEDIUM ANODIZED STEEL | 23| 4 +Brand#33 |MEDIUM ANODIZED STEEL | 36| 4 +Brand#33 |MEDIUM ANODIZED STEEL | 49| 4 +Brand#33 |MEDIUM ANODIZED TIN | 3| 4 +Brand#33 |MEDIUM ANODIZED TIN | 19| 4 +Brand#33 |MEDIUM BRUSHED BRASS | 3| 4 +Brand#33 |MEDIUM BRUSHED BRASS | 14| 4 +Brand#33 |MEDIUM BRUSHED BRASS | 36| 4 +Brand#33 |MEDIUM BRUSHED COPPER | 23| 4 +Brand#33 |MEDIUM BRUSHED NICKEL | 19| 4 +Brand#33 |MEDIUM BRUSHED NICKEL | 36| 4 +Brand#33 |MEDIUM BRUSHED NICKEL | 49| 4 +Brand#33 |MEDIUM BRUSHED STEEL | 3| 4 +Brand#33 |MEDIUM BRUSHED STEEL | 9| 4 +Brand#33 |MEDIUM BRUSHED STEEL | 19| 4 +Brand#33 |MEDIUM BRUSHED STEEL | 49| 4 +Brand#33 |MEDIUM BRUSHED TIN | 3| 4 +Brand#33 |MEDIUM BRUSHED TIN | 14| 4 +Brand#33 |MEDIUM BURNISHED BRASS | 14| 4 +Brand#33 |MEDIUM BURNISHED BRASS | 19| 4 +Brand#33 |MEDIUM BURNISHED BRASS | 45| 4 +Brand#33 |MEDIUM BURNISHED COPPER | 19| 4 +Brand#33 |MEDIUM BURNISHED COPPER | 36| 4 +Brand#33 |MEDIUM BURNISHED NICKEL | 14| 4 +Brand#33 |MEDIUM BURNISHED NICKEL | 23| 4 +Brand#33 |MEDIUM BURNISHED NICKEL | 36| 4 +Brand#33 |MEDIUM BURNISHED STEEL | 23| 4 +Brand#33 |MEDIUM BURNISHED STEEL | 36| 4 +Brand#33 |MEDIUM BURNISHED STEEL | 45| 4 +Brand#33 |MEDIUM BURNISHED TIN | 19| 4 +Brand#33 |MEDIUM PLATED BRASS | 9| 4 +Brand#33 |MEDIUM PLATED BRASS | 36| 4 +Brand#33 |MEDIUM PLATED BRASS | 45| 4 +Brand#33 |MEDIUM PLATED BRASS | 49| 4 +Brand#33 |MEDIUM PLATED COPPER | 45| 4 +Brand#33 |MEDIUM PLATED COPPER | 49| 4 +Brand#33 |MEDIUM PLATED NICKEL | 14| 4 +Brand#33 |MEDIUM PLATED STEEL | 9| 4 +Brand#33 |MEDIUM PLATED STEEL | 23| 4 +Brand#33 |MEDIUM PLATED STEEL | 36| 4 +Brand#33 |MEDIUM PLATED STEEL | 49| 4 +Brand#33 |MEDIUM PLATED TIN | 3| 4 +Brand#33 |PROMO ANODIZED BRASS | 36| 4 +Brand#33 |PROMO ANODIZED COPPER | 3| 4 +Brand#33 |PROMO ANODIZED COPPER | 9| 4 +Brand#33 |PROMO ANODIZED NICKEL | 3| 4 +Brand#33 |PROMO ANODIZED NICKEL | 19| 4 +Brand#33 |PROMO ANODIZED NICKEL | 49| 4 +Brand#33 |PROMO ANODIZED STEEL | 3| 4 +Brand#33 |PROMO ANODIZED STEEL | 49| 4 +Brand#33 |PROMO BRUSHED BRASS | 9| 4 +Brand#33 |PROMO BRUSHED BRASS | 14| 4 +Brand#33 |PROMO BRUSHED BRASS | 36| 4 +Brand#33 |PROMO BRUSHED BRASS | 45| 4 +Brand#33 |PROMO BRUSHED BRASS | 49| 4 +Brand#33 |PROMO BRUSHED COPPER | 19| 4 +Brand#33 |PROMO BRUSHED COPPER | 23| 4 +Brand#33 |PROMO BRUSHED COPPER | 36| 4 +Brand#33 |PROMO BRUSHED NICKEL | 3| 4 +Brand#33 |PROMO BRUSHED NICKEL | 23| 4 +Brand#33 |PROMO BRUSHED STEEL | 19| 4 +Brand#33 |PROMO BRUSHED STEEL | 23| 4 +Brand#33 |PROMO BRUSHED TIN | 14| 4 +Brand#33 |PROMO BRUSHED TIN | 19| 4 +Brand#33 |PROMO BRUSHED TIN | 36| 4 +Brand#33 |PROMO BRUSHED TIN | 45| 4 +Brand#33 |PROMO BURNISHED BRASS | 14| 4 +Brand#33 |PROMO BURNISHED BRASS | 23| 4 +Brand#33 |PROMO BURNISHED COPPER | 9| 4 +Brand#33 |PROMO BURNISHED COPPER | 14| 4 +Brand#33 |PROMO BURNISHED COPPER | 36| 4 +Brand#33 |PROMO BURNISHED NICKEL | 9| 4 +Brand#33 |PROMO BURNISHED NICKEL | 36| 4 +Brand#33 |PROMO BURNISHED NICKEL | 45| 4 +Brand#33 |PROMO BURNISHED STEEL | 23| 4 +Brand#33 |PROMO BURNISHED STEEL | 49| 4 +Brand#33 |PROMO BURNISHED TIN | 14| 4 +Brand#33 |PROMO PLATED BRASS | 19| 4 +Brand#33 |PROMO PLATED COPPER | 9| 4 +Brand#33 |PROMO PLATED COPPER | 23| 4 +Brand#33 |PROMO PLATED COPPER | 36| 4 +Brand#33 |PROMO PLATED COPPER | 49| 4 +Brand#33 |PROMO PLATED NICKEL | 3| 4 +Brand#33 |PROMO PLATED NICKEL | 36| 4 +Brand#33 |PROMO PLATED STEEL | 3| 4 +Brand#33 |PROMO PLATED STEEL | 14| 4 +Brand#33 |PROMO PLATED STEEL | 45| 4 +Brand#33 |PROMO PLATED TIN | 3| 4 +Brand#33 |PROMO PLATED TIN | 9| 4 +Brand#33 |PROMO PLATED TIN | 19| 4 +Brand#33 |PROMO PLATED TIN | 49| 4 +Brand#33 |PROMO POLISHED COPPER | 9| 4 +Brand#33 |PROMO POLISHED COPPER | 19| 4 +Brand#33 |PROMO POLISHED COPPER | 23| 4 +Brand#33 |PROMO POLISHED NICKEL | 19| 4 +Brand#33 |PROMO POLISHED NICKEL | 36| 4 +Brand#33 |PROMO POLISHED STEEL | 36| 4 +Brand#33 |PROMO POLISHED TIN | 9| 4 +Brand#33 |PROMO POLISHED TIN | 23| 4 +Brand#33 |SMALL ANODIZED BRASS | 14| 4 +Brand#33 |SMALL ANODIZED BRASS | 19| 4 +Brand#33 |SMALL ANODIZED BRASS | 45| 4 +Brand#33 |SMALL ANODIZED BRASS | 49| 4 +Brand#33 |SMALL ANODIZED COPPER | 36| 4 +Brand#33 |SMALL ANODIZED NICKEL | 19| 4 +Brand#33 |SMALL ANODIZED STEEL | 23| 4 +Brand#33 |SMALL ANODIZED STEEL | 45| 4 +Brand#33 |SMALL ANODIZED TIN | 3| 4 +Brand#33 |SMALL ANODIZED TIN | 14| 4 +Brand#33 |SMALL ANODIZED TIN | 36| 4 +Brand#33 |SMALL BRUSHED BRASS | 19| 4 +Brand#33 |SMALL BRUSHED BRASS | 45| 4 +Brand#33 |SMALL BRUSHED COPPER | 3| 4 +Brand#33 |SMALL BRUSHED COPPER | 19| 4 +Brand#33 |SMALL BRUSHED NICKEL | 14| 4 +Brand#33 |SMALL BRUSHED NICKEL | 23| 4 +Brand#33 |SMALL BRUSHED NICKEL | 36| 4 +Brand#33 |SMALL BRUSHED NICKEL | 49| 4 +Brand#33 |SMALL BRUSHED STEEL | 14| 4 +Brand#33 |SMALL BRUSHED STEEL | 19| 4 +Brand#33 |SMALL BRUSHED STEEL | 36| 4 +Brand#33 |SMALL BRUSHED STEEL | 45| 4 +Brand#33 |SMALL BRUSHED STEEL | 49| 4 +Brand#33 |SMALL BRUSHED TIN | 3| 4 +Brand#33 |SMALL BRUSHED TIN | 14| 4 +Brand#33 |SMALL BRUSHED TIN | 23| 4 +Brand#33 |SMALL BRUSHED TIN | 49| 4 +Brand#33 |SMALL BURNISHED BRASS | 3| 4 +Brand#33 |SMALL BURNISHED BRASS | 19| 4 +Brand#33 |SMALL BURNISHED BRASS | 49| 4 +Brand#33 |SMALL BURNISHED COPPER | 3| 4 +Brand#33 |SMALL BURNISHED COPPER | 9| 4 +Brand#33 |SMALL BURNISHED COPPER | 14| 4 +Brand#33 |SMALL BURNISHED COPPER | 23| 4 +Brand#33 |SMALL BURNISHED NICKEL | 3| 4 +Brand#33 |SMALL BURNISHED NICKEL | 9| 4 +Brand#33 |SMALL BURNISHED NICKEL | 14| 4 +Brand#33 |SMALL BURNISHED NICKEL | 23| 4 +Brand#33 |SMALL BURNISHED NICKEL | 45| 4 +Brand#33 |SMALL BURNISHED NICKEL | 49| 4 +Brand#33 |SMALL BURNISHED STEEL | 3| 4 +Brand#33 |SMALL BURNISHED STEEL | 49| 4 +Brand#33 |SMALL BURNISHED TIN | 3| 4 +Brand#33 |SMALL BURNISHED TIN | 14| 4 +Brand#33 |SMALL PLATED BRASS | 3| 4 +Brand#33 |SMALL PLATED BRASS | 36| 4 +Brand#33 |SMALL PLATED BRASS | 45| 4 +Brand#33 |SMALL PLATED COPPER | 14| 4 +Brand#33 |SMALL PLATED COPPER | 19| 4 +Brand#33 |SMALL PLATED COPPER | 23| 4 +Brand#33 |SMALL PLATED COPPER | 36| 4 +Brand#33 |SMALL PLATED COPPER | 49| 4 +Brand#33 |SMALL PLATED NICKEL | 3| 4 +Brand#33 |SMALL PLATED NICKEL | 9| 4 +Brand#33 |SMALL PLATED STEEL | 9| 4 +Brand#33 |SMALL PLATED STEEL | 36| 4 +Brand#33 |SMALL PLATED STEEL | 45| 4 +Brand#33 |SMALL PLATED TIN | 3| 4 +Brand#33 |SMALL POLISHED BRASS | 49| 4 +Brand#33 |SMALL POLISHED COPPER | 14| 4 +Brand#33 |SMALL POLISHED COPPER | 23| 4 +Brand#33 |SMALL POLISHED COPPER | 45| 4 +Brand#33 |SMALL POLISHED NICKEL | 14| 4 +Brand#33 |SMALL POLISHED NICKEL | 23| 4 +Brand#33 |SMALL POLISHED NICKEL | 36| 4 +Brand#33 |SMALL POLISHED NICKEL | 45| 4 +Brand#33 |SMALL POLISHED STEEL | 19| 4 +Brand#33 |SMALL POLISHED STEEL | 36| 4 +Brand#33 |SMALL POLISHED STEEL | 45| 4 +Brand#33 |SMALL POLISHED TIN | 36| 4 +Brand#33 |STANDARD ANODIZED BRASS | 3| 4 +Brand#33 |STANDARD ANODIZED BRASS | 14| 4 +Brand#33 |STANDARD ANODIZED BRASS | 19| 4 +Brand#33 |STANDARD ANODIZED BRASS | 45| 4 +Brand#33 |STANDARD ANODIZED COPPER | 9| 4 +Brand#33 |STANDARD ANODIZED COPPER | 45| 4 +Brand#33 |STANDARD ANODIZED NICKEL | 3| 4 +Brand#33 |STANDARD ANODIZED NICKEL | 14| 4 +Brand#33 |STANDARD ANODIZED NICKEL | 19| 4 +Brand#33 |STANDARD ANODIZED NICKEL | 23| 4 +Brand#33 |STANDARD ANODIZED STEEL | 23| 4 +Brand#33 |STANDARD ANODIZED TIN | 14| 4 +Brand#33 |STANDARD ANODIZED TIN | 45| 4 +Brand#33 |STANDARD BRUSHED BRASS | 3| 4 +Brand#33 |STANDARD BRUSHED BRASS | 14| 4 +Brand#33 |STANDARD BRUSHED COPPER | 14| 4 +Brand#33 |STANDARD BRUSHED COPPER | 23| 4 +Brand#33 |STANDARD BRUSHED COPPER | 49| 4 +Brand#33 |STANDARD BRUSHED NICKEL | 3| 4 +Brand#33 |STANDARD BRUSHED NICKEL | 9| 4 +Brand#33 |STANDARD BRUSHED NICKEL | 19| 4 +Brand#33 |STANDARD BRUSHED NICKEL | 45| 4 +Brand#33 |STANDARD BRUSHED STEEL | 23| 4 +Brand#33 |STANDARD BRUSHED STEEL | 36| 4 +Brand#33 |STANDARD BRUSHED STEEL | 49| 4 +Brand#33 |STANDARD BRUSHED TIN | 3| 4 +Brand#33 |STANDARD BRUSHED TIN | 9| 4 +Brand#33 |STANDARD BRUSHED TIN | 36| 4 +Brand#33 |STANDARD BRUSHED TIN | 45| 4 +Brand#33 |STANDARD BRUSHED TIN | 49| 4 +Brand#33 |STANDARD BURNISHED BRASS | 3| 4 +Brand#33 |STANDARD BURNISHED BRASS | 36| 4 +Brand#33 |STANDARD BURNISHED COPPER| 3| 4 +Brand#33 |STANDARD BURNISHED COPPER| 9| 4 +Brand#33 |STANDARD BURNISHED COPPER| 36| 4 +Brand#33 |STANDARD BURNISHED COPPER| 45| 4 +Brand#33 |STANDARD BURNISHED COPPER| 49| 4 +Brand#33 |STANDARD BURNISHED NICKEL| 3| 4 +Brand#33 |STANDARD BURNISHED NICKEL| 9| 4 +Brand#33 |STANDARD BURNISHED NICKEL| 14| 4 +Brand#33 |STANDARD BURNISHED NICKEL| 45| 4 +Brand#33 |STANDARD BURNISHED STEEL | 9| 4 +Brand#33 |STANDARD BURNISHED STEEL | 19| 4 +Brand#33 |STANDARD BURNISHED STEEL | 23| 4 +Brand#33 |STANDARD BURNISHED TIN | 9| 4 +Brand#33 |STANDARD BURNISHED TIN | 45| 4 +Brand#33 |STANDARD BURNISHED TIN | 49| 4 +Brand#33 |STANDARD PLATED BRASS | 9| 4 +Brand#33 |STANDARD PLATED COPPER | 3| 4 +Brand#33 |STANDARD PLATED COPPER | 9| 4 +Brand#33 |STANDARD PLATED COPPER | 36| 4 +Brand#33 |STANDARD PLATED NICKEL | 14| 4 +Brand#33 |STANDARD PLATED NICKEL | 19| 4 +Brand#33 |STANDARD PLATED NICKEL | 23| 4 +Brand#33 |STANDARD PLATED NICKEL | 45| 4 +Brand#33 |STANDARD PLATED STEEL | 36| 4 +Brand#33 |STANDARD PLATED STEEL | 45| 4 +Brand#33 |STANDARD PLATED TIN | 19| 4 +Brand#33 |STANDARD PLATED TIN | 23| 4 +Brand#33 |STANDARD PLATED TIN | 36| 4 +Brand#33 |STANDARD PLATED TIN | 45| 4 +Brand#33 |STANDARD POLISHED BRASS | 45| 4 +Brand#33 |STANDARD POLISHED BRASS | 49| 4 +Brand#33 |STANDARD POLISHED COPPER | 19| 4 +Brand#33 |STANDARD POLISHED COPPER | 36| 4 +Brand#33 |STANDARD POLISHED NICKEL | 19| 4 +Brand#33 |STANDARD POLISHED NICKEL | 23| 4 +Brand#33 |STANDARD POLISHED STEEL | 9| 4 +Brand#33 |STANDARD POLISHED STEEL | 36| 4 +Brand#33 |STANDARD POLISHED TIN | 3| 4 +Brand#33 |STANDARD POLISHED TIN | 9| 4 +Brand#33 |STANDARD POLISHED TIN | 19| 4 +Brand#33 |STANDARD POLISHED TIN | 23| 4 +Brand#33 |STANDARD POLISHED TIN | 36| 4 +Brand#34 |ECONOMY ANODIZED BRASS | 9| 4 +Brand#34 |ECONOMY ANODIZED BRASS | 23| 4 +Brand#34 |ECONOMY ANODIZED BRASS | 36| 4 +Brand#34 |ECONOMY ANODIZED BRASS | 45| 4 +Brand#34 |ECONOMY ANODIZED COPPER | 3| 4 +Brand#34 |ECONOMY ANODIZED COPPER | 19| 4 +Brand#34 |ECONOMY ANODIZED COPPER | 36| 4 +Brand#34 |ECONOMY ANODIZED COPPER | 49| 4 +Brand#34 |ECONOMY ANODIZED NICKEL | 19| 4 +Brand#34 |ECONOMY ANODIZED NICKEL | 36| 4 +Brand#34 |ECONOMY ANODIZED STEEL | 9| 4 +Brand#34 |ECONOMY ANODIZED STEEL | 14| 4 +Brand#34 |ECONOMY ANODIZED STEEL | 19| 4 +Brand#34 |ECONOMY ANODIZED STEEL | 36| 4 +Brand#34 |ECONOMY BRUSHED BRASS | 23| 4 +Brand#34 |ECONOMY BRUSHED BRASS | 45| 4 +Brand#34 |ECONOMY BRUSHED BRASS | 49| 4 +Brand#34 |ECONOMY BRUSHED COPPER | 36| 4 +Brand#34 |ECONOMY BRUSHED NICKEL | 3| 4 +Brand#34 |ECONOMY BRUSHED NICKEL | 14| 4 +Brand#34 |ECONOMY BRUSHED NICKEL | 19| 4 +Brand#34 |ECONOMY BRUSHED NICKEL | 45| 4 +Brand#34 |ECONOMY BRUSHED STEEL | 45| 4 +Brand#34 |ECONOMY BRUSHED STEEL | 49| 4 +Brand#34 |ECONOMY BRUSHED TIN | 9| 4 +Brand#34 |ECONOMY BRUSHED TIN | 23| 4 +Brand#34 |ECONOMY BRUSHED TIN | 36| 4 +Brand#34 |ECONOMY BRUSHED TIN | 45| 4 +Brand#34 |ECONOMY BURNISHED BRASS | 3| 4 +Brand#34 |ECONOMY BURNISHED BRASS | 49| 4 +Brand#34 |ECONOMY BURNISHED COPPER | 3| 4 +Brand#34 |ECONOMY BURNISHED COPPER | 49| 4 +Brand#34 |ECONOMY BURNISHED NICKEL | 3| 4 +Brand#34 |ECONOMY BURNISHED NICKEL | 9| 4 +Brand#34 |ECONOMY BURNISHED NICKEL | 23| 4 +Brand#34 |ECONOMY BURNISHED STEEL | 19| 4 +Brand#34 |ECONOMY BURNISHED STEEL | 23| 4 +Brand#34 |ECONOMY BURNISHED STEEL | 36| 4 +Brand#34 |ECONOMY BURNISHED STEEL | 45| 4 +Brand#34 |ECONOMY BURNISHED TIN | 23| 4 +Brand#34 |ECONOMY PLATED BRASS | 36| 4 +Brand#34 |ECONOMY PLATED BRASS | 49| 4 +Brand#34 |ECONOMY PLATED COPPER | 14| 4 +Brand#34 |ECONOMY PLATED COPPER | 19| 4 +Brand#34 |ECONOMY PLATED NICKEL | 14| 4 +Brand#34 |ECONOMY PLATED NICKEL | 19| 4 +Brand#34 |ECONOMY PLATED STEEL | 19| 4 +Brand#34 |ECONOMY PLATED STEEL | 23| 4 +Brand#34 |ECONOMY PLATED STEEL | 36| 4 +Brand#34 |ECONOMY PLATED STEEL | 45| 4 +Brand#34 |ECONOMY PLATED STEEL | 49| 4 +Brand#34 |ECONOMY PLATED TIN | 19| 4 +Brand#34 |ECONOMY PLATED TIN | 23| 4 +Brand#34 |ECONOMY PLATED TIN | 36| 4 +Brand#34 |ECONOMY PLATED TIN | 49| 4 +Brand#34 |ECONOMY POLISHED BRASS | 3| 4 +Brand#34 |ECONOMY POLISHED BRASS | 23| 4 +Brand#34 |ECONOMY POLISHED BRASS | 45| 4 +Brand#34 |ECONOMY POLISHED COPPER | 3| 4 +Brand#34 |ECONOMY POLISHED COPPER | 9| 4 +Brand#34 |ECONOMY POLISHED COPPER | 23| 4 +Brand#34 |ECONOMY POLISHED COPPER | 49| 4 +Brand#34 |ECONOMY POLISHED NICKEL | 3| 4 +Brand#34 |ECONOMY POLISHED NICKEL | 23| 4 +Brand#34 |ECONOMY POLISHED NICKEL | 36| 4 +Brand#34 |ECONOMY POLISHED NICKEL | 49| 4 +Brand#34 |ECONOMY POLISHED STEEL | 19| 4 +Brand#34 |ECONOMY POLISHED TIN | 3| 4 +Brand#34 |ECONOMY POLISHED TIN | 19| 4 +Brand#34 |ECONOMY POLISHED TIN | 45| 4 +Brand#34 |LARGE ANODIZED BRASS | 3| 4 +Brand#34 |LARGE ANODIZED BRASS | 14| 4 +Brand#34 |LARGE ANODIZED BRASS | 23| 4 +Brand#34 |LARGE ANODIZED BRASS | 36| 4 +Brand#34 |LARGE ANODIZED BRASS | 45| 4 +Brand#34 |LARGE ANODIZED BRASS | 49| 4 +Brand#34 |LARGE ANODIZED COPPER | 14| 4 +Brand#34 |LARGE ANODIZED COPPER | 19| 4 +Brand#34 |LARGE ANODIZED COPPER | 36| 4 +Brand#34 |LARGE ANODIZED COPPER | 45| 4 +Brand#34 |LARGE ANODIZED NICKEL | 14| 4 +Brand#34 |LARGE ANODIZED NICKEL | 36| 4 +Brand#34 |LARGE ANODIZED STEEL | 9| 4 +Brand#34 |LARGE ANODIZED STEEL | 23| 4 +Brand#34 |LARGE ANODIZED TIN | 3| 4 +Brand#34 |LARGE ANODIZED TIN | 9| 4 +Brand#34 |LARGE ANODIZED TIN | 19| 4 +Brand#34 |LARGE ANODIZED TIN | 49| 4 +Brand#34 |LARGE BRUSHED BRASS | 3| 4 +Brand#34 |LARGE BRUSHED COPPER | 14| 4 +Brand#34 |LARGE BRUSHED COPPER | 23| 4 +Brand#34 |LARGE BRUSHED COPPER | 45| 4 +Brand#34 |LARGE BRUSHED COPPER | 49| 4 +Brand#34 |LARGE BRUSHED NICKEL | 3| 4 +Brand#34 |LARGE BRUSHED NICKEL | 9| 4 +Brand#34 |LARGE BRUSHED NICKEL | 23| 4 +Brand#34 |LARGE BRUSHED NICKEL | 45| 4 +Brand#34 |LARGE BRUSHED STEEL | 3| 4 +Brand#34 |LARGE BRUSHED STEEL | 14| 4 +Brand#34 |LARGE BRUSHED STEEL | 23| 4 +Brand#34 |LARGE BRUSHED TIN | 19| 4 +Brand#34 |LARGE BRUSHED TIN | 45| 4 +Brand#34 |LARGE BURNISHED BRASS | 3| 4 +Brand#34 |LARGE BURNISHED BRASS | 9| 4 +Brand#34 |LARGE BURNISHED BRASS | 19| 4 +Brand#34 |LARGE BURNISHED BRASS | 49| 4 +Brand#34 |LARGE BURNISHED COPPER | 9| 4 +Brand#34 |LARGE BURNISHED COPPER | 45| 4 +Brand#34 |LARGE BURNISHED NICKEL | 9| 4 +Brand#34 |LARGE BURNISHED NICKEL | 19| 4 +Brand#34 |LARGE BURNISHED NICKEL | 36| 4 +Brand#34 |LARGE BURNISHED NICKEL | 45| 4 +Brand#34 |LARGE BURNISHED STEEL | 3| 4 +Brand#34 |LARGE BURNISHED STEEL | 23| 4 +Brand#34 |LARGE BURNISHED STEEL | 49| 4 +Brand#34 |LARGE BURNISHED TIN | 19| 4 +Brand#34 |LARGE BURNISHED TIN | 36| 4 +Brand#34 |LARGE PLATED BRASS | 3| 4 +Brand#34 |LARGE PLATED BRASS | 14| 4 +Brand#34 |LARGE PLATED BRASS | 23| 4 +Brand#34 |LARGE PLATED BRASS | 45| 4 +Brand#34 |LARGE PLATED BRASS | 49| 4 +Brand#34 |LARGE PLATED COPPER | 23| 4 +Brand#34 |LARGE PLATED COPPER | 45| 4 +Brand#34 |LARGE PLATED NICKEL | 19| 4 +Brand#34 |LARGE PLATED NICKEL | 23| 4 +Brand#34 |LARGE PLATED NICKEL | 36| 4 +Brand#34 |LARGE PLATED NICKEL | 49| 4 +Brand#34 |LARGE PLATED STEEL | 19| 4 +Brand#34 |LARGE PLATED STEEL | 36| 4 +Brand#34 |LARGE PLATED STEEL | 45| 4 +Brand#34 |LARGE PLATED STEEL | 49| 4 +Brand#34 |LARGE PLATED TIN | 9| 4 +Brand#34 |LARGE PLATED TIN | 49| 4 +Brand#34 |LARGE POLISHED BRASS | 9| 4 +Brand#34 |LARGE POLISHED COPPER | 49| 4 +Brand#34 |LARGE POLISHED NICKEL | 23| 4 +Brand#34 |LARGE POLISHED NICKEL | 36| 4 +Brand#34 |LARGE POLISHED STEEL | 9| 4 +Brand#34 |LARGE POLISHED STEEL | 45| 4 +Brand#34 |LARGE POLISHED TIN | 9| 4 +Brand#34 |LARGE POLISHED TIN | 49| 4 +Brand#34 |MEDIUM ANODIZED BRASS | 3| 4 +Brand#34 |MEDIUM ANODIZED BRASS | 14| 4 +Brand#34 |MEDIUM ANODIZED BRASS | 19| 4 +Brand#34 |MEDIUM ANODIZED COPPER | 9| 4 +Brand#34 |MEDIUM ANODIZED COPPER | 14| 4 +Brand#34 |MEDIUM ANODIZED COPPER | 49| 4 +Brand#34 |MEDIUM ANODIZED NICKEL | 3| 4 +Brand#34 |MEDIUM ANODIZED NICKEL | 9| 4 +Brand#34 |MEDIUM ANODIZED NICKEL | 19| 4 +Brand#34 |MEDIUM ANODIZED NICKEL | 23| 4 +Brand#34 |MEDIUM ANODIZED NICKEL | 36| 4 +Brand#34 |MEDIUM ANODIZED NICKEL | 45| 4 +Brand#34 |MEDIUM ANODIZED STEEL | 14| 4 +Brand#34 |MEDIUM ANODIZED STEEL | 23| 4 +Brand#34 |MEDIUM ANODIZED STEEL | 36| 4 +Brand#34 |MEDIUM ANODIZED STEEL | 45| 4 +Brand#34 |MEDIUM ANODIZED TIN | 3| 4 +Brand#34 |MEDIUM ANODIZED TIN | 19| 4 +Brand#34 |MEDIUM ANODIZED TIN | 36| 4 +Brand#34 |MEDIUM BRUSHED BRASS | 14| 4 +Brand#34 |MEDIUM BRUSHED BRASS | 36| 4 +Brand#34 |MEDIUM BRUSHED BRASS | 45| 4 +Brand#34 |MEDIUM BRUSHED COPPER | 3| 4 +Brand#34 |MEDIUM BRUSHED NICKEL | 3| 4 +Brand#34 |MEDIUM BRUSHED NICKEL | 19| 4 +Brand#34 |MEDIUM BRUSHED NICKEL | 36| 4 +Brand#34 |MEDIUM BRUSHED NICKEL | 45| 4 +Brand#34 |MEDIUM BRUSHED STEEL | 3| 4 +Brand#34 |MEDIUM BRUSHED STEEL | 14| 4 +Brand#34 |MEDIUM BRUSHED STEEL | 49| 4 +Brand#34 |MEDIUM BRUSHED TIN | 3| 4 +Brand#34 |MEDIUM BRUSHED TIN | 14| 4 +Brand#34 |MEDIUM BRUSHED TIN | 19| 4 +Brand#34 |MEDIUM BRUSHED TIN | 23| 4 +Brand#34 |MEDIUM BRUSHED TIN | 45| 4 +Brand#34 |MEDIUM BURNISHED BRASS | 3| 4 +Brand#34 |MEDIUM BURNISHED BRASS | 19| 4 +Brand#34 |MEDIUM BURNISHED BRASS | 36| 4 +Brand#34 |MEDIUM BURNISHED BRASS | 45| 4 +Brand#34 |MEDIUM BURNISHED COPPER | 9| 4 +Brand#34 |MEDIUM BURNISHED COPPER | 19| 4 +Brand#34 |MEDIUM BURNISHED COPPER | 36| 4 +Brand#34 |MEDIUM BURNISHED COPPER | 45| 4 +Brand#34 |MEDIUM BURNISHED NICKEL | 14| 4 +Brand#34 |MEDIUM BURNISHED NICKEL | 23| 4 +Brand#34 |MEDIUM BURNISHED NICKEL | 45| 4 +Brand#34 |MEDIUM BURNISHED STEEL | 3| 4 +Brand#34 |MEDIUM BURNISHED STEEL | 9| 4 +Brand#34 |MEDIUM BURNISHED STEEL | 14| 4 +Brand#34 |MEDIUM BURNISHED STEEL | 19| 4 +Brand#34 |MEDIUM BURNISHED STEEL | 45| 4 +Brand#34 |MEDIUM BURNISHED STEEL | 49| 4 +Brand#34 |MEDIUM BURNISHED TIN | 9| 4 +Brand#34 |MEDIUM BURNISHED TIN | 14| 4 +Brand#34 |MEDIUM BURNISHED TIN | 19| 4 +Brand#34 |MEDIUM BURNISHED TIN | 49| 4 +Brand#34 |MEDIUM PLATED BRASS | 3| 4 +Brand#34 |MEDIUM PLATED BRASS | 14| 4 +Brand#34 |MEDIUM PLATED BRASS | 45| 4 +Brand#34 |MEDIUM PLATED COPPER | 3| 4 +Brand#34 |MEDIUM PLATED COPPER | 23| 4 +Brand#34 |MEDIUM PLATED COPPER | 36| 4 +Brand#34 |MEDIUM PLATED COPPER | 45| 4 +Brand#34 |MEDIUM PLATED NICKEL | 3| 4 +Brand#34 |MEDIUM PLATED NICKEL | 14| 4 +Brand#34 |MEDIUM PLATED STEEL | 3| 4 +Brand#34 |MEDIUM PLATED STEEL | 9| 4 +Brand#34 |MEDIUM PLATED TIN | 3| 4 +Brand#34 |MEDIUM PLATED TIN | 45| 4 +Brand#34 |PROMO ANODIZED BRASS | 19| 4 +Brand#34 |PROMO ANODIZED BRASS | 45| 4 +Brand#34 |PROMO ANODIZED COPPER | 19| 4 +Brand#34 |PROMO ANODIZED COPPER | 23| 4 +Brand#34 |PROMO ANODIZED COPPER | 49| 4 +Brand#34 |PROMO ANODIZED NICKEL | 23| 4 +Brand#34 |PROMO ANODIZED NICKEL | 49| 4 +Brand#34 |PROMO ANODIZED STEEL | 3| 4 +Brand#34 |PROMO ANODIZED STEEL | 36| 4 +Brand#34 |PROMO ANODIZED STEEL | 49| 4 +Brand#34 |PROMO ANODIZED TIN | 19| 4 +Brand#34 |PROMO ANODIZED TIN | 45| 4 +Brand#34 |PROMO BRUSHED BRASS | 3| 4 +Brand#34 |PROMO BRUSHED BRASS | 14| 4 +Brand#34 |PROMO BRUSHED BRASS | 36| 4 +Brand#34 |PROMO BRUSHED COPPER | 19| 4 +Brand#34 |PROMO BRUSHED COPPER | 23| 4 +Brand#34 |PROMO BRUSHED COPPER | 36| 4 +Brand#34 |PROMO BRUSHED NICKEL | 3| 4 +Brand#34 |PROMO BRUSHED NICKEL | 9| 4 +Brand#34 |PROMO BRUSHED NICKEL | 14| 4 +Brand#34 |PROMO BRUSHED NICKEL | 23| 4 +Brand#34 |PROMO BRUSHED NICKEL | 36| 4 +Brand#34 |PROMO BRUSHED STEEL | 14| 4 +Brand#34 |PROMO BRUSHED STEEL | 23| 4 +Brand#34 |PROMO BRUSHED STEEL | 49| 4 +Brand#34 |PROMO BRUSHED TIN | 9| 4 +Brand#34 |PROMO BRUSHED TIN | 19| 4 +Brand#34 |PROMO BRUSHED TIN | 23| 4 +Brand#34 |PROMO BRUSHED TIN | 49| 4 +Brand#34 |PROMO BURNISHED BRASS | 3| 4 +Brand#34 |PROMO BURNISHED BRASS | 19| 4 +Brand#34 |PROMO BURNISHED BRASS | 23| 4 +Brand#34 |PROMO BURNISHED BRASS | 49| 4 +Brand#34 |PROMO BURNISHED COPPER | 9| 4 +Brand#34 |PROMO BURNISHED COPPER | 19| 4 +Brand#34 |PROMO BURNISHED COPPER | 23| 4 +Brand#34 |PROMO BURNISHED COPPER | 36| 4 +Brand#34 |PROMO BURNISHED COPPER | 45| 4 +Brand#34 |PROMO BURNISHED NICKEL | 3| 4 +Brand#34 |PROMO BURNISHED NICKEL | 9| 4 +Brand#34 |PROMO BURNISHED NICKEL | 36| 4 +Brand#34 |PROMO BURNISHED STEEL | 3| 4 +Brand#34 |PROMO BURNISHED STEEL | 19| 4 +Brand#34 |PROMO BURNISHED STEEL | 36| 4 +Brand#34 |PROMO BURNISHED TIN | 3| 4 +Brand#34 |PROMO BURNISHED TIN | 9| 4 +Brand#34 |PROMO BURNISHED TIN | 19| 4 +Brand#34 |PROMO BURNISHED TIN | 23| 4 +Brand#34 |PROMO BURNISHED TIN | 49| 4 +Brand#34 |PROMO PLATED BRASS | 14| 4 +Brand#34 |PROMO PLATED COPPER | 3| 4 +Brand#34 |PROMO PLATED COPPER | 9| 4 +Brand#34 |PROMO PLATED COPPER | 19| 4 +Brand#34 |PROMO PLATED NICKEL | 45| 4 +Brand#34 |PROMO PLATED STEEL | 3| 4 +Brand#34 |PROMO PLATED STEEL | 19| 4 +Brand#34 |PROMO PLATED STEEL | 49| 4 +Brand#34 |PROMO PLATED TIN | 3| 4 +Brand#34 |PROMO PLATED TIN | 23| 4 +Brand#34 |PROMO POLISHED BRASS | 3| 4 +Brand#34 |PROMO POLISHED BRASS | 36| 4 +Brand#34 |PROMO POLISHED BRASS | 45| 4 +Brand#34 |PROMO POLISHED BRASS | 49| 4 +Brand#34 |PROMO POLISHED COPPER | 3| 4 +Brand#34 |PROMO POLISHED COPPER | 45| 4 +Brand#34 |PROMO POLISHED COPPER | 49| 4 +Brand#34 |PROMO POLISHED NICKEL | 3| 4 +Brand#34 |PROMO POLISHED NICKEL | 9| 4 +Brand#34 |PROMO POLISHED NICKEL | 14| 4 +Brand#34 |PROMO POLISHED NICKEL | 23| 4 +Brand#34 |PROMO POLISHED NICKEL | 36| 4 +Brand#34 |PROMO POLISHED NICKEL | 45| 4 +Brand#34 |PROMO POLISHED STEEL | 36| 4 +Brand#34 |PROMO POLISHED STEEL | 45| 4 +Brand#34 |PROMO POLISHED TIN | 36| 4 +Brand#34 |SMALL ANODIZED BRASS | 3| 4 +Brand#34 |SMALL ANODIZED BRASS | 36| 4 +Brand#34 |SMALL ANODIZED BRASS | 45| 4 +Brand#34 |SMALL ANODIZED COPPER | 3| 4 +Brand#34 |SMALL ANODIZED COPPER | 36| 4 +Brand#34 |SMALL ANODIZED COPPER | 45| 4 +Brand#34 |SMALL ANODIZED NICKEL | 19| 4 +Brand#34 |SMALL ANODIZED STEEL | 3| 4 +Brand#34 |SMALL ANODIZED STEEL | 14| 4 +Brand#34 |SMALL ANODIZED STEEL | 23| 4 +Brand#34 |SMALL ANODIZED STEEL | 36| 4 +Brand#34 |SMALL ANODIZED TIN | 3| 4 +Brand#34 |SMALL ANODIZED TIN | 19| 4 +Brand#34 |SMALL ANODIZED TIN | 23| 4 +Brand#34 |SMALL ANODIZED TIN | 36| 4 +Brand#34 |SMALL BRUSHED BRASS | 3| 4 +Brand#34 |SMALL BRUSHED BRASS | 23| 4 +Brand#34 |SMALL BRUSHED BRASS | 36| 4 +Brand#34 |SMALL BRUSHED BRASS | 45| 4 +Brand#34 |SMALL BRUSHED BRASS | 49| 4 +Brand#34 |SMALL BRUSHED COPPER | 3| 4 +Brand#34 |SMALL BRUSHED COPPER | 9| 4 +Brand#34 |SMALL BRUSHED NICKEL | 3| 4 +Brand#34 |SMALL BRUSHED NICKEL | 23| 4 +Brand#34 |SMALL BRUSHED NICKEL | 36| 4 +Brand#34 |SMALL BRUSHED NICKEL | 49| 4 +Brand#34 |SMALL BRUSHED STEEL | 19| 4 +Brand#34 |SMALL BRUSHED STEEL | 23| 4 +Brand#34 |SMALL BRUSHED STEEL | 36| 4 +Brand#34 |SMALL BRUSHED STEEL | 49| 4 +Brand#34 |SMALL BRUSHED TIN | 9| 4 +Brand#34 |SMALL BRUSHED TIN | 14| 4 +Brand#34 |SMALL BRUSHED TIN | 19| 4 +Brand#34 |SMALL BRUSHED TIN | 23| 4 +Brand#34 |SMALL BRUSHED TIN | 36| 4 +Brand#34 |SMALL BRUSHED TIN | 45| 4 +Brand#34 |SMALL BURNISHED BRASS | 3| 4 +Brand#34 |SMALL BURNISHED BRASS | 36| 4 +Brand#34 |SMALL BURNISHED BRASS | 49| 4 +Brand#34 |SMALL BURNISHED COPPER | 3| 4 +Brand#34 |SMALL BURNISHED COPPER | 49| 4 +Brand#34 |SMALL BURNISHED NICKEL | 19| 4 +Brand#34 |SMALL BURNISHED NICKEL | 23| 4 +Brand#34 |SMALL BURNISHED STEEL | 3| 4 +Brand#34 |SMALL BURNISHED STEEL | 9| 4 +Brand#34 |SMALL BURNISHED STEEL | 19| 4 +Brand#34 |SMALL BURNISHED STEEL | 36| 4 +Brand#34 |SMALL BURNISHED STEEL | 49| 4 +Brand#34 |SMALL BURNISHED TIN | 14| 4 +Brand#34 |SMALL BURNISHED TIN | 23| 4 +Brand#34 |SMALL BURNISHED TIN | 45| 4 +Brand#34 |SMALL PLATED BRASS | 9| 4 +Brand#34 |SMALL PLATED BRASS | 45| 4 +Brand#34 |SMALL PLATED COPPER | 3| 4 +Brand#34 |SMALL PLATED COPPER | 9| 4 +Brand#34 |SMALL PLATED COPPER | 14| 4 +Brand#34 |SMALL PLATED COPPER | 36| 4 +Brand#34 |SMALL PLATED COPPER | 45| 4 +Brand#34 |SMALL PLATED NICKEL | 14| 4 +Brand#34 |SMALL PLATED NICKEL | 19| 4 +Brand#34 |SMALL PLATED NICKEL | 49| 4 +Brand#34 |SMALL PLATED STEEL | 3| 4 +Brand#34 |SMALL PLATED STEEL | 14| 4 +Brand#34 |SMALL PLATED STEEL | 23| 4 +Brand#34 |SMALL PLATED STEEL | 36| 4 +Brand#34 |SMALL PLATED STEEL | 49| 4 +Brand#34 |SMALL PLATED TIN | 3| 4 +Brand#34 |SMALL PLATED TIN | 23| 4 +Brand#34 |SMALL PLATED TIN | 36| 4 +Brand#34 |SMALL PLATED TIN | 49| 4 +Brand#34 |SMALL POLISHED BRASS | 3| 4 +Brand#34 |SMALL POLISHED BRASS | 9| 4 +Brand#34 |SMALL POLISHED BRASS | 19| 4 +Brand#34 |SMALL POLISHED BRASS | 36| 4 +Brand#34 |SMALL POLISHED BRASS | 49| 4 +Brand#34 |SMALL POLISHED COPPER | 3| 4 +Brand#34 |SMALL POLISHED COPPER | 14| 4 +Brand#34 |SMALL POLISHED NICKEL | 9| 4 +Brand#34 |SMALL POLISHED NICKEL | 14| 4 +Brand#34 |SMALL POLISHED NICKEL | 45| 4 +Brand#34 |SMALL POLISHED NICKEL | 49| 4 +Brand#34 |SMALL POLISHED STEEL | 3| 4 +Brand#34 |SMALL POLISHED STEEL | 14| 4 +Brand#34 |SMALL POLISHED STEEL | 23| 4 +Brand#34 |SMALL POLISHED STEEL | 45| 4 +Brand#34 |SMALL POLISHED TIN | 3| 4 +Brand#34 |SMALL POLISHED TIN | 9| 4 +Brand#34 |SMALL POLISHED TIN | 14| 4 +Brand#34 |SMALL POLISHED TIN | 19| 4 +Brand#34 |SMALL POLISHED TIN | 23| 4 +Brand#34 |SMALL POLISHED TIN | 45| 4 +Brand#34 |STANDARD ANODIZED BRASS | 3| 4 +Brand#34 |STANDARD ANODIZED COPPER | 49| 4 +Brand#34 |STANDARD ANODIZED STEEL | 14| 4 +Brand#34 |STANDARD ANODIZED STEEL | 19| 4 +Brand#34 |STANDARD ANODIZED STEEL | 23| 4 +Brand#34 |STANDARD ANODIZED STEEL | 36| 4 +Brand#34 |STANDARD ANODIZED STEEL | 49| 4 +Brand#34 |STANDARD ANODIZED TIN | 9| 4 +Brand#34 |STANDARD ANODIZED TIN | 19| 4 +Brand#34 |STANDARD ANODIZED TIN | 23| 4 +Brand#34 |STANDARD BRUSHED BRASS | 9| 4 +Brand#34 |STANDARD BRUSHED BRASS | 19| 4 +Brand#34 |STANDARD BRUSHED BRASS | 23| 4 +Brand#34 |STANDARD BRUSHED COPPER | 9| 4 +Brand#34 |STANDARD BRUSHED COPPER | 36| 4 +Brand#34 |STANDARD BRUSHED COPPER | 45| 4 +Brand#34 |STANDARD BRUSHED NICKEL | 3| 4 +Brand#34 |STANDARD BRUSHED NICKEL | 9| 4 +Brand#34 |STANDARD BRUSHED NICKEL | 14| 4 +Brand#34 |STANDARD BRUSHED NICKEL | 23| 4 +Brand#34 |STANDARD BRUSHED NICKEL | 49| 4 +Brand#34 |STANDARD BRUSHED STEEL | 3| 4 +Brand#34 |STANDARD BRUSHED STEEL | 9| 4 +Brand#34 |STANDARD BRUSHED STEEL | 36| 4 +Brand#34 |STANDARD BRUSHED TIN | 19| 4 +Brand#34 |STANDARD BRUSHED TIN | 23| 4 +Brand#34 |STANDARD BRUSHED TIN | 36| 4 +Brand#34 |STANDARD BURNISHED BRASS | 3| 4 +Brand#34 |STANDARD BURNISHED BRASS | 23| 4 +Brand#34 |STANDARD BURNISHED BRASS | 36| 4 +Brand#34 |STANDARD BURNISHED BRASS | 45| 4 +Brand#34 |STANDARD BURNISHED COPPER| 14| 4 +Brand#34 |STANDARD BURNISHED COPPER| 19| 4 +Brand#34 |STANDARD BURNISHED COPPER| 36| 4 +Brand#34 |STANDARD BURNISHED NICKEL| 3| 4 +Brand#34 |STANDARD BURNISHED NICKEL| 9| 4 +Brand#34 |STANDARD BURNISHED NICKEL| 45| 4 +Brand#34 |STANDARD BURNISHED STEEL | 3| 4 +Brand#34 |STANDARD BURNISHED STEEL | 36| 4 +Brand#34 |STANDARD BURNISHED STEEL | 45| 4 +Brand#34 |STANDARD BURNISHED TIN | 3| 4 +Brand#34 |STANDARD BURNISHED TIN | 14| 4 +Brand#34 |STANDARD BURNISHED TIN | 19| 4 +Brand#34 |STANDARD BURNISHED TIN | 36| 4 +Brand#34 |STANDARD PLATED BRASS | 9| 4 +Brand#34 |STANDARD PLATED BRASS | 23| 4 +Brand#34 |STANDARD PLATED BRASS | 36| 4 +Brand#34 |STANDARD PLATED COPPER | 3| 4 +Brand#34 |STANDARD PLATED COPPER | 19| 4 +Brand#34 |STANDARD PLATED COPPER | 49| 4 +Brand#34 |STANDARD PLATED NICKEL | 9| 4 +Brand#34 |STANDARD PLATED NICKEL | 23| 4 +Brand#34 |STANDARD PLATED STEEL | 3| 4 +Brand#34 |STANDARD PLATED STEEL | 14| 4 +Brand#34 |STANDARD PLATED STEEL | 19| 4 +Brand#34 |STANDARD PLATED TIN | 23| 4 +Brand#34 |STANDARD PLATED TIN | 49| 4 +Brand#34 |STANDARD POLISHED BRASS | 3| 4 +Brand#34 |STANDARD POLISHED BRASS | 14| 4 +Brand#34 |STANDARD POLISHED COPPER | 3| 4 +Brand#34 |STANDARD POLISHED COPPER | 9| 4 +Brand#34 |STANDARD POLISHED NICKEL | 3| 4 +Brand#34 |STANDARD POLISHED NICKEL | 9| 4 +Brand#34 |STANDARD POLISHED NICKEL | 14| 4 +Brand#34 |STANDARD POLISHED NICKEL | 19| 4 +Brand#34 |STANDARD POLISHED NICKEL | 23| 4 +Brand#34 |STANDARD POLISHED NICKEL | 45| 4 +Brand#34 |STANDARD POLISHED STEEL | 45| 4 +Brand#34 |STANDARD POLISHED TIN | 14| 4 +Brand#34 |STANDARD POLISHED TIN | 49| 4 +Brand#35 |ECONOMY ANODIZED COPPER | 14| 4 +Brand#35 |ECONOMY ANODIZED NICKEL | 45| 4 +Brand#35 |ECONOMY ANODIZED STEEL | 3| 4 +Brand#35 |ECONOMY ANODIZED STEEL | 9| 4 +Brand#35 |ECONOMY ANODIZED TIN | 3| 4 +Brand#35 |ECONOMY ANODIZED TIN | 9| 4 +Brand#35 |ECONOMY ANODIZED TIN | 49| 4 +Brand#35 |ECONOMY BRUSHED BRASS | 23| 4 +Brand#35 |ECONOMY BRUSHED BRASS | 45| 4 +Brand#35 |ECONOMY BRUSHED COPPER | 9| 4 +Brand#35 |ECONOMY BRUSHED COPPER | 14| 4 +Brand#35 |ECONOMY BRUSHED COPPER | 36| 4 +Brand#35 |ECONOMY BRUSHED COPPER | 49| 4 +Brand#35 |ECONOMY BRUSHED NICKEL | 3| 4 +Brand#35 |ECONOMY BRUSHED NICKEL | 9| 4 +Brand#35 |ECONOMY BRUSHED NICKEL | 19| 4 +Brand#35 |ECONOMY BRUSHED NICKEL | 23| 4 +Brand#35 |ECONOMY BRUSHED NICKEL | 36| 4 +Brand#35 |ECONOMY BRUSHED STEEL | 3| 4 +Brand#35 |ECONOMY BRUSHED STEEL | 9| 4 +Brand#35 |ECONOMY BRUSHED TIN | 14| 4 +Brand#35 |ECONOMY BRUSHED TIN | 45| 4 +Brand#35 |ECONOMY BURNISHED BRASS | 23| 4 +Brand#35 |ECONOMY BURNISHED BRASS | 45| 4 +Brand#35 |ECONOMY BURNISHED BRASS | 49| 4 +Brand#35 |ECONOMY BURNISHED COPPER | 3| 4 +Brand#35 |ECONOMY BURNISHED COPPER | 49| 4 +Brand#35 |ECONOMY BURNISHED NICKEL | 9| 4 +Brand#35 |ECONOMY BURNISHED NICKEL | 14| 4 +Brand#35 |ECONOMY BURNISHED NICKEL | 36| 4 +Brand#35 |ECONOMY BURNISHED NICKEL | 45| 4 +Brand#35 |ECONOMY BURNISHED STEEL | 3| 4 +Brand#35 |ECONOMY BURNISHED STEEL | 9| 4 +Brand#35 |ECONOMY BURNISHED STEEL | 14| 4 +Brand#35 |ECONOMY BURNISHED STEEL | 23| 4 +Brand#35 |ECONOMY BURNISHED STEEL | 49| 4 +Brand#35 |ECONOMY BURNISHED TIN | 19| 4 +Brand#35 |ECONOMY BURNISHED TIN | 36| 4 +Brand#35 |ECONOMY BURNISHED TIN | 49| 4 +Brand#35 |ECONOMY PLATED BRASS | 19| 4 +Brand#35 |ECONOMY PLATED COPPER | 36| 4 +Brand#35 |ECONOMY PLATED COPPER | 49| 4 +Brand#35 |ECONOMY PLATED NICKEL | 9| 4 +Brand#35 |ECONOMY PLATED STEEL | 3| 4 +Brand#35 |ECONOMY PLATED STEEL | 9| 4 +Brand#35 |ECONOMY PLATED STEEL | 45| 4 +Brand#35 |ECONOMY PLATED TIN | 3| 4 +Brand#35 |ECONOMY PLATED TIN | 9| 4 +Brand#35 |ECONOMY PLATED TIN | 19| 4 +Brand#35 |ECONOMY PLATED TIN | 23| 4 +Brand#35 |ECONOMY POLISHED BRASS | 19| 4 +Brand#35 |ECONOMY POLISHED BRASS | 23| 4 +Brand#35 |ECONOMY POLISHED BRASS | 49| 4 +Brand#35 |ECONOMY POLISHED COPPER | 19| 4 +Brand#35 |ECONOMY POLISHED COPPER | 23| 4 +Brand#35 |ECONOMY POLISHED COPPER | 45| 4 +Brand#35 |ECONOMY POLISHED COPPER | 49| 4 +Brand#35 |ECONOMY POLISHED NICKEL | 3| 4 +Brand#35 |ECONOMY POLISHED NICKEL | 14| 4 +Brand#35 |ECONOMY POLISHED NICKEL | 36| 4 +Brand#35 |ECONOMY POLISHED NICKEL | 45| 4 +Brand#35 |ECONOMY POLISHED STEEL | 23| 4 +Brand#35 |ECONOMY POLISHED TIN | 9| 4 +Brand#35 |ECONOMY POLISHED TIN | 36| 4 +Brand#35 |ECONOMY POLISHED TIN | 45| 4 +Brand#35 |LARGE ANODIZED BRASS | 14| 4 +Brand#35 |LARGE ANODIZED BRASS | 19| 4 +Brand#35 |LARGE ANODIZED NICKEL | 19| 4 +Brand#35 |LARGE ANODIZED NICKEL | 36| 4 +Brand#35 |LARGE ANODIZED STEEL | 14| 4 +Brand#35 |LARGE ANODIZED STEEL | 36| 4 +Brand#35 |LARGE BRUSHED BRASS | 9| 4 +Brand#35 |LARGE BRUSHED BRASS | 19| 4 +Brand#35 |LARGE BRUSHED BRASS | 36| 4 +Brand#35 |LARGE BRUSHED BRASS | 45| 4 +Brand#35 |LARGE BRUSHED BRASS | 49| 4 +Brand#35 |LARGE BRUSHED COPPER | 36| 4 +Brand#35 |LARGE BRUSHED COPPER | 45| 4 +Brand#35 |LARGE BRUSHED COPPER | 49| 4 +Brand#35 |LARGE BRUSHED NICKEL | 14| 4 +Brand#35 |LARGE BRUSHED NICKEL | 45| 4 +Brand#35 |LARGE BRUSHED STEEL | 9| 4 +Brand#35 |LARGE BRUSHED STEEL | 45| 4 +Brand#35 |LARGE BRUSHED STEEL | 49| 4 +Brand#35 |LARGE BRUSHED TIN | 3| 4 +Brand#35 |LARGE BRUSHED TIN | 9| 4 +Brand#35 |LARGE BRUSHED TIN | 19| 4 +Brand#35 |LARGE BURNISHED BRASS | 9| 4 +Brand#35 |LARGE BURNISHED BRASS | 23| 4 +Brand#35 |LARGE BURNISHED COPPER | 45| 4 +Brand#35 |LARGE BURNISHED COPPER | 49| 4 +Brand#35 |LARGE BURNISHED NICKEL | 36| 4 +Brand#35 |LARGE BURNISHED STEEL | 23| 4 +Brand#35 |LARGE BURNISHED TIN | 45| 4 +Brand#35 |LARGE PLATED COPPER | 3| 4 +Brand#35 |LARGE PLATED COPPER | 9| 4 +Brand#35 |LARGE PLATED COPPER | 14| 4 +Brand#35 |LARGE PLATED COPPER | 36| 4 +Brand#35 |LARGE PLATED COPPER | 49| 4 +Brand#35 |LARGE PLATED NICKEL | 9| 4 +Brand#35 |LARGE PLATED NICKEL | 14| 4 +Brand#35 |LARGE PLATED NICKEL | 23| 4 +Brand#35 |LARGE PLATED NICKEL | 49| 4 +Brand#35 |LARGE PLATED STEEL | 36| 4 +Brand#35 |LARGE PLATED STEEL | 45| 4 +Brand#35 |LARGE PLATED TIN | 3| 4 +Brand#35 |LARGE PLATED TIN | 49| 4 +Brand#35 |LARGE POLISHED BRASS | 3| 4 +Brand#35 |LARGE POLISHED BRASS | 9| 4 +Brand#35 |LARGE POLISHED BRASS | 14| 4 +Brand#35 |LARGE POLISHED BRASS | 23| 4 +Brand#35 |LARGE POLISHED BRASS | 36| 4 +Brand#35 |LARGE POLISHED BRASS | 45| 4 +Brand#35 |LARGE POLISHED COPPER | 9| 4 +Brand#35 |LARGE POLISHED COPPER | 45| 4 +Brand#35 |LARGE POLISHED NICKEL | 3| 4 +Brand#35 |LARGE POLISHED NICKEL | 9| 4 +Brand#35 |LARGE POLISHED NICKEL | 14| 4 +Brand#35 |LARGE POLISHED NICKEL | 19| 4 +Brand#35 |LARGE POLISHED NICKEL | 49| 4 +Brand#35 |LARGE POLISHED STEEL | 3| 4 +Brand#35 |LARGE POLISHED STEEL | 9| 4 +Brand#35 |LARGE POLISHED STEEL | 45| 4 +Brand#35 |LARGE POLISHED STEEL | 49| 4 +Brand#35 |LARGE POLISHED TIN | 19| 4 +Brand#35 |LARGE POLISHED TIN | 36| 4 +Brand#35 |LARGE POLISHED TIN | 45| 4 +Brand#35 |LARGE POLISHED TIN | 49| 4 +Brand#35 |MEDIUM ANODIZED BRASS | 3| 4 +Brand#35 |MEDIUM ANODIZED BRASS | 9| 4 +Brand#35 |MEDIUM ANODIZED BRASS | 14| 4 +Brand#35 |MEDIUM ANODIZED BRASS | 19| 4 +Brand#35 |MEDIUM ANODIZED BRASS | 36| 4 +Brand#35 |MEDIUM ANODIZED COPPER | 3| 4 +Brand#35 |MEDIUM ANODIZED COPPER | 23| 4 +Brand#35 |MEDIUM ANODIZED COPPER | 45| 4 +Brand#35 |MEDIUM ANODIZED COPPER | 49| 4 +Brand#35 |MEDIUM ANODIZED NICKEL | 36| 4 +Brand#35 |MEDIUM ANODIZED NICKEL | 45| 4 +Brand#35 |MEDIUM ANODIZED NICKEL | 49| 4 +Brand#35 |MEDIUM ANODIZED STEEL | 9| 4 +Brand#35 |MEDIUM ANODIZED STEEL | 14| 4 +Brand#35 |MEDIUM ANODIZED STEEL | 36| 4 +Brand#35 |MEDIUM ANODIZED STEEL | 49| 4 +Brand#35 |MEDIUM ANODIZED TIN | 9| 4 +Brand#35 |MEDIUM BRUSHED BRASS | 14| 4 +Brand#35 |MEDIUM BRUSHED COPPER | 9| 4 +Brand#35 |MEDIUM BRUSHED COPPER | 49| 4 +Brand#35 |MEDIUM BRUSHED NICKEL | 14| 4 +Brand#35 |MEDIUM BRUSHED NICKEL | 36| 4 +Brand#35 |MEDIUM BRUSHED STEEL | 9| 4 +Brand#35 |MEDIUM BRUSHED STEEL | 19| 4 +Brand#35 |MEDIUM BRUSHED STEEL | 36| 4 +Brand#35 |MEDIUM BRUSHED TIN | 3| 4 +Brand#35 |MEDIUM BRUSHED TIN | 36| 4 +Brand#35 |MEDIUM BRUSHED TIN | 45| 4 +Brand#35 |MEDIUM BURNISHED BRASS | 14| 4 +Brand#35 |MEDIUM BURNISHED BRASS | 19| 4 +Brand#35 |MEDIUM BURNISHED BRASS | 23| 4 +Brand#35 |MEDIUM BURNISHED COPPER | 3| 4 +Brand#35 |MEDIUM BURNISHED COPPER | 9| 4 +Brand#35 |MEDIUM BURNISHED COPPER | 14| 4 +Brand#35 |MEDIUM BURNISHED COPPER | 19| 4 +Brand#35 |MEDIUM BURNISHED COPPER | 45| 4 +Brand#35 |MEDIUM BURNISHED NICKEL | 36| 4 +Brand#35 |MEDIUM BURNISHED NICKEL | 45| 4 +Brand#35 |MEDIUM BURNISHED NICKEL | 49| 4 +Brand#35 |MEDIUM BURNISHED STEEL | 14| 4 +Brand#35 |MEDIUM PLATED BRASS | 9| 4 +Brand#35 |MEDIUM PLATED BRASS | 19| 4 +Brand#35 |MEDIUM PLATED BRASS | 49| 4 +Brand#35 |MEDIUM PLATED COPPER | 14| 4 +Brand#35 |MEDIUM PLATED NICKEL | 3| 4 +Brand#35 |MEDIUM PLATED NICKEL | 19| 4 +Brand#35 |MEDIUM PLATED STEEL | 9| 4 +Brand#35 |MEDIUM PLATED STEEL | 19| 4 +Brand#35 |MEDIUM PLATED STEEL | 45| 4 +Brand#35 |MEDIUM PLATED STEEL | 49| 4 +Brand#35 |MEDIUM PLATED TIN | 3| 4 +Brand#35 |MEDIUM PLATED TIN | 9| 4 +Brand#35 |MEDIUM PLATED TIN | 45| 4 +Brand#35 |PROMO ANODIZED BRASS | 19| 4 +Brand#35 |PROMO ANODIZED BRASS | 23| 4 +Brand#35 |PROMO ANODIZED BRASS | 36| 4 +Brand#35 |PROMO ANODIZED BRASS | 49| 4 +Brand#35 |PROMO ANODIZED COPPER | 19| 4 +Brand#35 |PROMO ANODIZED NICKEL | 9| 4 +Brand#35 |PROMO ANODIZED NICKEL | 19| 4 +Brand#35 |PROMO ANODIZED NICKEL | 23| 4 +Brand#35 |PROMO ANODIZED STEEL | 9| 4 +Brand#35 |PROMO ANODIZED STEEL | 19| 4 +Brand#35 |PROMO ANODIZED TIN | 3| 4 +Brand#35 |PROMO ANODIZED TIN | 19| 4 +Brand#35 |PROMO ANODIZED TIN | 23| 4 +Brand#35 |PROMO ANODIZED TIN | 36| 4 +Brand#35 |PROMO ANODIZED TIN | 45| 4 +Brand#35 |PROMO BRUSHED BRASS | 9| 4 +Brand#35 |PROMO BRUSHED BRASS | 19| 4 +Brand#35 |PROMO BRUSHED BRASS | 36| 4 +Brand#35 |PROMO BRUSHED BRASS | 49| 4 +Brand#35 |PROMO BRUSHED COPPER | 19| 4 +Brand#35 |PROMO BRUSHED COPPER | 45| 4 +Brand#35 |PROMO BRUSHED NICKEL | 23| 4 +Brand#35 |PROMO BRUSHED STEEL | 3| 4 +Brand#35 |PROMO BRUSHED STEEL | 45| 4 +Brand#35 |PROMO BRUSHED STEEL | 49| 4 +Brand#35 |PROMO BRUSHED TIN | 9| 4 +Brand#35 |PROMO BRUSHED TIN | 14| 4 +Brand#35 |PROMO BRUSHED TIN | 23| 4 +Brand#35 |PROMO BRUSHED TIN | 36| 4 +Brand#35 |PROMO BURNISHED BRASS | 9| 4 +Brand#35 |PROMO BURNISHED BRASS | 36| 4 +Brand#35 |PROMO BURNISHED BRASS | 45| 4 +Brand#35 |PROMO BURNISHED NICKEL | 9| 4 +Brand#35 |PROMO BURNISHED STEEL | 19| 4 +Brand#35 |PROMO BURNISHED STEEL | 23| 4 +Brand#35 |PROMO BURNISHED STEEL | 36| 4 +Brand#35 |PROMO BURNISHED TIN | 49| 4 +Brand#35 |PROMO PLATED BRASS | 3| 4 +Brand#35 |PROMO PLATED BRASS | 9| 4 +Brand#35 |PROMO PLATED BRASS | 36| 4 +Brand#35 |PROMO PLATED COPPER | 9| 4 +Brand#35 |PROMO PLATED COPPER | 14| 4 +Brand#35 |PROMO PLATED COPPER | 19| 4 +Brand#35 |PROMO PLATED COPPER | 45| 4 +Brand#35 |PROMO PLATED COPPER | 49| 4 +Brand#35 |PROMO PLATED NICKEL | 3| 4 +Brand#35 |PROMO PLATED NICKEL | 36| 4 +Brand#35 |PROMO PLATED NICKEL | 49| 4 +Brand#35 |PROMO PLATED STEEL | 19| 4 +Brand#35 |PROMO PLATED TIN | 49| 4 +Brand#35 |PROMO POLISHED BRASS | 14| 4 +Brand#35 |PROMO POLISHED BRASS | 36| 4 +Brand#35 |PROMO POLISHED BRASS | 45| 4 +Brand#35 |PROMO POLISHED BRASS | 49| 4 +Brand#35 |PROMO POLISHED COPPER | 9| 4 +Brand#35 |PROMO POLISHED COPPER | 45| 4 +Brand#35 |PROMO POLISHED NICKEL | 3| 4 +Brand#35 |PROMO POLISHED NICKEL | 14| 4 +Brand#35 |PROMO POLISHED NICKEL | 36| 4 +Brand#35 |PROMO POLISHED STEEL | 3| 4 +Brand#35 |PROMO POLISHED STEEL | 23| 4 +Brand#35 |PROMO POLISHED STEEL | 36| 4 +Brand#35 |PROMO POLISHED STEEL | 49| 4 +Brand#35 |PROMO POLISHED TIN | 9| 4 +Brand#35 |PROMO POLISHED TIN | 19| 4 +Brand#35 |SMALL ANODIZED BRASS | 3| 4 +Brand#35 |SMALL ANODIZED COPPER | 3| 4 +Brand#35 |SMALL ANODIZED COPPER | 9| 4 +Brand#35 |SMALL ANODIZED COPPER | 23| 4 +Brand#35 |SMALL ANODIZED COPPER | 36| 4 +Brand#35 |SMALL ANODIZED COPPER | 45| 4 +Brand#35 |SMALL ANODIZED COPPER | 49| 4 +Brand#35 |SMALL ANODIZED NICKEL | 3| 4 +Brand#35 |SMALL ANODIZED NICKEL | 14| 4 +Brand#35 |SMALL ANODIZED NICKEL | 45| 4 +Brand#35 |SMALL ANODIZED STEEL | 3| 4 +Brand#35 |SMALL ANODIZED STEEL | 9| 4 +Brand#35 |SMALL ANODIZED STEEL | 23| 4 +Brand#35 |SMALL ANODIZED STEEL | 36| 4 +Brand#35 |SMALL ANODIZED TIN | 9| 4 +Brand#35 |SMALL ANODIZED TIN | 19| 4 +Brand#35 |SMALL ANODIZED TIN | 23| 4 +Brand#35 |SMALL ANODIZED TIN | 45| 4 +Brand#35 |SMALL BRUSHED BRASS | 3| 4 +Brand#35 |SMALL BRUSHED BRASS | 9| 4 +Brand#35 |SMALL BRUSHED BRASS | 19| 4 +Brand#35 |SMALL BRUSHED BRASS | 36| 4 +Brand#35 |SMALL BRUSHED COPPER | 14| 4 +Brand#35 |SMALL BRUSHED COPPER | 23| 4 +Brand#35 |SMALL BRUSHED COPPER | 36| 4 +Brand#35 |SMALL BRUSHED NICKEL | 36| 4 +Brand#35 |SMALL BRUSHED NICKEL | 45| 4 +Brand#35 |SMALL BRUSHED STEEL | 3| 4 +Brand#35 |SMALL BRUSHED STEEL | 14| 4 +Brand#35 |SMALL BRUSHED TIN | 3| 4 +Brand#35 |SMALL BRUSHED TIN | 36| 4 +Brand#35 |SMALL BURNISHED BRASS | 3| 4 +Brand#35 |SMALL BURNISHED BRASS | 19| 4 +Brand#35 |SMALL BURNISHED COPPER | 9| 4 +Brand#35 |SMALL BURNISHED COPPER | 19| 4 +Brand#35 |SMALL BURNISHED COPPER | 23| 4 +Brand#35 |SMALL BURNISHED NICKEL | 14| 4 +Brand#35 |SMALL BURNISHED NICKEL | 45| 4 +Brand#35 |SMALL BURNISHED NICKEL | 49| 4 +Brand#35 |SMALL BURNISHED STEEL | 9| 4 +Brand#35 |SMALL BURNISHED TIN | 3| 4 +Brand#35 |SMALL BURNISHED TIN | 9| 4 +Brand#35 |SMALL BURNISHED TIN | 14| 4 +Brand#35 |SMALL BURNISHED TIN | 23| 4 +Brand#35 |SMALL BURNISHED TIN | 36| 4 +Brand#35 |SMALL BURNISHED TIN | 49| 4 +Brand#35 |SMALL PLATED BRASS | 3| 4 +Brand#35 |SMALL PLATED BRASS | 14| 4 +Brand#35 |SMALL PLATED BRASS | 23| 4 +Brand#35 |SMALL PLATED BRASS | 45| 4 +Brand#35 |SMALL PLATED BRASS | 49| 4 +Brand#35 |SMALL PLATED COPPER | 9| 4 +Brand#35 |SMALL PLATED COPPER | 19| 4 +Brand#35 |SMALL PLATED COPPER | 23| 4 +Brand#35 |SMALL PLATED COPPER | 36| 4 +Brand#35 |SMALL PLATED NICKEL | 3| 4 +Brand#35 |SMALL PLATED NICKEL | 14| 4 +Brand#35 |SMALL PLATED NICKEL | 19| 4 +Brand#35 |SMALL PLATED STEEL | 9| 4 +Brand#35 |SMALL PLATED STEEL | 19| 4 +Brand#35 |SMALL PLATED STEEL | 45| 4 +Brand#35 |SMALL PLATED STEEL | 49| 4 +Brand#35 |SMALL PLATED TIN | 19| 4 +Brand#35 |SMALL PLATED TIN | 23| 4 +Brand#35 |SMALL POLISHED BRASS | 19| 4 +Brand#35 |SMALL POLISHED BRASS | 49| 4 +Brand#35 |SMALL POLISHED COPPER | 9| 4 +Brand#35 |SMALL POLISHED NICKEL | 3| 4 +Brand#35 |SMALL POLISHED NICKEL | 9| 4 +Brand#35 |SMALL POLISHED NICKEL | 23| 4 +Brand#35 |SMALL POLISHED NICKEL | 45| 4 +Brand#35 |SMALL POLISHED STEEL | 3| 4 +Brand#35 |SMALL POLISHED STEEL | 9| 4 +Brand#35 |SMALL POLISHED STEEL | 14| 4 +Brand#35 |SMALL POLISHED TIN | 36| 4 +Brand#35 |STANDARD ANODIZED BRASS | 3| 4 +Brand#35 |STANDARD ANODIZED BRASS | 23| 4 +Brand#35 |STANDARD ANODIZED BRASS | 36| 4 +Brand#35 |STANDARD ANODIZED BRASS | 49| 4 +Brand#35 |STANDARD ANODIZED COPPER | 9| 4 +Brand#35 |STANDARD ANODIZED COPPER | 19| 4 +Brand#35 |STANDARD ANODIZED COPPER | 49| 4 +Brand#35 |STANDARD ANODIZED NICKEL | 3| 4 +Brand#35 |STANDARD ANODIZED NICKEL | 9| 4 +Brand#35 |STANDARD ANODIZED NICKEL | 19| 4 +Brand#35 |STANDARD ANODIZED NICKEL | 23| 4 +Brand#35 |STANDARD ANODIZED NICKEL | 45| 4 +Brand#35 |STANDARD ANODIZED STEEL | 19| 4 +Brand#35 |STANDARD ANODIZED STEEL | 23| 4 +Brand#35 |STANDARD ANODIZED STEEL | 36| 4 +Brand#35 |STANDARD ANODIZED STEEL | 45| 4 +Brand#35 |STANDARD ANODIZED TIN | 14| 4 +Brand#35 |STANDARD ANODIZED TIN | 19| 4 +Brand#35 |STANDARD BRUSHED BRASS | 3| 4 +Brand#35 |STANDARD BRUSHED BRASS | 9| 4 +Brand#35 |STANDARD BRUSHED BRASS | 49| 4 +Brand#35 |STANDARD BRUSHED COPPER | 9| 4 +Brand#35 |STANDARD BRUSHED COPPER | 49| 4 +Brand#35 |STANDARD BRUSHED NICKEL | 3| 4 +Brand#35 |STANDARD BRUSHED NICKEL | 19| 4 +Brand#35 |STANDARD BRUSHED NICKEL | 23| 4 +Brand#35 |STANDARD BRUSHED NICKEL | 36| 4 +Brand#35 |STANDARD BRUSHED STEEL | 19| 4 +Brand#35 |STANDARD BRUSHED STEEL | 23| 4 +Brand#35 |STANDARD BRUSHED STEEL | 45| 4 +Brand#35 |STANDARD BRUSHED TIN | 9| 4 +Brand#35 |STANDARD BRUSHED TIN | 14| 4 +Brand#35 |STANDARD BRUSHED TIN | 19| 4 +Brand#35 |STANDARD BRUSHED TIN | 36| 4 +Brand#35 |STANDARD BRUSHED TIN | 49| 4 +Brand#35 |STANDARD BURNISHED BRASS | 3| 4 +Brand#35 |STANDARD BURNISHED BRASS | 9| 4 +Brand#35 |STANDARD BURNISHED BRASS | 14| 4 +Brand#35 |STANDARD BURNISHED BRASS | 19| 4 +Brand#35 |STANDARD BURNISHED BRASS | 23| 4 +Brand#35 |STANDARD BURNISHED BRASS | 49| 4 +Brand#35 |STANDARD BURNISHED COPPER| 14| 4 +Brand#35 |STANDARD BURNISHED COPPER| 19| 4 +Brand#35 |STANDARD BURNISHED COPPER| 23| 4 +Brand#35 |STANDARD BURNISHED COPPER| 45| 4 +Brand#35 |STANDARD BURNISHED NICKEL| 3| 4 +Brand#35 |STANDARD BURNISHED NICKEL| 19| 4 +Brand#35 |STANDARD BURNISHED NICKEL| 23| 4 +Brand#35 |STANDARD BURNISHED STEEL | 9| 4 +Brand#35 |STANDARD BURNISHED STEEL | 19| 4 +Brand#35 |STANDARD BURNISHED TIN | 9| 4 +Brand#35 |STANDARD BURNISHED TIN | 14| 4 +Brand#35 |STANDARD BURNISHED TIN | 23| 4 +Brand#35 |STANDARD PLATED BRASS | 3| 4 +Brand#35 |STANDARD PLATED BRASS | 9| 4 +Brand#35 |STANDARD PLATED COPPER | 23| 4 +Brand#35 |STANDARD PLATED COPPER | 49| 4 +Brand#35 |STANDARD PLATED NICKEL | 36| 4 +Brand#35 |STANDARD PLATED NICKEL | 45| 4 +Brand#35 |STANDARD PLATED STEEL | 3| 4 +Brand#35 |STANDARD PLATED STEEL | 14| 4 +Brand#35 |STANDARD PLATED STEEL | 49| 4 +Brand#35 |STANDARD PLATED TIN | 3| 4 +Brand#35 |STANDARD POLISHED BRASS | 3| 4 +Brand#35 |STANDARD POLISHED BRASS | 14| 4 +Brand#35 |STANDARD POLISHED BRASS | 45| 4 +Brand#35 |STANDARD POLISHED COPPER | 3| 4 +Brand#35 |STANDARD POLISHED COPPER | 9| 4 +Brand#35 |STANDARD POLISHED COPPER | 14| 4 +Brand#35 |STANDARD POLISHED COPPER | 19| 4 +Brand#35 |STANDARD POLISHED COPPER | 36| 4 +Brand#35 |STANDARD POLISHED COPPER | 45| 4 +Brand#35 |STANDARD POLISHED NICKEL | 19| 4 +Brand#35 |STANDARD POLISHED NICKEL | 45| 4 +Brand#35 |STANDARD POLISHED STEEL | 9| 4 +Brand#35 |STANDARD POLISHED STEEL | 19| 4 +Brand#35 |STANDARD POLISHED TIN | 19| 4 +Brand#35 |STANDARD POLISHED TIN | 23| 4 +Brand#35 |STANDARD POLISHED TIN | 49| 4 +Brand#41 |ECONOMY ANODIZED BRASS | 3| 4 +Brand#41 |ECONOMY ANODIZED BRASS | 9| 4 +Brand#41 |ECONOMY ANODIZED COPPER | 3| 4 +Brand#41 |ECONOMY ANODIZED COPPER | 9| 4 +Brand#41 |ECONOMY ANODIZED NICKEL | 3| 4 +Brand#41 |ECONOMY ANODIZED NICKEL | 9| 4 +Brand#41 |ECONOMY ANODIZED NICKEL | 14| 4 +Brand#41 |ECONOMY ANODIZED NICKEL | 23| 4 +Brand#41 |ECONOMY ANODIZED NICKEL | 36| 4 +Brand#41 |ECONOMY ANODIZED NICKEL | 49| 4 +Brand#41 |ECONOMY ANODIZED STEEL | 9| 4 +Brand#41 |ECONOMY ANODIZED STEEL | 14| 4 +Brand#41 |ECONOMY ANODIZED STEEL | 23| 4 +Brand#41 |ECONOMY ANODIZED TIN | 9| 4 +Brand#41 |ECONOMY ANODIZED TIN | 19| 4 +Brand#41 |ECONOMY ANODIZED TIN | 49| 4 +Brand#41 |ECONOMY BRUSHED BRASS | 9| 4 +Brand#41 |ECONOMY BRUSHED BRASS | 19| 4 +Brand#41 |ECONOMY BRUSHED BRASS | 45| 4 +Brand#41 |ECONOMY BRUSHED BRASS | 49| 4 +Brand#41 |ECONOMY BRUSHED COPPER | 9| 4 +Brand#41 |ECONOMY BRUSHED COPPER | 45| 4 +Brand#41 |ECONOMY BRUSHED NICKEL | 3| 4 +Brand#41 |ECONOMY BRUSHED NICKEL | 9| 4 +Brand#41 |ECONOMY BRUSHED NICKEL | 14| 4 +Brand#41 |ECONOMY BRUSHED NICKEL | 23| 4 +Brand#41 |ECONOMY BRUSHED STEEL | 14| 4 +Brand#41 |ECONOMY BRUSHED STEEL | 23| 4 +Brand#41 |ECONOMY BRUSHED STEEL | 49| 4 +Brand#41 |ECONOMY BRUSHED TIN | 19| 4 +Brand#41 |ECONOMY BURNISHED BRASS | 9| 4 +Brand#41 |ECONOMY BURNISHED COPPER | 19| 4 +Brand#41 |ECONOMY BURNISHED COPPER | 23| 4 +Brand#41 |ECONOMY BURNISHED COPPER | 36| 4 +Brand#41 |ECONOMY BURNISHED NICKEL | 9| 4 +Brand#41 |ECONOMY BURNISHED NICKEL | 19| 4 +Brand#41 |ECONOMY BURNISHED NICKEL | 23| 4 +Brand#41 |ECONOMY BURNISHED STEEL | 9| 4 +Brand#41 |ECONOMY BURNISHED STEEL | 45| 4 +Brand#41 |ECONOMY BURNISHED TIN | 19| 4 +Brand#41 |ECONOMY BURNISHED TIN | 45| 4 +Brand#41 |ECONOMY BURNISHED TIN | 49| 4 +Brand#41 |ECONOMY PLATED COPPER | 3| 4 +Brand#41 |ECONOMY PLATED COPPER | 9| 4 +Brand#41 |ECONOMY PLATED COPPER | 19| 4 +Brand#41 |ECONOMY PLATED COPPER | 23| 4 +Brand#41 |ECONOMY PLATED COPPER | 36| 4 +Brand#41 |ECONOMY PLATED NICKEL | 19| 4 +Brand#41 |ECONOMY PLATED NICKEL | 49| 4 +Brand#41 |ECONOMY PLATED TIN | 14| 4 +Brand#41 |ECONOMY PLATED TIN | 36| 4 +Brand#41 |ECONOMY POLISHED BRASS | 3| 4 +Brand#41 |ECONOMY POLISHED BRASS | 9| 4 +Brand#41 |ECONOMY POLISHED COPPER | 3| 4 +Brand#41 |ECONOMY POLISHED COPPER | 9| 4 +Brand#41 |ECONOMY POLISHED COPPER | 19| 4 +Brand#41 |ECONOMY POLISHED COPPER | 23| 4 +Brand#41 |ECONOMY POLISHED NICKEL | 3| 4 +Brand#41 |ECONOMY POLISHED NICKEL | 14| 4 +Brand#41 |ECONOMY POLISHED NICKEL | 36| 4 +Brand#41 |ECONOMY POLISHED STEEL | 9| 4 +Brand#41 |ECONOMY POLISHED STEEL | 14| 4 +Brand#41 |ECONOMY POLISHED STEEL | 36| 4 +Brand#41 |ECONOMY POLISHED TIN | 9| 4 +Brand#41 |LARGE ANODIZED BRASS | 19| 4 +Brand#41 |LARGE ANODIZED BRASS | 49| 4 +Brand#41 |LARGE ANODIZED COPPER | 19| 4 +Brand#41 |LARGE ANODIZED COPPER | 23| 4 +Brand#41 |LARGE ANODIZED COPPER | 49| 4 +Brand#41 |LARGE ANODIZED NICKEL | 14| 4 +Brand#41 |LARGE ANODIZED NICKEL | 23| 4 +Brand#41 |LARGE ANODIZED NICKEL | 36| 4 +Brand#41 |LARGE ANODIZED NICKEL | 45| 4 +Brand#41 |LARGE ANODIZED NICKEL | 49| 4 +Brand#41 |LARGE ANODIZED STEEL | 9| 4 +Brand#41 |LARGE ANODIZED STEEL | 45| 4 +Brand#41 |LARGE ANODIZED STEEL | 49| 4 +Brand#41 |LARGE ANODIZED TIN | 9| 4 +Brand#41 |LARGE ANODIZED TIN | 14| 4 +Brand#41 |LARGE ANODIZED TIN | 36| 4 +Brand#41 |LARGE ANODIZED TIN | 49| 4 +Brand#41 |LARGE BRUSHED BRASS | 19| 4 +Brand#41 |LARGE BRUSHED BRASS | 36| 4 +Brand#41 |LARGE BRUSHED BRASS | 45| 4 +Brand#41 |LARGE BRUSHED BRASS | 49| 4 +Brand#41 |LARGE BRUSHED COPPER | 3| 4 +Brand#41 |LARGE BRUSHED COPPER | 14| 4 +Brand#41 |LARGE BRUSHED COPPER | 45| 4 +Brand#41 |LARGE BRUSHED NICKEL | 3| 4 +Brand#41 |LARGE BRUSHED NICKEL | 9| 4 +Brand#41 |LARGE BRUSHED NICKEL | 49| 4 +Brand#41 |LARGE BRUSHED STEEL | 3| 4 +Brand#41 |LARGE BRUSHED STEEL | 19| 4 +Brand#41 |LARGE BRUSHED TIN | 9| 4 +Brand#41 |LARGE BRUSHED TIN | 23| 4 +Brand#41 |LARGE BURNISHED BRASS | 9| 4 +Brand#41 |LARGE BURNISHED BRASS | 14| 4 +Brand#41 |LARGE BURNISHED BRASS | 45| 4 +Brand#41 |LARGE BURNISHED BRASS | 49| 4 +Brand#41 |LARGE BURNISHED COPPER | 9| 4 +Brand#41 |LARGE BURNISHED COPPER | 36| 4 +Brand#41 |LARGE BURNISHED NICKEL | 3| 4 +Brand#41 |LARGE BURNISHED NICKEL | 9| 4 +Brand#41 |LARGE BURNISHED NICKEL | 23| 4 +Brand#41 |LARGE BURNISHED STEEL | 36| 4 +Brand#41 |LARGE BURNISHED TIN | 23| 4 +Brand#41 |LARGE BURNISHED TIN | 49| 4 +Brand#41 |LARGE PLATED BRASS | 49| 4 +Brand#41 |LARGE PLATED NICKEL | 23| 4 +Brand#41 |LARGE PLATED NICKEL | 45| 4 +Brand#41 |LARGE PLATED STEEL | 9| 4 +Brand#41 |LARGE PLATED STEEL | 45| 4 +Brand#41 |LARGE PLATED TIN | 9| 4 +Brand#41 |LARGE PLATED TIN | 49| 4 +Brand#41 |LARGE POLISHED BRASS | 9| 4 +Brand#41 |LARGE POLISHED BRASS | 23| 4 +Brand#41 |LARGE POLISHED COPPER | 9| 4 +Brand#41 |LARGE POLISHED COPPER | 45| 4 +Brand#41 |LARGE POLISHED NICKEL | 9| 4 +Brand#41 |LARGE POLISHED NICKEL | 19| 4 +Brand#41 |LARGE POLISHED NICKEL | 36| 4 +Brand#41 |LARGE POLISHED STEEL | 19| 4 +Brand#41 |LARGE POLISHED STEEL | 36| 4 +Brand#41 |LARGE POLISHED STEEL | 45| 4 +Brand#41 |LARGE POLISHED STEEL | 49| 4 +Brand#41 |LARGE POLISHED TIN | 23| 4 +Brand#41 |LARGE POLISHED TIN | 36| 4 +Brand#41 |LARGE POLISHED TIN | 45| 4 +Brand#41 |LARGE POLISHED TIN | 49| 4 +Brand#41 |MEDIUM ANODIZED BRASS | 14| 4 +Brand#41 |MEDIUM ANODIZED BRASS | 19| 4 +Brand#41 |MEDIUM ANODIZED BRASS | 23| 4 +Brand#41 |MEDIUM ANODIZED COPPER | 9| 4 +Brand#41 |MEDIUM ANODIZED COPPER | 14| 4 +Brand#41 |MEDIUM ANODIZED COPPER | 19| 4 +Brand#41 |MEDIUM ANODIZED COPPER | 36| 4 +Brand#41 |MEDIUM ANODIZED COPPER | 45| 4 +Brand#41 |MEDIUM ANODIZED COPPER | 49| 4 +Brand#41 |MEDIUM ANODIZED NICKEL | 9| 4 +Brand#41 |MEDIUM ANODIZED NICKEL | 14| 4 +Brand#41 |MEDIUM ANODIZED NICKEL | 23| 4 +Brand#41 |MEDIUM ANODIZED NICKEL | 45| 4 +Brand#41 |MEDIUM ANODIZED STEEL | 9| 4 +Brand#41 |MEDIUM ANODIZED STEEL | 14| 4 +Brand#41 |MEDIUM ANODIZED STEEL | 19| 4 +Brand#41 |MEDIUM BRUSHED BRASS | 23| 4 +Brand#41 |MEDIUM BRUSHED COPPER | 9| 4 +Brand#41 |MEDIUM BRUSHED COPPER | 19| 4 +Brand#41 |MEDIUM BRUSHED COPPER | 23| 4 +Brand#41 |MEDIUM BRUSHED COPPER | 36| 4 +Brand#41 |MEDIUM BRUSHED COPPER | 45| 4 +Brand#41 |MEDIUM BRUSHED NICKEL | 9| 4 +Brand#41 |MEDIUM BRUSHED NICKEL | 19| 4 +Brand#41 |MEDIUM BRUSHED NICKEL | 36| 4 +Brand#41 |MEDIUM BRUSHED NICKEL | 45| 4 +Brand#41 |MEDIUM BRUSHED STEEL | 3| 4 +Brand#41 |MEDIUM BRUSHED STEEL | 14| 4 +Brand#41 |MEDIUM BRUSHED STEEL | 23| 4 +Brand#41 |MEDIUM BRUSHED TIN | 14| 4 +Brand#41 |MEDIUM BRUSHED TIN | 36| 4 +Brand#41 |MEDIUM BRUSHED TIN | 45| 4 +Brand#41 |MEDIUM BURNISHED BRASS | 9| 4 +Brand#41 |MEDIUM BURNISHED BRASS | 19| 4 +Brand#41 |MEDIUM BURNISHED BRASS | 45| 4 +Brand#41 |MEDIUM BURNISHED COPPER | 45| 4 +Brand#41 |MEDIUM BURNISHED COPPER | 49| 4 +Brand#41 |MEDIUM BURNISHED NICKEL | 14| 4 +Brand#41 |MEDIUM BURNISHED NICKEL | 36| 4 +Brand#41 |MEDIUM BURNISHED STEEL | 9| 4 +Brand#41 |MEDIUM BURNISHED STEEL | 14| 4 +Brand#41 |MEDIUM BURNISHED STEEL | 19| 4 +Brand#41 |MEDIUM BURNISHED STEEL | 49| 4 +Brand#41 |MEDIUM BURNISHED TIN | 9| 4 +Brand#41 |MEDIUM BURNISHED TIN | 23| 4 +Brand#41 |MEDIUM BURNISHED TIN | 36| 4 +Brand#41 |MEDIUM PLATED BRASS | 3| 4 +Brand#41 |MEDIUM PLATED BRASS | 9| 4 +Brand#41 |MEDIUM PLATED BRASS | 14| 4 +Brand#41 |MEDIUM PLATED BRASS | 36| 4 +Brand#41 |MEDIUM PLATED COPPER | 3| 4 +Brand#41 |MEDIUM PLATED COPPER | 14| 4 +Brand#41 |MEDIUM PLATED COPPER | 36| 4 +Brand#41 |MEDIUM PLATED NICKEL | 3| 4 +Brand#41 |MEDIUM PLATED NICKEL | 14| 4 +Brand#41 |MEDIUM PLATED STEEL | 14| 4 +Brand#41 |MEDIUM PLATED TIN | 14| 4 +Brand#41 |MEDIUM PLATED TIN | 19| 4 +Brand#41 |MEDIUM PLATED TIN | 23| 4 +Brand#41 |MEDIUM PLATED TIN | 49| 4 +Brand#41 |PROMO ANODIZED BRASS | 19| 4 +Brand#41 |PROMO ANODIZED BRASS | 23| 4 +Brand#41 |PROMO ANODIZED BRASS | 45| 4 +Brand#41 |PROMO ANODIZED COPPER | 9| 4 +Brand#41 |PROMO ANODIZED COPPER | 19| 4 +Brand#41 |PROMO ANODIZED COPPER | 23| 4 +Brand#41 |PROMO ANODIZED COPPER | 49| 4 +Brand#41 |PROMO ANODIZED NICKEL | 9| 4 +Brand#41 |PROMO ANODIZED NICKEL | 14| 4 +Brand#41 |PROMO ANODIZED NICKEL | 23| 4 +Brand#41 |PROMO ANODIZED NICKEL | 36| 4 +Brand#41 |PROMO ANODIZED STEEL | 3| 4 +Brand#41 |PROMO ANODIZED STEEL | 36| 4 +Brand#41 |PROMO ANODIZED STEEL | 45| 4 +Brand#41 |PROMO ANODIZED TIN | 3| 4 +Brand#41 |PROMO ANODIZED TIN | 14| 4 +Brand#41 |PROMO ANODIZED TIN | 19| 4 +Brand#41 |PROMO ANODIZED TIN | 23| 4 +Brand#41 |PROMO ANODIZED TIN | 45| 4 +Brand#41 |PROMO ANODIZED TIN | 49| 4 +Brand#41 |PROMO BRUSHED BRASS | 45| 4 +Brand#41 |PROMO BRUSHED BRASS | 49| 4 +Brand#41 |PROMO BRUSHED COPPER | 3| 4 +Brand#41 |PROMO BRUSHED COPPER | 9| 4 +Brand#41 |PROMO BRUSHED COPPER | 23| 4 +Brand#41 |PROMO BRUSHED NICKEL | 14| 4 +Brand#41 |PROMO BRUSHED NICKEL | 19| 4 +Brand#41 |PROMO BRUSHED NICKEL | 45| 4 +Brand#41 |PROMO BRUSHED STEEL | 14| 4 +Brand#41 |PROMO BRUSHED TIN | 3| 4 +Brand#41 |PROMO BRUSHED TIN | 19| 4 +Brand#41 |PROMO BRUSHED TIN | 23| 4 +Brand#41 |PROMO BRUSHED TIN | 36| 4 +Brand#41 |PROMO BURNISHED BRASS | 3| 4 +Brand#41 |PROMO BURNISHED BRASS | 19| 4 +Brand#41 |PROMO BURNISHED BRASS | 36| 4 +Brand#41 |PROMO BURNISHED BRASS | 45| 4 +Brand#41 |PROMO BURNISHED BRASS | 49| 4 +Brand#41 |PROMO BURNISHED COPPER | 3| 4 +Brand#41 |PROMO BURNISHED COPPER | 14| 4 +Brand#41 |PROMO BURNISHED NICKEL | 3| 4 +Brand#41 |PROMO BURNISHED NICKEL | 9| 4 +Brand#41 |PROMO BURNISHED NICKEL | 45| 4 +Brand#41 |PROMO BURNISHED NICKEL | 49| 4 +Brand#41 |PROMO BURNISHED STEEL | 3| 4 +Brand#41 |PROMO BURNISHED STEEL | 9| 4 +Brand#41 |PROMO BURNISHED STEEL | 19| 4 +Brand#41 |PROMO BURNISHED STEEL | 23| 4 +Brand#41 |PROMO BURNISHED STEEL | 45| 4 +Brand#41 |PROMO BURNISHED STEEL | 49| 4 +Brand#41 |PROMO BURNISHED TIN | 9| 4 +Brand#41 |PROMO BURNISHED TIN | 36| 4 +Brand#41 |PROMO BURNISHED TIN | 45| 4 +Brand#41 |PROMO BURNISHED TIN | 49| 4 +Brand#41 |PROMO PLATED BRASS | 19| 4 +Brand#41 |PROMO PLATED BRASS | 23| 4 +Brand#41 |PROMO PLATED BRASS | 45| 4 +Brand#41 |PROMO PLATED COPPER | 3| 4 +Brand#41 |PROMO PLATED COPPER | 19| 4 +Brand#41 |PROMO PLATED NICKEL | 23| 4 +Brand#41 |PROMO PLATED NICKEL | 45| 4 +Brand#41 |PROMO PLATED STEEL | 9| 4 +Brand#41 |PROMO PLATED STEEL | 23| 4 +Brand#41 |PROMO PLATED TIN | 9| 4 +Brand#41 |PROMO PLATED TIN | 23| 4 +Brand#41 |PROMO POLISHED BRASS | 3| 4 +Brand#41 |PROMO POLISHED BRASS | 49| 4 +Brand#41 |PROMO POLISHED NICKEL | 9| 4 +Brand#41 |PROMO POLISHED NICKEL | 23| 4 +Brand#41 |PROMO POLISHED NICKEL | 36| 4 +Brand#41 |PROMO POLISHED NICKEL | 45| 4 +Brand#41 |PROMO POLISHED NICKEL | 49| 4 +Brand#41 |PROMO POLISHED STEEL | 14| 4 +Brand#41 |PROMO POLISHED STEEL | 23| 4 +Brand#41 |PROMO POLISHED TIN | 3| 4 +Brand#41 |PROMO POLISHED TIN | 36| 4 +Brand#41 |PROMO POLISHED TIN | 49| 4 +Brand#41 |SMALL ANODIZED BRASS | 19| 4 +Brand#41 |SMALL ANODIZED BRASS | 49| 4 +Brand#41 |SMALL ANODIZED COPPER | 36| 4 +Brand#41 |SMALL ANODIZED COPPER | 45| 4 +Brand#41 |SMALL ANODIZED NICKEL | 3| 4 +Brand#41 |SMALL ANODIZED NICKEL | 23| 4 +Brand#41 |SMALL ANODIZED NICKEL | 49| 4 +Brand#41 |SMALL ANODIZED STEEL | 19| 4 +Brand#41 |SMALL ANODIZED TIN | 14| 4 +Brand#41 |SMALL ANODIZED TIN | 36| 4 +Brand#41 |SMALL ANODIZED TIN | 49| 4 +Brand#41 |SMALL BRUSHED BRASS | 14| 4 +Brand#41 |SMALL BRUSHED BRASS | 19| 4 +Brand#41 |SMALL BRUSHED BRASS | 36| 4 +Brand#41 |SMALL BRUSHED COPPER | 23| 4 +Brand#41 |SMALL BRUSHED COPPER | 36| 4 +Brand#41 |SMALL BRUSHED NICKEL | 3| 4 +Brand#41 |SMALL BRUSHED NICKEL | 19| 4 +Brand#41 |SMALL BRUSHED NICKEL | 49| 4 +Brand#41 |SMALL BRUSHED STEEL | 9| 4 +Brand#41 |SMALL BRUSHED STEEL | 14| 4 +Brand#41 |SMALL BRUSHED TIN | 23| 4 +Brand#41 |SMALL BRUSHED TIN | 45| 4 +Brand#41 |SMALL BRUSHED TIN | 49| 4 +Brand#41 |SMALL BURNISHED BRASS | 23| 4 +Brand#41 |SMALL BURNISHED BRASS | 36| 4 +Brand#41 |SMALL BURNISHED COPPER | 14| 4 +Brand#41 |SMALL BURNISHED COPPER | 36| 4 +Brand#41 |SMALL BURNISHED COPPER | 49| 4 +Brand#41 |SMALL BURNISHED NICKEL | 14| 4 +Brand#41 |SMALL BURNISHED NICKEL | 49| 4 +Brand#41 |SMALL BURNISHED STEEL | 14| 4 +Brand#41 |SMALL BURNISHED STEEL | 19| 4 +Brand#41 |SMALL BURNISHED STEEL | 36| 4 +Brand#41 |SMALL BURNISHED TIN | 9| 4 +Brand#41 |SMALL BURNISHED TIN | 19| 4 +Brand#41 |SMALL BURNISHED TIN | 36| 4 +Brand#41 |SMALL BURNISHED TIN | 45| 4 +Brand#41 |SMALL BURNISHED TIN | 49| 4 +Brand#41 |SMALL PLATED BRASS | 19| 4 +Brand#41 |SMALL PLATED BRASS | 45| 4 +Brand#41 |SMALL PLATED COPPER | 3| 4 +Brand#41 |SMALL PLATED COPPER | 36| 4 +Brand#41 |SMALL PLATED COPPER | 45| 4 +Brand#41 |SMALL PLATED COPPER | 49| 4 +Brand#41 |SMALL PLATED NICKEL | 14| 4 +Brand#41 |SMALL PLATED NICKEL | 45| 4 +Brand#41 |SMALL PLATED NICKEL | 49| 4 +Brand#41 |SMALL PLATED STEEL | 3| 4 +Brand#41 |SMALL PLATED STEEL | 19| 4 +Brand#41 |SMALL PLATED STEEL | 23| 4 +Brand#41 |SMALL PLATED TIN | 14| 4 +Brand#41 |SMALL PLATED TIN | 36| 4 +Brand#41 |SMALL PLATED TIN | 45| 4 +Brand#41 |SMALL POLISHED BRASS | 3| 4 +Brand#41 |SMALL POLISHED BRASS | 9| 4 +Brand#41 |SMALL POLISHED BRASS | 14| 4 +Brand#41 |SMALL POLISHED BRASS | 23| 4 +Brand#41 |SMALL POLISHED COPPER | 9| 4 +Brand#41 |SMALL POLISHED COPPER | 19| 4 +Brand#41 |SMALL POLISHED COPPER | 49| 4 +Brand#41 |SMALL POLISHED NICKEL | 36| 4 +Brand#41 |SMALL POLISHED NICKEL | 45| 4 +Brand#41 |SMALL POLISHED STEEL | 3| 4 +Brand#41 |SMALL POLISHED STEEL | 9| 4 +Brand#41 |SMALL POLISHED STEEL | 14| 4 +Brand#41 |SMALL POLISHED STEEL | 19| 4 +Brand#41 |SMALL POLISHED STEEL | 23| 4 +Brand#41 |SMALL POLISHED TIN | 3| 4 +Brand#41 |STANDARD ANODIZED BRASS | 9| 4 +Brand#41 |STANDARD ANODIZED BRASS | 19| 4 +Brand#41 |STANDARD ANODIZED BRASS | 23| 4 +Brand#41 |STANDARD ANODIZED BRASS | 45| 4 +Brand#41 |STANDARD ANODIZED BRASS | 49| 4 +Brand#41 |STANDARD ANODIZED COPPER | 19| 4 +Brand#41 |STANDARD ANODIZED COPPER | 45| 4 +Brand#41 |STANDARD ANODIZED NICKEL | 14| 4 +Brand#41 |STANDARD ANODIZED NICKEL | 19| 4 +Brand#41 |STANDARD ANODIZED STEEL | 3| 4 +Brand#41 |STANDARD ANODIZED STEEL | 9| 4 +Brand#41 |STANDARD ANODIZED STEEL | 14| 4 +Brand#41 |STANDARD ANODIZED STEEL | 19| 4 +Brand#41 |STANDARD ANODIZED STEEL | 36| 4 +Brand#41 |STANDARD ANODIZED TIN | 9| 4 +Brand#41 |STANDARD ANODIZED TIN | 14| 4 +Brand#41 |STANDARD ANODIZED TIN | 36| 4 +Brand#41 |STANDARD ANODIZED TIN | 45| 4 +Brand#41 |STANDARD ANODIZED TIN | 49| 4 +Brand#41 |STANDARD BRUSHED BRASS | 3| 4 +Brand#41 |STANDARD BRUSHED BRASS | 14| 4 +Brand#41 |STANDARD BRUSHED BRASS | 19| 4 +Brand#41 |STANDARD BRUSHED BRASS | 23| 4 +Brand#41 |STANDARD BRUSHED BRASS | 45| 4 +Brand#41 |STANDARD BRUSHED BRASS | 49| 4 +Brand#41 |STANDARD BRUSHED COPPER | 14| 4 +Brand#41 |STANDARD BRUSHED COPPER | 23| 4 +Brand#41 |STANDARD BRUSHED COPPER | 36| 4 +Brand#41 |STANDARD BRUSHED COPPER | 49| 4 +Brand#41 |STANDARD BRUSHED NICKEL | 23| 4 +Brand#41 |STANDARD BRUSHED NICKEL | 36| 4 +Brand#41 |STANDARD BRUSHED STEEL | 9| 4 +Brand#41 |STANDARD BRUSHED STEEL | 23| 4 +Brand#41 |STANDARD BRUSHED STEEL | 36| 4 +Brand#41 |STANDARD BRUSHED TIN | 14| 4 +Brand#41 |STANDARD BURNISHED BRASS | 19| 4 +Brand#41 |STANDARD BURNISHED BRASS | 23| 4 +Brand#41 |STANDARD BURNISHED BRASS | 45| 4 +Brand#41 |STANDARD BURNISHED BRASS | 49| 4 +Brand#41 |STANDARD BURNISHED COPPER| 3| 4 +Brand#41 |STANDARD BURNISHED COPPER| 23| 4 +Brand#41 |STANDARD BURNISHED COPPER| 45| 4 +Brand#41 |STANDARD BURNISHED COPPER| 49| 4 +Brand#41 |STANDARD BURNISHED NICKEL| 3| 4 +Brand#41 |STANDARD BURNISHED NICKEL| 9| 4 +Brand#41 |STANDARD BURNISHED NICKEL| 45| 4 +Brand#41 |STANDARD BURNISHED STEEL | 19| 4 +Brand#41 |STANDARD BURNISHED STEEL | 36| 4 +Brand#41 |STANDARD BURNISHED STEEL | 45| 4 +Brand#41 |STANDARD BURNISHED TIN | 9| 4 +Brand#41 |STANDARD BURNISHED TIN | 49| 4 +Brand#41 |STANDARD PLATED BRASS | 3| 4 +Brand#41 |STANDARD PLATED BRASS | 23| 4 +Brand#41 |STANDARD PLATED COPPER | 14| 4 +Brand#41 |STANDARD PLATED COPPER | 19| 4 +Brand#41 |STANDARD PLATED COPPER | 23| 4 +Brand#41 |STANDARD PLATED NICKEL | 3| 4 +Brand#41 |STANDARD PLATED NICKEL | 36| 4 +Brand#41 |STANDARD PLATED STEEL | 23| 4 +Brand#41 |STANDARD PLATED STEEL | 45| 4 +Brand#41 |STANDARD PLATED TIN | 19| 4 +Brand#41 |STANDARD PLATED TIN | 23| 4 +Brand#41 |STANDARD PLATED TIN | 36| 4 +Brand#41 |STANDARD POLISHED BRASS | 9| 4 +Brand#41 |STANDARD POLISHED BRASS | 23| 4 +Brand#41 |STANDARD POLISHED BRASS | 45| 4 +Brand#41 |STANDARD POLISHED BRASS | 49| 4 +Brand#41 |STANDARD POLISHED COPPER | 19| 4 +Brand#41 |STANDARD POLISHED COPPER | 45| 4 +Brand#41 |STANDARD POLISHED COPPER | 49| 4 +Brand#41 |STANDARD POLISHED NICKEL | 9| 4 +Brand#41 |STANDARD POLISHED NICKEL | 19| 4 +Brand#41 |STANDARD POLISHED NICKEL | 23| 4 +Brand#41 |STANDARD POLISHED NICKEL | 49| 4 +Brand#41 |STANDARD POLISHED STEEL | 9| 4 +Brand#41 |STANDARD POLISHED STEEL | 14| 4 +Brand#41 |STANDARD POLISHED STEEL | 19| 4 +Brand#41 |STANDARD POLISHED STEEL | 23| 4 +Brand#41 |STANDARD POLISHED STEEL | 49| 4 +Brand#41 |STANDARD POLISHED TIN | 3| 4 +Brand#41 |STANDARD POLISHED TIN | 9| 4 +Brand#41 |STANDARD POLISHED TIN | 49| 4 +Brand#42 |ECONOMY ANODIZED BRASS | 3| 4 +Brand#42 |ECONOMY ANODIZED BRASS | 45| 4 +Brand#42 |ECONOMY ANODIZED COPPER | 3| 4 +Brand#42 |ECONOMY ANODIZED COPPER | 9| 4 +Brand#42 |ECONOMY ANODIZED COPPER | 19| 4 +Brand#42 |ECONOMY ANODIZED NICKEL | 9| 4 +Brand#42 |ECONOMY ANODIZED NICKEL | 23| 4 +Brand#42 |ECONOMY ANODIZED STEEL | 14| 4 +Brand#42 |ECONOMY ANODIZED STEEL | 36| 4 +Brand#42 |ECONOMY ANODIZED TIN | 3| 4 +Brand#42 |ECONOMY ANODIZED TIN | 9| 4 +Brand#42 |ECONOMY BRUSHED BRASS | 14| 4 +Brand#42 |ECONOMY BRUSHED BRASS | 19| 4 +Brand#42 |ECONOMY BRUSHED BRASS | 36| 4 +Brand#42 |ECONOMY BRUSHED BRASS | 45| 4 +Brand#42 |ECONOMY BRUSHED COPPER | 14| 4 +Brand#42 |ECONOMY BRUSHED COPPER | 19| 4 +Brand#42 |ECONOMY BRUSHED COPPER | 23| 4 +Brand#42 |ECONOMY BRUSHED COPPER | 45| 4 +Brand#42 |ECONOMY BRUSHED NICKEL | 23| 4 +Brand#42 |ECONOMY BRUSHED NICKEL | 36| 4 +Brand#42 |ECONOMY BRUSHED STEEL | 36| 4 +Brand#42 |ECONOMY BRUSHED TIN | 23| 4 +Brand#42 |ECONOMY BURNISHED BRASS | 9| 4 +Brand#42 |ECONOMY BURNISHED BRASS | 19| 4 +Brand#42 |ECONOMY BURNISHED BRASS | 36| 4 +Brand#42 |ECONOMY BURNISHED BRASS | 45| 4 +Brand#42 |ECONOMY BURNISHED BRASS | 49| 4 +Brand#42 |ECONOMY BURNISHED COPPER | 9| 4 +Brand#42 |ECONOMY BURNISHED COPPER | 14| 4 +Brand#42 |ECONOMY BURNISHED COPPER | 23| 4 +Brand#42 |ECONOMY BURNISHED COPPER | 36| 4 +Brand#42 |ECONOMY BURNISHED COPPER | 45| 4 +Brand#42 |ECONOMY BURNISHED NICKEL | 9| 4 +Brand#42 |ECONOMY BURNISHED NICKEL | 14| 4 +Brand#42 |ECONOMY BURNISHED NICKEL | 19| 4 +Brand#42 |ECONOMY BURNISHED NICKEL | 36| 4 +Brand#42 |ECONOMY BURNISHED NICKEL | 45| 4 +Brand#42 |ECONOMY BURNISHED STEEL | 3| 4 +Brand#42 |ECONOMY BURNISHED STEEL | 36| 4 +Brand#42 |ECONOMY BURNISHED TIN | 3| 4 +Brand#42 |ECONOMY PLATED BRASS | 19| 4 +Brand#42 |ECONOMY PLATED BRASS | 36| 4 +Brand#42 |ECONOMY PLATED BRASS | 45| 4 +Brand#42 |ECONOMY PLATED COPPER | 19| 4 +Brand#42 |ECONOMY PLATED COPPER | 45| 4 +Brand#42 |ECONOMY PLATED COPPER | 49| 4 +Brand#42 |ECONOMY PLATED NICKEL | 3| 4 +Brand#42 |ECONOMY PLATED NICKEL | 14| 4 +Brand#42 |ECONOMY PLATED NICKEL | 23| 4 +Brand#42 |ECONOMY PLATED NICKEL | 45| 4 +Brand#42 |ECONOMY PLATED STEEL | 3| 4 +Brand#42 |ECONOMY PLATED STEEL | 23| 4 +Brand#42 |ECONOMY PLATED TIN | 36| 4 +Brand#42 |ECONOMY POLISHED BRASS | 3| 4 +Brand#42 |ECONOMY POLISHED BRASS | 14| 4 +Brand#42 |ECONOMY POLISHED BRASS | 19| 4 +Brand#42 |ECONOMY POLISHED BRASS | 23| 4 +Brand#42 |ECONOMY POLISHED BRASS | 36| 4 +Brand#42 |ECONOMY POLISHED BRASS | 45| 4 +Brand#42 |ECONOMY POLISHED BRASS | 49| 4 +Brand#42 |ECONOMY POLISHED COPPER | 14| 4 +Brand#42 |ECONOMY POLISHED COPPER | 19| 4 +Brand#42 |ECONOMY POLISHED COPPER | 49| 4 +Brand#42 |ECONOMY POLISHED NICKEL | 3| 4 +Brand#42 |ECONOMY POLISHED NICKEL | 9| 4 +Brand#42 |ECONOMY POLISHED NICKEL | 19| 4 +Brand#42 |ECONOMY POLISHED STEEL | 3| 4 +Brand#42 |ECONOMY POLISHED STEEL | 19| 4 +Brand#42 |ECONOMY POLISHED STEEL | 45| 4 +Brand#42 |ECONOMY POLISHED STEEL | 49| 4 +Brand#42 |ECONOMY POLISHED TIN | 9| 4 +Brand#42 |ECONOMY POLISHED TIN | 14| 4 +Brand#42 |ECONOMY POLISHED TIN | 19| 4 +Brand#42 |ECONOMY POLISHED TIN | 45| 4 +Brand#42 |ECONOMY POLISHED TIN | 49| 4 +Brand#42 |LARGE ANODIZED BRASS | 14| 4 +Brand#42 |LARGE ANODIZED BRASS | 36| 4 +Brand#42 |LARGE ANODIZED COPPER | 9| 4 +Brand#42 |LARGE ANODIZED COPPER | 19| 4 +Brand#42 |LARGE ANODIZED COPPER | 45| 4 +Brand#42 |LARGE ANODIZED NICKEL | 14| 4 +Brand#42 |LARGE ANODIZED NICKEL | 19| 4 +Brand#42 |LARGE ANODIZED NICKEL | 23| 4 +Brand#42 |LARGE ANODIZED NICKEL | 36| 4 +Brand#42 |LARGE ANODIZED STEEL | 19| 4 +Brand#42 |LARGE ANODIZED STEEL | 23| 4 +Brand#42 |LARGE ANODIZED STEEL | 45| 4 +Brand#42 |LARGE ANODIZED STEEL | 49| 4 +Brand#42 |LARGE ANODIZED TIN | 19| 4 +Brand#42 |LARGE ANODIZED TIN | 36| 4 +Brand#42 |LARGE ANODIZED TIN | 49| 4 +Brand#42 |LARGE BRUSHED BRASS | 9| 4 +Brand#42 |LARGE BRUSHED BRASS | 36| 4 +Brand#42 |LARGE BRUSHED COPPER | 14| 4 +Brand#42 |LARGE BRUSHED COPPER | 23| 4 +Brand#42 |LARGE BRUSHED COPPER | 36| 4 +Brand#42 |LARGE BRUSHED COPPER | 45| 4 +Brand#42 |LARGE BRUSHED NICKEL | 3| 4 +Brand#42 |LARGE BRUSHED NICKEL | 9| 4 +Brand#42 |LARGE BRUSHED NICKEL | 14| 4 +Brand#42 |LARGE BRUSHED NICKEL | 45| 4 +Brand#42 |LARGE BRUSHED STEEL | 3| 4 +Brand#42 |LARGE BRUSHED STEEL | 36| 4 +Brand#42 |LARGE BRUSHED STEEL | 49| 4 +Brand#42 |LARGE BRUSHED TIN | 9| 4 +Brand#42 |LARGE BRUSHED TIN | 14| 4 +Brand#42 |LARGE BRUSHED TIN | 36| 4 +Brand#42 |LARGE BURNISHED BRASS | 19| 4 +Brand#42 |LARGE BURNISHED BRASS | 23| 4 +Brand#42 |LARGE BURNISHED BRASS | 36| 4 +Brand#42 |LARGE BURNISHED BRASS | 45| 4 +Brand#42 |LARGE BURNISHED COPPER | 3| 4 +Brand#42 |LARGE BURNISHED COPPER | 23| 4 +Brand#42 |LARGE BURNISHED COPPER | 45| 4 +Brand#42 |LARGE BURNISHED COPPER | 49| 4 +Brand#42 |LARGE BURNISHED NICKEL | 36| 4 +Brand#42 |LARGE BURNISHED NICKEL | 45| 4 +Brand#42 |LARGE BURNISHED STEEL | 14| 4 +Brand#42 |LARGE BURNISHED STEEL | 19| 4 +Brand#42 |LARGE BURNISHED STEEL | 45| 4 +Brand#42 |LARGE BURNISHED TIN | 3| 4 +Brand#42 |LARGE BURNISHED TIN | 14| 4 +Brand#42 |LARGE BURNISHED TIN | 36| 4 +Brand#42 |LARGE PLATED BRASS | 45| 4 +Brand#42 |LARGE PLATED BRASS | 49| 4 +Brand#42 |LARGE PLATED COPPER | 3| 4 +Brand#42 |LARGE PLATED COPPER | 23| 4 +Brand#42 |LARGE PLATED NICKEL | 14| 4 +Brand#42 |LARGE PLATED NICKEL | 19| 4 +Brand#42 |LARGE PLATED NICKEL | 36| 4 +Brand#42 |LARGE PLATED NICKEL | 49| 4 +Brand#42 |LARGE PLATED STEEL | 3| 4 +Brand#42 |LARGE PLATED STEEL | 14| 4 +Brand#42 |LARGE PLATED STEEL | 19| 4 +Brand#42 |LARGE PLATED STEEL | 23| 4 +Brand#42 |LARGE PLATED STEEL | 36| 4 +Brand#42 |LARGE PLATED STEEL | 49| 4 +Brand#42 |LARGE PLATED TIN | 23| 4 +Brand#42 |LARGE PLATED TIN | 36| 4 +Brand#42 |LARGE POLISHED BRASS | 3| 4 +Brand#42 |LARGE POLISHED BRASS | 9| 4 +Brand#42 |LARGE POLISHED BRASS | 23| 4 +Brand#42 |LARGE POLISHED BRASS | 45| 4 +Brand#42 |LARGE POLISHED BRASS | 49| 4 +Brand#42 |LARGE POLISHED COPPER | 9| 4 +Brand#42 |LARGE POLISHED COPPER | 19| 4 +Brand#42 |LARGE POLISHED COPPER | 45| 4 +Brand#42 |LARGE POLISHED NICKEL | 3| 4 +Brand#42 |LARGE POLISHED NICKEL | 9| 4 +Brand#42 |LARGE POLISHED NICKEL | 14| 4 +Brand#42 |LARGE POLISHED NICKEL | 19| 4 +Brand#42 |LARGE POLISHED NICKEL | 45| 4 +Brand#42 |LARGE POLISHED STEEL | 19| 4 +Brand#42 |LARGE POLISHED STEEL | 23| 4 +Brand#42 |LARGE POLISHED STEEL | 49| 4 +Brand#42 |LARGE POLISHED TIN | 36| 4 +Brand#42 |MEDIUM ANODIZED BRASS | 14| 4 +Brand#42 |MEDIUM ANODIZED BRASS | 23| 4 +Brand#42 |MEDIUM ANODIZED BRASS | 36| 4 +Brand#42 |MEDIUM ANODIZED BRASS | 45| 4 +Brand#42 |MEDIUM ANODIZED COPPER | 9| 4 +Brand#42 |MEDIUM ANODIZED COPPER | 14| 4 +Brand#42 |MEDIUM ANODIZED NICKEL | 3| 4 +Brand#42 |MEDIUM ANODIZED NICKEL | 9| 4 +Brand#42 |MEDIUM ANODIZED NICKEL | 14| 4 +Brand#42 |MEDIUM ANODIZED NICKEL | 23| 4 +Brand#42 |MEDIUM ANODIZED NICKEL | 45| 4 +Brand#42 |MEDIUM ANODIZED STEEL | 9| 4 +Brand#42 |MEDIUM ANODIZED TIN | 3| 4 +Brand#42 |MEDIUM ANODIZED TIN | 9| 4 +Brand#42 |MEDIUM ANODIZED TIN | 23| 4 +Brand#42 |MEDIUM BRUSHED BRASS | 14| 4 +Brand#42 |MEDIUM BRUSHED BRASS | 23| 4 +Brand#42 |MEDIUM BRUSHED BRASS | 36| 4 +Brand#42 |MEDIUM BRUSHED BRASS | 45| 4 +Brand#42 |MEDIUM BRUSHED COPPER | 23| 4 +Brand#42 |MEDIUM BRUSHED COPPER | 36| 4 +Brand#42 |MEDIUM BRUSHED COPPER | 45| 4 +Brand#42 |MEDIUM BRUSHED NICKEL | 23| 4 +Brand#42 |MEDIUM BRUSHED NICKEL | 45| 4 +Brand#42 |MEDIUM BRUSHED STEEL | 9| 4 +Brand#42 |MEDIUM BRUSHED STEEL | 23| 4 +Brand#42 |MEDIUM BRUSHED TIN | 3| 4 +Brand#42 |MEDIUM BRUSHED TIN | 9| 4 +Brand#42 |MEDIUM BRUSHED TIN | 36| 4 +Brand#42 |MEDIUM BRUSHED TIN | 45| 4 +Brand#42 |MEDIUM BURNISHED BRASS | 3| 4 +Brand#42 |MEDIUM BURNISHED BRASS | 9| 4 +Brand#42 |MEDIUM BURNISHED BRASS | 19| 4 +Brand#42 |MEDIUM BURNISHED BRASS | 23| 4 +Brand#42 |MEDIUM BURNISHED BRASS | 49| 4 +Brand#42 |MEDIUM BURNISHED COPPER | 19| 4 +Brand#42 |MEDIUM BURNISHED COPPER | 36| 4 +Brand#42 |MEDIUM BURNISHED NICKEL | 45| 4 +Brand#42 |MEDIUM BURNISHED NICKEL | 49| 4 +Brand#42 |MEDIUM BURNISHED STEEL | 45| 4 +Brand#42 |MEDIUM BURNISHED TIN | 9| 4 +Brand#42 |MEDIUM BURNISHED TIN | 23| 4 +Brand#42 |MEDIUM BURNISHED TIN | 45| 4 +Brand#42 |MEDIUM PLATED BRASS | 3| 4 +Brand#42 |MEDIUM PLATED BRASS | 14| 4 +Brand#42 |MEDIUM PLATED BRASS | 23| 4 +Brand#42 |MEDIUM PLATED COPPER | 9| 4 +Brand#42 |MEDIUM PLATED COPPER | 14| 4 +Brand#42 |MEDIUM PLATED COPPER | 19| 4 +Brand#42 |MEDIUM PLATED NICKEL | 3| 4 +Brand#42 |MEDIUM PLATED NICKEL | 45| 4 +Brand#42 |MEDIUM PLATED NICKEL | 49| 4 +Brand#42 |MEDIUM PLATED STEEL | 23| 4 +Brand#42 |MEDIUM PLATED STEEL | 49| 4 +Brand#42 |MEDIUM PLATED TIN | 3| 4 +Brand#42 |MEDIUM PLATED TIN | 19| 4 +Brand#42 |MEDIUM PLATED TIN | 23| 4 +Brand#42 |PROMO ANODIZED BRASS | 3| 4 +Brand#42 |PROMO ANODIZED BRASS | 23| 4 +Brand#42 |PROMO ANODIZED BRASS | 49| 4 +Brand#42 |PROMO ANODIZED COPPER | 19| 4 +Brand#42 |PROMO ANODIZED COPPER | 36| 4 +Brand#42 |PROMO ANODIZED COPPER | 49| 4 +Brand#42 |PROMO ANODIZED NICKEL | 3| 4 +Brand#42 |PROMO ANODIZED NICKEL | 19| 4 +Brand#42 |PROMO ANODIZED NICKEL | 36| 4 +Brand#42 |PROMO ANODIZED STEEL | 9| 4 +Brand#42 |PROMO ANODIZED STEEL | 14| 4 +Brand#42 |PROMO ANODIZED STEEL | 45| 4 +Brand#42 |PROMO ANODIZED TIN | 9| 4 +Brand#42 |PROMO ANODIZED TIN | 19| 4 +Brand#42 |PROMO ANODIZED TIN | 45| 4 +Brand#42 |PROMO BRUSHED BRASS | 3| 4 +Brand#42 |PROMO BRUSHED BRASS | 14| 4 +Brand#42 |PROMO BRUSHED BRASS | 23| 4 +Brand#42 |PROMO BRUSHED COPPER | 3| 4 +Brand#42 |PROMO BRUSHED COPPER | 19| 4 +Brand#42 |PROMO BRUSHED COPPER | 23| 4 +Brand#42 |PROMO BRUSHED COPPER | 36| 4 +Brand#42 |PROMO BRUSHED COPPER | 45| 4 +Brand#42 |PROMO BRUSHED COPPER | 49| 4 +Brand#42 |PROMO BRUSHED NICKEL | 9| 4 +Brand#42 |PROMO BRUSHED NICKEL | 14| 4 +Brand#42 |PROMO BRUSHED STEEL | 3| 4 +Brand#42 |PROMO BRUSHED STEEL | 14| 4 +Brand#42 |PROMO BRUSHED STEEL | 49| 4 +Brand#42 |PROMO BRUSHED TIN | 9| 4 +Brand#42 |PROMO BRUSHED TIN | 23| 4 +Brand#42 |PROMO BRUSHED TIN | 49| 4 +Brand#42 |PROMO BURNISHED BRASS | 9| 4 +Brand#42 |PROMO BURNISHED BRASS | 36| 4 +Brand#42 |PROMO BURNISHED COPPER | 3| 4 +Brand#42 |PROMO BURNISHED COPPER | 14| 4 +Brand#42 |PROMO BURNISHED COPPER | 19| 4 +Brand#42 |PROMO BURNISHED NICKEL | 9| 4 +Brand#42 |PROMO BURNISHED NICKEL | 19| 4 +Brand#42 |PROMO BURNISHED NICKEL | 49| 4 +Brand#42 |PROMO BURNISHED STEEL | 3| 4 +Brand#42 |PROMO BURNISHED STEEL | 9| 4 +Brand#42 |PROMO BURNISHED STEEL | 14| 4 +Brand#42 |PROMO BURNISHED STEEL | 36| 4 +Brand#42 |PROMO BURNISHED STEEL | 45| 4 +Brand#42 |PROMO BURNISHED TIN | 3| 4 +Brand#42 |PROMO BURNISHED TIN | 19| 4 +Brand#42 |PROMO BURNISHED TIN | 36| 4 +Brand#42 |PROMO PLATED BRASS | 45| 4 +Brand#42 |PROMO PLATED BRASS | 49| 4 +Brand#42 |PROMO PLATED COPPER | 3| 4 +Brand#42 |PROMO PLATED COPPER | 14| 4 +Brand#42 |PROMO PLATED COPPER | 23| 4 +Brand#42 |PROMO PLATED COPPER | 49| 4 +Brand#42 |PROMO PLATED NICKEL | 3| 4 +Brand#42 |PROMO PLATED NICKEL | 9| 4 +Brand#42 |PROMO PLATED NICKEL | 14| 4 +Brand#42 |PROMO PLATED NICKEL | 19| 4 +Brand#42 |PROMO PLATED NICKEL | 49| 4 +Brand#42 |PROMO PLATED STEEL | 3| 4 +Brand#42 |PROMO PLATED STEEL | 9| 4 +Brand#42 |PROMO PLATED STEEL | 36| 4 +Brand#42 |PROMO PLATED TIN | 3| 4 +Brand#42 |PROMO POLISHED BRASS | 3| 4 +Brand#42 |PROMO POLISHED COPPER | 9| 4 +Brand#42 |PROMO POLISHED COPPER | 23| 4 +Brand#42 |PROMO POLISHED COPPER | 45| 4 +Brand#42 |PROMO POLISHED NICKEL | 14| 4 +Brand#42 |PROMO POLISHED NICKEL | 23| 4 +Brand#42 |PROMO POLISHED NICKEL | 36| 4 +Brand#42 |PROMO POLISHED NICKEL | 45| 4 +Brand#42 |PROMO POLISHED NICKEL | 49| 4 +Brand#42 |PROMO POLISHED TIN | 14| 4 +Brand#42 |PROMO POLISHED TIN | 19| 4 +Brand#42 |PROMO POLISHED TIN | 23| 4 +Brand#42 |PROMO POLISHED TIN | 36| 4 +Brand#42 |PROMO POLISHED TIN | 45| 4 +Brand#42 |SMALL ANODIZED BRASS | 9| 4 +Brand#42 |SMALL ANODIZED BRASS | 14| 4 +Brand#42 |SMALL ANODIZED BRASS | 49| 4 +Brand#42 |SMALL ANODIZED COPPER | 3| 4 +Brand#42 |SMALL ANODIZED COPPER | 9| 4 +Brand#42 |SMALL ANODIZED COPPER | 45| 4 +Brand#42 |SMALL ANODIZED NICKEL | 3| 4 +Brand#42 |SMALL ANODIZED STEEL | 14| 4 +Brand#42 |SMALL ANODIZED STEEL | 45| 4 +Brand#42 |SMALL ANODIZED TIN | 9| 4 +Brand#42 |SMALL ANODIZED TIN | 14| 4 +Brand#42 |SMALL BRUSHED BRASS | 3| 4 +Brand#42 |SMALL BRUSHED BRASS | 9| 4 +Brand#42 |SMALL BRUSHED BRASS | 19| 4 +Brand#42 |SMALL BRUSHED BRASS | 23| 4 +Brand#42 |SMALL BRUSHED BRASS | 49| 4 +Brand#42 |SMALL BRUSHED COPPER | 23| 4 +Brand#42 |SMALL BRUSHED COPPER | 45| 4 +Brand#42 |SMALL BRUSHED NICKEL | 19| 4 +Brand#42 |SMALL BRUSHED NICKEL | 36| 4 +Brand#42 |SMALL BRUSHED NICKEL | 45| 4 +Brand#42 |SMALL BRUSHED TIN | 3| 4 +Brand#42 |SMALL BRUSHED TIN | 19| 4 +Brand#42 |SMALL BRUSHED TIN | 36| 4 +Brand#42 |SMALL BURNISHED BRASS | 14| 4 +Brand#42 |SMALL BURNISHED BRASS | 19| 4 +Brand#42 |SMALL BURNISHED BRASS | 45| 4 +Brand#42 |SMALL BURNISHED COPPER | 9| 4 +Brand#42 |SMALL BURNISHED COPPER | 14| 4 +Brand#42 |SMALL BURNISHED COPPER | 19| 4 +Brand#42 |SMALL BURNISHED COPPER | 23| 4 +Brand#42 |SMALL BURNISHED COPPER | 49| 4 +Brand#42 |SMALL BURNISHED NICKEL | 9| 4 +Brand#42 |SMALL BURNISHED NICKEL | 14| 4 +Brand#42 |SMALL BURNISHED STEEL | 9| 4 +Brand#42 |SMALL BURNISHED STEEL | 36| 4 +Brand#42 |SMALL BURNISHED STEEL | 45| 4 +Brand#42 |SMALL BURNISHED STEEL | 49| 4 +Brand#42 |SMALL BURNISHED TIN | 3| 4 +Brand#42 |SMALL BURNISHED TIN | 45| 4 +Brand#42 |SMALL PLATED BRASS | 3| 4 +Brand#42 |SMALL PLATED BRASS | 9| 4 +Brand#42 |SMALL PLATED BRASS | 23| 4 +Brand#42 |SMALL PLATED BRASS | 45| 4 +Brand#42 |SMALL PLATED BRASS | 49| 4 +Brand#42 |SMALL PLATED COPPER | 3| 4 +Brand#42 |SMALL PLATED COPPER | 45| 4 +Brand#42 |SMALL PLATED NICKEL | 9| 4 +Brand#42 |SMALL PLATED NICKEL | 14| 4 +Brand#42 |SMALL PLATED NICKEL | 36| 4 +Brand#42 |SMALL PLATED NICKEL | 45| 4 +Brand#42 |SMALL PLATED STEEL | 9| 4 +Brand#42 |SMALL PLATED STEEL | 14| 4 +Brand#42 |SMALL PLATED STEEL | 45| 4 +Brand#42 |SMALL PLATED TIN | 49| 4 +Brand#42 |SMALL POLISHED BRASS | 14| 4 +Brand#42 |SMALL POLISHED BRASS | 19| 4 +Brand#42 |SMALL POLISHED BRASS | 49| 4 +Brand#42 |SMALL POLISHED COPPER | 9| 4 +Brand#42 |SMALL POLISHED COPPER | 19| 4 +Brand#42 |SMALL POLISHED COPPER | 49| 4 +Brand#42 |SMALL POLISHED NICKEL | 3| 4 +Brand#42 |SMALL POLISHED NICKEL | 36| 4 +Brand#42 |SMALL POLISHED NICKEL | 49| 4 +Brand#42 |SMALL POLISHED STEEL | 3| 4 +Brand#42 |SMALL POLISHED STEEL | 19| 4 +Brand#42 |SMALL POLISHED TIN | 3| 4 +Brand#42 |SMALL POLISHED TIN | 19| 4 +Brand#42 |STANDARD ANODIZED BRASS | 3| 4 +Brand#42 |STANDARD ANODIZED BRASS | 14| 4 +Brand#42 |STANDARD ANODIZED BRASS | 19| 4 +Brand#42 |STANDARD ANODIZED BRASS | 49| 4 +Brand#42 |STANDARD ANODIZED COPPER | 3| 4 +Brand#42 |STANDARD ANODIZED COPPER | 9| 4 +Brand#42 |STANDARD ANODIZED COPPER | 23| 4 +Brand#42 |STANDARD ANODIZED COPPER | 49| 4 +Brand#42 |STANDARD ANODIZED NICKEL | 3| 4 +Brand#42 |STANDARD ANODIZED NICKEL | 23| 4 +Brand#42 |STANDARD ANODIZED NICKEL | 36| 4 +Brand#42 |STANDARD ANODIZED NICKEL | 45| 4 +Brand#42 |STANDARD ANODIZED NICKEL | 49| 4 +Brand#42 |STANDARD ANODIZED TIN | 14| 4 +Brand#42 |STANDARD ANODIZED TIN | 19| 4 +Brand#42 |STANDARD ANODIZED TIN | 49| 4 +Brand#42 |STANDARD BRUSHED BRASS | 14| 4 +Brand#42 |STANDARD BRUSHED BRASS | 45| 4 +Brand#42 |STANDARD BRUSHED COPPER | 9| 4 +Brand#42 |STANDARD BRUSHED COPPER | 14| 4 +Brand#42 |STANDARD BRUSHED COPPER | 19| 4 +Brand#42 |STANDARD BRUSHED COPPER | 45| 4 +Brand#42 |STANDARD BRUSHED NICKEL | 19| 4 +Brand#42 |STANDARD BRUSHED STEEL | 3| 4 +Brand#42 |STANDARD BRUSHED STEEL | 36| 4 +Brand#42 |STANDARD BRUSHED STEEL | 45| 4 +Brand#42 |STANDARD BRUSHED TIN | 14| 4 +Brand#42 |STANDARD BRUSHED TIN | 19| 4 +Brand#42 |STANDARD BURNISHED BRASS | 19| 4 +Brand#42 |STANDARD BURNISHED BRASS | 23| 4 +Brand#42 |STANDARD BURNISHED COPPER| 3| 4 +Brand#42 |STANDARD BURNISHED COPPER| 9| 4 +Brand#42 |STANDARD BURNISHED COPPER| 14| 4 +Brand#42 |STANDARD BURNISHED COPPER| 19| 4 +Brand#42 |STANDARD BURNISHED COPPER| 23| 4 +Brand#42 |STANDARD BURNISHED NICKEL| 9| 4 +Brand#42 |STANDARD BURNISHED NICKEL| 19| 4 +Brand#42 |STANDARD BURNISHED NICKEL| 36| 4 +Brand#42 |STANDARD BURNISHED NICKEL| 45| 4 +Brand#42 |STANDARD BURNISHED STEEL | 3| 4 +Brand#42 |STANDARD BURNISHED STEEL | 14| 4 +Brand#42 |STANDARD BURNISHED STEEL | 23| 4 +Brand#42 |STANDARD BURNISHED STEEL | 45| 4 +Brand#42 |STANDARD BURNISHED TIN | 3| 4 +Brand#42 |STANDARD BURNISHED TIN | 14| 4 +Brand#42 |STANDARD BURNISHED TIN | 19| 4 +Brand#42 |STANDARD BURNISHED TIN | 36| 4 +Brand#42 |STANDARD PLATED BRASS | 3| 4 +Brand#42 |STANDARD PLATED BRASS | 9| 4 +Brand#42 |STANDARD PLATED BRASS | 19| 4 +Brand#42 |STANDARD PLATED BRASS | 23| 4 +Brand#42 |STANDARD PLATED BRASS | 36| 4 +Brand#42 |STANDARD PLATED BRASS | 49| 4 +Brand#42 |STANDARD PLATED COPPER | 36| 4 +Brand#42 |STANDARD PLATED NICKEL | 9| 4 +Brand#42 |STANDARD PLATED NICKEL | 36| 4 +Brand#42 |STANDARD PLATED NICKEL | 49| 4 +Brand#42 |STANDARD PLATED STEEL | 3| 4 +Brand#42 |STANDARD PLATED STEEL | 9| 4 +Brand#42 |STANDARD PLATED STEEL | 23| 4 +Brand#42 |STANDARD PLATED TIN | 19| 4 +Brand#42 |STANDARD POLISHED BRASS | 3| 4 +Brand#42 |STANDARD POLISHED BRASS | 14| 4 +Brand#42 |STANDARD POLISHED BRASS | 45| 4 +Brand#42 |STANDARD POLISHED BRASS | 49| 4 +Brand#42 |STANDARD POLISHED COPPER | 3| 4 +Brand#42 |STANDARD POLISHED COPPER | 9| 4 +Brand#42 |STANDARD POLISHED COPPER | 36| 4 +Brand#42 |STANDARD POLISHED COPPER | 45| 4 +Brand#42 |STANDARD POLISHED NICKEL | 36| 4 +Brand#42 |STANDARD POLISHED NICKEL | 45| 4 +Brand#42 |STANDARD POLISHED NICKEL | 49| 4 +Brand#42 |STANDARD POLISHED STEEL | 45| 4 +Brand#42 |STANDARD POLISHED TIN | 3| 4 +Brand#42 |STANDARD POLISHED TIN | 9| 4 +Brand#42 |STANDARD POLISHED TIN | 19| 4 +Brand#43 |ECONOMY ANODIZED BRASS | 19| 4 +Brand#43 |ECONOMY ANODIZED COPPER | 23| 4 +Brand#43 |ECONOMY ANODIZED COPPER | 36| 4 +Brand#43 |ECONOMY ANODIZED COPPER | 49| 4 +Brand#43 |ECONOMY ANODIZED NICKEL | 9| 4 +Brand#43 |ECONOMY ANODIZED NICKEL | 14| 4 +Brand#43 |ECONOMY ANODIZED NICKEL | 19| 4 +Brand#43 |ECONOMY ANODIZED NICKEL | 23| 4 +Brand#43 |ECONOMY ANODIZED NICKEL | 45| 4 +Brand#43 |ECONOMY ANODIZED STEEL | 3| 4 +Brand#43 |ECONOMY ANODIZED STEEL | 9| 4 +Brand#43 |ECONOMY ANODIZED STEEL | 14| 4 +Brand#43 |ECONOMY ANODIZED STEEL | 19| 4 +Brand#43 |ECONOMY ANODIZED TIN | 19| 4 +Brand#43 |ECONOMY ANODIZED TIN | 23| 4 +Brand#43 |ECONOMY ANODIZED TIN | 36| 4 +Brand#43 |ECONOMY BRUSHED BRASS | 3| 4 +Brand#43 |ECONOMY BRUSHED BRASS | 23| 4 +Brand#43 |ECONOMY BRUSHED BRASS | 36| 4 +Brand#43 |ECONOMY BRUSHED BRASS | 49| 4 +Brand#43 |ECONOMY BRUSHED COPPER | 14| 4 +Brand#43 |ECONOMY BRUSHED COPPER | 19| 4 +Brand#43 |ECONOMY BRUSHED COPPER | 36| 4 +Brand#43 |ECONOMY BRUSHED NICKEL | 23| 4 +Brand#43 |ECONOMY BRUSHED NICKEL | 36| 4 +Brand#43 |ECONOMY BRUSHED NICKEL | 45| 4 +Brand#43 |ECONOMY BRUSHED STEEL | 19| 4 +Brand#43 |ECONOMY BRUSHED TIN | 3| 4 +Brand#43 |ECONOMY BRUSHED TIN | 14| 4 +Brand#43 |ECONOMY BRUSHED TIN | 19| 4 +Brand#43 |ECONOMY BRUSHED TIN | 23| 4 +Brand#43 |ECONOMY BURNISHED BRASS | 9| 4 +Brand#43 |ECONOMY BURNISHED BRASS | 14| 4 +Brand#43 |ECONOMY BURNISHED BRASS | 23| 4 +Brand#43 |ECONOMY BURNISHED BRASS | 36| 4 +Brand#43 |ECONOMY BURNISHED BRASS | 45| 4 +Brand#43 |ECONOMY BURNISHED COPPER | 3| 4 +Brand#43 |ECONOMY BURNISHED COPPER | 19| 4 +Brand#43 |ECONOMY BURNISHED COPPER | 23| 4 +Brand#43 |ECONOMY BURNISHED COPPER | 45| 4 +Brand#43 |ECONOMY BURNISHED NICKEL | 49| 4 +Brand#43 |ECONOMY BURNISHED STEEL | 14| 4 +Brand#43 |ECONOMY BURNISHED STEEL | 45| 4 +Brand#43 |ECONOMY BURNISHED TIN | 14| 4 +Brand#43 |ECONOMY BURNISHED TIN | 36| 4 +Brand#43 |ECONOMY PLATED BRASS | 3| 4 +Brand#43 |ECONOMY PLATED BRASS | 14| 4 +Brand#43 |ECONOMY PLATED BRASS | 19| 4 +Brand#43 |ECONOMY PLATED BRASS | 23| 4 +Brand#43 |ECONOMY PLATED BRASS | 36| 4 +Brand#43 |ECONOMY PLATED BRASS | 49| 4 +Brand#43 |ECONOMY PLATED COPPER | 14| 4 +Brand#43 |ECONOMY PLATED COPPER | 36| 4 +Brand#43 |ECONOMY PLATED NICKEL | 36| 4 +Brand#43 |ECONOMY PLATED NICKEL | 45| 4 +Brand#43 |ECONOMY PLATED STEEL | 9| 4 +Brand#43 |ECONOMY PLATED STEEL | 45| 4 +Brand#43 |ECONOMY PLATED STEEL | 49| 4 +Brand#43 |ECONOMY PLATED TIN | 3| 4 +Brand#43 |ECONOMY PLATED TIN | 14| 4 +Brand#43 |ECONOMY PLATED TIN | 36| 4 +Brand#43 |ECONOMY PLATED TIN | 45| 4 +Brand#43 |ECONOMY POLISHED BRASS | 3| 4 +Brand#43 |ECONOMY POLISHED BRASS | 9| 4 +Brand#43 |ECONOMY POLISHED BRASS | 14| 4 +Brand#43 |ECONOMY POLISHED BRASS | 36| 4 +Brand#43 |ECONOMY POLISHED BRASS | 49| 4 +Brand#43 |ECONOMY POLISHED COPPER | 3| 4 +Brand#43 |ECONOMY POLISHED COPPER | 14| 4 +Brand#43 |ECONOMY POLISHED COPPER | 23| 4 +Brand#43 |ECONOMY POLISHED COPPER | 45| 4 +Brand#43 |ECONOMY POLISHED NICKEL | 3| 4 +Brand#43 |ECONOMY POLISHED NICKEL | 9| 4 +Brand#43 |ECONOMY POLISHED NICKEL | 14| 4 +Brand#43 |ECONOMY POLISHED NICKEL | 23| 4 +Brand#43 |ECONOMY POLISHED NICKEL | 49| 4 +Brand#43 |ECONOMY POLISHED STEEL | 19| 4 +Brand#43 |ECONOMY POLISHED STEEL | 45| 4 +Brand#43 |LARGE ANODIZED BRASS | 19| 4 +Brand#43 |LARGE ANODIZED BRASS | 23| 4 +Brand#43 |LARGE ANODIZED COPPER | 3| 4 +Brand#43 |LARGE ANODIZED COPPER | 36| 4 +Brand#43 |LARGE ANODIZED COPPER | 45| 4 +Brand#43 |LARGE ANODIZED NICKEL | 14| 4 +Brand#43 |LARGE ANODIZED STEEL | 3| 4 +Brand#43 |LARGE ANODIZED STEEL | 9| 4 +Brand#43 |LARGE ANODIZED STEEL | 14| 4 +Brand#43 |LARGE ANODIZED TIN | 3| 4 +Brand#43 |LARGE ANODIZED TIN | 49| 4 +Brand#43 |LARGE BRUSHED BRASS | 14| 4 +Brand#43 |LARGE BRUSHED BRASS | 19| 4 +Brand#43 |LARGE BRUSHED BRASS | 36| 4 +Brand#43 |LARGE BRUSHED COPPER | 3| 4 +Brand#43 |LARGE BRUSHED COPPER | 23| 4 +Brand#43 |LARGE BRUSHED COPPER | 45| 4 +Brand#43 |LARGE BRUSHED NICKEL | 3| 4 +Brand#43 |LARGE BRUSHED NICKEL | 45| 4 +Brand#43 |LARGE BRUSHED STEEL | 19| 4 +Brand#43 |LARGE BRUSHED STEEL | 49| 4 +Brand#43 |LARGE BRUSHED TIN | 3| 4 +Brand#43 |LARGE BRUSHED TIN | 14| 4 +Brand#43 |LARGE BRUSHED TIN | 45| 4 +Brand#43 |LARGE BRUSHED TIN | 49| 4 +Brand#43 |LARGE BURNISHED BRASS | 3| 4 +Brand#43 |LARGE BURNISHED BRASS | 19| 4 +Brand#43 |LARGE BURNISHED COPPER | 9| 4 +Brand#43 |LARGE BURNISHED COPPER | 19| 4 +Brand#43 |LARGE BURNISHED COPPER | 23| 4 +Brand#43 |LARGE BURNISHED COPPER | 49| 4 +Brand#43 |LARGE BURNISHED NICKEL | 9| 4 +Brand#43 |LARGE BURNISHED NICKEL | 19| 4 +Brand#43 |LARGE BURNISHED NICKEL | 45| 4 +Brand#43 |LARGE BURNISHED STEEL | 19| 4 +Brand#43 |LARGE BURNISHED STEEL | 23| 4 +Brand#43 |LARGE BURNISHED STEEL | 45| 4 +Brand#43 |LARGE BURNISHED STEEL | 49| 4 +Brand#43 |LARGE BURNISHED TIN | 9| 4 +Brand#43 |LARGE BURNISHED TIN | 49| 4 +Brand#43 |LARGE PLATED BRASS | 3| 4 +Brand#43 |LARGE PLATED BRASS | 36| 4 +Brand#43 |LARGE PLATED COPPER | 3| 4 +Brand#43 |LARGE PLATED COPPER | 14| 4 +Brand#43 |LARGE PLATED COPPER | 19| 4 +Brand#43 |LARGE PLATED COPPER | 23| 4 +Brand#43 |LARGE PLATED COPPER | 49| 4 +Brand#43 |LARGE PLATED NICKEL | 19| 4 +Brand#43 |LARGE PLATED NICKEL | 23| 4 +Brand#43 |LARGE PLATED NICKEL | 36| 4 +Brand#43 |LARGE PLATED STEEL | 9| 4 +Brand#43 |LARGE PLATED STEEL | 19| 4 +Brand#43 |LARGE PLATED STEEL | 45| 4 +Brand#43 |LARGE PLATED TIN | 3| 4 +Brand#43 |LARGE PLATED TIN | 49| 4 +Brand#43 |LARGE POLISHED BRASS | 19| 4 +Brand#43 |LARGE POLISHED BRASS | 23| 4 +Brand#43 |LARGE POLISHED BRASS | 45| 4 +Brand#43 |LARGE POLISHED BRASS | 49| 4 +Brand#43 |LARGE POLISHED COPPER | 9| 4 +Brand#43 |LARGE POLISHED COPPER | 45| 4 +Brand#43 |LARGE POLISHED NICKEL | 14| 4 +Brand#43 |LARGE POLISHED NICKEL | 19| 4 +Brand#43 |LARGE POLISHED NICKEL | 36| 4 +Brand#43 |LARGE POLISHED NICKEL | 45| 4 +Brand#43 |LARGE POLISHED NICKEL | 49| 4 +Brand#43 |LARGE POLISHED STEEL | 3| 4 +Brand#43 |LARGE POLISHED STEEL | 23| 4 +Brand#43 |LARGE POLISHED STEEL | 45| 4 +Brand#43 |LARGE POLISHED STEEL | 49| 4 +Brand#43 |LARGE POLISHED TIN | 3| 4 +Brand#43 |LARGE POLISHED TIN | 19| 4 +Brand#43 |LARGE POLISHED TIN | 23| 4 +Brand#43 |LARGE POLISHED TIN | 36| 4 +Brand#43 |MEDIUM ANODIZED BRASS | 9| 4 +Brand#43 |MEDIUM ANODIZED BRASS | 23| 4 +Brand#43 |MEDIUM ANODIZED BRASS | 45| 4 +Brand#43 |MEDIUM ANODIZED COPPER | 36| 4 +Brand#43 |MEDIUM ANODIZED NICKEL | 19| 4 +Brand#43 |MEDIUM ANODIZED NICKEL | 23| 4 +Brand#43 |MEDIUM ANODIZED NICKEL | 45| 4 +Brand#43 |MEDIUM ANODIZED STEEL | 14| 4 +Brand#43 |MEDIUM ANODIZED STEEL | 23| 4 +Brand#43 |MEDIUM ANODIZED STEEL | 49| 4 +Brand#43 |MEDIUM ANODIZED TIN | 3| 4 +Brand#43 |MEDIUM ANODIZED TIN | 9| 4 +Brand#43 |MEDIUM ANODIZED TIN | 14| 4 +Brand#43 |MEDIUM ANODIZED TIN | 19| 4 +Brand#43 |MEDIUM BRUSHED BRASS | 19| 4 +Brand#43 |MEDIUM BRUSHED BRASS | 49| 4 +Brand#43 |MEDIUM BRUSHED COPPER | 3| 4 +Brand#43 |MEDIUM BRUSHED COPPER | 9| 4 +Brand#43 |MEDIUM BRUSHED COPPER | 19| 4 +Brand#43 |MEDIUM BRUSHED COPPER | 36| 4 +Brand#43 |MEDIUM BRUSHED COPPER | 49| 4 +Brand#43 |MEDIUM BRUSHED NICKEL | 9| 4 +Brand#43 |MEDIUM BRUSHED NICKEL | 14| 4 +Brand#43 |MEDIUM BRUSHED NICKEL | 19| 4 +Brand#43 |MEDIUM BRUSHED NICKEL | 36| 4 +Brand#43 |MEDIUM BRUSHED NICKEL | 45| 4 +Brand#43 |MEDIUM BRUSHED NICKEL | 49| 4 +Brand#43 |MEDIUM BRUSHED STEEL | 3| 4 +Brand#43 |MEDIUM BRUSHED STEEL | 9| 4 +Brand#43 |MEDIUM BRUSHED STEEL | 23| 4 +Brand#43 |MEDIUM BRUSHED STEEL | 45| 4 +Brand#43 |MEDIUM BRUSHED TIN | 9| 4 +Brand#43 |MEDIUM BRUSHED TIN | 14| 4 +Brand#43 |MEDIUM BRUSHED TIN | 36| 4 +Brand#43 |MEDIUM BRUSHED TIN | 45| 4 +Brand#43 |MEDIUM BURNISHED BRASS | 9| 4 +Brand#43 |MEDIUM BURNISHED BRASS | 14| 4 +Brand#43 |MEDIUM BURNISHED COPPER | 9| 4 +Brand#43 |MEDIUM BURNISHED COPPER | 36| 4 +Brand#43 |MEDIUM BURNISHED COPPER | 45| 4 +Brand#43 |MEDIUM BURNISHED NICKEL | 3| 4 +Brand#43 |MEDIUM BURNISHED STEEL | 9| 4 +Brand#43 |MEDIUM BURNISHED STEEL | 36| 4 +Brand#43 |MEDIUM BURNISHED TIN | 23| 4 +Brand#43 |MEDIUM PLATED BRASS | 9| 4 +Brand#43 |MEDIUM PLATED BRASS | 14| 4 +Brand#43 |MEDIUM PLATED COPPER | 14| 4 +Brand#43 |MEDIUM PLATED COPPER | 45| 4 +Brand#43 |MEDIUM PLATED NICKEL | 23| 4 +Brand#43 |MEDIUM PLATED NICKEL | 49| 4 +Brand#43 |MEDIUM PLATED STEEL | 9| 4 +Brand#43 |MEDIUM PLATED STEEL | 14| 4 +Brand#43 |MEDIUM PLATED STEEL | 19| 4 +Brand#43 |MEDIUM PLATED STEEL | 23| 4 +Brand#43 |MEDIUM PLATED TIN | 9| 4 +Brand#43 |MEDIUM PLATED TIN | 14| 4 +Brand#43 |MEDIUM PLATED TIN | 49| 4 +Brand#43 |PROMO ANODIZED BRASS | 19| 4 +Brand#43 |PROMO ANODIZED BRASS | 23| 4 +Brand#43 |PROMO ANODIZED BRASS | 49| 4 +Brand#43 |PROMO ANODIZED COPPER | 3| 4 +Brand#43 |PROMO ANODIZED COPPER | 9| 4 +Brand#43 |PROMO ANODIZED COPPER | 14| 4 +Brand#43 |PROMO ANODIZED COPPER | 19| 4 +Brand#43 |PROMO ANODIZED COPPER | 49| 4 +Brand#43 |PROMO ANODIZED NICKEL | 9| 4 +Brand#43 |PROMO ANODIZED NICKEL | 36| 4 +Brand#43 |PROMO ANODIZED STEEL | 3| 4 +Brand#43 |PROMO ANODIZED STEEL | 19| 4 +Brand#43 |PROMO ANODIZED STEEL | 36| 4 +Brand#43 |PROMO ANODIZED STEEL | 45| 4 +Brand#43 |PROMO ANODIZED TIN | 36| 4 +Brand#43 |PROMO ANODIZED TIN | 45| 4 +Brand#43 |PROMO BRUSHED BRASS | 9| 4 +Brand#43 |PROMO BRUSHED BRASS | 23| 4 +Brand#43 |PROMO BRUSHED BRASS | 49| 4 +Brand#43 |PROMO BRUSHED COPPER | 14| 4 +Brand#43 |PROMO BRUSHED COPPER | 45| 4 +Brand#43 |PROMO BRUSHED COPPER | 49| 4 +Brand#43 |PROMO BRUSHED NICKEL | 3| 4 +Brand#43 |PROMO BRUSHED STEEL | 3| 4 +Brand#43 |PROMO BRUSHED STEEL | 23| 4 +Brand#43 |PROMO BRUSHED TIN | 9| 4 +Brand#43 |PROMO BRUSHED TIN | 14| 4 +Brand#43 |PROMO BRUSHED TIN | 19| 4 +Brand#43 |PROMO BRUSHED TIN | 23| 4 +Brand#43 |PROMO BRUSHED TIN | 36| 4 +Brand#43 |PROMO BRUSHED TIN | 45| 4 +Brand#43 |PROMO BURNISHED BRASS | 9| 4 +Brand#43 |PROMO BURNISHED BRASS | 36| 4 +Brand#43 |PROMO BURNISHED BRASS | 45| 4 +Brand#43 |PROMO BURNISHED COPPER | 3| 4 +Brand#43 |PROMO BURNISHED COPPER | 9| 4 +Brand#43 |PROMO BURNISHED COPPER | 19| 4 +Brand#43 |PROMO BURNISHED COPPER | 23| 4 +Brand#43 |PROMO BURNISHED COPPER | 45| 4 +Brand#43 |PROMO BURNISHED NICKEL | 9| 4 +Brand#43 |PROMO BURNISHED NICKEL | 19| 4 +Brand#43 |PROMO BURNISHED NICKEL | 23| 4 +Brand#43 |PROMO BURNISHED NICKEL | 36| 4 +Brand#43 |PROMO BURNISHED NICKEL | 45| 4 +Brand#43 |PROMO BURNISHED STEEL | 3| 4 +Brand#43 |PROMO BURNISHED STEEL | 9| 4 +Brand#43 |PROMO BURNISHED STEEL | 19| 4 +Brand#43 |PROMO BURNISHED STEEL | 23| 4 +Brand#43 |PROMO BURNISHED TIN | 19| 4 +Brand#43 |PROMO BURNISHED TIN | 45| 4 +Brand#43 |PROMO PLATED BRASS | 3| 4 +Brand#43 |PROMO PLATED BRASS | 9| 4 +Brand#43 |PROMO PLATED BRASS | 19| 4 +Brand#43 |PROMO PLATED BRASS | 23| 4 +Brand#43 |PROMO PLATED COPPER | 3| 4 +Brand#43 |PROMO PLATED COPPER | 23| 4 +Brand#43 |PROMO PLATED COPPER | 36| 4 +Brand#43 |PROMO PLATED COPPER | 49| 4 +Brand#43 |PROMO PLATED NICKEL | 3| 4 +Brand#43 |PROMO PLATED NICKEL | 19| 4 +Brand#43 |PROMO PLATED NICKEL | 36| 4 +Brand#43 |PROMO PLATED NICKEL | 49| 4 +Brand#43 |PROMO PLATED STEEL | 3| 4 +Brand#43 |PROMO PLATED STEEL | 19| 4 +Brand#43 |PROMO PLATED STEEL | 23| 4 +Brand#43 |PROMO PLATED STEEL | 36| 4 +Brand#43 |PROMO PLATED STEEL | 49| 4 +Brand#43 |PROMO PLATED TIN | 3| 4 +Brand#43 |PROMO PLATED TIN | 14| 4 +Brand#43 |PROMO PLATED TIN | 49| 4 +Brand#43 |PROMO POLISHED BRASS | 3| 4 +Brand#43 |PROMO POLISHED BRASS | 14| 4 +Brand#43 |PROMO POLISHED BRASS | 19| 4 +Brand#43 |PROMO POLISHED BRASS | 49| 4 +Brand#43 |PROMO POLISHED COPPER | 9| 4 +Brand#43 |PROMO POLISHED COPPER | 45| 4 +Brand#43 |PROMO POLISHED COPPER | 49| 4 +Brand#43 |PROMO POLISHED NICKEL | 9| 4 +Brand#43 |PROMO POLISHED NICKEL | 23| 4 +Brand#43 |PROMO POLISHED NICKEL | 36| 4 +Brand#43 |PROMO POLISHED NICKEL | 45| 4 +Brand#43 |PROMO POLISHED NICKEL | 49| 4 +Brand#43 |PROMO POLISHED STEEL | 19| 4 +Brand#43 |PROMO POLISHED STEEL | 45| 4 +Brand#43 |PROMO POLISHED STEEL | 49| 4 +Brand#43 |PROMO POLISHED TIN | 9| 4 +Brand#43 |PROMO POLISHED TIN | 19| 4 +Brand#43 |PROMO POLISHED TIN | 23| 4 +Brand#43 |PROMO POLISHED TIN | 49| 4 +Brand#43 |SMALL ANODIZED BRASS | 3| 4 +Brand#43 |SMALL ANODIZED BRASS | 9| 4 +Brand#43 |SMALL ANODIZED BRASS | 14| 4 +Brand#43 |SMALL ANODIZED COPPER | 23| 4 +Brand#43 |SMALL ANODIZED COPPER | 45| 4 +Brand#43 |SMALL ANODIZED NICKEL | 9| 4 +Brand#43 |SMALL ANODIZED NICKEL | 49| 4 +Brand#43 |SMALL ANODIZED STEEL | 9| 4 +Brand#43 |SMALL ANODIZED STEEL | 14| 4 +Brand#43 |SMALL ANODIZED STEEL | 19| 4 +Brand#43 |SMALL ANODIZED TIN | 19| 4 +Brand#43 |SMALL ANODIZED TIN | 45| 4 +Brand#43 |SMALL BRUSHED BRASS | 3| 4 +Brand#43 |SMALL BRUSHED BRASS | 14| 4 +Brand#43 |SMALL BRUSHED BRASS | 23| 4 +Brand#43 |SMALL BRUSHED BRASS | 36| 4 +Brand#43 |SMALL BRUSHED COPPER | 14| 4 +Brand#43 |SMALL BRUSHED NICKEL | 3| 4 +Brand#43 |SMALL BRUSHED NICKEL | 14| 4 +Brand#43 |SMALL BRUSHED NICKEL | 19| 4 +Brand#43 |SMALL BRUSHED NICKEL | 45| 4 +Brand#43 |SMALL BRUSHED STEEL | 19| 4 +Brand#43 |SMALL BRUSHED STEEL | 23| 4 +Brand#43 |SMALL BRUSHED STEEL | 49| 4 +Brand#43 |SMALL BRUSHED TIN | 23| 4 +Brand#43 |SMALL BRUSHED TIN | 36| 4 +Brand#43 |SMALL BURNISHED BRASS | 19| 4 +Brand#43 |SMALL BURNISHED BRASS | 23| 4 +Brand#43 |SMALL BURNISHED BRASS | 49| 4 +Brand#43 |SMALL BURNISHED COPPER | 3| 4 +Brand#43 |SMALL BURNISHED COPPER | 9| 4 +Brand#43 |SMALL BURNISHED COPPER | 36| 4 +Brand#43 |SMALL BURNISHED NICKEL | 3| 4 +Brand#43 |SMALL BURNISHED NICKEL | 36| 4 +Brand#43 |SMALL BURNISHED STEEL | 14| 4 +Brand#43 |SMALL BURNISHED STEEL | 19| 4 +Brand#43 |SMALL BURNISHED STEEL | 23| 4 +Brand#43 |SMALL BURNISHED STEEL | 49| 4 +Brand#43 |SMALL BURNISHED TIN | 14| 4 +Brand#43 |SMALL BURNISHED TIN | 19| 4 +Brand#43 |SMALL BURNISHED TIN | 36| 4 +Brand#43 |SMALL PLATED BRASS | 3| 4 +Brand#43 |SMALL PLATED BRASS | 9| 4 +Brand#43 |SMALL PLATED BRASS | 14| 4 +Brand#43 |SMALL PLATED COPPER | 3| 4 +Brand#43 |SMALL PLATED COPPER | 36| 4 +Brand#43 |SMALL PLATED NICKEL | 14| 4 +Brand#43 |SMALL PLATED NICKEL | 36| 4 +Brand#43 |SMALL PLATED STEEL | 9| 4 +Brand#43 |SMALL PLATED STEEL | 23| 4 +Brand#43 |SMALL PLATED STEEL | 45| 4 +Brand#43 |SMALL PLATED STEEL | 49| 4 +Brand#43 |SMALL PLATED TIN | 3| 4 +Brand#43 |SMALL PLATED TIN | 36| 4 +Brand#43 |SMALL PLATED TIN | 49| 4 +Brand#43 |SMALL POLISHED BRASS | 36| 4 +Brand#43 |SMALL POLISHED BRASS | 49| 4 +Brand#43 |SMALL POLISHED COPPER | 23| 4 +Brand#43 |SMALL POLISHED COPPER | 36| 4 +Brand#43 |SMALL POLISHED NICKEL | 9| 4 +Brand#43 |SMALL POLISHED NICKEL | 49| 4 +Brand#43 |SMALL POLISHED STEEL | 3| 4 +Brand#43 |SMALL POLISHED STEEL | 14| 4 +Brand#43 |SMALL POLISHED STEEL | 23| 4 +Brand#43 |SMALL POLISHED STEEL | 36| 4 +Brand#43 |SMALL POLISHED TIN | 3| 4 +Brand#43 |SMALL POLISHED TIN | 9| 4 +Brand#43 |SMALL POLISHED TIN | 23| 4 +Brand#43 |STANDARD ANODIZED BRASS | 3| 4 +Brand#43 |STANDARD ANODIZED BRASS | 9| 4 +Brand#43 |STANDARD ANODIZED BRASS | 14| 4 +Brand#43 |STANDARD ANODIZED BRASS | 19| 4 +Brand#43 |STANDARD ANODIZED BRASS | 45| 4 +Brand#43 |STANDARD ANODIZED COPPER | 19| 4 +Brand#43 |STANDARD ANODIZED COPPER | 23| 4 +Brand#43 |STANDARD ANODIZED COPPER | 45| 4 +Brand#43 |STANDARD ANODIZED NICKEL | 19| 4 +Brand#43 |STANDARD ANODIZED NICKEL | 36| 4 +Brand#43 |STANDARD ANODIZED NICKEL | 45| 4 +Brand#43 |STANDARD ANODIZED STEEL | 19| 4 +Brand#43 |STANDARD ANODIZED TIN | 3| 4 +Brand#43 |STANDARD BRUSHED BRASS | 9| 4 +Brand#43 |STANDARD BRUSHED BRASS | 19| 4 +Brand#43 |STANDARD BRUSHED BRASS | 23| 4 +Brand#43 |STANDARD BRUSHED COPPER | 3| 4 +Brand#43 |STANDARD BRUSHED COPPER | 14| 4 +Brand#43 |STANDARD BRUSHED COPPER | 23| 4 +Brand#43 |STANDARD BRUSHED COPPER | 36| 4 +Brand#43 |STANDARD BRUSHED COPPER | 45| 4 +Brand#43 |STANDARD BRUSHED COPPER | 49| 4 +Brand#43 |STANDARD BRUSHED NICKEL | 14| 4 +Brand#43 |STANDARD BRUSHED STEEL | 3| 4 +Brand#43 |STANDARD BRUSHED STEEL | 9| 4 +Brand#43 |STANDARD BRUSHED STEEL | 23| 4 +Brand#43 |STANDARD BRUSHED STEEL | 45| 4 +Brand#43 |STANDARD BRUSHED STEEL | 49| 4 +Brand#43 |STANDARD BRUSHED TIN | 3| 4 +Brand#43 |STANDARD BRUSHED TIN | 14| 4 +Brand#43 |STANDARD BRUSHED TIN | 23| 4 +Brand#43 |STANDARD BRUSHED TIN | 36| 4 +Brand#43 |STANDARD BURNISHED BRASS | 19| 4 +Brand#43 |STANDARD BURNISHED COPPER| 19| 4 +Brand#43 |STANDARD BURNISHED COPPER| 23| 4 +Brand#43 |STANDARD BURNISHED NICKEL| 3| 4 +Brand#43 |STANDARD BURNISHED NICKEL| 14| 4 +Brand#43 |STANDARD BURNISHED STEEL | 3| 4 +Brand#43 |STANDARD BURNISHED STEEL | 14| 4 +Brand#43 |STANDARD BURNISHED TIN | 9| 4 +Brand#43 |STANDARD BURNISHED TIN | 45| 4 +Brand#43 |STANDARD PLATED BRASS | 9| 4 +Brand#43 |STANDARD PLATED BRASS | 36| 4 +Brand#43 |STANDARD PLATED BRASS | 49| 4 +Brand#43 |STANDARD PLATED COPPER | 9| 4 +Brand#43 |STANDARD PLATED COPPER | 14| 4 +Brand#43 |STANDARD PLATED COPPER | 49| 4 +Brand#43 |STANDARD PLATED NICKEL | 3| 4 +Brand#43 |STANDARD PLATED NICKEL | 9| 4 +Brand#43 |STANDARD PLATED NICKEL | 45| 4 +Brand#43 |STANDARD PLATED STEEL | 9| 4 +Brand#43 |STANDARD PLATED STEEL | 14| 4 +Brand#43 |STANDARD PLATED STEEL | 19| 4 +Brand#43 |STANDARD PLATED STEEL | 45| 4 +Brand#43 |STANDARD PLATED STEEL | 49| 4 +Brand#43 |STANDARD PLATED TIN | 36| 4 +Brand#43 |STANDARD POLISHED BRASS | 23| 4 +Brand#43 |STANDARD POLISHED BRASS | 45| 4 +Brand#43 |STANDARD POLISHED BRASS | 49| 4 +Brand#43 |STANDARD POLISHED COPPER | 9| 4 +Brand#43 |STANDARD POLISHED COPPER | 14| 4 +Brand#43 |STANDARD POLISHED COPPER | 23| 4 +Brand#43 |STANDARD POLISHED COPPER | 36| 4 +Brand#43 |STANDARD POLISHED NICKEL | 9| 4 +Brand#43 |STANDARD POLISHED NICKEL | 19| 4 +Brand#43 |STANDARD POLISHED NICKEL | 49| 4 +Brand#43 |STANDARD POLISHED STEEL | 19| 4 +Brand#43 |STANDARD POLISHED STEEL | 23| 4 +Brand#43 |STANDARD POLISHED STEEL | 45| 4 +Brand#43 |STANDARD POLISHED TIN | 3| 4 +Brand#43 |STANDARD POLISHED TIN | 19| 4 +Brand#43 |STANDARD POLISHED TIN | 45| 4 +Brand#43 |STANDARD POLISHED TIN | 49| 4 +Brand#44 |ECONOMY ANODIZED BRASS | 23| 4 +Brand#44 |ECONOMY ANODIZED BRASS | 36| 4 +Brand#44 |ECONOMY ANODIZED BRASS | 49| 4 +Brand#44 |ECONOMY ANODIZED COPPER | 14| 4 +Brand#44 |ECONOMY ANODIZED COPPER | 19| 4 +Brand#44 |ECONOMY ANODIZED COPPER | 36| 4 +Brand#44 |ECONOMY ANODIZED NICKEL | 9| 4 +Brand#44 |ECONOMY ANODIZED NICKEL | 14| 4 +Brand#44 |ECONOMY ANODIZED NICKEL | 19| 4 +Brand#44 |ECONOMY ANODIZED NICKEL | 49| 4 +Brand#44 |ECONOMY ANODIZED STEEL | 36| 4 +Brand#44 |ECONOMY ANODIZED STEEL | 49| 4 +Brand#44 |ECONOMY ANODIZED TIN | 23| 4 +Brand#44 |ECONOMY ANODIZED TIN | 45| 4 +Brand#44 |ECONOMY ANODIZED TIN | 49| 4 +Brand#44 |ECONOMY BRUSHED BRASS | 3| 4 +Brand#44 |ECONOMY BRUSHED BRASS | 9| 4 +Brand#44 |ECONOMY BRUSHED BRASS | 19| 4 +Brand#44 |ECONOMY BRUSHED BRASS | 45| 4 +Brand#44 |ECONOMY BRUSHED BRASS | 49| 4 +Brand#44 |ECONOMY BRUSHED COPPER | 3| 4 +Brand#44 |ECONOMY BRUSHED COPPER | 9| 4 +Brand#44 |ECONOMY BRUSHED COPPER | 14| 4 +Brand#44 |ECONOMY BRUSHED COPPER | 36| 4 +Brand#44 |ECONOMY BRUSHED COPPER | 45| 4 +Brand#44 |ECONOMY BRUSHED NICKEL | 3| 4 +Brand#44 |ECONOMY BRUSHED NICKEL | 14| 4 +Brand#44 |ECONOMY BRUSHED STEEL | 3| 4 +Brand#44 |ECONOMY BRUSHED STEEL | 23| 4 +Brand#44 |ECONOMY BRUSHED STEEL | 45| 4 +Brand#44 |ECONOMY BRUSHED STEEL | 49| 4 +Brand#44 |ECONOMY BRUSHED TIN | 9| 4 +Brand#44 |ECONOMY BRUSHED TIN | 23| 4 +Brand#44 |ECONOMY BRUSHED TIN | 36| 4 +Brand#44 |ECONOMY BURNISHED BRASS | 3| 4 +Brand#44 |ECONOMY BURNISHED BRASS | 14| 4 +Brand#44 |ECONOMY BURNISHED BRASS | 19| 4 +Brand#44 |ECONOMY BURNISHED BRASS | 49| 4 +Brand#44 |ECONOMY BURNISHED COPPER | 19| 4 +Brand#44 |ECONOMY BURNISHED COPPER | 45| 4 +Brand#44 |ECONOMY BURNISHED NICKEL | 19| 4 +Brand#44 |ECONOMY BURNISHED NICKEL | 36| 4 +Brand#44 |ECONOMY BURNISHED NICKEL | 45| 4 +Brand#44 |ECONOMY BURNISHED STEEL | 3| 4 +Brand#44 |ECONOMY BURNISHED STEEL | 23| 4 +Brand#44 |ECONOMY BURNISHED TIN | 23| 4 +Brand#44 |ECONOMY BURNISHED TIN | 36| 4 +Brand#44 |ECONOMY PLATED BRASS | 14| 4 +Brand#44 |ECONOMY PLATED BRASS | 19| 4 +Brand#44 |ECONOMY PLATED BRASS | 36| 4 +Brand#44 |ECONOMY PLATED BRASS | 45| 4 +Brand#44 |ECONOMY PLATED COPPER | 19| 4 +Brand#44 |ECONOMY PLATED COPPER | 49| 4 +Brand#44 |ECONOMY PLATED NICKEL | 3| 4 +Brand#44 |ECONOMY PLATED NICKEL | 9| 4 +Brand#44 |ECONOMY PLATED NICKEL | 19| 4 +Brand#44 |ECONOMY PLATED NICKEL | 45| 4 +Brand#44 |ECONOMY PLATED NICKEL | 49| 4 +Brand#44 |ECONOMY PLATED STEEL | 9| 4 +Brand#44 |ECONOMY PLATED STEEL | 19| 4 +Brand#44 |ECONOMY PLATED STEEL | 49| 4 +Brand#44 |ECONOMY PLATED TIN | 3| 4 +Brand#44 |ECONOMY PLATED TIN | 9| 4 +Brand#44 |ECONOMY PLATED TIN | 14| 4 +Brand#44 |ECONOMY PLATED TIN | 23| 4 +Brand#44 |ECONOMY PLATED TIN | 36| 4 +Brand#44 |ECONOMY PLATED TIN | 49| 4 +Brand#44 |ECONOMY POLISHED BRASS | 9| 4 +Brand#44 |ECONOMY POLISHED BRASS | 14| 4 +Brand#44 |ECONOMY POLISHED COPPER | 3| 4 +Brand#44 |ECONOMY POLISHED COPPER | 45| 4 +Brand#44 |ECONOMY POLISHED COPPER | 49| 4 +Brand#44 |ECONOMY POLISHED NICKEL | 3| 4 +Brand#44 |ECONOMY POLISHED NICKEL | 14| 4 +Brand#44 |ECONOMY POLISHED NICKEL | 19| 4 +Brand#44 |ECONOMY POLISHED STEEL | 3| 4 +Brand#44 |ECONOMY POLISHED STEEL | 14| 4 +Brand#44 |ECONOMY POLISHED STEEL | 19| 4 +Brand#44 |ECONOMY POLISHED STEEL | 36| 4 +Brand#44 |ECONOMY POLISHED STEEL | 45| 4 +Brand#44 |ECONOMY POLISHED TIN | 9| 4 +Brand#44 |ECONOMY POLISHED TIN | 14| 4 +Brand#44 |ECONOMY POLISHED TIN | 23| 4 +Brand#44 |LARGE ANODIZED BRASS | 14| 4 +Brand#44 |LARGE ANODIZED BRASS | 19| 4 +Brand#44 |LARGE ANODIZED BRASS | 36| 4 +Brand#44 |LARGE ANODIZED COPPER | 23| 4 +Brand#44 |LARGE ANODIZED COPPER | 49| 4 +Brand#44 |LARGE ANODIZED NICKEL | 9| 4 +Brand#44 |LARGE ANODIZED NICKEL | 45| 4 +Brand#44 |LARGE ANODIZED STEEL | 3| 4 +Brand#44 |LARGE ANODIZED STEEL | 9| 4 +Brand#44 |LARGE ANODIZED STEEL | 14| 4 +Brand#44 |LARGE ANODIZED STEEL | 36| 4 +Brand#44 |LARGE ANODIZED STEEL | 45| 4 +Brand#44 |LARGE ANODIZED STEEL | 49| 4 +Brand#44 |LARGE ANODIZED TIN | 9| 4 +Brand#44 |LARGE ANODIZED TIN | 19| 4 +Brand#44 |LARGE ANODIZED TIN | 36| 4 +Brand#44 |LARGE ANODIZED TIN | 45| 4 +Brand#44 |LARGE ANODIZED TIN | 49| 4 +Brand#44 |LARGE BRUSHED BRASS | 3| 4 +Brand#44 |LARGE BRUSHED BRASS | 23| 4 +Brand#44 |LARGE BRUSHED BRASS | 36| 4 +Brand#44 |LARGE BRUSHED BRASS | 45| 4 +Brand#44 |LARGE BRUSHED BRASS | 49| 4 +Brand#44 |LARGE BRUSHED COPPER | 3| 4 +Brand#44 |LARGE BRUSHED COPPER | 19| 4 +Brand#44 |LARGE BRUSHED COPPER | 45| 4 +Brand#44 |LARGE BRUSHED COPPER | 49| 4 +Brand#44 |LARGE BRUSHED NICKEL | 36| 4 +Brand#44 |LARGE BRUSHED NICKEL | 49| 4 +Brand#44 |LARGE BRUSHED STEEL | 19| 4 +Brand#44 |LARGE BRUSHED STEEL | 45| 4 +Brand#44 |LARGE BRUSHED TIN | 36| 4 +Brand#44 |LARGE BRUSHED TIN | 45| 4 +Brand#44 |LARGE BRUSHED TIN | 49| 4 +Brand#44 |LARGE BURNISHED BRASS | 9| 4 +Brand#44 |LARGE BURNISHED BRASS | 23| 4 +Brand#44 |LARGE BURNISHED BRASS | 45| 4 +Brand#44 |LARGE BURNISHED COPPER | 3| 4 +Brand#44 |LARGE BURNISHED COPPER | 36| 4 +Brand#44 |LARGE BURNISHED COPPER | 45| 4 +Brand#44 |LARGE BURNISHED COPPER | 49| 4 +Brand#44 |LARGE BURNISHED NICKEL | 19| 4 +Brand#44 |LARGE BURNISHED NICKEL | 45| 4 +Brand#44 |LARGE BURNISHED STEEL | 9| 4 +Brand#44 |LARGE BURNISHED TIN | 9| 4 +Brand#44 |LARGE BURNISHED TIN | 45| 4 +Brand#44 |LARGE BURNISHED TIN | 49| 4 +Brand#44 |LARGE PLATED BRASS | 36| 4 +Brand#44 |LARGE PLATED COPPER | 3| 4 +Brand#44 |LARGE PLATED NICKEL | 19| 4 +Brand#44 |LARGE PLATED NICKEL | 45| 4 +Brand#44 |LARGE PLATED NICKEL | 49| 4 +Brand#44 |LARGE PLATED STEEL | 19| 4 +Brand#44 |LARGE PLATED STEEL | 49| 4 +Brand#44 |LARGE PLATED TIN | 23| 4 +Brand#44 |LARGE PLATED TIN | 45| 4 +Brand#44 |LARGE POLISHED BRASS | 3| 4 +Brand#44 |LARGE POLISHED COPPER | 3| 4 +Brand#44 |LARGE POLISHED COPPER | 14| 4 +Brand#44 |LARGE POLISHED COPPER | 19| 4 +Brand#44 |LARGE POLISHED NICKEL | 14| 4 +Brand#44 |LARGE POLISHED NICKEL | 45| 4 +Brand#44 |LARGE POLISHED STEEL | 3| 4 +Brand#44 |LARGE POLISHED STEEL | 14| 4 +Brand#44 |LARGE POLISHED STEEL | 23| 4 +Brand#44 |LARGE POLISHED STEEL | 49| 4 +Brand#44 |LARGE POLISHED TIN | 23| 4 +Brand#44 |LARGE POLISHED TIN | 45| 4 +Brand#44 |MEDIUM ANODIZED BRASS | 14| 4 +Brand#44 |MEDIUM ANODIZED BRASS | 19| 4 +Brand#44 |MEDIUM ANODIZED COPPER | 3| 4 +Brand#44 |MEDIUM ANODIZED COPPER | 19| 4 +Brand#44 |MEDIUM ANODIZED COPPER | 23| 4 +Brand#44 |MEDIUM ANODIZED NICKEL | 3| 4 +Brand#44 |MEDIUM ANODIZED STEEL | 19| 4 +Brand#44 |MEDIUM ANODIZED TIN | 3| 4 +Brand#44 |MEDIUM ANODIZED TIN | 14| 4 +Brand#44 |MEDIUM ANODIZED TIN | 19| 4 +Brand#44 |MEDIUM ANODIZED TIN | 23| 4 +Brand#44 |MEDIUM ANODIZED TIN | 36| 4 +Brand#44 |MEDIUM BRUSHED BRASS | 14| 4 +Brand#44 |MEDIUM BRUSHED BRASS | 19| 4 +Brand#44 |MEDIUM BRUSHED BRASS | 23| 4 +Brand#44 |MEDIUM BRUSHED COPPER | 45| 4 +Brand#44 |MEDIUM BRUSHED NICKEL | 9| 4 +Brand#44 |MEDIUM BRUSHED NICKEL | 19| 4 +Brand#44 |MEDIUM BRUSHED STEEL | 9| 4 +Brand#44 |MEDIUM BRUSHED STEEL | 23| 4 +Brand#44 |MEDIUM BRUSHED STEEL | 49| 4 +Brand#44 |MEDIUM BRUSHED TIN | 3| 4 +Brand#44 |MEDIUM BRUSHED TIN | 23| 4 +Brand#44 |MEDIUM BRUSHED TIN | 49| 4 +Brand#44 |MEDIUM BURNISHED BRASS | 14| 4 +Brand#44 |MEDIUM BURNISHED BRASS | 19| 4 +Brand#44 |MEDIUM BURNISHED BRASS | 23| 4 +Brand#44 |MEDIUM BURNISHED BRASS | 49| 4 +Brand#44 |MEDIUM BURNISHED COPPER | 14| 4 +Brand#44 |MEDIUM BURNISHED COPPER | 23| 4 +Brand#44 |MEDIUM BURNISHED NICKEL | 9| 4 +Brand#44 |MEDIUM BURNISHED NICKEL | 19| 4 +Brand#44 |MEDIUM BURNISHED STEEL | 9| 4 +Brand#44 |MEDIUM BURNISHED STEEL | 36| 4 +Brand#44 |MEDIUM BURNISHED STEEL | 45| 4 +Brand#44 |MEDIUM BURNISHED TIN | 3| 4 +Brand#44 |MEDIUM BURNISHED TIN | 9| 4 +Brand#44 |MEDIUM BURNISHED TIN | 14| 4 +Brand#44 |MEDIUM BURNISHED TIN | 36| 4 +Brand#44 |MEDIUM PLATED COPPER | 14| 4 +Brand#44 |MEDIUM PLATED COPPER | 23| 4 +Brand#44 |MEDIUM PLATED COPPER | 36| 4 +Brand#44 |MEDIUM PLATED NICKEL | 9| 4 +Brand#44 |MEDIUM PLATED NICKEL | 14| 4 +Brand#44 |MEDIUM PLATED NICKEL | 19| 4 +Brand#44 |MEDIUM PLATED NICKEL | 36| 4 +Brand#44 |MEDIUM PLATED STEEL | 3| 4 +Brand#44 |MEDIUM PLATED STEEL | 36| 4 +Brand#44 |MEDIUM PLATED TIN | 19| 4 +Brand#44 |MEDIUM PLATED TIN | 45| 4 +Brand#44 |PROMO ANODIZED BRASS | 23| 4 +Brand#44 |PROMO ANODIZED BRASS | 45| 4 +Brand#44 |PROMO ANODIZED COPPER | 3| 4 +Brand#44 |PROMO ANODIZED COPPER | 9| 4 +Brand#44 |PROMO ANODIZED COPPER | 14| 4 +Brand#44 |PROMO ANODIZED COPPER | 49| 4 +Brand#44 |PROMO ANODIZED NICKEL | 3| 4 +Brand#44 |PROMO ANODIZED NICKEL | 49| 4 +Brand#44 |PROMO ANODIZED STEEL | 14| 4 +Brand#44 |PROMO ANODIZED STEEL | 19| 4 +Brand#44 |PROMO ANODIZED STEEL | 45| 4 +Brand#44 |PROMO ANODIZED TIN | 9| 4 +Brand#44 |PROMO ANODIZED TIN | 14| 4 +Brand#44 |PROMO ANODIZED TIN | 36| 4 +Brand#44 |PROMO ANODIZED TIN | 49| 4 +Brand#44 |PROMO BRUSHED BRASS | 14| 4 +Brand#44 |PROMO BRUSHED BRASS | 23| 4 +Brand#44 |PROMO BRUSHED BRASS | 36| 4 +Brand#44 |PROMO BRUSHED BRASS | 45| 4 +Brand#44 |PROMO BRUSHED BRASS | 49| 4 +Brand#44 |PROMO BRUSHED COPPER | 14| 4 +Brand#44 |PROMO BRUSHED COPPER | 19| 4 +Brand#44 |PROMO BRUSHED COPPER | 45| 4 +Brand#44 |PROMO BRUSHED COPPER | 49| 4 +Brand#44 |PROMO BRUSHED NICKEL | 9| 4 +Brand#44 |PROMO BRUSHED NICKEL | 36| 4 +Brand#44 |PROMO BRUSHED NICKEL | 49| 4 +Brand#44 |PROMO BRUSHED TIN | 14| 4 +Brand#44 |PROMO BRUSHED TIN | 23| 4 +Brand#44 |PROMO BRUSHED TIN | 36| 4 +Brand#44 |PROMO BURNISHED BRASS | 9| 4 +Brand#44 |PROMO BURNISHED BRASS | 14| 4 +Brand#44 |PROMO BURNISHED BRASS | 23| 4 +Brand#44 |PROMO BURNISHED BRASS | 45| 4 +Brand#44 |PROMO BURNISHED COPPER | 14| 4 +Brand#44 |PROMO BURNISHED COPPER | 19| 4 +Brand#44 |PROMO BURNISHED COPPER | 36| 4 +Brand#44 |PROMO BURNISHED NICKEL | 9| 4 +Brand#44 |PROMO BURNISHED NICKEL | 19| 4 +Brand#44 |PROMO BURNISHED NICKEL | 23| 4 +Brand#44 |PROMO BURNISHED STEEL | 3| 4 +Brand#44 |PROMO BURNISHED STEEL | 36| 4 +Brand#44 |PROMO BURNISHED TIN | 9| 4 +Brand#44 |PROMO BURNISHED TIN | 23| 4 +Brand#44 |PROMO BURNISHED TIN | 36| 4 +Brand#44 |PROMO BURNISHED TIN | 49| 4 +Brand#44 |PROMO PLATED BRASS | 3| 4 +Brand#44 |PROMO PLATED BRASS | 49| 4 +Brand#44 |PROMO PLATED COPPER | 3| 4 +Brand#44 |PROMO PLATED COPPER | 9| 4 +Brand#44 |PROMO PLATED COPPER | 14| 4 +Brand#44 |PROMO PLATED COPPER | 36| 4 +Brand#44 |PROMO PLATED COPPER | 49| 4 +Brand#44 |PROMO PLATED NICKEL | 14| 4 +Brand#44 |PROMO PLATED NICKEL | 49| 4 +Brand#44 |PROMO PLATED STEEL | 3| 4 +Brand#44 |PROMO PLATED STEEL | 9| 4 +Brand#44 |PROMO PLATED STEEL | 19| 4 +Brand#44 |PROMO PLATED STEEL | 45| 4 +Brand#44 |PROMO PLATED TIN | 23| 4 +Brand#44 |PROMO POLISHED BRASS | 3| 4 +Brand#44 |PROMO POLISHED BRASS | 14| 4 +Brand#44 |PROMO POLISHED BRASS | 19| 4 +Brand#44 |PROMO POLISHED BRASS | 49| 4 +Brand#44 |PROMO POLISHED COPPER | 19| 4 +Brand#44 |PROMO POLISHED COPPER | 49| 4 +Brand#44 |PROMO POLISHED NICKEL | 3| 4 +Brand#44 |PROMO POLISHED NICKEL | 23| 4 +Brand#44 |PROMO POLISHED NICKEL | 36| 4 +Brand#44 |PROMO POLISHED NICKEL | 49| 4 +Brand#44 |PROMO POLISHED STEEL | 14| 4 +Brand#44 |PROMO POLISHED STEEL | 23| 4 +Brand#44 |PROMO POLISHED TIN | 9| 4 +Brand#44 |SMALL ANODIZED BRASS | 14| 4 +Brand#44 |SMALL ANODIZED BRASS | 23| 4 +Brand#44 |SMALL ANODIZED COPPER | 36| 4 +Brand#44 |SMALL ANODIZED COPPER | 45| 4 +Brand#44 |SMALL ANODIZED NICKEL | 3| 4 +Brand#44 |SMALL ANODIZED NICKEL | 9| 4 +Brand#44 |SMALL ANODIZED NICKEL | 14| 4 +Brand#44 |SMALL ANODIZED NICKEL | 19| 4 +Brand#44 |SMALL ANODIZED NICKEL | 36| 4 +Brand#44 |SMALL ANODIZED NICKEL | 45| 4 +Brand#44 |SMALL ANODIZED NICKEL | 49| 4 +Brand#44 |SMALL ANODIZED STEEL | 3| 4 +Brand#44 |SMALL ANODIZED STEEL | 23| 4 +Brand#44 |SMALL ANODIZED STEEL | 49| 4 +Brand#44 |SMALL ANODIZED TIN | 3| 4 +Brand#44 |SMALL ANODIZED TIN | 9| 4 +Brand#44 |SMALL ANODIZED TIN | 36| 4 +Brand#44 |SMALL ANODIZED TIN | 49| 4 +Brand#44 |SMALL BRUSHED BRASS | 3| 4 +Brand#44 |SMALL BRUSHED BRASS | 9| 4 +Brand#44 |SMALL BRUSHED BRASS | 36| 4 +Brand#44 |SMALL BRUSHED COPPER | 9| 4 +Brand#44 |SMALL BRUSHED COPPER | 14| 4 +Brand#44 |SMALL BRUSHED NICKEL | 14| 4 +Brand#44 |SMALL BRUSHED NICKEL | 36| 4 +Brand#44 |SMALL BRUSHED NICKEL | 49| 4 +Brand#44 |SMALL BRUSHED STEEL | 3| 4 +Brand#44 |SMALL BRUSHED STEEL | 9| 4 +Brand#44 |SMALL BRUSHED STEEL | 45| 4 +Brand#44 |SMALL BRUSHED STEEL | 49| 4 +Brand#44 |SMALL BRUSHED TIN | 9| 4 +Brand#44 |SMALL BRUSHED TIN | 23| 4 +Brand#44 |SMALL BURNISHED BRASS | 9| 4 +Brand#44 |SMALL BURNISHED BRASS | 14| 4 +Brand#44 |SMALL BURNISHED BRASS | 19| 4 +Brand#44 |SMALL BURNISHED BRASS | 23| 4 +Brand#44 |SMALL BURNISHED BRASS | 45| 4 +Brand#44 |SMALL BURNISHED COPPER | 9| 4 +Brand#44 |SMALL BURNISHED COPPER | 19| 4 +Brand#44 |SMALL BURNISHED COPPER | 36| 4 +Brand#44 |SMALL BURNISHED COPPER | 45| 4 +Brand#44 |SMALL BURNISHED COPPER | 49| 4 +Brand#44 |SMALL BURNISHED NICKEL | 9| 4 +Brand#44 |SMALL BURNISHED NICKEL | 19| 4 +Brand#44 |SMALL BURNISHED NICKEL | 23| 4 +Brand#44 |SMALL BURNISHED NICKEL | 36| 4 +Brand#44 |SMALL BURNISHED STEEL | 45| 4 +Brand#44 |SMALL BURNISHED STEEL | 49| 4 +Brand#44 |SMALL BURNISHED TIN | 19| 4 +Brand#44 |SMALL PLATED BRASS | 9| 4 +Brand#44 |SMALL PLATED BRASS | 14| 4 +Brand#44 |SMALL PLATED BRASS | 45| 4 +Brand#44 |SMALL PLATED COPPER | 9| 4 +Brand#44 |SMALL PLATED COPPER | 19| 4 +Brand#44 |SMALL PLATED COPPER | 23| 4 +Brand#44 |SMALL PLATED COPPER | 36| 4 +Brand#44 |SMALL PLATED COPPER | 49| 4 +Brand#44 |SMALL PLATED NICKEL | 3| 4 +Brand#44 |SMALL PLATED NICKEL | 19| 4 +Brand#44 |SMALL PLATED NICKEL | 23| 4 +Brand#44 |SMALL PLATED STEEL | 23| 4 +Brand#44 |SMALL PLATED STEEL | 36| 4 +Brand#44 |SMALL PLATED TIN | 3| 4 +Brand#44 |SMALL PLATED TIN | 23| 4 +Brand#44 |SMALL PLATED TIN | 45| 4 +Brand#44 |SMALL PLATED TIN | 49| 4 +Brand#44 |SMALL POLISHED BRASS | 14| 4 +Brand#44 |SMALL POLISHED BRASS | 19| 4 +Brand#44 |SMALL POLISHED BRASS | 23| 4 +Brand#44 |SMALL POLISHED BRASS | 36| 4 +Brand#44 |SMALL POLISHED COPPER | 3| 4 +Brand#44 |SMALL POLISHED COPPER | 19| 4 +Brand#44 |SMALL POLISHED COPPER | 45| 4 +Brand#44 |SMALL POLISHED NICKEL | 36| 4 +Brand#44 |SMALL POLISHED STEEL | 23| 4 +Brand#44 |SMALL POLISHED STEEL | 36| 4 +Brand#44 |SMALL POLISHED STEEL | 45| 4 +Brand#44 |SMALL POLISHED STEEL | 49| 4 +Brand#44 |STANDARD ANODIZED BRASS | 23| 4 +Brand#44 |STANDARD ANODIZED BRASS | 36| 4 +Brand#44 |STANDARD ANODIZED COPPER | 14| 4 +Brand#44 |STANDARD ANODIZED COPPER | 23| 4 +Brand#44 |STANDARD ANODIZED COPPER | 36| 4 +Brand#44 |STANDARD ANODIZED NICKEL | 3| 4 +Brand#44 |STANDARD ANODIZED NICKEL | 14| 4 +Brand#44 |STANDARD ANODIZED NICKEL | 23| 4 +Brand#44 |STANDARD ANODIZED NICKEL | 49| 4 +Brand#44 |STANDARD ANODIZED STEEL | 49| 4 +Brand#44 |STANDARD ANODIZED TIN | 3| 4 +Brand#44 |STANDARD ANODIZED TIN | 14| 4 +Brand#44 |STANDARD ANODIZED TIN | 19| 4 +Brand#44 |STANDARD BRUSHED BRASS | 19| 4 +Brand#44 |STANDARD BRUSHED BRASS | 49| 4 +Brand#44 |STANDARD BRUSHED COPPER | 3| 4 +Brand#44 |STANDARD BRUSHED COPPER | 45| 4 +Brand#44 |STANDARD BRUSHED NICKEL | 3| 4 +Brand#44 |STANDARD BRUSHED NICKEL | 19| 4 +Brand#44 |STANDARD BRUSHED NICKEL | 36| 4 +Brand#44 |STANDARD BRUSHED NICKEL | 45| 4 +Brand#44 |STANDARD BRUSHED NICKEL | 49| 4 +Brand#44 |STANDARD BRUSHED STEEL | 9| 4 +Brand#44 |STANDARD BRUSHED STEEL | 14| 4 +Brand#44 |STANDARD BRUSHED STEEL | 36| 4 +Brand#44 |STANDARD BRUSHED TIN | 14| 4 +Brand#44 |STANDARD BRUSHED TIN | 36| 4 +Brand#44 |STANDARD BURNISHED BRASS | 3| 4 +Brand#44 |STANDARD BURNISHED BRASS | 14| 4 +Brand#44 |STANDARD BURNISHED BRASS | 23| 4 +Brand#44 |STANDARD BURNISHED BRASS | 36| 4 +Brand#44 |STANDARD BURNISHED BRASS | 45| 4 +Brand#44 |STANDARD BURNISHED COPPER| 9| 4 +Brand#44 |STANDARD BURNISHED COPPER| 14| 4 +Brand#44 |STANDARD BURNISHED COPPER| 23| 4 +Brand#44 |STANDARD BURNISHED NICKEL| 3| 4 +Brand#44 |STANDARD BURNISHED NICKEL| 36| 4 +Brand#44 |STANDARD BURNISHED NICKEL| 49| 4 +Brand#44 |STANDARD BURNISHED STEEL | 9| 4 +Brand#44 |STANDARD BURNISHED TIN | 3| 4 +Brand#44 |STANDARD BURNISHED TIN | 23| 4 +Brand#44 |STANDARD BURNISHED TIN | 45| 4 +Brand#44 |STANDARD BURNISHED TIN | 49| 4 +Brand#44 |STANDARD PLATED BRASS | 14| 4 +Brand#44 |STANDARD PLATED BRASS | 19| 4 +Brand#44 |STANDARD PLATED BRASS | 23| 4 +Brand#44 |STANDARD PLATED COPPER | 3| 4 +Brand#44 |STANDARD PLATED COPPER | 36| 4 +Brand#44 |STANDARD PLATED NICKEL | 3| 4 +Brand#44 |STANDARD PLATED NICKEL | 9| 4 +Brand#44 |STANDARD PLATED NICKEL | 23| 4 +Brand#44 |STANDARD PLATED NICKEL | 36| 4 +Brand#44 |STANDARD PLATED NICKEL | 49| 4 +Brand#44 |STANDARD PLATED STEEL | 3| 4 +Brand#44 |STANDARD PLATED STEEL | 9| 4 +Brand#44 |STANDARD PLATED STEEL | 14| 4 +Brand#44 |STANDARD PLATED STEEL | 23| 4 +Brand#44 |STANDARD PLATED STEEL | 49| 4 +Brand#44 |STANDARD PLATED TIN | 14| 4 +Brand#44 |STANDARD PLATED TIN | 36| 4 +Brand#44 |STANDARD PLATED TIN | 45| 4 +Brand#44 |STANDARD POLISHED BRASS | 3| 4 +Brand#44 |STANDARD POLISHED BRASS | 9| 4 +Brand#44 |STANDARD POLISHED BRASS | 19| 4 +Brand#44 |STANDARD POLISHED COPPER | 9| 4 +Brand#44 |STANDARD POLISHED NICKEL | 9| 4 +Brand#44 |STANDARD POLISHED NICKEL | 14| 4 +Brand#44 |STANDARD POLISHED NICKEL | 23| 4 +Brand#44 |STANDARD POLISHED NICKEL | 49| 4 +Brand#44 |STANDARD POLISHED STEEL | 3| 4 +Brand#44 |STANDARD POLISHED STEEL | 36| 4 +Brand#44 |STANDARD POLISHED STEEL | 45| 4 +Brand#44 |STANDARD POLISHED TIN | 3| 4 +Brand#44 |STANDARD POLISHED TIN | 49| 4 +Brand#51 |ECONOMY ANODIZED BRASS | 3| 4 +Brand#51 |ECONOMY ANODIZED BRASS | 14| 4 +Brand#51 |ECONOMY ANODIZED BRASS | 23| 4 +Brand#51 |ECONOMY ANODIZED COPPER | 9| 4 +Brand#51 |ECONOMY ANODIZED COPPER | 14| 4 +Brand#51 |ECONOMY ANODIZED COPPER | 36| 4 +Brand#51 |ECONOMY ANODIZED NICKEL | 9| 4 +Brand#51 |ECONOMY ANODIZED NICKEL | 49| 4 +Brand#51 |ECONOMY ANODIZED STEEL | 19| 4 +Brand#51 |ECONOMY ANODIZED STEEL | 23| 4 +Brand#51 |ECONOMY ANODIZED TIN | 3| 4 +Brand#51 |ECONOMY ANODIZED TIN | 45| 4 +Brand#51 |ECONOMY ANODIZED TIN | 49| 4 +Brand#51 |ECONOMY BRUSHED BRASS | 9| 4 +Brand#51 |ECONOMY BRUSHED BRASS | 14| 4 +Brand#51 |ECONOMY BRUSHED BRASS | 19| 4 +Brand#51 |ECONOMY BRUSHED BRASS | 23| 4 +Brand#51 |ECONOMY BRUSHED BRASS | 36| 4 +Brand#51 |ECONOMY BRUSHED BRASS | 45| 4 +Brand#51 |ECONOMY BRUSHED COPPER | 9| 4 +Brand#51 |ECONOMY BRUSHED COPPER | 19| 4 +Brand#51 |ECONOMY BRUSHED NICKEL | 3| 4 +Brand#51 |ECONOMY BRUSHED NICKEL | 23| 4 +Brand#51 |ECONOMY BRUSHED NICKEL | 45| 4 +Brand#51 |ECONOMY BRUSHED STEEL | 3| 4 +Brand#51 |ECONOMY BRUSHED STEEL | 19| 4 +Brand#51 |ECONOMY BRUSHED TIN | 3| 4 +Brand#51 |ECONOMY BRUSHED TIN | 9| 4 +Brand#51 |ECONOMY BRUSHED TIN | 14| 4 +Brand#51 |ECONOMY BRUSHED TIN | 49| 4 +Brand#51 |ECONOMY BURNISHED BRASS | 9| 4 +Brand#51 |ECONOMY BURNISHED BRASS | 19| 4 +Brand#51 |ECONOMY BURNISHED BRASS | 45| 4 +Brand#51 |ECONOMY BURNISHED BRASS | 49| 4 +Brand#51 |ECONOMY BURNISHED COPPER | 3| 4 +Brand#51 |ECONOMY BURNISHED COPPER | 9| 4 +Brand#51 |ECONOMY BURNISHED COPPER | 14| 4 +Brand#51 |ECONOMY BURNISHED COPPER | 19| 4 +Brand#51 |ECONOMY BURNISHED COPPER | 36| 4 +Brand#51 |ECONOMY BURNISHED COPPER | 49| 4 +Brand#51 |ECONOMY BURNISHED NICKEL | 3| 4 +Brand#51 |ECONOMY BURNISHED NICKEL | 14| 4 +Brand#51 |ECONOMY BURNISHED NICKEL | 23| 4 +Brand#51 |ECONOMY BURNISHED NICKEL | 45| 4 +Brand#51 |ECONOMY BURNISHED STEEL | 3| 4 +Brand#51 |ECONOMY BURNISHED STEEL | 45| 4 +Brand#51 |ECONOMY BURNISHED STEEL | 49| 4 +Brand#51 |ECONOMY BURNISHED TIN | 9| 4 +Brand#51 |ECONOMY BURNISHED TIN | 19| 4 +Brand#51 |ECONOMY BURNISHED TIN | 49| 4 +Brand#51 |ECONOMY PLATED BRASS | 14| 4 +Brand#51 |ECONOMY PLATED BRASS | 19| 4 +Brand#51 |ECONOMY PLATED BRASS | 49| 4 +Brand#51 |ECONOMY PLATED COPPER | 3| 4 +Brand#51 |ECONOMY PLATED COPPER | 9| 4 +Brand#51 |ECONOMY PLATED COPPER | 14| 4 +Brand#51 |ECONOMY PLATED COPPER | 19| 4 +Brand#51 |ECONOMY PLATED STEEL | 3| 4 +Brand#51 |ECONOMY PLATED STEEL | 14| 4 +Brand#51 |ECONOMY PLATED STEEL | 36| 4 +Brand#51 |ECONOMY PLATED STEEL | 45| 4 +Brand#51 |ECONOMY PLATED STEEL | 49| 4 +Brand#51 |ECONOMY POLISHED BRASS | 3| 4 +Brand#51 |ECONOMY POLISHED BRASS | 9| 4 +Brand#51 |ECONOMY POLISHED BRASS | 19| 4 +Brand#51 |ECONOMY POLISHED COPPER | 3| 4 +Brand#51 |ECONOMY POLISHED COPPER | 14| 4 +Brand#51 |ECONOMY POLISHED COPPER | 19| 4 +Brand#51 |ECONOMY POLISHED COPPER | 45| 4 +Brand#51 |ECONOMY POLISHED COPPER | 49| 4 +Brand#51 |ECONOMY POLISHED NICKEL | 45| 4 +Brand#51 |ECONOMY POLISHED NICKEL | 49| 4 +Brand#51 |ECONOMY POLISHED STEEL | 23| 4 +Brand#51 |ECONOMY POLISHED STEEL | 49| 4 +Brand#51 |ECONOMY POLISHED TIN | 3| 4 +Brand#51 |ECONOMY POLISHED TIN | 9| 4 +Brand#51 |ECONOMY POLISHED TIN | 23| 4 +Brand#51 |ECONOMY POLISHED TIN | 45| 4 +Brand#51 |ECONOMY POLISHED TIN | 49| 4 +Brand#51 |LARGE ANODIZED BRASS | 9| 4 +Brand#51 |LARGE ANODIZED BRASS | 14| 4 +Brand#51 |LARGE ANODIZED BRASS | 36| 4 +Brand#51 |LARGE ANODIZED BRASS | 45| 4 +Brand#51 |LARGE ANODIZED BRASS | 49| 4 +Brand#51 |LARGE ANODIZED COPPER | 3| 4 +Brand#51 |LARGE ANODIZED COPPER | 9| 4 +Brand#51 |LARGE ANODIZED NICKEL | 3| 4 +Brand#51 |LARGE ANODIZED NICKEL | 9| 4 +Brand#51 |LARGE ANODIZED NICKEL | 23| 4 +Brand#51 |LARGE ANODIZED STEEL | 14| 4 +Brand#51 |LARGE ANODIZED STEEL | 19| 4 +Brand#51 |LARGE ANODIZED STEEL | 23| 4 +Brand#51 |LARGE ANODIZED STEEL | 36| 4 +Brand#51 |LARGE ANODIZED STEEL | 49| 4 +Brand#51 |LARGE ANODIZED TIN | 36| 4 +Brand#51 |LARGE BRUSHED BRASS | 3| 4 +Brand#51 |LARGE BRUSHED BRASS | 14| 4 +Brand#51 |LARGE BRUSHED BRASS | 19| 4 +Brand#51 |LARGE BRUSHED BRASS | 36| 4 +Brand#51 |LARGE BRUSHED COPPER | 3| 4 +Brand#51 |LARGE BRUSHED COPPER | 45| 4 +Brand#51 |LARGE BRUSHED NICKEL | 3| 4 +Brand#51 |LARGE BRUSHED NICKEL | 23| 4 +Brand#51 |LARGE BRUSHED STEEL | 3| 4 +Brand#51 |LARGE BRUSHED STEEL | 9| 4 +Brand#51 |LARGE BRUSHED STEEL | 14| 4 +Brand#51 |LARGE BRUSHED STEEL | 23| 4 +Brand#51 |LARGE BRUSHED TIN | 3| 4 +Brand#51 |LARGE BRUSHED TIN | 9| 4 +Brand#51 |LARGE BRUSHED TIN | 19| 4 +Brand#51 |LARGE BRUSHED TIN | 23| 4 +Brand#51 |LARGE BRUSHED TIN | 36| 4 +Brand#51 |LARGE BRUSHED TIN | 45| 4 +Brand#51 |LARGE BURNISHED BRASS | 9| 4 +Brand#51 |LARGE BURNISHED BRASS | 19| 4 +Brand#51 |LARGE BURNISHED BRASS | 23| 4 +Brand#51 |LARGE BURNISHED COPPER | 19| 4 +Brand#51 |LARGE BURNISHED COPPER | 45| 4 +Brand#51 |LARGE BURNISHED NICKEL | 9| 4 +Brand#51 |LARGE BURNISHED NICKEL | 14| 4 +Brand#51 |LARGE BURNISHED NICKEL | 19| 4 +Brand#51 |LARGE BURNISHED NICKEL | 36| 4 +Brand#51 |LARGE BURNISHED NICKEL | 45| 4 +Brand#51 |LARGE BURNISHED NICKEL | 49| 4 +Brand#51 |LARGE BURNISHED STEEL | 19| 4 +Brand#51 |LARGE BURNISHED STEEL | 45| 4 +Brand#51 |LARGE BURNISHED TIN | 3| 4 +Brand#51 |LARGE BURNISHED TIN | 9| 4 +Brand#51 |LARGE BURNISHED TIN | 14| 4 +Brand#51 |LARGE BURNISHED TIN | 36| 4 +Brand#51 |LARGE BURNISHED TIN | 49| 4 +Brand#51 |LARGE PLATED BRASS | 9| 4 +Brand#51 |LARGE PLATED BRASS | 14| 4 +Brand#51 |LARGE PLATED BRASS | 19| 4 +Brand#51 |LARGE PLATED COPPER | 3| 4 +Brand#51 |LARGE PLATED COPPER | 14| 4 +Brand#51 |LARGE PLATED COPPER | 19| 4 +Brand#51 |LARGE PLATED NICKEL | 14| 4 +Brand#51 |LARGE PLATED STEEL | 9| 4 +Brand#51 |LARGE PLATED STEEL | 14| 4 +Brand#51 |LARGE PLATED STEEL | 19| 4 +Brand#51 |LARGE PLATED STEEL | 23| 4 +Brand#51 |LARGE PLATED TIN | 45| 4 +Brand#51 |LARGE PLATED TIN | 49| 4 +Brand#51 |LARGE POLISHED BRASS | 14| 4 +Brand#51 |LARGE POLISHED BRASS | 19| 4 +Brand#51 |LARGE POLISHED BRASS | 49| 4 +Brand#51 |LARGE POLISHED COPPER | 9| 4 +Brand#51 |LARGE POLISHED COPPER | 19| 4 +Brand#51 |LARGE POLISHED COPPER | 49| 4 +Brand#51 |LARGE POLISHED NICKEL | 3| 4 +Brand#51 |LARGE POLISHED NICKEL | 23| 4 +Brand#51 |LARGE POLISHED NICKEL | 36| 4 +Brand#51 |LARGE POLISHED NICKEL | 45| 4 +Brand#51 |LARGE POLISHED STEEL | 19| 4 +Brand#51 |LARGE POLISHED STEEL | 23| 4 +Brand#51 |LARGE POLISHED STEEL | 36| 4 +Brand#51 |LARGE POLISHED TIN | 19| 4 +Brand#51 |MEDIUM ANODIZED BRASS | 14| 4 +Brand#51 |MEDIUM ANODIZED BRASS | 19| 4 +Brand#51 |MEDIUM ANODIZED BRASS | 36| 4 +Brand#51 |MEDIUM ANODIZED COPPER | 19| 4 +Brand#51 |MEDIUM ANODIZED COPPER | 36| 4 +Brand#51 |MEDIUM ANODIZED STEEL | 19| 4 +Brand#51 |MEDIUM ANODIZED STEEL | 45| 4 +Brand#51 |MEDIUM ANODIZED TIN | 49| 4 +Brand#51 |MEDIUM BRUSHED BRASS | 3| 4 +Brand#51 |MEDIUM BRUSHED BRASS | 23| 4 +Brand#51 |MEDIUM BRUSHED BRASS | 36| 4 +Brand#51 |MEDIUM BRUSHED BRASS | 45| 4 +Brand#51 |MEDIUM BRUSHED COPPER | 9| 4 +Brand#51 |MEDIUM BRUSHED COPPER | 14| 4 +Brand#51 |MEDIUM BRUSHED COPPER | 23| 4 +Brand#51 |MEDIUM BRUSHED COPPER | 36| 4 +Brand#51 |MEDIUM BRUSHED NICKEL | 3| 4 +Brand#51 |MEDIUM BRUSHED NICKEL | 9| 4 +Brand#51 |MEDIUM BRUSHED NICKEL | 19| 4 +Brand#51 |MEDIUM BRUSHED NICKEL | 23| 4 +Brand#51 |MEDIUM BRUSHED NICKEL | 36| 4 +Brand#51 |MEDIUM BRUSHED STEEL | 3| 4 +Brand#51 |MEDIUM BRUSHED STEEL | 9| 4 +Brand#51 |MEDIUM BRUSHED STEEL | 19| 4 +Brand#51 |MEDIUM BRUSHED STEEL | 45| 4 +Brand#51 |MEDIUM BRUSHED TIN | 3| 4 +Brand#51 |MEDIUM BRUSHED TIN | 19| 4 +Brand#51 |MEDIUM BRUSHED TIN | 36| 4 +Brand#51 |MEDIUM BURNISHED BRASS | 3| 4 +Brand#51 |MEDIUM BURNISHED BRASS | 19| 4 +Brand#51 |MEDIUM BURNISHED BRASS | 23| 4 +Brand#51 |MEDIUM BURNISHED COPPER | 14| 4 +Brand#51 |MEDIUM BURNISHED COPPER | 23| 4 +Brand#51 |MEDIUM BURNISHED COPPER | 36| 4 +Brand#51 |MEDIUM BURNISHED COPPER | 45| 4 +Brand#51 |MEDIUM BURNISHED COPPER | 49| 4 +Brand#51 |MEDIUM BURNISHED NICKEL | 3| 4 +Brand#51 |MEDIUM BURNISHED NICKEL | 19| 4 +Brand#51 |MEDIUM BURNISHED NICKEL | 45| 4 +Brand#51 |MEDIUM BURNISHED STEEL | 19| 4 +Brand#51 |MEDIUM BURNISHED STEEL | 36| 4 +Brand#51 |MEDIUM BURNISHED TIN | 3| 4 +Brand#51 |MEDIUM BURNISHED TIN | 14| 4 +Brand#51 |MEDIUM BURNISHED TIN | 19| 4 +Brand#51 |MEDIUM BURNISHED TIN | 23| 4 +Brand#51 |MEDIUM BURNISHED TIN | 36| 4 +Brand#51 |MEDIUM BURNISHED TIN | 45| 4 +Brand#51 |MEDIUM PLATED BRASS | 3| 4 +Brand#51 |MEDIUM PLATED BRASS | 23| 4 +Brand#51 |MEDIUM PLATED BRASS | 36| 4 +Brand#51 |MEDIUM PLATED COPPER | 3| 4 +Brand#51 |MEDIUM PLATED COPPER | 14| 4 +Brand#51 |MEDIUM PLATED COPPER | 23| 4 +Brand#51 |MEDIUM PLATED COPPER | 36| 4 +Brand#51 |MEDIUM PLATED COPPER | 45| 4 +Brand#51 |MEDIUM PLATED COPPER | 49| 4 +Brand#51 |MEDIUM PLATED NICKEL | 19| 4 +Brand#51 |MEDIUM PLATED STEEL | 14| 4 +Brand#51 |MEDIUM PLATED STEEL | 19| 4 +Brand#51 |MEDIUM PLATED STEEL | 23| 4 +Brand#51 |MEDIUM PLATED STEEL | 36| 4 +Brand#51 |MEDIUM PLATED STEEL | 45| 4 +Brand#51 |MEDIUM PLATED TIN | 3| 4 +Brand#51 |MEDIUM PLATED TIN | 9| 4 +Brand#51 |MEDIUM PLATED TIN | 19| 4 +Brand#51 |MEDIUM PLATED TIN | 49| 4 +Brand#51 |PROMO ANODIZED BRASS | 45| 4 +Brand#51 |PROMO ANODIZED BRASS | 49| 4 +Brand#51 |PROMO ANODIZED COPPER | 3| 4 +Brand#51 |PROMO ANODIZED COPPER | 9| 4 +Brand#51 |PROMO ANODIZED COPPER | 14| 4 +Brand#51 |PROMO ANODIZED NICKEL | 3| 4 +Brand#51 |PROMO ANODIZED NICKEL | 36| 4 +Brand#51 |PROMO ANODIZED STEEL | 3| 4 +Brand#51 |PROMO ANODIZED STEEL | 23| 4 +Brand#51 |PROMO ANODIZED TIN | 3| 4 +Brand#51 |PROMO ANODIZED TIN | 45| 4 +Brand#51 |PROMO BRUSHED BRASS | 3| 4 +Brand#51 |PROMO BRUSHED BRASS | 14| 4 +Brand#51 |PROMO BRUSHED BRASS | 36| 4 +Brand#51 |PROMO BRUSHED BRASS | 49| 4 +Brand#51 |PROMO BRUSHED COPPER | 3| 4 +Brand#51 |PROMO BRUSHED COPPER | 9| 4 +Brand#51 |PROMO BRUSHED COPPER | 14| 4 +Brand#51 |PROMO BRUSHED COPPER | 45| 4 +Brand#51 |PROMO BRUSHED NICKEL | 45| 4 +Brand#51 |PROMO BRUSHED STEEL | 3| 4 +Brand#51 |PROMO BRUSHED STEEL | 14| 4 +Brand#51 |PROMO BRUSHED STEEL | 23| 4 +Brand#51 |PROMO BRUSHED STEEL | 45| 4 +Brand#51 |PROMO BRUSHED TIN | 9| 4 +Brand#51 |PROMO BRUSHED TIN | 19| 4 +Brand#51 |PROMO BRUSHED TIN | 49| 4 +Brand#51 |PROMO BURNISHED BRASS | 36| 4 +Brand#51 |PROMO BURNISHED BRASS | 49| 4 +Brand#51 |PROMO BURNISHED COPPER | 14| 4 +Brand#51 |PROMO BURNISHED COPPER | 36| 4 +Brand#51 |PROMO BURNISHED COPPER | 45| 4 +Brand#51 |PROMO BURNISHED COPPER | 49| 4 +Brand#51 |PROMO BURNISHED NICKEL | 9| 4 +Brand#51 |PROMO BURNISHED NICKEL | 19| 4 +Brand#51 |PROMO BURNISHED NICKEL | 23| 4 +Brand#51 |PROMO BURNISHED NICKEL | 36| 4 +Brand#51 |PROMO BURNISHED NICKEL | 45| 4 +Brand#51 |PROMO BURNISHED NICKEL | 49| 4 +Brand#51 |PROMO BURNISHED STEEL | 3| 4 +Brand#51 |PROMO BURNISHED STEEL | 19| 4 +Brand#51 |PROMO BURNISHED STEEL | 45| 4 +Brand#51 |PROMO BURNISHED TIN | 49| 4 +Brand#51 |PROMO PLATED BRASS | 3| 4 +Brand#51 |PROMO PLATED BRASS | 23| 4 +Brand#51 |PROMO PLATED BRASS | 45| 4 +Brand#51 |PROMO PLATED COPPER | 3| 4 +Brand#51 |PROMO PLATED COPPER | 45| 4 +Brand#51 |PROMO PLATED COPPER | 49| 4 +Brand#51 |PROMO PLATED NICKEL | 3| 4 +Brand#51 |PROMO PLATED STEEL | 19| 4 +Brand#51 |PROMO PLATED TIN | 23| 4 +Brand#51 |PROMO PLATED TIN | 36| 4 +Brand#51 |PROMO PLATED TIN | 45| 4 +Brand#51 |PROMO POLISHED BRASS | 14| 4 +Brand#51 |PROMO POLISHED BRASS | 36| 4 +Brand#51 |PROMO POLISHED BRASS | 45| 4 +Brand#51 |PROMO POLISHED COPPER | 23| 4 +Brand#51 |PROMO POLISHED COPPER | 45| 4 +Brand#51 |PROMO POLISHED COPPER | 49| 4 +Brand#51 |PROMO POLISHED NICKEL | 9| 4 +Brand#51 |PROMO POLISHED NICKEL | 14| 4 +Brand#51 |PROMO POLISHED NICKEL | 36| 4 +Brand#51 |PROMO POLISHED NICKEL | 45| 4 +Brand#51 |PROMO POLISHED NICKEL | 49| 4 +Brand#51 |PROMO POLISHED STEEL | 3| 4 +Brand#51 |PROMO POLISHED STEEL | 9| 4 +Brand#51 |PROMO POLISHED STEEL | 14| 4 +Brand#51 |PROMO POLISHED STEEL | 23| 4 +Brand#51 |PROMO POLISHED STEEL | 49| 4 +Brand#51 |PROMO POLISHED TIN | 3| 4 +Brand#51 |PROMO POLISHED TIN | 19| 4 +Brand#51 |PROMO POLISHED TIN | 23| 4 +Brand#51 |PROMO POLISHED TIN | 45| 4 +Brand#51 |PROMO POLISHED TIN | 49| 4 +Brand#51 |SMALL ANODIZED BRASS | 3| 4 +Brand#51 |SMALL ANODIZED BRASS | 14| 4 +Brand#51 |SMALL ANODIZED BRASS | 19| 4 +Brand#51 |SMALL ANODIZED BRASS | 36| 4 +Brand#51 |SMALL ANODIZED BRASS | 49| 4 +Brand#51 |SMALL ANODIZED COPPER | 3| 4 +Brand#51 |SMALL ANODIZED COPPER | 14| 4 +Brand#51 |SMALL ANODIZED COPPER | 19| 4 +Brand#51 |SMALL ANODIZED NICKEL | 3| 4 +Brand#51 |SMALL ANODIZED NICKEL | 23| 4 +Brand#51 |SMALL ANODIZED STEEL | 9| 4 +Brand#51 |SMALL ANODIZED STEEL | 19| 4 +Brand#51 |SMALL ANODIZED TIN | 23| 4 +Brand#51 |SMALL ANODIZED TIN | 36| 4 +Brand#51 |SMALL ANODIZED TIN | 45| 4 +Brand#51 |SMALL ANODIZED TIN | 49| 4 +Brand#51 |SMALL BRUSHED BRASS | 14| 4 +Brand#51 |SMALL BRUSHED BRASS | 23| 4 +Brand#51 |SMALL BRUSHED BRASS | 36| 4 +Brand#51 |SMALL BRUSHED COPPER | 14| 4 +Brand#51 |SMALL BRUSHED COPPER | 23| 4 +Brand#51 |SMALL BRUSHED NICKEL | 19| 4 +Brand#51 |SMALL BRUSHED NICKEL | 49| 4 +Brand#51 |SMALL BRUSHED STEEL | 19| 4 +Brand#51 |SMALL BRUSHED STEEL | 23| 4 +Brand#51 |SMALL BRUSHED STEEL | 45| 4 +Brand#51 |SMALL BRUSHED STEEL | 49| 4 +Brand#51 |SMALL BRUSHED TIN | 3| 4 +Brand#51 |SMALL BRUSHED TIN | 14| 4 +Brand#51 |SMALL BRUSHED TIN | 49| 4 +Brand#51 |SMALL BURNISHED BRASS | 3| 4 +Brand#51 |SMALL BURNISHED BRASS | 45| 4 +Brand#51 |SMALL BURNISHED COPPER | 9| 4 +Brand#51 |SMALL BURNISHED COPPER | 49| 4 +Brand#51 |SMALL BURNISHED NICKEL | 9| 4 +Brand#51 |SMALL BURNISHED NICKEL | 36| 4 +Brand#51 |SMALL BURNISHED STEEL | 3| 4 +Brand#51 |SMALL BURNISHED STEEL | 9| 4 +Brand#51 |SMALL BURNISHED STEEL | 23| 4 +Brand#51 |SMALL BURNISHED TIN | 14| 4 +Brand#51 |SMALL BURNISHED TIN | 19| 4 +Brand#51 |SMALL BURNISHED TIN | 36| 4 +Brand#51 |SMALL BURNISHED TIN | 45| 4 +Brand#51 |SMALL PLATED BRASS | 9| 4 +Brand#51 |SMALL PLATED BRASS | 14| 4 +Brand#51 |SMALL PLATED BRASS | 45| 4 +Brand#51 |SMALL PLATED BRASS | 49| 4 +Brand#51 |SMALL PLATED COPPER | 3| 4 +Brand#51 |SMALL PLATED NICKEL | 3| 4 +Brand#51 |SMALL PLATED NICKEL | 19| 4 +Brand#51 |SMALL PLATED NICKEL | 23| 4 +Brand#51 |SMALL PLATED NICKEL | 45| 4 +Brand#51 |SMALL PLATED STEEL | 3| 4 +Brand#51 |SMALL PLATED STEEL | 14| 4 +Brand#51 |SMALL PLATED STEEL | 23| 4 +Brand#51 |SMALL PLATED TIN | 3| 4 +Brand#51 |SMALL PLATED TIN | 45| 4 +Brand#51 |SMALL PLATED TIN | 49| 4 +Brand#51 |SMALL POLISHED BRASS | 3| 4 +Brand#51 |SMALL POLISHED BRASS | 9| 4 +Brand#51 |SMALL POLISHED BRASS | 14| 4 +Brand#51 |SMALL POLISHED BRASS | 23| 4 +Brand#51 |SMALL POLISHED BRASS | 49| 4 +Brand#51 |SMALL POLISHED COPPER | 9| 4 +Brand#51 |SMALL POLISHED COPPER | 14| 4 +Brand#51 |SMALL POLISHED COPPER | 19| 4 +Brand#51 |SMALL POLISHED COPPER | 49| 4 +Brand#51 |SMALL POLISHED NICKEL | 9| 4 +Brand#51 |SMALL POLISHED NICKEL | 14| 4 +Brand#51 |SMALL POLISHED NICKEL | 36| 4 +Brand#51 |SMALL POLISHED NICKEL | 45| 4 +Brand#51 |SMALL POLISHED NICKEL | 49| 4 +Brand#51 |SMALL POLISHED STEEL | 9| 4 +Brand#51 |SMALL POLISHED STEEL | 19| 4 +Brand#51 |SMALL POLISHED STEEL | 36| 4 +Brand#51 |SMALL POLISHED STEEL | 49| 4 +Brand#51 |SMALL POLISHED TIN | 3| 4 +Brand#51 |SMALL POLISHED TIN | 9| 4 +Brand#51 |SMALL POLISHED TIN | 14| 4 +Brand#51 |SMALL POLISHED TIN | 45| 4 +Brand#51 |STANDARD ANODIZED BRASS | 3| 4 +Brand#51 |STANDARD ANODIZED BRASS | 14| 4 +Brand#51 |STANDARD ANODIZED BRASS | 45| 4 +Brand#51 |STANDARD ANODIZED COPPER | 3| 4 +Brand#51 |STANDARD ANODIZED COPPER | 9| 4 +Brand#51 |STANDARD ANODIZED COPPER | 23| 4 +Brand#51 |STANDARD ANODIZED COPPER | 45| 4 +Brand#51 |STANDARD ANODIZED NICKEL | 14| 4 +Brand#51 |STANDARD ANODIZED STEEL | 3| 4 +Brand#51 |STANDARD ANODIZED STEEL | 14| 4 +Brand#51 |STANDARD ANODIZED STEEL | 23| 4 +Brand#51 |STANDARD ANODIZED STEEL | 45| 4 +Brand#51 |STANDARD ANODIZED TIN | 3| 4 +Brand#51 |STANDARD ANODIZED TIN | 36| 4 +Brand#51 |STANDARD ANODIZED TIN | 49| 4 +Brand#51 |STANDARD BRUSHED BRASS | 3| 4 +Brand#51 |STANDARD BRUSHED BRASS | 14| 4 +Brand#51 |STANDARD BRUSHED BRASS | 23| 4 +Brand#51 |STANDARD BRUSHED BRASS | 49| 4 +Brand#51 |STANDARD BRUSHED COPPER | 9| 4 +Brand#51 |STANDARD BRUSHED COPPER | 14| 4 +Brand#51 |STANDARD BRUSHED COPPER | 49| 4 +Brand#51 |STANDARD BRUSHED NICKEL | 3| 4 +Brand#51 |STANDARD BRUSHED NICKEL | 36| 4 +Brand#51 |STANDARD BRUSHED STEEL | 3| 4 +Brand#51 |STANDARD BRUSHED STEEL | 9| 4 +Brand#51 |STANDARD BRUSHED TIN | 3| 4 +Brand#51 |STANDARD BRUSHED TIN | 14| 4 +Brand#51 |STANDARD BRUSHED TIN | 49| 4 +Brand#51 |STANDARD BURNISHED BRASS | 9| 4 +Brand#51 |STANDARD BURNISHED BRASS | 36| 4 +Brand#51 |STANDARD BURNISHED BRASS | 45| 4 +Brand#51 |STANDARD BURNISHED BRASS | 49| 4 +Brand#51 |STANDARD BURNISHED COPPER| 9| 4 +Brand#51 |STANDARD BURNISHED COPPER| 19| 4 +Brand#51 |STANDARD BURNISHED COPPER| 45| 4 +Brand#51 |STANDARD BURNISHED COPPER| 49| 4 +Brand#51 |STANDARD BURNISHED NICKEL| 45| 4 +Brand#51 |STANDARD BURNISHED STEEL | 3| 4 +Brand#51 |STANDARD BURNISHED STEEL | 49| 4 +Brand#51 |STANDARD BURNISHED TIN | 3| 4 +Brand#51 |STANDARD BURNISHED TIN | 23| 4 +Brand#51 |STANDARD BURNISHED TIN | 45| 4 +Brand#51 |STANDARD BURNISHED TIN | 49| 4 +Brand#51 |STANDARD PLATED BRASS | 9| 4 +Brand#51 |STANDARD PLATED BRASS | 14| 4 +Brand#51 |STANDARD PLATED COPPER | 3| 4 +Brand#51 |STANDARD PLATED COPPER | 14| 4 +Brand#51 |STANDARD PLATED COPPER | 23| 4 +Brand#51 |STANDARD PLATED COPPER | 49| 4 +Brand#51 |STANDARD PLATED NICKEL | 3| 4 +Brand#51 |STANDARD PLATED NICKEL | 23| 4 +Brand#51 |STANDARD PLATED NICKEL | 36| 4 +Brand#51 |STANDARD PLATED NICKEL | 45| 4 +Brand#51 |STANDARD PLATED NICKEL | 49| 4 +Brand#51 |STANDARD PLATED STEEL | 3| 4 +Brand#51 |STANDARD PLATED STEEL | 9| 4 +Brand#51 |STANDARD PLATED STEEL | 14| 4 +Brand#51 |STANDARD PLATED STEEL | 23| 4 +Brand#51 |STANDARD PLATED STEEL | 36| 4 +Brand#51 |STANDARD PLATED STEEL | 49| 4 +Brand#51 |STANDARD PLATED TIN | 3| 4 +Brand#51 |STANDARD PLATED TIN | 49| 4 +Brand#51 |STANDARD POLISHED BRASS | 9| 4 +Brand#51 |STANDARD POLISHED BRASS | 14| 4 +Brand#51 |STANDARD POLISHED BRASS | 19| 4 +Brand#51 |STANDARD POLISHED BRASS | 36| 4 +Brand#51 |STANDARD POLISHED BRASS | 49| 4 +Brand#51 |STANDARD POLISHED COPPER | 14| 4 +Brand#51 |STANDARD POLISHED COPPER | 19| 4 +Brand#51 |STANDARD POLISHED COPPER | 23| 4 +Brand#51 |STANDARD POLISHED COPPER | 36| 4 +Brand#51 |STANDARD POLISHED NICKEL | 14| 4 +Brand#51 |STANDARD POLISHED NICKEL | 23| 4 +Brand#51 |STANDARD POLISHED STEEL | 3| 4 +Brand#51 |STANDARD POLISHED STEEL | 23| 4 +Brand#51 |STANDARD POLISHED TIN | 9| 4 +Brand#51 |STANDARD POLISHED TIN | 36| 4 +Brand#51 |STANDARD POLISHED TIN | 45| 4 +Brand#52 |ECONOMY ANODIZED BRASS | 23| 4 +Brand#52 |ECONOMY ANODIZED COPPER | 3| 4 +Brand#52 |ECONOMY ANODIZED COPPER | 9| 4 +Brand#52 |ECONOMY ANODIZED COPPER | 14| 4 +Brand#52 |ECONOMY ANODIZED COPPER | 45| 4 +Brand#52 |ECONOMY ANODIZED COPPER | 49| 4 +Brand#52 |ECONOMY ANODIZED NICKEL | 19| 4 +Brand#52 |ECONOMY ANODIZED NICKEL | 36| 4 +Brand#52 |ECONOMY ANODIZED NICKEL | 49| 4 +Brand#52 |ECONOMY ANODIZED STEEL | 9| 4 +Brand#52 |ECONOMY ANODIZED STEEL | 45| 4 +Brand#52 |ECONOMY ANODIZED TIN | 23| 4 +Brand#52 |ECONOMY BRUSHED BRASS | 3| 4 +Brand#52 |ECONOMY BRUSHED BRASS | 23| 4 +Brand#52 |ECONOMY BRUSHED COPPER | 3| 4 +Brand#52 |ECONOMY BRUSHED COPPER | 9| 4 +Brand#52 |ECONOMY BRUSHED COPPER | 14| 4 +Brand#52 |ECONOMY BRUSHED COPPER | 36| 4 +Brand#52 |ECONOMY BRUSHED NICKEL | 14| 4 +Brand#52 |ECONOMY BRUSHED NICKEL | 19| 4 +Brand#52 |ECONOMY BRUSHED NICKEL | 36| 4 +Brand#52 |ECONOMY BRUSHED NICKEL | 49| 4 +Brand#52 |ECONOMY BRUSHED STEEL | 45| 4 +Brand#52 |ECONOMY BRUSHED STEEL | 49| 4 +Brand#52 |ECONOMY BRUSHED TIN | 3| 4 +Brand#52 |ECONOMY BRUSHED TIN | 19| 4 +Brand#52 |ECONOMY BRUSHED TIN | 23| 4 +Brand#52 |ECONOMY BURNISHED BRASS | 3| 4 +Brand#52 |ECONOMY BURNISHED BRASS | 9| 4 +Brand#52 |ECONOMY BURNISHED BRASS | 14| 4 +Brand#52 |ECONOMY BURNISHED BRASS | 19| 4 +Brand#52 |ECONOMY BURNISHED BRASS | 23| 4 +Brand#52 |ECONOMY BURNISHED BRASS | 36| 4 +Brand#52 |ECONOMY BURNISHED BRASS | 45| 4 +Brand#52 |ECONOMY BURNISHED BRASS | 49| 4 +Brand#52 |ECONOMY BURNISHED COPPER | 23| 4 +Brand#52 |ECONOMY BURNISHED COPPER | 36| 4 +Brand#52 |ECONOMY BURNISHED COPPER | 49| 4 +Brand#52 |ECONOMY BURNISHED NICKEL | 3| 4 +Brand#52 |ECONOMY BURNISHED NICKEL | 9| 4 +Brand#52 |ECONOMY BURNISHED STEEL | 3| 4 +Brand#52 |ECONOMY BURNISHED STEEL | 23| 4 +Brand#52 |ECONOMY BURNISHED STEEL | 49| 4 +Brand#52 |ECONOMY BURNISHED TIN | 9| 4 +Brand#52 |ECONOMY BURNISHED TIN | 23| 4 +Brand#52 |ECONOMY BURNISHED TIN | 36| 4 +Brand#52 |ECONOMY BURNISHED TIN | 45| 4 +Brand#52 |ECONOMY PLATED BRASS | 9| 4 +Brand#52 |ECONOMY PLATED COPPER | 14| 4 +Brand#52 |ECONOMY PLATED COPPER | 23| 4 +Brand#52 |ECONOMY PLATED COPPER | 45| 4 +Brand#52 |ECONOMY PLATED NICKEL | 9| 4 +Brand#52 |ECONOMY PLATED NICKEL | 19| 4 +Brand#52 |ECONOMY PLATED STEEL | 9| 4 +Brand#52 |ECONOMY PLATED STEEL | 19| 4 +Brand#52 |ECONOMY PLATED STEEL | 23| 4 +Brand#52 |ECONOMY PLATED STEEL | 36| 4 +Brand#52 |ECONOMY PLATED TIN | 45| 4 +Brand#52 |ECONOMY PLATED TIN | 49| 4 +Brand#52 |ECONOMY POLISHED BRASS | 9| 4 +Brand#52 |ECONOMY POLISHED COPPER | 9| 4 +Brand#52 |ECONOMY POLISHED COPPER | 36| 4 +Brand#52 |ECONOMY POLISHED NICKEL | 3| 4 +Brand#52 |ECONOMY POLISHED NICKEL | 9| 4 +Brand#52 |ECONOMY POLISHED NICKEL | 36| 4 +Brand#52 |ECONOMY POLISHED NICKEL | 49| 4 +Brand#52 |ECONOMY POLISHED STEEL | 14| 4 +Brand#52 |ECONOMY POLISHED STEEL | 19| 4 +Brand#52 |ECONOMY POLISHED STEEL | 23| 4 +Brand#52 |ECONOMY POLISHED STEEL | 36| 4 +Brand#52 |ECONOMY POLISHED TIN | 3| 4 +Brand#52 |ECONOMY POLISHED TIN | 9| 4 +Brand#52 |LARGE ANODIZED BRASS | 19| 4 +Brand#52 |LARGE ANODIZED BRASS | 36| 4 +Brand#52 |LARGE ANODIZED BRASS | 49| 4 +Brand#52 |LARGE ANODIZED COPPER | 3| 4 +Brand#52 |LARGE ANODIZED COPPER | 9| 4 +Brand#52 |LARGE ANODIZED COPPER | 19| 4 +Brand#52 |LARGE ANODIZED COPPER | 23| 4 +Brand#52 |LARGE ANODIZED COPPER | 36| 4 +Brand#52 |LARGE ANODIZED NICKEL | 9| 4 +Brand#52 |LARGE ANODIZED NICKEL | 14| 4 +Brand#52 |LARGE ANODIZED NICKEL | 19| 4 +Brand#52 |LARGE ANODIZED NICKEL | 49| 4 +Brand#52 |LARGE ANODIZED STEEL | 3| 4 +Brand#52 |LARGE ANODIZED STEEL | 14| 4 +Brand#52 |LARGE ANODIZED TIN | 19| 4 +Brand#52 |LARGE ANODIZED TIN | 23| 4 +Brand#52 |LARGE ANODIZED TIN | 45| 4 +Brand#52 |LARGE ANODIZED TIN | 49| 4 +Brand#52 |LARGE BRUSHED BRASS | 9| 4 +Brand#52 |LARGE BRUSHED BRASS | 36| 4 +Brand#52 |LARGE BRUSHED COPPER | 9| 4 +Brand#52 |LARGE BRUSHED COPPER | 19| 4 +Brand#52 |LARGE BRUSHED COPPER | 45| 4 +Brand#52 |LARGE BRUSHED NICKEL | 3| 4 +Brand#52 |LARGE BRUSHED NICKEL | 9| 4 +Brand#52 |LARGE BRUSHED NICKEL | 19| 4 +Brand#52 |LARGE BRUSHED NICKEL | 23| 4 +Brand#52 |LARGE BRUSHED NICKEL | 45| 4 +Brand#52 |LARGE BRUSHED NICKEL | 49| 4 +Brand#52 |LARGE BRUSHED STEEL | 9| 4 +Brand#52 |LARGE BRUSHED STEEL | 45| 4 +Brand#52 |LARGE BRUSHED STEEL | 49| 4 +Brand#52 |LARGE BRUSHED TIN | 3| 4 +Brand#52 |LARGE BRUSHED TIN | 14| 4 +Brand#52 |LARGE BRUSHED TIN | 36| 4 +Brand#52 |LARGE BURNISHED BRASS | 3| 4 +Brand#52 |LARGE BURNISHED BRASS | 9| 4 +Brand#52 |LARGE BURNISHED BRASS | 23| 4 +Brand#52 |LARGE BURNISHED BRASS | 45| 4 +Brand#52 |LARGE BURNISHED COPPER | 36| 4 +Brand#52 |LARGE BURNISHED COPPER | 49| 4 +Brand#52 |LARGE BURNISHED NICKEL | 14| 4 +Brand#52 |LARGE BURNISHED NICKEL | 19| 4 +Brand#52 |LARGE BURNISHED NICKEL | 36| 4 +Brand#52 |LARGE BURNISHED NICKEL | 45| 4 +Brand#52 |LARGE BURNISHED STEEL | 36| 4 +Brand#52 |LARGE BURNISHED TIN | 9| 4 +Brand#52 |LARGE BURNISHED TIN | 19| 4 +Brand#52 |LARGE BURNISHED TIN | 36| 4 +Brand#52 |LARGE BURNISHED TIN | 49| 4 +Brand#52 |LARGE PLATED BRASS | 3| 4 +Brand#52 |LARGE PLATED COPPER | 9| 4 +Brand#52 |LARGE PLATED COPPER | 49| 4 +Brand#52 |LARGE PLATED NICKEL | 9| 4 +Brand#52 |LARGE PLATED NICKEL | 36| 4 +Brand#52 |LARGE PLATED STEEL | 9| 4 +Brand#52 |LARGE PLATED STEEL | 19| 4 +Brand#52 |LARGE PLATED STEEL | 45| 4 +Brand#52 |LARGE PLATED TIN | 9| 4 +Brand#52 |LARGE POLISHED BRASS | 36| 4 +Brand#52 |LARGE POLISHED COPPER | 23| 4 +Brand#52 |LARGE POLISHED COPPER | 45| 4 +Brand#52 |LARGE POLISHED NICKEL | 3| 4 +Brand#52 |LARGE POLISHED NICKEL | 14| 4 +Brand#52 |LARGE POLISHED NICKEL | 19| 4 +Brand#52 |LARGE POLISHED NICKEL | 36| 4 +Brand#52 |LARGE POLISHED NICKEL | 45| 4 +Brand#52 |LARGE POLISHED STEEL | 3| 4 +Brand#52 |LARGE POLISHED STEEL | 9| 4 +Brand#52 |LARGE POLISHED TIN | 3| 4 +Brand#52 |MEDIUM ANODIZED BRASS | 14| 4 +Brand#52 |MEDIUM ANODIZED BRASS | 23| 4 +Brand#52 |MEDIUM ANODIZED BRASS | 45| 4 +Brand#52 |MEDIUM ANODIZED BRASS | 49| 4 +Brand#52 |MEDIUM ANODIZED COPPER | 9| 4 +Brand#52 |MEDIUM ANODIZED NICKEL | 3| 4 +Brand#52 |MEDIUM ANODIZED NICKEL | 19| 4 +Brand#52 |MEDIUM ANODIZED NICKEL | 36| 4 +Brand#52 |MEDIUM ANODIZED STEEL | 3| 4 +Brand#52 |MEDIUM ANODIZED STEEL | 14| 4 +Brand#52 |MEDIUM ANODIZED TIN | 14| 4 +Brand#52 |MEDIUM ANODIZED TIN | 36| 4 +Brand#52 |MEDIUM BRUSHED BRASS | 19| 4 +Brand#52 |MEDIUM BRUSHED COPPER | 19| 4 +Brand#52 |MEDIUM BRUSHED COPPER | 23| 4 +Brand#52 |MEDIUM BRUSHED COPPER | 45| 4 +Brand#52 |MEDIUM BRUSHED NICKEL | 3| 4 +Brand#52 |MEDIUM BRUSHED NICKEL | 9| 4 +Brand#52 |MEDIUM BRUSHED STEEL | 3| 4 +Brand#52 |MEDIUM BRUSHED STEEL | 9| 4 +Brand#52 |MEDIUM BRUSHED STEEL | 19| 4 +Brand#52 |MEDIUM BRUSHED TIN | 3| 4 +Brand#52 |MEDIUM BRUSHED TIN | 45| 4 +Brand#52 |MEDIUM BURNISHED BRASS | 19| 4 +Brand#52 |MEDIUM BURNISHED BRASS | 23| 4 +Brand#52 |MEDIUM BURNISHED BRASS | 36| 4 +Brand#52 |MEDIUM BURNISHED COPPER | 9| 4 +Brand#52 |MEDIUM BURNISHED COPPER | 19| 4 +Brand#52 |MEDIUM BURNISHED COPPER | 45| 4 +Brand#52 |MEDIUM BURNISHED COPPER | 49| 4 +Brand#52 |MEDIUM BURNISHED NICKEL | 3| 4 +Brand#52 |MEDIUM BURNISHED NICKEL | 9| 4 +Brand#52 |MEDIUM BURNISHED STEEL | 9| 4 +Brand#52 |MEDIUM BURNISHED STEEL | 14| 4 +Brand#52 |MEDIUM BURNISHED STEEL | 23| 4 +Brand#52 |MEDIUM BURNISHED STEEL | 36| 4 +Brand#52 |MEDIUM BURNISHED STEEL | 45| 4 +Brand#52 |MEDIUM BURNISHED STEEL | 49| 4 +Brand#52 |MEDIUM BURNISHED TIN | 36| 4 +Brand#52 |MEDIUM PLATED BRASS | 3| 4 +Brand#52 |MEDIUM PLATED BRASS | 9| 4 +Brand#52 |MEDIUM PLATED BRASS | 19| 4 +Brand#52 |MEDIUM PLATED BRASS | 36| 4 +Brand#52 |MEDIUM PLATED BRASS | 45| 4 +Brand#52 |MEDIUM PLATED COPPER | 3| 4 +Brand#52 |MEDIUM PLATED COPPER | 45| 4 +Brand#52 |MEDIUM PLATED COPPER | 49| 4 +Brand#52 |MEDIUM PLATED NICKEL | 9| 4 +Brand#52 |MEDIUM PLATED NICKEL | 14| 4 +Brand#52 |MEDIUM PLATED NICKEL | 45| 4 +Brand#52 |MEDIUM PLATED STEEL | 3| 4 +Brand#52 |MEDIUM PLATED STEEL | 9| 4 +Brand#52 |MEDIUM PLATED STEEL | 14| 4 +Brand#52 |MEDIUM PLATED STEEL | 19| 4 +Brand#52 |MEDIUM PLATED STEEL | 23| 4 +Brand#52 |MEDIUM PLATED STEEL | 45| 4 +Brand#52 |MEDIUM PLATED STEEL | 49| 4 +Brand#52 |MEDIUM PLATED TIN | 19| 4 +Brand#52 |PROMO ANODIZED BRASS | 14| 4 +Brand#52 |PROMO ANODIZED BRASS | 19| 4 +Brand#52 |PROMO ANODIZED COPPER | 3| 4 +Brand#52 |PROMO ANODIZED COPPER | 9| 4 +Brand#52 |PROMO ANODIZED COPPER | 45| 4 +Brand#52 |PROMO ANODIZED NICKEL | 14| 4 +Brand#52 |PROMO ANODIZED NICKEL | 19| 4 +Brand#52 |PROMO ANODIZED NICKEL | 23| 4 +Brand#52 |PROMO ANODIZED NICKEL | 36| 4 +Brand#52 |PROMO ANODIZED NICKEL | 45| 4 +Brand#52 |PROMO ANODIZED STEEL | 3| 4 +Brand#52 |PROMO ANODIZED STEEL | 14| 4 +Brand#52 |PROMO ANODIZED STEEL | 45| 4 +Brand#52 |PROMO ANODIZED TIN | 45| 4 +Brand#52 |PROMO BRUSHED BRASS | 19| 4 +Brand#52 |PROMO BRUSHED BRASS | 23| 4 +Brand#52 |PROMO BRUSHED BRASS | 49| 4 +Brand#52 |PROMO BRUSHED COPPER | 3| 4 +Brand#52 |PROMO BRUSHED COPPER | 9| 4 +Brand#52 |PROMO BRUSHED COPPER | 19| 4 +Brand#52 |PROMO BRUSHED COPPER | 23| 4 +Brand#52 |PROMO BRUSHED COPPER | 36| 4 +Brand#52 |PROMO BRUSHED NICKEL | 14| 4 +Brand#52 |PROMO BRUSHED NICKEL | 36| 4 +Brand#52 |PROMO BRUSHED STEEL | 3| 4 +Brand#52 |PROMO BRUSHED STEEL | 19| 4 +Brand#52 |PROMO BRUSHED STEEL | 45| 4 +Brand#52 |PROMO BRUSHED STEEL | 49| 4 +Brand#52 |PROMO BRUSHED TIN | 3| 4 +Brand#52 |PROMO BRUSHED TIN | 19| 4 +Brand#52 |PROMO BRUSHED TIN | 23| 4 +Brand#52 |PROMO BRUSHED TIN | 45| 4 +Brand#52 |PROMO BRUSHED TIN | 49| 4 +Brand#52 |PROMO BURNISHED BRASS | 45| 4 +Brand#52 |PROMO BURNISHED BRASS | 49| 4 +Brand#52 |PROMO BURNISHED COPPER | 9| 4 +Brand#52 |PROMO BURNISHED COPPER | 36| 4 +Brand#52 |PROMO BURNISHED NICKEL | 45| 4 +Brand#52 |PROMO BURNISHED STEEL | 9| 4 +Brand#52 |PROMO BURNISHED STEEL | 14| 4 +Brand#52 |PROMO BURNISHED STEEL | 23| 4 +Brand#52 |PROMO BURNISHED STEEL | 36| 4 +Brand#52 |PROMO BURNISHED STEEL | 49| 4 +Brand#52 |PROMO BURNISHED TIN | 9| 4 +Brand#52 |PROMO BURNISHED TIN | 14| 4 +Brand#52 |PROMO BURNISHED TIN | 36| 4 +Brand#52 |PROMO BURNISHED TIN | 49| 4 +Brand#52 |PROMO PLATED BRASS | 19| 4 +Brand#52 |PROMO PLATED BRASS | 23| 4 +Brand#52 |PROMO PLATED BRASS | 36| 4 +Brand#52 |PROMO PLATED COPPER | 19| 4 +Brand#52 |PROMO PLATED COPPER | 23| 4 +Brand#52 |PROMO PLATED NICKEL | 3| 4 +Brand#52 |PROMO PLATED STEEL | 36| 4 +Brand#52 |PROMO PLATED STEEL | 45| 4 +Brand#52 |PROMO PLATED TIN | 14| 4 +Brand#52 |PROMO PLATED TIN | 19| 4 +Brand#52 |PROMO PLATED TIN | 49| 4 +Brand#52 |PROMO POLISHED BRASS | 9| 4 +Brand#52 |PROMO POLISHED BRASS | 49| 4 +Brand#52 |PROMO POLISHED COPPER | 3| 4 +Brand#52 |PROMO POLISHED COPPER | 9| 4 +Brand#52 |PROMO POLISHED NICKEL | 3| 4 +Brand#52 |PROMO POLISHED NICKEL | 9| 4 +Brand#52 |PROMO POLISHED NICKEL | 19| 4 +Brand#52 |PROMO POLISHED NICKEL | 36| 4 +Brand#52 |PROMO POLISHED NICKEL | 45| 4 +Brand#52 |PROMO POLISHED STEEL | 3| 4 +Brand#52 |PROMO POLISHED STEEL | 9| 4 +Brand#52 |PROMO POLISHED STEEL | 14| 4 +Brand#52 |PROMO POLISHED STEEL | 36| 4 +Brand#52 |PROMO POLISHED TIN | 36| 4 +Brand#52 |SMALL ANODIZED BRASS | 49| 4 +Brand#52 |SMALL ANODIZED COPPER | 49| 4 +Brand#52 |SMALL ANODIZED NICKEL | 9| 4 +Brand#52 |SMALL ANODIZED NICKEL | 23| 4 +Brand#52 |SMALL ANODIZED NICKEL | 49| 4 +Brand#52 |SMALL ANODIZED STEEL | 9| 4 +Brand#52 |SMALL ANODIZED STEEL | 19| 4 +Brand#52 |SMALL ANODIZED STEEL | 49| 4 +Brand#52 |SMALL ANODIZED TIN | 3| 4 +Brand#52 |SMALL BRUSHED BRASS | 3| 4 +Brand#52 |SMALL BRUSHED BRASS | 23| 4 +Brand#52 |SMALL BRUSHED BRASS | 45| 4 +Brand#52 |SMALL BRUSHED COPPER | 3| 4 +Brand#52 |SMALL BRUSHED COPPER | 19| 4 +Brand#52 |SMALL BRUSHED COPPER | 36| 4 +Brand#52 |SMALL BRUSHED COPPER | 45| 4 +Brand#52 |SMALL BRUSHED COPPER | 49| 4 +Brand#52 |SMALL BRUSHED NICKEL | 3| 4 +Brand#52 |SMALL BRUSHED NICKEL | 23| 4 +Brand#52 |SMALL BRUSHED NICKEL | 36| 4 +Brand#52 |SMALL BRUSHED NICKEL | 45| 4 +Brand#52 |SMALL BRUSHED STEEL | 3| 4 +Brand#52 |SMALL BRUSHED STEEL | 14| 4 +Brand#52 |SMALL BRUSHED STEEL | 23| 4 +Brand#52 |SMALL BRUSHED TIN | 9| 4 +Brand#52 |SMALL BRUSHED TIN | 14| 4 +Brand#52 |SMALL BURNISHED BRASS | 3| 4 +Brand#52 |SMALL BURNISHED BRASS | 23| 4 +Brand#52 |SMALL BURNISHED BRASS | 36| 4 +Brand#52 |SMALL BURNISHED BRASS | 49| 4 +Brand#52 |SMALL BURNISHED COPPER | 3| 4 +Brand#52 |SMALL BURNISHED COPPER | 36| 4 +Brand#52 |SMALL BURNISHED COPPER | 49| 4 +Brand#52 |SMALL BURNISHED NICKEL | 23| 4 +Brand#52 |SMALL BURNISHED STEEL | 36| 4 +Brand#52 |SMALL BURNISHED STEEL | 45| 4 +Brand#52 |SMALL BURNISHED STEEL | 49| 4 +Brand#52 |SMALL BURNISHED TIN | 9| 4 +Brand#52 |SMALL BURNISHED TIN | 19| 4 +Brand#52 |SMALL BURNISHED TIN | 23| 4 +Brand#52 |SMALL BURNISHED TIN | 45| 4 +Brand#52 |SMALL BURNISHED TIN | 49| 4 +Brand#52 |SMALL PLATED BRASS | 14| 4 +Brand#52 |SMALL PLATED BRASS | 19| 4 +Brand#52 |SMALL PLATED COPPER | 9| 4 +Brand#52 |SMALL PLATED COPPER | 45| 4 +Brand#52 |SMALL PLATED NICKEL | 9| 4 +Brand#52 |SMALL PLATED NICKEL | 49| 4 +Brand#52 |SMALL PLATED STEEL | 9| 4 +Brand#52 |SMALL PLATED STEEL | 49| 4 +Brand#52 |SMALL PLATED TIN | 9| 4 +Brand#52 |SMALL PLATED TIN | 45| 4 +Brand#52 |SMALL PLATED TIN | 49| 4 +Brand#52 |SMALL POLISHED BRASS | 9| 4 +Brand#52 |SMALL POLISHED BRASS | 36| 4 +Brand#52 |SMALL POLISHED BRASS | 45| 4 +Brand#52 |SMALL POLISHED COPPER | 3| 4 +Brand#52 |SMALL POLISHED COPPER | 14| 4 +Brand#52 |SMALL POLISHED COPPER | 23| 4 +Brand#52 |SMALL POLISHED NICKEL | 14| 4 +Brand#52 |SMALL POLISHED NICKEL | 23| 4 +Brand#52 |SMALL POLISHED NICKEL | 36| 4 +Brand#52 |SMALL POLISHED NICKEL | 45| 4 +Brand#52 |SMALL POLISHED NICKEL | 49| 4 +Brand#52 |SMALL POLISHED STEEL | 45| 4 +Brand#52 |SMALL POLISHED TIN | 3| 4 +Brand#52 |SMALL POLISHED TIN | 23| 4 +Brand#52 |SMALL POLISHED TIN | 36| 4 +Brand#52 |SMALL POLISHED TIN | 45| 4 +Brand#52 |SMALL POLISHED TIN | 49| 4 +Brand#52 |STANDARD ANODIZED BRASS | 3| 4 +Brand#52 |STANDARD ANODIZED BRASS | 19| 4 +Brand#52 |STANDARD ANODIZED BRASS | 36| 4 +Brand#52 |STANDARD ANODIZED COPPER | 14| 4 +Brand#52 |STANDARD ANODIZED COPPER | 23| 4 +Brand#52 |STANDARD ANODIZED NICKEL | 9| 4 +Brand#52 |STANDARD ANODIZED NICKEL | 19| 4 +Brand#52 |STANDARD ANODIZED NICKEL | 36| 4 +Brand#52 |STANDARD ANODIZED NICKEL | 45| 4 +Brand#52 |STANDARD ANODIZED NICKEL | 49| 4 +Brand#52 |STANDARD ANODIZED STEEL | 9| 4 +Brand#52 |STANDARD ANODIZED STEEL | 36| 4 +Brand#52 |STANDARD ANODIZED STEEL | 45| 4 +Brand#52 |STANDARD ANODIZED TIN | 9| 4 +Brand#52 |STANDARD ANODIZED TIN | 23| 4 +Brand#52 |STANDARD ANODIZED TIN | 36| 4 +Brand#52 |STANDARD ANODIZED TIN | 49| 4 +Brand#52 |STANDARD BRUSHED BRASS | 9| 4 +Brand#52 |STANDARD BRUSHED BRASS | 23| 4 +Brand#52 |STANDARD BRUSHED BRASS | 45| 4 +Brand#52 |STANDARD BRUSHED BRASS | 49| 4 +Brand#52 |STANDARD BRUSHED COPPER | 23| 4 +Brand#52 |STANDARD BRUSHED COPPER | 49| 4 +Brand#52 |STANDARD BRUSHED NICKEL | 45| 4 +Brand#52 |STANDARD BRUSHED STEEL | 3| 4 +Brand#52 |STANDARD BRUSHED STEEL | 19| 4 +Brand#52 |STANDARD BRUSHED STEEL | 36| 4 +Brand#52 |STANDARD BRUSHED STEEL | 45| 4 +Brand#52 |STANDARD BRUSHED TIN | 14| 4 +Brand#52 |STANDARD BRUSHED TIN | 19| 4 +Brand#52 |STANDARD BRUSHED TIN | 23| 4 +Brand#52 |STANDARD BRUSHED TIN | 45| 4 +Brand#52 |STANDARD BURNISHED BRASS | 9| 4 +Brand#52 |STANDARD BURNISHED BRASS | 45| 4 +Brand#52 |STANDARD BURNISHED COPPER| 9| 4 +Brand#52 |STANDARD BURNISHED COPPER| 36| 4 +Brand#52 |STANDARD BURNISHED COPPER| 45| 4 +Brand#52 |STANDARD BURNISHED NICKEL| 9| 4 +Brand#52 |STANDARD BURNISHED NICKEL| 14| 4 +Brand#52 |STANDARD BURNISHED NICKEL| 19| 4 +Brand#52 |STANDARD BURNISHED NICKEL| 23| 4 +Brand#52 |STANDARD BURNISHED NICKEL| 45| 4 +Brand#52 |STANDARD BURNISHED STEEL | 19| 4 +Brand#52 |STANDARD BURNISHED STEEL | 45| 4 +Brand#52 |STANDARD BURNISHED TIN | 3| 4 +Brand#52 |STANDARD BURNISHED TIN | 36| 4 +Brand#52 |STANDARD PLATED BRASS | 3| 4 +Brand#52 |STANDARD PLATED BRASS | 9| 4 +Brand#52 |STANDARD PLATED BRASS | 14| 4 +Brand#52 |STANDARD PLATED COPPER | 14| 4 +Brand#52 |STANDARD PLATED COPPER | 19| 4 +Brand#52 |STANDARD PLATED COPPER | 36| 4 +Brand#52 |STANDARD PLATED NICKEL | 19| 4 +Brand#52 |STANDARD PLATED NICKEL | 23| 4 +Brand#52 |STANDARD PLATED NICKEL | 36| 4 +Brand#52 |STANDARD PLATED NICKEL | 49| 4 +Brand#52 |STANDARD PLATED STEEL | 23| 4 +Brand#52 |STANDARD PLATED STEEL | 49| 4 +Brand#52 |STANDARD PLATED TIN | 19| 4 +Brand#52 |STANDARD POLISHED BRASS | 19| 4 +Brand#52 |STANDARD POLISHED BRASS | 23| 4 +Brand#52 |STANDARD POLISHED COPPER | 3| 4 +Brand#52 |STANDARD POLISHED COPPER | 19| 4 +Brand#52 |STANDARD POLISHED COPPER | 23| 4 +Brand#52 |STANDARD POLISHED COPPER | 45| 4 +Brand#52 |STANDARD POLISHED COPPER | 49| 4 +Brand#52 |STANDARD POLISHED NICKEL | 9| 4 +Brand#52 |STANDARD POLISHED STEEL | 3| 4 +Brand#52 |STANDARD POLISHED STEEL | 14| 4 +Brand#52 |STANDARD POLISHED STEEL | 19| 4 +Brand#52 |STANDARD POLISHED TIN | 9| 4 +Brand#52 |STANDARD POLISHED TIN | 45| 4 +Brand#53 |ECONOMY ANODIZED BRASS | 3| 4 +Brand#53 |ECONOMY ANODIZED BRASS | 14| 4 +Brand#53 |ECONOMY ANODIZED BRASS | 23| 4 +Brand#53 |ECONOMY ANODIZED COPPER | 3| 4 +Brand#53 |ECONOMY ANODIZED COPPER | 9| 4 +Brand#53 |ECONOMY ANODIZED COPPER | 14| 4 +Brand#53 |ECONOMY ANODIZED COPPER | 49| 4 +Brand#53 |ECONOMY ANODIZED NICKEL | 3| 4 +Brand#53 |ECONOMY ANODIZED NICKEL | 23| 4 +Brand#53 |ECONOMY ANODIZED NICKEL | 45| 4 +Brand#53 |ECONOMY ANODIZED NICKEL | 49| 4 +Brand#53 |ECONOMY ANODIZED STEEL | 3| 4 +Brand#53 |ECONOMY ANODIZED STEEL | 19| 4 +Brand#53 |ECONOMY ANODIZED STEEL | 36| 4 +Brand#53 |ECONOMY ANODIZED STEEL | 49| 4 +Brand#53 |ECONOMY ANODIZED TIN | 19| 4 +Brand#53 |ECONOMY ANODIZED TIN | 49| 4 +Brand#53 |ECONOMY BRUSHED BRASS | 9| 4 +Brand#53 |ECONOMY BRUSHED BRASS | 14| 4 +Brand#53 |ECONOMY BRUSHED COPPER | 9| 4 +Brand#53 |ECONOMY BRUSHED COPPER | 14| 4 +Brand#53 |ECONOMY BRUSHED COPPER | 19| 4 +Brand#53 |ECONOMY BRUSHED COPPER | 23| 4 +Brand#53 |ECONOMY BRUSHED COPPER | 36| 4 +Brand#53 |ECONOMY BRUSHED NICKEL | 3| 4 +Brand#53 |ECONOMY BRUSHED NICKEL | 45| 4 +Brand#53 |ECONOMY BRUSHED STEEL | 9| 4 +Brand#53 |ECONOMY BRUSHED STEEL | 14| 4 +Brand#53 |ECONOMY BRUSHED STEEL | 36| 4 +Brand#53 |ECONOMY BRUSHED TIN | 14| 4 +Brand#53 |ECONOMY BRUSHED TIN | 23| 4 +Brand#53 |ECONOMY BRUSHED TIN | 45| 4 +Brand#53 |ECONOMY BRUSHED TIN | 49| 4 +Brand#53 |ECONOMY BURNISHED BRASS | 3| 4 +Brand#53 |ECONOMY BURNISHED BRASS | 14| 4 +Brand#53 |ECONOMY BURNISHED BRASS | 19| 4 +Brand#53 |ECONOMY BURNISHED BRASS | 23| 4 +Brand#53 |ECONOMY BURNISHED BRASS | 36| 4 +Brand#53 |ECONOMY BURNISHED COPPER | 3| 4 +Brand#53 |ECONOMY BURNISHED COPPER | 36| 4 +Brand#53 |ECONOMY BURNISHED COPPER | 49| 4 +Brand#53 |ECONOMY BURNISHED NICKEL | 9| 4 +Brand#53 |ECONOMY BURNISHED NICKEL | 49| 4 +Brand#53 |ECONOMY BURNISHED STEEL | 3| 4 +Brand#53 |ECONOMY BURNISHED STEEL | 9| 4 +Brand#53 |ECONOMY BURNISHED STEEL | 14| 4 +Brand#53 |ECONOMY BURNISHED STEEL | 49| 4 +Brand#53 |ECONOMY BURNISHED TIN | 9| 4 +Brand#53 |ECONOMY BURNISHED TIN | 19| 4 +Brand#53 |ECONOMY BURNISHED TIN | 36| 4 +Brand#53 |ECONOMY BURNISHED TIN | 45| 4 +Brand#53 |ECONOMY PLATED BRASS | 3| 4 +Brand#53 |ECONOMY PLATED BRASS | 49| 4 +Brand#53 |ECONOMY PLATED COPPER | 14| 4 +Brand#53 |ECONOMY PLATED NICKEL | 14| 4 +Brand#53 |ECONOMY PLATED NICKEL | 19| 4 +Brand#53 |ECONOMY PLATED NICKEL | 36| 4 +Brand#53 |ECONOMY PLATED NICKEL | 45| 4 +Brand#53 |ECONOMY PLATED NICKEL | 49| 4 +Brand#53 |ECONOMY PLATED STEEL | 14| 4 +Brand#53 |ECONOMY PLATED STEEL | 19| 4 +Brand#53 |ECONOMY PLATED STEEL | 23| 4 +Brand#53 |ECONOMY PLATED TIN | 36| 4 +Brand#53 |ECONOMY PLATED TIN | 49| 4 +Brand#53 |ECONOMY POLISHED BRASS | 3| 4 +Brand#53 |ECONOMY POLISHED BRASS | 9| 4 +Brand#53 |ECONOMY POLISHED BRASS | 23| 4 +Brand#53 |ECONOMY POLISHED BRASS | 36| 4 +Brand#53 |ECONOMY POLISHED BRASS | 45| 4 +Brand#53 |ECONOMY POLISHED BRASS | 49| 4 +Brand#53 |ECONOMY POLISHED COPPER | 9| 4 +Brand#53 |ECONOMY POLISHED COPPER | 36| 4 +Brand#53 |ECONOMY POLISHED COPPER | 45| 4 +Brand#53 |ECONOMY POLISHED COPPER | 49| 4 +Brand#53 |ECONOMY POLISHED NICKEL | 14| 4 +Brand#53 |ECONOMY POLISHED NICKEL | 19| 4 +Brand#53 |ECONOMY POLISHED NICKEL | 45| 4 +Brand#53 |ECONOMY POLISHED NICKEL | 49| 4 +Brand#53 |ECONOMY POLISHED STEEL | 19| 4 +Brand#53 |ECONOMY POLISHED TIN | 23| 4 +Brand#53 |LARGE ANODIZED BRASS | 3| 4 +Brand#53 |LARGE ANODIZED BRASS | 9| 4 +Brand#53 |LARGE ANODIZED BRASS | 49| 4 +Brand#53 |LARGE ANODIZED COPPER | 3| 4 +Brand#53 |LARGE ANODIZED COPPER | 23| 4 +Brand#53 |LARGE ANODIZED COPPER | 36| 4 +Brand#53 |LARGE ANODIZED NICKEL | 3| 4 +Brand#53 |LARGE ANODIZED NICKEL | 14| 4 +Brand#53 |LARGE ANODIZED NICKEL | 19| 4 +Brand#53 |LARGE ANODIZED NICKEL | 23| 4 +Brand#53 |LARGE ANODIZED NICKEL | 36| 4 +Brand#53 |LARGE ANODIZED NICKEL | 45| 4 +Brand#53 |LARGE ANODIZED NICKEL | 49| 4 +Brand#53 |LARGE ANODIZED STEEL | 9| 4 +Brand#53 |LARGE ANODIZED STEEL | 14| 4 +Brand#53 |LARGE ANODIZED STEEL | 36| 4 +Brand#53 |LARGE ANODIZED TIN | 3| 4 +Brand#53 |LARGE ANODIZED TIN | 14| 4 +Brand#53 |LARGE ANODIZED TIN | 19| 4 +Brand#53 |LARGE BRUSHED BRASS | 3| 4 +Brand#53 |LARGE BRUSHED BRASS | 23| 4 +Brand#53 |LARGE BRUSHED BRASS | 45| 4 +Brand#53 |LARGE BRUSHED COPPER | 3| 4 +Brand#53 |LARGE BRUSHED COPPER | 9| 4 +Brand#53 |LARGE BRUSHED COPPER | 23| 4 +Brand#53 |LARGE BRUSHED NICKEL | 3| 4 +Brand#53 |LARGE BRUSHED NICKEL | 14| 4 +Brand#53 |LARGE BRUSHED NICKEL | 19| 4 +Brand#53 |LARGE BRUSHED NICKEL | 36| 4 +Brand#53 |LARGE BRUSHED NICKEL | 49| 4 +Brand#53 |LARGE BRUSHED STEEL | 3| 4 +Brand#53 |LARGE BRUSHED STEEL | 14| 4 +Brand#53 |LARGE BRUSHED STEEL | 23| 4 +Brand#53 |LARGE BRUSHED STEEL | 49| 4 +Brand#53 |LARGE BRUSHED TIN | 14| 4 +Brand#53 |LARGE BRUSHED TIN | 45| 4 +Brand#53 |LARGE BRUSHED TIN | 49| 4 +Brand#53 |LARGE BURNISHED BRASS | 19| 4 +Brand#53 |LARGE BURNISHED BRASS | 23| 4 +Brand#53 |LARGE BURNISHED BRASS | 36| 4 +Brand#53 |LARGE BURNISHED BRASS | 45| 4 +Brand#53 |LARGE BURNISHED COPPER | 19| 4 +Brand#53 |LARGE BURNISHED COPPER | 45| 4 +Brand#53 |LARGE BURNISHED COPPER | 49| 4 +Brand#53 |LARGE BURNISHED NICKEL | 36| 4 +Brand#53 |LARGE BURNISHED STEEL | 9| 4 +Brand#53 |LARGE BURNISHED STEEL | 49| 4 +Brand#53 |LARGE BURNISHED TIN | 3| 4 +Brand#53 |LARGE BURNISHED TIN | 23| 4 +Brand#53 |LARGE BURNISHED TIN | 49| 4 +Brand#53 |LARGE PLATED BRASS | 14| 4 +Brand#53 |LARGE PLATED BRASS | 19| 4 +Brand#53 |LARGE PLATED BRASS | 45| 4 +Brand#53 |LARGE PLATED COPPER | 14| 4 +Brand#53 |LARGE PLATED COPPER | 23| 4 +Brand#53 |LARGE PLATED COPPER | 45| 4 +Brand#53 |LARGE PLATED NICKEL | 19| 4 +Brand#53 |LARGE PLATED NICKEL | 23| 4 +Brand#53 |LARGE PLATED NICKEL | 36| 4 +Brand#53 |LARGE PLATED STEEL | 19| 4 +Brand#53 |LARGE PLATED STEEL | 49| 4 +Brand#53 |LARGE PLATED TIN | 3| 4 +Brand#53 |LARGE PLATED TIN | 19| 4 +Brand#53 |LARGE POLISHED BRASS | 9| 4 +Brand#53 |LARGE POLISHED BRASS | 19| 4 +Brand#53 |LARGE POLISHED COPPER | 14| 4 +Brand#53 |LARGE POLISHED COPPER | 19| 4 +Brand#53 |LARGE POLISHED COPPER | 36| 4 +Brand#53 |LARGE POLISHED NICKEL | 45| 4 +Brand#53 |LARGE POLISHED STEEL | 9| 4 +Brand#53 |LARGE POLISHED TIN | 14| 4 +Brand#53 |LARGE POLISHED TIN | 19| 4 +Brand#53 |LARGE POLISHED TIN | 36| 4 +Brand#53 |LARGE POLISHED TIN | 45| 4 +Brand#53 |MEDIUM ANODIZED BRASS | 9| 4 +Brand#53 |MEDIUM ANODIZED BRASS | 19| 4 +Brand#53 |MEDIUM ANODIZED BRASS | 23| 4 +Brand#53 |MEDIUM ANODIZED BRASS | 45| 4 +Brand#53 |MEDIUM ANODIZED COPPER | 36| 4 +Brand#53 |MEDIUM ANODIZED COPPER | 49| 4 +Brand#53 |MEDIUM ANODIZED NICKEL | 3| 4 +Brand#53 |MEDIUM ANODIZED NICKEL | 9| 4 +Brand#53 |MEDIUM ANODIZED STEEL | 3| 4 +Brand#53 |MEDIUM ANODIZED STEEL | 19| 4 +Brand#53 |MEDIUM ANODIZED STEEL | 45| 4 +Brand#53 |MEDIUM ANODIZED TIN | 9| 4 +Brand#53 |MEDIUM ANODIZED TIN | 19| 4 +Brand#53 |MEDIUM ANODIZED TIN | 45| 4 +Brand#53 |MEDIUM BRUSHED BRASS | 14| 4 +Brand#53 |MEDIUM BRUSHED BRASS | 19| 4 +Brand#53 |MEDIUM BRUSHED BRASS | 36| 4 +Brand#53 |MEDIUM BRUSHED BRASS | 45| 4 +Brand#53 |MEDIUM BRUSHED COPPER | 3| 4 +Brand#53 |MEDIUM BRUSHED COPPER | 14| 4 +Brand#53 |MEDIUM BRUSHED COPPER | 19| 4 +Brand#53 |MEDIUM BRUSHED COPPER | 23| 4 +Brand#53 |MEDIUM BRUSHED NICKEL | 36| 4 +Brand#53 |MEDIUM BRUSHED STEEL | 9| 4 +Brand#53 |MEDIUM BRUSHED STEEL | 19| 4 +Brand#53 |MEDIUM BRUSHED TIN | 14| 4 +Brand#53 |MEDIUM BRUSHED TIN | 49| 4 +Brand#53 |MEDIUM BURNISHED BRASS | 9| 4 +Brand#53 |MEDIUM BURNISHED BRASS | 19| 4 +Brand#53 |MEDIUM BURNISHED BRASS | 23| 4 +Brand#53 |MEDIUM BURNISHED BRASS | 36| 4 +Brand#53 |MEDIUM BURNISHED BRASS | 45| 4 +Brand#53 |MEDIUM BURNISHED COPPER | 23| 4 +Brand#53 |MEDIUM BURNISHED COPPER | 36| 4 +Brand#53 |MEDIUM BURNISHED STEEL | 3| 4 +Brand#53 |MEDIUM BURNISHED STEEL | 45| 4 +Brand#53 |MEDIUM BURNISHED TIN | 3| 4 +Brand#53 |MEDIUM BURNISHED TIN | 19| 4 +Brand#53 |MEDIUM BURNISHED TIN | 23| 4 +Brand#53 |MEDIUM BURNISHED TIN | 36| 4 +Brand#53 |MEDIUM BURNISHED TIN | 49| 4 +Brand#53 |MEDIUM PLATED BRASS | 3| 4 +Brand#53 |MEDIUM PLATED BRASS | 23| 4 +Brand#53 |MEDIUM PLATED COPPER | 36| 4 +Brand#53 |MEDIUM PLATED COPPER | 45| 4 +Brand#53 |MEDIUM PLATED COPPER | 49| 4 +Brand#53 |MEDIUM PLATED NICKEL | 9| 4 +Brand#53 |MEDIUM PLATED NICKEL | 14| 4 +Brand#53 |MEDIUM PLATED NICKEL | 19| 4 +Brand#53 |MEDIUM PLATED NICKEL | 49| 4 +Brand#53 |MEDIUM PLATED STEEL | 3| 4 +Brand#53 |MEDIUM PLATED STEEL | 9| 4 +Brand#53 |MEDIUM PLATED STEEL | 36| 4 +Brand#53 |MEDIUM PLATED STEEL | 49| 4 +Brand#53 |MEDIUM PLATED TIN | 3| 4 +Brand#53 |MEDIUM PLATED TIN | 9| 4 +Brand#53 |MEDIUM PLATED TIN | 19| 4 +Brand#53 |MEDIUM PLATED TIN | 23| 4 +Brand#53 |MEDIUM PLATED TIN | 36| 4 +Brand#53 |MEDIUM PLATED TIN | 49| 4 +Brand#53 |PROMO ANODIZED BRASS | 14| 4 +Brand#53 |PROMO ANODIZED COPPER | 19| 4 +Brand#53 |PROMO ANODIZED COPPER | 45| 4 +Brand#53 |PROMO ANODIZED NICKEL | 9| 4 +Brand#53 |PROMO ANODIZED NICKEL | 14| 4 +Brand#53 |PROMO ANODIZED NICKEL | 19| 4 +Brand#53 |PROMO ANODIZED NICKEL | 23| 4 +Brand#53 |PROMO ANODIZED NICKEL | 45| 4 +Brand#53 |PROMO ANODIZED STEEL | 23| 4 +Brand#53 |PROMO ANODIZED STEEL | 36| 4 +Brand#53 |PROMO ANODIZED STEEL | 49| 4 +Brand#53 |PROMO ANODIZED TIN | 3| 4 +Brand#53 |PROMO ANODIZED TIN | 9| 4 +Brand#53 |PROMO ANODIZED TIN | 14| 4 +Brand#53 |PROMO ANODIZED TIN | 23| 4 +Brand#53 |PROMO BRUSHED BRASS | 3| 4 +Brand#53 |PROMO BRUSHED BRASS | 9| 4 +Brand#53 |PROMO BRUSHED BRASS | 14| 4 +Brand#53 |PROMO BRUSHED BRASS | 19| 4 +Brand#53 |PROMO BRUSHED BRASS | 23| 4 +Brand#53 |PROMO BRUSHED COPPER | 19| 4 +Brand#53 |PROMO BRUSHED COPPER | 45| 4 +Brand#53 |PROMO BRUSHED NICKEL | 36| 4 +Brand#53 |PROMO BRUSHED NICKEL | 45| 4 +Brand#53 |PROMO BRUSHED STEEL | 9| 4 +Brand#53 |PROMO BRUSHED STEEL | 36| 4 +Brand#53 |PROMO BRUSHED STEEL | 45| 4 +Brand#53 |PROMO BRUSHED TIN | 3| 4 +Brand#53 |PROMO BRUSHED TIN | 45| 4 +Brand#53 |PROMO BURNISHED BRASS | 3| 4 +Brand#53 |PROMO BURNISHED BRASS | 9| 4 +Brand#53 |PROMO BURNISHED BRASS | 45| 4 +Brand#53 |PROMO BURNISHED COPPER | 3| 4 +Brand#53 |PROMO BURNISHED COPPER | 19| 4 +Brand#53 |PROMO BURNISHED COPPER | 23| 4 +Brand#53 |PROMO BURNISHED NICKEL | 3| 4 +Brand#53 |PROMO BURNISHED NICKEL | 23| 4 +Brand#53 |PROMO BURNISHED STEEL | 19| 4 +Brand#53 |PROMO BURNISHED TIN | 14| 4 +Brand#53 |PROMO BURNISHED TIN | 36| 4 +Brand#53 |PROMO PLATED BRASS | 3| 4 +Brand#53 |PROMO PLATED BRASS | 9| 4 +Brand#53 |PROMO PLATED BRASS | 14| 4 +Brand#53 |PROMO PLATED COPPER | 19| 4 +Brand#53 |PROMO PLATED NICKEL | 3| 4 +Brand#53 |PROMO PLATED NICKEL | 9| 4 +Brand#53 |PROMO PLATED NICKEL | 14| 4 +Brand#53 |PROMO PLATED NICKEL | 19| 4 +Brand#53 |PROMO PLATED NICKEL | 23| 4 +Brand#53 |PROMO PLATED NICKEL | 45| 4 +Brand#53 |PROMO PLATED STEEL | 3| 4 +Brand#53 |PROMO PLATED STEEL | 14| 4 +Brand#53 |PROMO PLATED STEEL | 23| 4 +Brand#53 |PROMO PLATED STEEL | 36| 4 +Brand#53 |PROMO PLATED STEEL | 45| 4 +Brand#53 |PROMO PLATED TIN | 36| 4 +Brand#53 |PROMO POLISHED BRASS | 23| 4 +Brand#53 |PROMO POLISHED BRASS | 49| 4 +Brand#53 |PROMO POLISHED COPPER | 9| 4 +Brand#53 |PROMO POLISHED COPPER | 14| 4 +Brand#53 |PROMO POLISHED COPPER | 36| 4 +Brand#53 |PROMO POLISHED COPPER | 45| 4 +Brand#53 |PROMO POLISHED NICKEL | 14| 4 +Brand#53 |PROMO POLISHED NICKEL | 36| 4 +Brand#53 |PROMO POLISHED STEEL | 14| 4 +Brand#53 |PROMO POLISHED STEEL | 19| 4 +Brand#53 |PROMO POLISHED STEEL | 23| 4 +Brand#53 |PROMO POLISHED TIN | 3| 4 +Brand#53 |PROMO POLISHED TIN | 9| 4 +Brand#53 |PROMO POLISHED TIN | 19| 4 +Brand#53 |PROMO POLISHED TIN | 23| 4 +Brand#53 |SMALL ANODIZED BRASS | 14| 4 +Brand#53 |SMALL ANODIZED BRASS | 36| 4 +Brand#53 |SMALL ANODIZED COPPER | 14| 4 +Brand#53 |SMALL ANODIZED COPPER | 45| 4 +Brand#53 |SMALL ANODIZED COPPER | 49| 4 +Brand#53 |SMALL ANODIZED NICKEL | 14| 4 +Brand#53 |SMALL ANODIZED STEEL | 14| 4 +Brand#53 |SMALL ANODIZED STEEL | 36| 4 +Brand#53 |SMALL ANODIZED TIN | 14| 4 +Brand#53 |SMALL ANODIZED TIN | 19| 4 +Brand#53 |SMALL ANODIZED TIN | 23| 4 +Brand#53 |SMALL BRUSHED BRASS | 3| 4 +Brand#53 |SMALL BRUSHED BRASS | 19| 4 +Brand#53 |SMALL BRUSHED BRASS | 23| 4 +Brand#53 |SMALL BRUSHED BRASS | 45| 4 +Brand#53 |SMALL BRUSHED BRASS | 49| 4 +Brand#53 |SMALL BRUSHED COPPER | 9| 4 +Brand#53 |SMALL BRUSHED COPPER | 19| 4 +Brand#53 |SMALL BRUSHED COPPER | 23| 4 +Brand#53 |SMALL BRUSHED COPPER | 45| 4 +Brand#53 |SMALL BRUSHED NICKEL | 9| 4 +Brand#53 |SMALL BRUSHED NICKEL | 14| 4 +Brand#53 |SMALL BRUSHED NICKEL | 19| 4 +Brand#53 |SMALL BRUSHED NICKEL | 23| 4 +Brand#53 |SMALL BRUSHED NICKEL | 36| 4 +Brand#53 |SMALL BRUSHED STEEL | 14| 4 +Brand#53 |SMALL BRUSHED STEEL | 19| 4 +Brand#53 |SMALL BRUSHED TIN | 9| 4 +Brand#53 |SMALL BRUSHED TIN | 36| 4 +Brand#53 |SMALL BURNISHED BRASS | 19| 4 +Brand#53 |SMALL BURNISHED NICKEL | 3| 4 +Brand#53 |SMALL BURNISHED NICKEL | 19| 4 +Brand#53 |SMALL BURNISHED STEEL | 3| 4 +Brand#53 |SMALL BURNISHED STEEL | 14| 4 +Brand#53 |SMALL BURNISHED STEEL | 23| 4 +Brand#53 |SMALL BURNISHED STEEL | 45| 4 +Brand#53 |SMALL BURNISHED TIN | 9| 4 +Brand#53 |SMALL BURNISHED TIN | 19| 4 +Brand#53 |SMALL BURNISHED TIN | 36| 4 +Brand#53 |SMALL BURNISHED TIN | 45| 4 +Brand#53 |SMALL BURNISHED TIN | 49| 4 +Brand#53 |SMALL PLATED BRASS | 14| 4 +Brand#53 |SMALL PLATED BRASS | 19| 4 +Brand#53 |SMALL PLATED BRASS | 23| 4 +Brand#53 |SMALL PLATED COPPER | 45| 4 +Brand#53 |SMALL PLATED NICKEL | 36| 4 +Brand#53 |SMALL PLATED NICKEL | 49| 4 +Brand#53 |SMALL PLATED STEEL | 9| 4 +Brand#53 |SMALL PLATED STEEL | 45| 4 +Brand#53 |SMALL PLATED STEEL | 49| 4 +Brand#53 |SMALL PLATED TIN | 3| 4 +Brand#53 |SMALL PLATED TIN | 23| 4 +Brand#53 |SMALL PLATED TIN | 49| 4 +Brand#53 |SMALL POLISHED BRASS | 23| 4 +Brand#53 |SMALL POLISHED COPPER | 3| 4 +Brand#53 |SMALL POLISHED COPPER | 14| 4 +Brand#53 |SMALL POLISHED COPPER | 36| 4 +Brand#53 |SMALL POLISHED COPPER | 45| 4 +Brand#53 |SMALL POLISHED COPPER | 49| 4 +Brand#53 |SMALL POLISHED NICKEL | 9| 4 +Brand#53 |SMALL POLISHED STEEL | 9| 4 +Brand#53 |SMALL POLISHED STEEL | 19| 4 +Brand#53 |SMALL POLISHED TIN | 9| 4 +Brand#53 |SMALL POLISHED TIN | 14| 4 +Brand#53 |STANDARD ANODIZED BRASS | 3| 4 +Brand#53 |STANDARD ANODIZED BRASS | 9| 4 +Brand#53 |STANDARD ANODIZED BRASS | 49| 4 +Brand#53 |STANDARD ANODIZED COPPER | 3| 4 +Brand#53 |STANDARD ANODIZED COPPER | 23| 4 +Brand#53 |STANDARD ANODIZED COPPER | 45| 4 +Brand#53 |STANDARD ANODIZED COPPER | 49| 4 +Brand#53 |STANDARD ANODIZED NICKEL | 23| 4 +Brand#53 |STANDARD ANODIZED NICKEL | 45| 4 +Brand#53 |STANDARD ANODIZED NICKEL | 49| 4 +Brand#53 |STANDARD ANODIZED STEEL | 3| 4 +Brand#53 |STANDARD ANODIZED STEEL | 14| 4 +Brand#53 |STANDARD ANODIZED STEEL | 36| 4 +Brand#53 |STANDARD ANODIZED TIN | 9| 4 +Brand#53 |STANDARD ANODIZED TIN | 36| 4 +Brand#53 |STANDARD BRUSHED BRASS | 23| 4 +Brand#53 |STANDARD BRUSHED BRASS | 45| 4 +Brand#53 |STANDARD BRUSHED COPPER | 14| 4 +Brand#53 |STANDARD BRUSHED COPPER | 19| 4 +Brand#53 |STANDARD BRUSHED COPPER | 23| 4 +Brand#53 |STANDARD BRUSHED COPPER | 36| 4 +Brand#53 |STANDARD BRUSHED COPPER | 45| 4 +Brand#53 |STANDARD BRUSHED NICKEL | 19| 4 +Brand#53 |STANDARD BRUSHED NICKEL | 23| 4 +Brand#53 |STANDARD BRUSHED STEEL | 3| 4 +Brand#53 |STANDARD BRUSHED STEEL | 9| 4 +Brand#53 |STANDARD BRUSHED STEEL | 14| 4 +Brand#53 |STANDARD BRUSHED STEEL | 19| 4 +Brand#53 |STANDARD BRUSHED STEEL | 36| 4 +Brand#53 |STANDARD BRUSHED TIN | 3| 4 +Brand#53 |STANDARD BRUSHED TIN | 9| 4 +Brand#53 |STANDARD BRUSHED TIN | 23| 4 +Brand#53 |STANDARD BURNISHED BRASS | 3| 4 +Brand#53 |STANDARD BURNISHED BRASS | 14| 4 +Brand#53 |STANDARD BURNISHED BRASS | 23| 4 +Brand#53 |STANDARD BURNISHED BRASS | 45| 4 +Brand#53 |STANDARD BURNISHED COPPER| 9| 4 +Brand#53 |STANDARD BURNISHED COPPER| 14| 4 +Brand#53 |STANDARD BURNISHED COPPER| 49| 4 +Brand#53 |STANDARD BURNISHED NICKEL| 3| 4 +Brand#53 |STANDARD BURNISHED NICKEL| 9| 4 +Brand#53 |STANDARD BURNISHED NICKEL| 14| 4 +Brand#53 |STANDARD BURNISHED NICKEL| 19| 4 +Brand#53 |STANDARD BURNISHED STEEL | 9| 4 +Brand#53 |STANDARD BURNISHED STEEL | 14| 4 +Brand#53 |STANDARD BURNISHED STEEL | 45| 4 +Brand#53 |STANDARD BURNISHED TIN | 9| 4 +Brand#53 |STANDARD BURNISHED TIN | 23| 4 +Brand#53 |STANDARD BURNISHED TIN | 45| 4 +Brand#53 |STANDARD BURNISHED TIN | 49| 4 +Brand#53 |STANDARD PLATED BRASS | 14| 4 +Brand#53 |STANDARD PLATED BRASS | 45| 4 +Brand#53 |STANDARD PLATED BRASS | 49| 4 +Brand#53 |STANDARD PLATED COPPER | 9| 4 +Brand#53 |STANDARD PLATED COPPER | 14| 4 +Brand#53 |STANDARD PLATED COPPER | 19| 4 +Brand#53 |STANDARD PLATED COPPER | 23| 4 +Brand#53 |STANDARD PLATED COPPER | 49| 4 +Brand#53 |STANDARD PLATED NICKEL | 3| 4 +Brand#53 |STANDARD PLATED NICKEL | 9| 4 +Brand#53 |STANDARD PLATED NICKEL | 23| 4 +Brand#53 |STANDARD PLATED NICKEL | 49| 4 +Brand#53 |STANDARD PLATED STEEL | 3| 4 +Brand#53 |STANDARD PLATED STEEL | 9| 4 +Brand#53 |STANDARD PLATED STEEL | 36| 4 +Brand#53 |STANDARD PLATED STEEL | 49| 4 +Brand#53 |STANDARD PLATED TIN | 3| 4 +Brand#53 |STANDARD PLATED TIN | 49| 4 +Brand#53 |STANDARD POLISHED BRASS | 9| 4 +Brand#53 |STANDARD POLISHED BRASS | 14| 4 +Brand#53 |STANDARD POLISHED BRASS | 23| 4 +Brand#53 |STANDARD POLISHED COPPER | 9| 4 +Brand#53 |STANDARD POLISHED COPPER | 23| 4 +Brand#53 |STANDARD POLISHED NICKEL | 19| 4 +Brand#53 |STANDARD POLISHED NICKEL | 45| 4 +Brand#53 |STANDARD POLISHED STEEL | 3| 4 +Brand#53 |STANDARD POLISHED STEEL | 36| 4 +Brand#53 |STANDARD POLISHED TIN | 3| 4 +Brand#53 |STANDARD POLISHED TIN | 36| 4 +Brand#54 |ECONOMY ANODIZED BRASS | 9| 4 +Brand#54 |ECONOMY ANODIZED BRASS | 19| 4 +Brand#54 |ECONOMY ANODIZED BRASS | 23| 4 +Brand#54 |ECONOMY ANODIZED BRASS | 45| 4 +Brand#54 |ECONOMY ANODIZED BRASS | 49| 4 +Brand#54 |ECONOMY ANODIZED COPPER | 3| 4 +Brand#54 |ECONOMY ANODIZED COPPER | 9| 4 +Brand#54 |ECONOMY ANODIZED COPPER | 23| 4 +Brand#54 |ECONOMY ANODIZED COPPER | 36| 4 +Brand#54 |ECONOMY ANODIZED COPPER | 45| 4 +Brand#54 |ECONOMY ANODIZED COPPER | 49| 4 +Brand#54 |ECONOMY ANODIZED NICKEL | 3| 4 +Brand#54 |ECONOMY ANODIZED NICKEL | 14| 4 +Brand#54 |ECONOMY ANODIZED NICKEL | 19| 4 +Brand#54 |ECONOMY ANODIZED NICKEL | 45| 4 +Brand#54 |ECONOMY ANODIZED STEEL | 3| 4 +Brand#54 |ECONOMY ANODIZED STEEL | 14| 4 +Brand#54 |ECONOMY ANODIZED STEEL | 36| 4 +Brand#54 |ECONOMY ANODIZED STEEL | 45| 4 +Brand#54 |ECONOMY ANODIZED TIN | 9| 4 +Brand#54 |ECONOMY ANODIZED TIN | 23| 4 +Brand#54 |ECONOMY ANODIZED TIN | 49| 4 +Brand#54 |ECONOMY BRUSHED COPPER | 19| 4 +Brand#54 |ECONOMY BRUSHED COPPER | 23| 4 +Brand#54 |ECONOMY BRUSHED COPPER | 36| 4 +Brand#54 |ECONOMY BRUSHED COPPER | 49| 4 +Brand#54 |ECONOMY BRUSHED NICKEL | 3| 4 +Brand#54 |ECONOMY BRUSHED NICKEL | 19| 4 +Brand#54 |ECONOMY BRUSHED NICKEL | 45| 4 +Brand#54 |ECONOMY BRUSHED STEEL | 9| 4 +Brand#54 |ECONOMY BRUSHED TIN | 19| 4 +Brand#54 |ECONOMY BRUSHED TIN | 49| 4 +Brand#54 |ECONOMY BURNISHED BRASS | 3| 4 +Brand#54 |ECONOMY BURNISHED BRASS | 23| 4 +Brand#54 |ECONOMY BURNISHED BRASS | 49| 4 +Brand#54 |ECONOMY BURNISHED COPPER | 23| 4 +Brand#54 |ECONOMY BURNISHED NICKEL | 3| 4 +Brand#54 |ECONOMY BURNISHED NICKEL | 14| 4 +Brand#54 |ECONOMY BURNISHED NICKEL | 45| 4 +Brand#54 |ECONOMY BURNISHED NICKEL | 49| 4 +Brand#54 |ECONOMY BURNISHED STEEL | 19| 4 +Brand#54 |ECONOMY BURNISHED TIN | 3| 4 +Brand#54 |ECONOMY BURNISHED TIN | 19| 4 +Brand#54 |ECONOMY BURNISHED TIN | 49| 4 +Brand#54 |ECONOMY PLATED BRASS | 9| 4 +Brand#54 |ECONOMY PLATED BRASS | 14| 4 +Brand#54 |ECONOMY PLATED BRASS | 19| 4 +Brand#54 |ECONOMY PLATED BRASS | 23| 4 +Brand#54 |ECONOMY PLATED BRASS | 36| 4 +Brand#54 |ECONOMY PLATED BRASS | 45| 4 +Brand#54 |ECONOMY PLATED COPPER | 3| 4 +Brand#54 |ECONOMY PLATED COPPER | 23| 4 +Brand#54 |ECONOMY PLATED NICKEL | 3| 4 +Brand#54 |ECONOMY PLATED NICKEL | 14| 4 +Brand#54 |ECONOMY PLATED NICKEL | 19| 4 +Brand#54 |ECONOMY PLATED STEEL | 14| 4 +Brand#54 |ECONOMY PLATED STEEL | 23| 4 +Brand#54 |ECONOMY PLATED STEEL | 36| 4 +Brand#54 |ECONOMY PLATED STEEL | 45| 4 +Brand#54 |ECONOMY PLATED STEEL | 49| 4 +Brand#54 |ECONOMY PLATED TIN | 3| 4 +Brand#54 |ECONOMY PLATED TIN | 9| 4 +Brand#54 |ECONOMY PLATED TIN | 14| 4 +Brand#54 |ECONOMY PLATED TIN | 19| 4 +Brand#54 |ECONOMY PLATED TIN | 45| 4 +Brand#54 |ECONOMY POLISHED BRASS | 3| 4 +Brand#54 |ECONOMY POLISHED BRASS | 19| 4 +Brand#54 |ECONOMY POLISHED COPPER | 14| 4 +Brand#54 |ECONOMY POLISHED NICKEL | 19| 4 +Brand#54 |ECONOMY POLISHED STEEL | 9| 4 +Brand#54 |ECONOMY POLISHED TIN | 14| 4 +Brand#54 |ECONOMY POLISHED TIN | 49| 4 +Brand#54 |LARGE ANODIZED BRASS | 14| 4 +Brand#54 |LARGE ANODIZED BRASS | 23| 4 +Brand#54 |LARGE ANODIZED BRASS | 36| 4 +Brand#54 |LARGE ANODIZED BRASS | 49| 4 +Brand#54 |LARGE ANODIZED COPPER | 19| 4 +Brand#54 |LARGE ANODIZED COPPER | 23| 4 +Brand#54 |LARGE ANODIZED COPPER | 36| 4 +Brand#54 |LARGE ANODIZED NICKEL | 3| 4 +Brand#54 |LARGE ANODIZED NICKEL | 14| 4 +Brand#54 |LARGE ANODIZED NICKEL | 19| 4 +Brand#54 |LARGE ANODIZED NICKEL | 36| 4 +Brand#54 |LARGE ANODIZED NICKEL | 45| 4 +Brand#54 |LARGE ANODIZED STEEL | 3| 4 +Brand#54 |LARGE ANODIZED STEEL | 19| 4 +Brand#54 |LARGE ANODIZED STEEL | 36| 4 +Brand#54 |LARGE ANODIZED TIN | 3| 4 +Brand#54 |LARGE ANODIZED TIN | 9| 4 +Brand#54 |LARGE ANODIZED TIN | 19| 4 +Brand#54 |LARGE ANODIZED TIN | 45| 4 +Brand#54 |LARGE BRUSHED BRASS | 36| 4 +Brand#54 |LARGE BRUSHED COPPER | 3| 4 +Brand#54 |LARGE BRUSHED COPPER | 36| 4 +Brand#54 |LARGE BRUSHED COPPER | 49| 4 +Brand#54 |LARGE BRUSHED NICKEL | 14| 4 +Brand#54 |LARGE BRUSHED NICKEL | 19| 4 +Brand#54 |LARGE BRUSHED NICKEL | 45| 4 +Brand#54 |LARGE BRUSHED NICKEL | 49| 4 +Brand#54 |LARGE BRUSHED STEEL | 3| 4 +Brand#54 |LARGE BRUSHED STEEL | 9| 4 +Brand#54 |LARGE BRUSHED STEEL | 19| 4 +Brand#54 |LARGE BRUSHED STEEL | 23| 4 +Brand#54 |LARGE BRUSHED STEEL | 45| 4 +Brand#54 |LARGE BRUSHED TIN | 14| 4 +Brand#54 |LARGE BRUSHED TIN | 19| 4 +Brand#54 |LARGE BRUSHED TIN | 45| 4 +Brand#54 |LARGE BURNISHED BRASS | 14| 4 +Brand#54 |LARGE BURNISHED BRASS | 19| 4 +Brand#54 |LARGE BURNISHED BRASS | 36| 4 +Brand#54 |LARGE BURNISHED NICKEL | 3| 4 +Brand#54 |LARGE BURNISHED NICKEL | 19| 4 +Brand#54 |LARGE BURNISHED NICKEL | 45| 4 +Brand#54 |LARGE BURNISHED STEEL | 9| 4 +Brand#54 |LARGE BURNISHED STEEL | 36| 4 +Brand#54 |LARGE BURNISHED STEEL | 45| 4 +Brand#54 |LARGE BURNISHED TIN | 9| 4 +Brand#54 |LARGE BURNISHED TIN | 23| 4 +Brand#54 |LARGE BURNISHED TIN | 36| 4 +Brand#54 |LARGE PLATED BRASS | 3| 4 +Brand#54 |LARGE PLATED BRASS | 14| 4 +Brand#54 |LARGE PLATED COPPER | 14| 4 +Brand#54 |LARGE PLATED COPPER | 36| 4 +Brand#54 |LARGE PLATED NICKEL | 9| 4 +Brand#54 |LARGE PLATED NICKEL | 14| 4 +Brand#54 |LARGE PLATED NICKEL | 19| 4 +Brand#54 |LARGE PLATED NICKEL | 45| 4 +Brand#54 |LARGE PLATED NICKEL | 49| 4 +Brand#54 |LARGE PLATED STEEL | 45| 4 +Brand#54 |LARGE PLATED TIN | 3| 4 +Brand#54 |LARGE PLATED TIN | 14| 4 +Brand#54 |LARGE PLATED TIN | 49| 4 +Brand#54 |LARGE POLISHED BRASS | 3| 4 +Brand#54 |LARGE POLISHED BRASS | 14| 4 +Brand#54 |LARGE POLISHED BRASS | 19| 4 +Brand#54 |LARGE POLISHED BRASS | 36| 4 +Brand#54 |LARGE POLISHED COPPER | 14| 4 +Brand#54 |LARGE POLISHED COPPER | 23| 4 +Brand#54 |LARGE POLISHED COPPER | 36| 4 +Brand#54 |LARGE POLISHED COPPER | 49| 4 +Brand#54 |LARGE POLISHED NICKEL | 45| 4 +Brand#54 |LARGE POLISHED NICKEL | 49| 4 +Brand#54 |LARGE POLISHED STEEL | 9| 4 +Brand#54 |LARGE POLISHED STEEL | 23| 4 +Brand#54 |LARGE POLISHED STEEL | 36| 4 +Brand#54 |LARGE POLISHED TIN | 3| 4 +Brand#54 |LARGE POLISHED TIN | 9| 4 +Brand#54 |LARGE POLISHED TIN | 23| 4 +Brand#54 |MEDIUM ANODIZED BRASS | 19| 4 +Brand#54 |MEDIUM ANODIZED BRASS | 23| 4 +Brand#54 |MEDIUM ANODIZED BRASS | 45| 4 +Brand#54 |MEDIUM ANODIZED COPPER | 3| 4 +Brand#54 |MEDIUM ANODIZED COPPER | 14| 4 +Brand#54 |MEDIUM ANODIZED COPPER | 36| 4 +Brand#54 |MEDIUM ANODIZED COPPER | 45| 4 +Brand#54 |MEDIUM ANODIZED NICKEL | 9| 4 +Brand#54 |MEDIUM ANODIZED STEEL | 14| 4 +Brand#54 |MEDIUM ANODIZED STEEL | 45| 4 +Brand#54 |MEDIUM ANODIZED TIN | 14| 4 +Brand#54 |MEDIUM ANODIZED TIN | 49| 4 +Brand#54 |MEDIUM BRUSHED BRASS | 36| 4 +Brand#54 |MEDIUM BRUSHED COPPER | 9| 4 +Brand#54 |MEDIUM BRUSHED COPPER | 45| 4 +Brand#54 |MEDIUM BRUSHED COPPER | 49| 4 +Brand#54 |MEDIUM BRUSHED NICKEL | 3| 4 +Brand#54 |MEDIUM BRUSHED NICKEL | 19| 4 +Brand#54 |MEDIUM BRUSHED NICKEL | 45| 4 +Brand#54 |MEDIUM BRUSHED NICKEL | 49| 4 +Brand#54 |MEDIUM BRUSHED STEEL | 3| 4 +Brand#54 |MEDIUM BRUSHED STEEL | 14| 4 +Brand#54 |MEDIUM BRUSHED STEEL | 19| 4 +Brand#54 |MEDIUM BRUSHED STEEL | 23| 4 +Brand#54 |MEDIUM BRUSHED TIN | 3| 4 +Brand#54 |MEDIUM BRUSHED TIN | 19| 4 +Brand#54 |MEDIUM BRUSHED TIN | 45| 4 +Brand#54 |MEDIUM BURNISHED BRASS | 3| 4 +Brand#54 |MEDIUM BURNISHED BRASS | 9| 4 +Brand#54 |MEDIUM BURNISHED BRASS | 14| 4 +Brand#54 |MEDIUM BURNISHED BRASS | 19| 4 +Brand#54 |MEDIUM BURNISHED BRASS | 45| 4 +Brand#54 |MEDIUM BURNISHED COPPER | 9| 4 +Brand#54 |MEDIUM BURNISHED COPPER | 49| 4 +Brand#54 |MEDIUM BURNISHED NICKEL | 3| 4 +Brand#54 |MEDIUM BURNISHED NICKEL | 14| 4 +Brand#54 |MEDIUM BURNISHED NICKEL | 23| 4 +Brand#54 |MEDIUM BURNISHED NICKEL | 36| 4 +Brand#54 |MEDIUM BURNISHED STEEL | 9| 4 +Brand#54 |MEDIUM BURNISHED STEEL | 23| 4 +Brand#54 |MEDIUM BURNISHED STEEL | 36| 4 +Brand#54 |MEDIUM BURNISHED TIN | 14| 4 +Brand#54 |MEDIUM BURNISHED TIN | 49| 4 +Brand#54 |MEDIUM PLATED BRASS | 9| 4 +Brand#54 |MEDIUM PLATED BRASS | 14| 4 +Brand#54 |MEDIUM PLATED BRASS | 19| 4 +Brand#54 |MEDIUM PLATED BRASS | 45| 4 +Brand#54 |MEDIUM PLATED COPPER | 3| 4 +Brand#54 |MEDIUM PLATED COPPER | 19| 4 +Brand#54 |MEDIUM PLATED NICKEL | 3| 4 +Brand#54 |MEDIUM PLATED NICKEL | 36| 4 +Brand#54 |MEDIUM PLATED STEEL | 3| 4 +Brand#54 |MEDIUM PLATED STEEL | 9| 4 +Brand#54 |MEDIUM PLATED STEEL | 19| 4 +Brand#54 |MEDIUM PLATED STEEL | 23| 4 +Brand#54 |MEDIUM PLATED STEEL | 36| 4 +Brand#54 |MEDIUM PLATED STEEL | 45| 4 +Brand#54 |MEDIUM PLATED STEEL | 49| 4 +Brand#54 |MEDIUM PLATED TIN | 3| 4 +Brand#54 |MEDIUM PLATED TIN | 9| 4 +Brand#54 |MEDIUM PLATED TIN | 14| 4 +Brand#54 |MEDIUM PLATED TIN | 36| 4 +Brand#54 |PROMO ANODIZED COPPER | 19| 4 +Brand#54 |PROMO ANODIZED NICKEL | 3| 4 +Brand#54 |PROMO ANODIZED NICKEL | 9| 4 +Brand#54 |PROMO ANODIZED NICKEL | 19| 4 +Brand#54 |PROMO ANODIZED NICKEL | 45| 4 +Brand#54 |PROMO ANODIZED NICKEL | 49| 4 +Brand#54 |PROMO ANODIZED STEEL | 45| 4 +Brand#54 |PROMO ANODIZED STEEL | 49| 4 +Brand#54 |PROMO ANODIZED TIN | 3| 4 +Brand#54 |PROMO ANODIZED TIN | 23| 4 +Brand#54 |PROMO ANODIZED TIN | 36| 4 +Brand#54 |PROMO BRUSHED BRASS | 3| 4 +Brand#54 |PROMO BRUSHED BRASS | 36| 4 +Brand#54 |PROMO BRUSHED BRASS | 45| 4 +Brand#54 |PROMO BRUSHED COPPER | 9| 4 +Brand#54 |PROMO BRUSHED COPPER | 19| 4 +Brand#54 |PROMO BRUSHED COPPER | 36| 4 +Brand#54 |PROMO BRUSHED NICKEL | 14| 4 +Brand#54 |PROMO BRUSHED NICKEL | 36| 4 +Brand#54 |PROMO BRUSHED NICKEL | 45| 4 +Brand#54 |PROMO BRUSHED NICKEL | 49| 4 +Brand#54 |PROMO BRUSHED STEEL | 9| 4 +Brand#54 |PROMO BRUSHED STEEL | 23| 4 +Brand#54 |PROMO BRUSHED TIN | 19| 4 +Brand#54 |PROMO BRUSHED TIN | 23| 4 +Brand#54 |PROMO BRUSHED TIN | 36| 4 +Brand#54 |PROMO BURNISHED BRASS | 3| 4 +Brand#54 |PROMO BURNISHED BRASS | 23| 4 +Brand#54 |PROMO BURNISHED BRASS | 45| 4 +Brand#54 |PROMO BURNISHED COPPER | 3| 4 +Brand#54 |PROMO BURNISHED COPPER | 19| 4 +Brand#54 |PROMO BURNISHED COPPER | 23| 4 +Brand#54 |PROMO BURNISHED COPPER | 36| 4 +Brand#54 |PROMO BURNISHED COPPER | 45| 4 +Brand#54 |PROMO BURNISHED NICKEL | 3| 4 +Brand#54 |PROMO BURNISHED NICKEL | 14| 4 +Brand#54 |PROMO BURNISHED STEEL | 19| 4 +Brand#54 |PROMO BURNISHED STEEL | 45| 4 +Brand#54 |PROMO BURNISHED STEEL | 49| 4 +Brand#54 |PROMO BURNISHED TIN | 49| 4 +Brand#54 |PROMO PLATED BRASS | 3| 4 +Brand#54 |PROMO PLATED BRASS | 9| 4 +Brand#54 |PROMO PLATED BRASS | 14| 4 +Brand#54 |PROMO PLATED BRASS | 36| 4 +Brand#54 |PROMO PLATED COPPER | 3| 4 +Brand#54 |PROMO PLATED COPPER | 14| 4 +Brand#54 |PROMO PLATED COPPER | 19| 4 +Brand#54 |PROMO PLATED NICKEL | 23| 4 +Brand#54 |PROMO PLATED NICKEL | 36| 4 +Brand#54 |PROMO PLATED NICKEL | 45| 4 +Brand#54 |PROMO PLATED STEEL | 3| 4 +Brand#54 |PROMO PLATED STEEL | 14| 4 +Brand#54 |PROMO PLATED STEEL | 19| 4 +Brand#54 |PROMO PLATED STEEL | 23| 4 +Brand#54 |PROMO PLATED TIN | 9| 4 +Brand#54 |PROMO PLATED TIN | 19| 4 +Brand#54 |PROMO POLISHED BRASS | 3| 4 +Brand#54 |PROMO POLISHED BRASS | 19| 4 +Brand#54 |PROMO POLISHED BRASS | 23| 4 +Brand#54 |PROMO POLISHED BRASS | 45| 4 +Brand#54 |PROMO POLISHED BRASS | 49| 4 +Brand#54 |PROMO POLISHED COPPER | 9| 4 +Brand#54 |PROMO POLISHED COPPER | 49| 4 +Brand#54 |PROMO POLISHED NICKEL | 3| 4 +Brand#54 |PROMO POLISHED NICKEL | 9| 4 +Brand#54 |PROMO POLISHED NICKEL | 45| 4 +Brand#54 |PROMO POLISHED NICKEL | 49| 4 +Brand#54 |PROMO POLISHED TIN | 9| 4 +Brand#54 |PROMO POLISHED TIN | 36| 4 +Brand#54 |SMALL ANODIZED BRASS | 3| 4 +Brand#54 |SMALL ANODIZED BRASS | 36| 4 +Brand#54 |SMALL ANODIZED BRASS | 49| 4 +Brand#54 |SMALL ANODIZED COPPER | 9| 4 +Brand#54 |SMALL ANODIZED COPPER | 36| 4 +Brand#54 |SMALL ANODIZED NICKEL | 3| 4 +Brand#54 |SMALL ANODIZED NICKEL | 9| 4 +Brand#54 |SMALL ANODIZED NICKEL | 36| 4 +Brand#54 |SMALL ANODIZED STEEL | 14| 4 +Brand#54 |SMALL ANODIZED STEEL | 19| 4 +Brand#54 |SMALL ANODIZED TIN | 3| 4 +Brand#54 |SMALL ANODIZED TIN | 9| 4 +Brand#54 |SMALL ANODIZED TIN | 19| 4 +Brand#54 |SMALL ANODIZED TIN | 23| 4 +Brand#54 |SMALL ANODIZED TIN | 45| 4 +Brand#54 |SMALL ANODIZED TIN | 49| 4 +Brand#54 |SMALL BRUSHED BRASS | 3| 4 +Brand#54 |SMALL BRUSHED BRASS | 14| 4 +Brand#54 |SMALL BRUSHED BRASS | 45| 4 +Brand#54 |SMALL BRUSHED COPPER | 3| 4 +Brand#54 |SMALL BRUSHED COPPER | 14| 4 +Brand#54 |SMALL BRUSHED COPPER | 36| 4 +Brand#54 |SMALL BRUSHED COPPER | 49| 4 +Brand#54 |SMALL BRUSHED NICKEL | 3| 4 +Brand#54 |SMALL BRUSHED NICKEL | 9| 4 +Brand#54 |SMALL BRUSHED NICKEL | 19| 4 +Brand#54 |SMALL BRUSHED NICKEL | 23| 4 +Brand#54 |SMALL BRUSHED NICKEL | 49| 4 +Brand#54 |SMALL BRUSHED STEEL | 3| 4 +Brand#54 |SMALL BRUSHED STEEL | 9| 4 +Brand#54 |SMALL BRUSHED STEEL | 45| 4 +Brand#54 |SMALL BRUSHED STEEL | 49| 4 +Brand#54 |SMALL BRUSHED TIN | 9| 4 +Brand#54 |SMALL BURNISHED BRASS | 19| 4 +Brand#54 |SMALL BURNISHED BRASS | 36| 4 +Brand#54 |SMALL BURNISHED BRASS | 49| 4 +Brand#54 |SMALL BURNISHED COPPER | 9| 4 +Brand#54 |SMALL BURNISHED COPPER | 36| 4 +Brand#54 |SMALL BURNISHED NICKEL | 9| 4 +Brand#54 |SMALL BURNISHED NICKEL | 19| 4 +Brand#54 |SMALL BURNISHED NICKEL | 23| 4 +Brand#54 |SMALL BURNISHED NICKEL | 45| 4 +Brand#54 |SMALL BURNISHED STEEL | 14| 4 +Brand#54 |SMALL BURNISHED STEEL | 23| 4 +Brand#54 |SMALL BURNISHED STEEL | 36| 4 +Brand#54 |SMALL BURNISHED STEEL | 49| 4 +Brand#54 |SMALL BURNISHED TIN | 3| 4 +Brand#54 |SMALL BURNISHED TIN | 14| 4 +Brand#54 |SMALL BURNISHED TIN | 36| 4 +Brand#54 |SMALL BURNISHED TIN | 45| 4 +Brand#54 |SMALL PLATED BRASS | 36| 4 +Brand#54 |SMALL PLATED BRASS | 45| 4 +Brand#54 |SMALL PLATED BRASS | 49| 4 +Brand#54 |SMALL PLATED COPPER | 9| 4 +Brand#54 |SMALL PLATED COPPER | 49| 4 +Brand#54 |SMALL PLATED NICKEL | 3| 4 +Brand#54 |SMALL PLATED NICKEL | 19| 4 +Brand#54 |SMALL PLATED NICKEL | 49| 4 +Brand#54 |SMALL PLATED STEEL | 3| 4 +Brand#54 |SMALL PLATED STEEL | 9| 4 +Brand#54 |SMALL PLATED STEEL | 19| 4 +Brand#54 |SMALL PLATED STEEL | 36| 4 +Brand#54 |SMALL PLATED STEEL | 45| 4 +Brand#54 |SMALL PLATED TIN | 9| 4 +Brand#54 |SMALL PLATED TIN | 49| 4 +Brand#54 |SMALL POLISHED BRASS | 14| 4 +Brand#54 |SMALL POLISHED BRASS | 23| 4 +Brand#54 |SMALL POLISHED BRASS | 49| 4 +Brand#54 |SMALL POLISHED COPPER | 9| 4 +Brand#54 |SMALL POLISHED COPPER | 23| 4 +Brand#54 |SMALL POLISHED NICKEL | 9| 4 +Brand#54 |SMALL POLISHED NICKEL | 19| 4 +Brand#54 |SMALL POLISHED NICKEL | 45| 4 +Brand#54 |SMALL POLISHED STEEL | 14| 4 +Brand#54 |SMALL POLISHED STEEL | 19| 4 +Brand#54 |SMALL POLISHED STEEL | 36| 4 +Brand#54 |SMALL POLISHED STEEL | 45| 4 +Brand#54 |SMALL POLISHED TIN | 3| 4 +Brand#54 |SMALL POLISHED TIN | 9| 4 +Brand#54 |SMALL POLISHED TIN | 14| 4 +Brand#54 |SMALL POLISHED TIN | 19| 4 +Brand#54 |STANDARD ANODIZED BRASS | 14| 4 +Brand#54 |STANDARD ANODIZED BRASS | 19| 4 +Brand#54 |STANDARD ANODIZED BRASS | 36| 4 +Brand#54 |STANDARD ANODIZED BRASS | 49| 4 +Brand#54 |STANDARD ANODIZED COPPER | 3| 4 +Brand#54 |STANDARD ANODIZED COPPER | 19| 4 +Brand#54 |STANDARD ANODIZED COPPER | 45| 4 +Brand#54 |STANDARD ANODIZED NICKEL | 14| 4 +Brand#54 |STANDARD ANODIZED NICKEL | 45| 4 +Brand#54 |STANDARD ANODIZED STEEL | 9| 4 +Brand#54 |STANDARD ANODIZED STEEL | 19| 4 +Brand#54 |STANDARD ANODIZED STEEL | 36| 4 +Brand#54 |STANDARD ANODIZED STEEL | 45| 4 +Brand#54 |STANDARD ANODIZED TIN | 3| 4 +Brand#54 |STANDARD ANODIZED TIN | 23| 4 +Brand#54 |STANDARD ANODIZED TIN | 36| 4 +Brand#54 |STANDARD ANODIZED TIN | 49| 4 +Brand#54 |STANDARD BRUSHED BRASS | 3| 4 +Brand#54 |STANDARD BRUSHED BRASS | 23| 4 +Brand#54 |STANDARD BRUSHED COPPER | 3| 4 +Brand#54 |STANDARD BRUSHED COPPER | 9| 4 +Brand#54 |STANDARD BRUSHED NICKEL | 19| 4 +Brand#54 |STANDARD BRUSHED NICKEL | 45| 4 +Brand#54 |STANDARD BRUSHED STEEL | 3| 4 +Brand#54 |STANDARD BRUSHED STEEL | 14| 4 +Brand#54 |STANDARD BRUSHED STEEL | 19| 4 +Brand#54 |STANDARD BRUSHED TIN | 3| 4 +Brand#54 |STANDARD BRUSHED TIN | 36| 4 +Brand#54 |STANDARD BURNISHED BRASS | 19| 4 +Brand#54 |STANDARD BURNISHED BRASS | 23| 4 +Brand#54 |STANDARD BURNISHED BRASS | 49| 4 +Brand#54 |STANDARD BURNISHED COPPER| 14| 4 +Brand#54 |STANDARD BURNISHED COPPER| 23| 4 +Brand#54 |STANDARD BURNISHED NICKEL| 9| 4 +Brand#54 |STANDARD BURNISHED NICKEL| 19| 4 +Brand#54 |STANDARD BURNISHED NICKEL| 36| 4 +Brand#54 |STANDARD BURNISHED STEEL | 3| 4 +Brand#54 |STANDARD BURNISHED STEEL | 9| 4 +Brand#54 |STANDARD BURNISHED STEEL | 36| 4 +Brand#54 |STANDARD BURNISHED STEEL | 45| 4 +Brand#54 |STANDARD BURNISHED TIN | 3| 4 +Brand#54 |STANDARD BURNISHED TIN | 9| 4 +Brand#54 |STANDARD BURNISHED TIN | 36| 4 +Brand#54 |STANDARD BURNISHED TIN | 45| 4 +Brand#54 |STANDARD PLATED BRASS | 9| 4 +Brand#54 |STANDARD PLATED BRASS | 14| 4 +Brand#54 |STANDARD PLATED BRASS | 36| 4 +Brand#54 |STANDARD PLATED BRASS | 49| 4 +Brand#54 |STANDARD PLATED COPPER | 14| 4 +Brand#54 |STANDARD PLATED NICKEL | 3| 4 +Brand#54 |STANDARD PLATED NICKEL | 23| 4 +Brand#54 |STANDARD PLATED STEEL | 3| 4 +Brand#54 |STANDARD PLATED STEEL | 9| 4 +Brand#54 |STANDARD PLATED STEEL | 14| 4 +Brand#54 |STANDARD PLATED STEEL | 19| 4 +Brand#54 |STANDARD PLATED STEEL | 23| 4 +Brand#54 |STANDARD PLATED STEEL | 49| 4 +Brand#54 |STANDARD PLATED TIN | 9| 4 +Brand#54 |STANDARD POLISHED BRASS | 36| 4 +Brand#54 |STANDARD POLISHED COPPER | 36| 4 +Brand#54 |STANDARD POLISHED COPPER | 49| 4 +Brand#54 |STANDARD POLISHED NICKEL | 3| 4 +Brand#54 |STANDARD POLISHED NICKEL | 9| 4 +Brand#54 |STANDARD POLISHED NICKEL | 19| 4 +Brand#54 |STANDARD POLISHED NICKEL | 45| 4 +Brand#54 |STANDARD POLISHED NICKEL | 49| 4 +Brand#54 |STANDARD POLISHED STEEL | 3| 4 +Brand#54 |STANDARD POLISHED STEEL | 23| 4 +Brand#54 |STANDARD POLISHED STEEL | 45| 4 +Brand#54 |STANDARD POLISHED TIN | 3| 4 +Brand#54 |STANDARD POLISHED TIN | 23| 4 +Brand#55 |ECONOMY ANODIZED BRASS | 3| 4 +Brand#55 |ECONOMY ANODIZED BRASS | 14| 4 +Brand#55 |ECONOMY ANODIZED BRASS | 19| 4 +Brand#55 |ECONOMY ANODIZED BRASS | 23| 4 +Brand#55 |ECONOMY ANODIZED BRASS | 49| 4 +Brand#55 |ECONOMY ANODIZED COPPER | 3| 4 +Brand#55 |ECONOMY ANODIZED COPPER | 19| 4 +Brand#55 |ECONOMY ANODIZED COPPER | 36| 4 +Brand#55 |ECONOMY ANODIZED NICKEL | 3| 4 +Brand#55 |ECONOMY ANODIZED NICKEL | 19| 4 +Brand#55 |ECONOMY ANODIZED NICKEL | 23| 4 +Brand#55 |ECONOMY ANODIZED NICKEL | 36| 4 +Brand#55 |ECONOMY ANODIZED STEEL | 3| 4 +Brand#55 |ECONOMY ANODIZED STEEL | 23| 4 +Brand#55 |ECONOMY ANODIZED STEEL | 45| 4 +Brand#55 |ECONOMY ANODIZED TIN | 3| 4 +Brand#55 |ECONOMY BRUSHED BRASS | 9| 4 +Brand#55 |ECONOMY BRUSHED BRASS | 14| 4 +Brand#55 |ECONOMY BRUSHED BRASS | 19| 4 +Brand#55 |ECONOMY BRUSHED BRASS | 36| 4 +Brand#55 |ECONOMY BRUSHED BRASS | 45| 4 +Brand#55 |ECONOMY BRUSHED BRASS | 49| 4 +Brand#55 |ECONOMY BRUSHED COPPER | 3| 4 +Brand#55 |ECONOMY BRUSHED COPPER | 19| 4 +Brand#55 |ECONOMY BRUSHED COPPER | 45| 4 +Brand#55 |ECONOMY BRUSHED COPPER | 49| 4 +Brand#55 |ECONOMY BRUSHED NICKEL | 3| 4 +Brand#55 |ECONOMY BRUSHED NICKEL | 9| 4 +Brand#55 |ECONOMY BRUSHED NICKEL | 14| 4 +Brand#55 |ECONOMY BRUSHED NICKEL | 36| 4 +Brand#55 |ECONOMY BRUSHED NICKEL | 49| 4 +Brand#55 |ECONOMY BRUSHED STEEL | 14| 4 +Brand#55 |ECONOMY BRUSHED STEEL | 19| 4 +Brand#55 |ECONOMY BRUSHED STEEL | 23| 4 +Brand#55 |ECONOMY BRUSHED STEEL | 45| 4 +Brand#55 |ECONOMY BRUSHED TIN | 9| 4 +Brand#55 |ECONOMY BRUSHED TIN | 14| 4 +Brand#55 |ECONOMY BRUSHED TIN | 19| 4 +Brand#55 |ECONOMY BRUSHED TIN | 49| 4 +Brand#55 |ECONOMY BURNISHED BRASS | 36| 4 +Brand#55 |ECONOMY BURNISHED BRASS | 45| 4 +Brand#55 |ECONOMY BURNISHED BRASS | 49| 4 +Brand#55 |ECONOMY BURNISHED COPPER | 3| 4 +Brand#55 |ECONOMY BURNISHED COPPER | 14| 4 +Brand#55 |ECONOMY BURNISHED COPPER | 36| 4 +Brand#55 |ECONOMY BURNISHED NICKEL | 9| 4 +Brand#55 |ECONOMY BURNISHED NICKEL | 36| 4 +Brand#55 |ECONOMY BURNISHED NICKEL | 45| 4 +Brand#55 |ECONOMY BURNISHED STEEL | 3| 4 +Brand#55 |ECONOMY BURNISHED STEEL | 14| 4 +Brand#55 |ECONOMY BURNISHED STEEL | 36| 4 +Brand#55 |ECONOMY BURNISHED STEEL | 45| 4 +Brand#55 |ECONOMY BURNISHED TIN | 14| 4 +Brand#55 |ECONOMY PLATED BRASS | 3| 4 +Brand#55 |ECONOMY PLATED BRASS | 9| 4 +Brand#55 |ECONOMY PLATED BRASS | 14| 4 +Brand#55 |ECONOMY PLATED BRASS | 23| 4 +Brand#55 |ECONOMY PLATED BRASS | 36| 4 +Brand#55 |ECONOMY PLATED COPPER | 3| 4 +Brand#55 |ECONOMY PLATED COPPER | 9| 4 +Brand#55 |ECONOMY PLATED COPPER | 14| 4 +Brand#55 |ECONOMY PLATED NICKEL | 45| 4 +Brand#55 |ECONOMY PLATED STEEL | 3| 4 +Brand#55 |ECONOMY PLATED STEEL | 19| 4 +Brand#55 |ECONOMY PLATED STEEL | 36| 4 +Brand#55 |ECONOMY PLATED TIN | 3| 4 +Brand#55 |ECONOMY PLATED TIN | 14| 4 +Brand#55 |ECONOMY PLATED TIN | 36| 4 +Brand#55 |ECONOMY PLATED TIN | 45| 4 +Brand#55 |ECONOMY PLATED TIN | 49| 4 +Brand#55 |ECONOMY POLISHED BRASS | 3| 4 +Brand#55 |ECONOMY POLISHED BRASS | 9| 4 +Brand#55 |ECONOMY POLISHED BRASS | 14| 4 +Brand#55 |ECONOMY POLISHED BRASS | 36| 4 +Brand#55 |ECONOMY POLISHED BRASS | 45| 4 +Brand#55 |ECONOMY POLISHED COPPER | 14| 4 +Brand#55 |ECONOMY POLISHED NICKEL | 3| 4 +Brand#55 |ECONOMY POLISHED NICKEL | 23| 4 +Brand#55 |ECONOMY POLISHED NICKEL | 36| 4 +Brand#55 |ECONOMY POLISHED STEEL | 9| 4 +Brand#55 |ECONOMY POLISHED STEEL | 36| 4 +Brand#55 |ECONOMY POLISHED STEEL | 45| 4 +Brand#55 |ECONOMY POLISHED TIN | 3| 4 +Brand#55 |ECONOMY POLISHED TIN | 23| 4 +Brand#55 |ECONOMY POLISHED TIN | 45| 4 +Brand#55 |ECONOMY POLISHED TIN | 49| 4 +Brand#55 |LARGE ANODIZED BRASS | 3| 4 +Brand#55 |LARGE ANODIZED BRASS | 14| 4 +Brand#55 |LARGE ANODIZED BRASS | 19| 4 +Brand#55 |LARGE ANODIZED BRASS | 45| 4 +Brand#55 |LARGE ANODIZED BRASS | 49| 4 +Brand#55 |LARGE ANODIZED COPPER | 19| 4 +Brand#55 |LARGE ANODIZED COPPER | 49| 4 +Brand#55 |LARGE ANODIZED NICKEL | 3| 4 +Brand#55 |LARGE ANODIZED NICKEL | 49| 4 +Brand#55 |LARGE ANODIZED STEEL | 3| 4 +Brand#55 |LARGE ANODIZED STEEL | 19| 4 +Brand#55 |LARGE ANODIZED STEEL | 36| 4 +Brand#55 |LARGE ANODIZED STEEL | 45| 4 +Brand#55 |LARGE ANODIZED STEEL | 49| 4 +Brand#55 |LARGE ANODIZED TIN | 9| 4 +Brand#55 |LARGE ANODIZED TIN | 23| 4 +Brand#55 |LARGE BRUSHED BRASS | 19| 4 +Brand#55 |LARGE BRUSHED BRASS | 23| 4 +Brand#55 |LARGE BRUSHED BRASS | 36| 4 +Brand#55 |LARGE BRUSHED BRASS | 45| 4 +Brand#55 |LARGE BRUSHED COPPER | 36| 4 +Brand#55 |LARGE BRUSHED COPPER | 45| 4 +Brand#55 |LARGE BRUSHED NICKEL | 9| 4 +Brand#55 |LARGE BRUSHED NICKEL | 45| 4 +Brand#55 |LARGE BRUSHED NICKEL | 49| 4 +Brand#55 |LARGE BRUSHED STEEL | 3| 4 +Brand#55 |LARGE BRUSHED STEEL | 19| 4 +Brand#55 |LARGE BRUSHED STEEL | 36| 4 +Brand#55 |LARGE BRUSHED STEEL | 45| 4 +Brand#55 |LARGE BRUSHED TIN | 3| 4 +Brand#55 |LARGE BRUSHED TIN | 14| 4 +Brand#55 |LARGE BRUSHED TIN | 23| 4 +Brand#55 |LARGE BRUSHED TIN | 36| 4 +Brand#55 |LARGE BURNISHED BRASS | 9| 4 +Brand#55 |LARGE BURNISHED BRASS | 23| 4 +Brand#55 |LARGE BURNISHED BRASS | 36| 4 +Brand#55 |LARGE BURNISHED COPPER | 23| 4 +Brand#55 |LARGE BURNISHED COPPER | 45| 4 +Brand#55 |LARGE BURNISHED NICKEL | 3| 4 +Brand#55 |LARGE BURNISHED NICKEL | 9| 4 +Brand#55 |LARGE BURNISHED STEEL | 14| 4 +Brand#55 |LARGE BURNISHED TIN | 23| 4 +Brand#55 |LARGE BURNISHED TIN | 45| 4 +Brand#55 |LARGE PLATED BRASS | 19| 4 +Brand#55 |LARGE PLATED BRASS | 36| 4 +Brand#55 |LARGE PLATED BRASS | 49| 4 +Brand#55 |LARGE PLATED COPPER | 3| 4 +Brand#55 |LARGE PLATED COPPER | 19| 4 +Brand#55 |LARGE PLATED COPPER | 36| 4 +Brand#55 |LARGE PLATED NICKEL | 3| 4 +Brand#55 |LARGE PLATED NICKEL | 23| 4 +Brand#55 |LARGE PLATED NICKEL | 45| 4 +Brand#55 |LARGE PLATED NICKEL | 49| 4 +Brand#55 |LARGE PLATED STEEL | 9| 4 +Brand#55 |LARGE PLATED STEEL | 45| 4 +Brand#55 |LARGE PLATED TIN | 3| 4 +Brand#55 |LARGE PLATED TIN | 23| 4 +Brand#55 |LARGE PLATED TIN | 49| 4 +Brand#55 |LARGE POLISHED BRASS | 9| 4 +Brand#55 |LARGE POLISHED BRASS | 14| 4 +Brand#55 |LARGE POLISHED BRASS | 19| 4 +Brand#55 |LARGE POLISHED COPPER | 3| 4 +Brand#55 |LARGE POLISHED COPPER | 14| 4 +Brand#55 |LARGE POLISHED COPPER | 19| 4 +Brand#55 |LARGE POLISHED COPPER | 23| 4 +Brand#55 |LARGE POLISHED COPPER | 45| 4 +Brand#55 |LARGE POLISHED COPPER | 49| 4 +Brand#55 |LARGE POLISHED NICKEL | 23| 4 +Brand#55 |LARGE POLISHED NICKEL | 45| 4 +Brand#55 |LARGE POLISHED STEEL | 23| 4 +Brand#55 |LARGE POLISHED STEEL | 36| 4 +Brand#55 |LARGE POLISHED STEEL | 49| 4 +Brand#55 |LARGE POLISHED TIN | 3| 4 +Brand#55 |LARGE POLISHED TIN | 19| 4 +Brand#55 |LARGE POLISHED TIN | 23| 4 +Brand#55 |LARGE POLISHED TIN | 49| 4 +Brand#55 |MEDIUM ANODIZED BRASS | 3| 4 +Brand#55 |MEDIUM ANODIZED BRASS | 14| 4 +Brand#55 |MEDIUM ANODIZED BRASS | 19| 4 +Brand#55 |MEDIUM ANODIZED BRASS | 45| 4 +Brand#55 |MEDIUM ANODIZED COPPER | 9| 4 +Brand#55 |MEDIUM ANODIZED COPPER | 23| 4 +Brand#55 |MEDIUM ANODIZED COPPER | 45| 4 +Brand#55 |MEDIUM ANODIZED NICKEL | 3| 4 +Brand#55 |MEDIUM ANODIZED NICKEL | 9| 4 +Brand#55 |MEDIUM ANODIZED NICKEL | 23| 4 +Brand#55 |MEDIUM ANODIZED STEEL | 9| 4 +Brand#55 |MEDIUM ANODIZED STEEL | 23| 4 +Brand#55 |MEDIUM ANODIZED STEEL | 36| 4 +Brand#55 |MEDIUM ANODIZED STEEL | 45| 4 +Brand#55 |MEDIUM ANODIZED STEEL | 49| 4 +Brand#55 |MEDIUM ANODIZED TIN | 3| 4 +Brand#55 |MEDIUM ANODIZED TIN | 9| 4 +Brand#55 |MEDIUM ANODIZED TIN | 14| 4 +Brand#55 |MEDIUM ANODIZED TIN | 19| 4 +Brand#55 |MEDIUM ANODIZED TIN | 36| 4 +Brand#55 |MEDIUM BRUSHED BRASS | 3| 4 +Brand#55 |MEDIUM BRUSHED BRASS | 9| 4 +Brand#55 |MEDIUM BRUSHED BRASS | 36| 4 +Brand#55 |MEDIUM BRUSHED BRASS | 45| 4 +Brand#55 |MEDIUM BRUSHED COPPER | 9| 4 +Brand#55 |MEDIUM BRUSHED COPPER | 14| 4 +Brand#55 |MEDIUM BRUSHED COPPER | 19| 4 +Brand#55 |MEDIUM BRUSHED COPPER | 36| 4 +Brand#55 |MEDIUM BRUSHED NICKEL | 14| 4 +Brand#55 |MEDIUM BRUSHED NICKEL | 49| 4 +Brand#55 |MEDIUM BRUSHED STEEL | 3| 4 +Brand#55 |MEDIUM BRUSHED TIN | 23| 4 +Brand#55 |MEDIUM BRUSHED TIN | 49| 4 +Brand#55 |MEDIUM BURNISHED BRASS | 9| 4 +Brand#55 |MEDIUM BURNISHED BRASS | 23| 4 +Brand#55 |MEDIUM BURNISHED BRASS | 45| 4 +Brand#55 |MEDIUM BURNISHED BRASS | 49| 4 +Brand#55 |MEDIUM BURNISHED COPPER | 9| 4 +Brand#55 |MEDIUM BURNISHED COPPER | 14| 4 +Brand#55 |MEDIUM BURNISHED COPPER | 19| 4 +Brand#55 |MEDIUM BURNISHED NICKEL | 3| 4 +Brand#55 |MEDIUM BURNISHED NICKEL | 9| 4 +Brand#55 |MEDIUM BURNISHED NICKEL | 14| 4 +Brand#55 |MEDIUM BURNISHED NICKEL | 36| 4 +Brand#55 |MEDIUM BURNISHED STEEL | 9| 4 +Brand#55 |MEDIUM BURNISHED TIN | 3| 4 +Brand#55 |MEDIUM BURNISHED TIN | 23| 4 +Brand#55 |MEDIUM BURNISHED TIN | 49| 4 +Brand#55 |MEDIUM PLATED BRASS | 9| 4 +Brand#55 |MEDIUM PLATED BRASS | 14| 4 +Brand#55 |MEDIUM PLATED BRASS | 36| 4 +Brand#55 |MEDIUM PLATED BRASS | 49| 4 +Brand#55 |MEDIUM PLATED COPPER | 49| 4 +Brand#55 |MEDIUM PLATED NICKEL | 9| 4 +Brand#55 |MEDIUM PLATED NICKEL | 14| 4 +Brand#55 |MEDIUM PLATED NICKEL | 45| 4 +Brand#55 |MEDIUM PLATED STEEL | 9| 4 +Brand#55 |MEDIUM PLATED STEEL | 23| 4 +Brand#55 |MEDIUM PLATED STEEL | 49| 4 +Brand#55 |MEDIUM PLATED TIN | 45| 4 +Brand#55 |MEDIUM PLATED TIN | 49| 4 +Brand#55 |PROMO ANODIZED BRASS | 9| 4 +Brand#55 |PROMO ANODIZED COPPER | 19| 4 +Brand#55 |PROMO ANODIZED NICKEL | 23| 4 +Brand#55 |PROMO ANODIZED NICKEL | 45| 4 +Brand#55 |PROMO ANODIZED STEEL | 9| 4 +Brand#55 |PROMO ANODIZED STEEL | 14| 4 +Brand#55 |PROMO ANODIZED STEEL | 23| 4 +Brand#55 |PROMO ANODIZED TIN | 9| 4 +Brand#55 |PROMO ANODIZED TIN | 23| 4 +Brand#55 |PROMO BRUSHED BRASS | 14| 4 +Brand#55 |PROMO BRUSHED BRASS | 19| 4 +Brand#55 |PROMO BRUSHED BRASS | 49| 4 +Brand#55 |PROMO BRUSHED COPPER | 14| 4 +Brand#55 |PROMO BRUSHED COPPER | 23| 4 +Brand#55 |PROMO BRUSHED COPPER | 36| 4 +Brand#55 |PROMO BRUSHED COPPER | 45| 4 +Brand#55 |PROMO BRUSHED COPPER | 49| 4 +Brand#55 |PROMO BRUSHED NICKEL | 3| 4 +Brand#55 |PROMO BRUSHED NICKEL | 19| 4 +Brand#55 |PROMO BRUSHED STEEL | 14| 4 +Brand#55 |PROMO BRUSHED STEEL | 19| 4 +Brand#55 |PROMO BRUSHED TIN | 19| 4 +Brand#55 |PROMO BURNISHED BRASS | 9| 4 +Brand#55 |PROMO BURNISHED BRASS | 14| 4 +Brand#55 |PROMO BURNISHED BRASS | 19| 4 +Brand#55 |PROMO BURNISHED COPPER | 3| 4 +Brand#55 |PROMO BURNISHED COPPER | 49| 4 +Brand#55 |PROMO BURNISHED NICKEL | 14| 4 +Brand#55 |PROMO BURNISHED NICKEL | 19| 4 +Brand#55 |PROMO BURNISHED NICKEL | 23| 4 +Brand#55 |PROMO BURNISHED NICKEL | 45| 4 +Brand#55 |PROMO BURNISHED NICKEL | 49| 4 +Brand#55 |PROMO BURNISHED STEEL | 19| 4 +Brand#55 |PROMO BURNISHED STEEL | 36| 4 +Brand#55 |PROMO BURNISHED TIN | 3| 4 +Brand#55 |PROMO BURNISHED TIN | 14| 4 +Brand#55 |PROMO BURNISHED TIN | 23| 4 +Brand#55 |PROMO PLATED BRASS | 3| 4 +Brand#55 |PROMO PLATED BRASS | 49| 4 +Brand#55 |PROMO PLATED COPPER | 3| 4 +Brand#55 |PROMO PLATED COPPER | 45| 4 +Brand#55 |PROMO PLATED NICKEL | 3| 4 +Brand#55 |PROMO PLATED NICKEL | 23| 4 +Brand#55 |PROMO PLATED STEEL | 3| 4 +Brand#55 |PROMO PLATED STEEL | 23| 4 +Brand#55 |PROMO PLATED STEEL | 36| 4 +Brand#55 |PROMO PLATED STEEL | 45| 4 +Brand#55 |PROMO PLATED STEEL | 49| 4 +Brand#55 |PROMO PLATED TIN | 3| 4 +Brand#55 |PROMO PLATED TIN | 19| 4 +Brand#55 |PROMO PLATED TIN | 23| 4 +Brand#55 |PROMO POLISHED BRASS | 14| 4 +Brand#55 |PROMO POLISHED COPPER | 3| 4 +Brand#55 |PROMO POLISHED COPPER | 19| 4 +Brand#55 |PROMO POLISHED COPPER | 45| 4 +Brand#55 |PROMO POLISHED COPPER | 49| 4 +Brand#55 |PROMO POLISHED NICKEL | 3| 4 +Brand#55 |PROMO POLISHED NICKEL | 14| 4 +Brand#55 |PROMO POLISHED NICKEL | 19| 4 +Brand#55 |PROMO POLISHED NICKEL | 23| 4 +Brand#55 |PROMO POLISHED NICKEL | 36| 4 +Brand#55 |PROMO POLISHED STEEL | 19| 4 +Brand#55 |PROMO POLISHED STEEL | 45| 4 +Brand#55 |PROMO POLISHED STEEL | 49| 4 +Brand#55 |PROMO POLISHED TIN | 3| 4 +Brand#55 |PROMO POLISHED TIN | 9| 4 +Brand#55 |PROMO POLISHED TIN | 14| 4 +Brand#55 |PROMO POLISHED TIN | 19| 4 +Brand#55 |PROMO POLISHED TIN | 23| 4 +Brand#55 |PROMO POLISHED TIN | 36| 4 +Brand#55 |PROMO POLISHED TIN | 45| 4 +Brand#55 |PROMO POLISHED TIN | 49| 4 +Brand#55 |SMALL ANODIZED BRASS | 23| 4 +Brand#55 |SMALL ANODIZED BRASS | 36| 4 +Brand#55 |SMALL ANODIZED BRASS | 45| 4 +Brand#55 |SMALL ANODIZED COPPER | 9| 4 +Brand#55 |SMALL ANODIZED COPPER | 19| 4 +Brand#55 |SMALL ANODIZED COPPER | 23| 4 +Brand#55 |SMALL ANODIZED NICKEL | 9| 4 +Brand#55 |SMALL ANODIZED NICKEL | 14| 4 +Brand#55 |SMALL ANODIZED NICKEL | 23| 4 +Brand#55 |SMALL ANODIZED NICKEL | 36| 4 +Brand#55 |SMALL ANODIZED NICKEL | 45| 4 +Brand#55 |SMALL ANODIZED STEEL | 36| 4 +Brand#55 |SMALL ANODIZED TIN | 9| 4 +Brand#55 |SMALL ANODIZED TIN | 36| 4 +Brand#55 |SMALL ANODIZED TIN | 45| 4 +Brand#55 |SMALL ANODIZED TIN | 49| 4 +Brand#55 |SMALL BRUSHED BRASS | 9| 4 +Brand#55 |SMALL BRUSHED BRASS | 36| 4 +Brand#55 |SMALL BRUSHED COPPER | 3| 4 +Brand#55 |SMALL BRUSHED COPPER | 9| 4 +Brand#55 |SMALL BRUSHED COPPER | 19| 4 +Brand#55 |SMALL BRUSHED COPPER | 23| 4 +Brand#55 |SMALL BRUSHED NICKEL | 3| 4 +Brand#55 |SMALL BRUSHED NICKEL | 9| 4 +Brand#55 |SMALL BRUSHED NICKEL | 19| 4 +Brand#55 |SMALL BRUSHED NICKEL | 23| 4 +Brand#55 |SMALL BRUSHED NICKEL | 45| 4 +Brand#55 |SMALL BRUSHED NICKEL | 49| 4 +Brand#55 |SMALL BRUSHED STEEL | 3| 4 +Brand#55 |SMALL BRUSHED STEEL | 14| 4 +Brand#55 |SMALL BRUSHED STEEL | 19| 4 +Brand#55 |SMALL BRUSHED STEEL | 23| 4 +Brand#55 |SMALL BRUSHED STEEL | 45| 4 +Brand#55 |SMALL BRUSHED STEEL | 49| 4 +Brand#55 |SMALL BRUSHED TIN | 9| 4 +Brand#55 |SMALL BRUSHED TIN | 49| 4 +Brand#55 |SMALL BURNISHED BRASS | 14| 4 +Brand#55 |SMALL BURNISHED BRASS | 23| 4 +Brand#55 |SMALL BURNISHED COPPER | 3| 4 +Brand#55 |SMALL BURNISHED COPPER | 9| 4 +Brand#55 |SMALL BURNISHED COPPER | 36| 4 +Brand#55 |SMALL BURNISHED NICKEL | 9| 4 +Brand#55 |SMALL BURNISHED NICKEL | 19| 4 +Brand#55 |SMALL BURNISHED NICKEL | 36| 4 +Brand#55 |SMALL BURNISHED NICKEL | 45| 4 +Brand#55 |SMALL BURNISHED STEEL | 14| 4 +Brand#55 |SMALL BURNISHED TIN | 9| 4 +Brand#55 |SMALL BURNISHED TIN | 23| 4 +Brand#55 |SMALL PLATED COPPER | 3| 4 +Brand#55 |SMALL PLATED COPPER | 14| 4 +Brand#55 |SMALL PLATED COPPER | 36| 4 +Brand#55 |SMALL PLATED COPPER | 49| 4 +Brand#55 |SMALL PLATED NICKEL | 14| 4 +Brand#55 |SMALL PLATED NICKEL | 49| 4 +Brand#55 |SMALL PLATED STEEL | 3| 4 +Brand#55 |SMALL PLATED STEEL | 23| 4 +Brand#55 |SMALL PLATED STEEL | 36| 4 +Brand#55 |SMALL PLATED TIN | 36| 4 +Brand#55 |SMALL PLATED TIN | 45| 4 +Brand#55 |SMALL POLISHED BRASS | 9| 4 +Brand#55 |SMALL POLISHED BRASS | 19| 4 +Brand#55 |SMALL POLISHED BRASS | 49| 4 +Brand#55 |SMALL POLISHED COPPER | 19| 4 +Brand#55 |SMALL POLISHED COPPER | 23| 4 +Brand#55 |SMALL POLISHED COPPER | 36| 4 +Brand#55 |SMALL POLISHED COPPER | 45| 4 +Brand#55 |SMALL POLISHED COPPER | 49| 4 +Brand#55 |SMALL POLISHED NICKEL | 9| 4 +Brand#55 |SMALL POLISHED NICKEL | 14| 4 +Brand#55 |SMALL POLISHED NICKEL | 19| 4 +Brand#55 |SMALL POLISHED NICKEL | 23| 4 +Brand#55 |SMALL POLISHED NICKEL | 45| 4 +Brand#55 |SMALL POLISHED NICKEL | 49| 4 +Brand#55 |SMALL POLISHED STEEL | 19| 4 +Brand#55 |SMALL POLISHED STEEL | 45| 4 +Brand#55 |SMALL POLISHED TIN | 14| 4 +Brand#55 |SMALL POLISHED TIN | 23| 4 +Brand#55 |SMALL POLISHED TIN | 45| 4 +Brand#55 |STANDARD ANODIZED BRASS | 9| 4 +Brand#55 |STANDARD ANODIZED BRASS | 23| 4 +Brand#55 |STANDARD ANODIZED BRASS | 49| 4 +Brand#55 |STANDARD ANODIZED COPPER | 9| 4 +Brand#55 |STANDARD ANODIZED COPPER | 14| 4 +Brand#55 |STANDARD ANODIZED COPPER | 45| 4 +Brand#55 |STANDARD ANODIZED NICKEL | 3| 4 +Brand#55 |STANDARD ANODIZED NICKEL | 14| 4 +Brand#55 |STANDARD ANODIZED NICKEL | 45| 4 +Brand#55 |STANDARD ANODIZED NICKEL | 49| 4 +Brand#55 |STANDARD ANODIZED STEEL | 3| 4 +Brand#55 |STANDARD ANODIZED STEEL | 14| 4 +Brand#55 |STANDARD ANODIZED TIN | 14| 4 +Brand#55 |STANDARD ANODIZED TIN | 36| 4 +Brand#55 |STANDARD ANODIZED TIN | 45| 4 +Brand#55 |STANDARD BRUSHED BRASS | 9| 4 +Brand#55 |STANDARD BRUSHED BRASS | 19| 4 +Brand#55 |STANDARD BRUSHED COPPER | 14| 4 +Brand#55 |STANDARD BRUSHED COPPER | 19| 4 +Brand#55 |STANDARD BRUSHED NICKEL | 3| 4 +Brand#55 |STANDARD BRUSHED NICKEL | 36| 4 +Brand#55 |STANDARD BRUSHED STEEL | 9| 4 +Brand#55 |STANDARD BRUSHED STEEL | 14| 4 +Brand#55 |STANDARD BRUSHED STEEL | 19| 4 +Brand#55 |STANDARD BRUSHED STEEL | 49| 4 +Brand#55 |STANDARD BRUSHED TIN | 19| 4 +Brand#55 |STANDARD BRUSHED TIN | 49| 4 +Brand#55 |STANDARD BURNISHED BRASS | 9| 4 +Brand#55 |STANDARD BURNISHED BRASS | 19| 4 +Brand#55 |STANDARD BURNISHED BRASS | 23| 4 +Brand#55 |STANDARD BURNISHED BRASS | 36| 4 +Brand#55 |STANDARD BURNISHED COPPER| 3| 4 +Brand#55 |STANDARD BURNISHED NICKEL| 9| 4 +Brand#55 |STANDARD BURNISHED NICKEL| 49| 4 +Brand#55 |STANDARD BURNISHED STEEL | 19| 4 +Brand#55 |STANDARD BURNISHED STEEL | 23| 4 +Brand#55 |STANDARD BURNISHED STEEL | 36| 4 +Brand#55 |STANDARD BURNISHED STEEL | 45| 4 +Brand#55 |STANDARD BURNISHED TIN | 9| 4 +Brand#55 |STANDARD BURNISHED TIN | 19| 4 +Brand#55 |STANDARD BURNISHED TIN | 36| 4 +Brand#55 |STANDARD BURNISHED TIN | 49| 4 +Brand#55 |STANDARD PLATED BRASS | 9| 4 +Brand#55 |STANDARD PLATED BRASS | 45| 4 +Brand#55 |STANDARD PLATED BRASS | 49| 4 +Brand#55 |STANDARD PLATED COPPER | 9| 4 +Brand#55 |STANDARD PLATED COPPER | 45| 4 +Brand#55 |STANDARD PLATED NICKEL | 3| 4 +Brand#55 |STANDARD PLATED NICKEL | 19| 4 +Brand#55 |STANDARD PLATED NICKEL | 45| 4 +Brand#55 |STANDARD PLATED STEEL | 14| 4 +Brand#55 |STANDARD PLATED STEEL | 23| 4 +Brand#55 |STANDARD PLATED STEEL | 49| 4 +Brand#55 |STANDARD PLATED TIN | 9| 4 +Brand#55 |STANDARD PLATED TIN | 14| 4 +Brand#55 |STANDARD PLATED TIN | 36| 4 +Brand#55 |STANDARD POLISHED BRASS | 3| 4 +Brand#55 |STANDARD POLISHED BRASS | 9| 4 +Brand#55 |STANDARD POLISHED BRASS | 23| 4 +Brand#55 |STANDARD POLISHED COPPER | 3| 4 +Brand#55 |STANDARD POLISHED COPPER | 23| 4 +Brand#55 |STANDARD POLISHED COPPER | 45| 4 +Brand#55 |STANDARD POLISHED NICKEL | 3| 4 +Brand#55 |STANDARD POLISHED NICKEL | 23| 4 +Brand#55 |STANDARD POLISHED NICKEL | 36| 4 +Brand#55 |STANDARD POLISHED NICKEL | 45| 4 +Brand#55 |STANDARD POLISHED NICKEL | 49| 4 +Brand#55 |STANDARD POLISHED STEEL | 14| 4 +Brand#55 |STANDARD POLISHED STEEL | 23| 4 +Brand#55 |STANDARD POLISHED TIN | 9| 4 +Brand#55 |STANDARD POLISHED TIN | 19| 4 +Brand#55 |STANDARD POLISHED TIN | 36| 4 +Brand#11 |SMALL BRUSHED TIN | 19| 3 +Brand#15 |LARGE PLATED NICKEL | 45| 3 +Brand#15 |LARGE POLISHED NICKEL | 9| 3 +Brand#21 |PROMO BURNISHED STEEL | 45| 3 +Brand#22 |STANDARD PLATED STEEL | 23| 3 +Brand#25 |LARGE PLATED STEEL | 19| 3 +Brand#32 |STANDARD ANODIZED COPPER | 23| 3 +Brand#33 |SMALL ANODIZED BRASS | 9| 3 +Brand#35 |MEDIUM ANODIZED TIN | 19| 3 +Brand#51 |SMALL PLATED BRASS | 23| 3 +Brand#52 |MEDIUM BRUSHED BRASS | 45| 3 +Brand#53 |MEDIUM BRUSHED TIN | 45| 3 +Brand#54 |ECONOMY POLISHED BRASS | 9| 3 +Brand#55 |PROMO PLATED BRASS | 19| 3 +Brand#55 |STANDARD PLATED TIN | 49| 3 diff --git a/examples/tpch/answers_sf1/q17.tbl b/examples/tpch/answers_sf1/q17.tbl new file mode 100644 index 000000000..c7e570a0f --- /dev/null +++ b/examples/tpch/answers_sf1/q17.tbl @@ -0,0 +1,2 @@ +avg_yearly +348406.02 diff --git a/examples/tpch/answers_sf1/q18.tbl b/examples/tpch/answers_sf1/q18.tbl new file mode 100644 index 000000000..9e036c6b1 --- /dev/null +++ b/examples/tpch/answers_sf1/q18.tbl @@ -0,0 +1,58 @@ +c_name |c_custkey |o_orderkey |o_orderdat|o_totalprice |col6 +Customer#000128120 | 128120| 4722021|1994-04-07|544089.09|323.00 +Customer#000144617 | 144617| 3043270|1997-02-12|530604.44|317.00 +Customer#000013940 | 13940| 2232932|1997-04-13|522720.61|304.00 +Customer#000066790 | 66790| 2199712|1996-09-30|515531.82|327.00 +Customer#000046435 | 46435| 4745607|1997-07-03|508047.99|309.00 +Customer#000015272 | 15272| 3883783|1993-07-28|500241.33|302.00 +Customer#000146608 | 146608| 3342468|1994-06-12|499794.58|303.00 +Customer#000096103 | 96103| 5984582|1992-03-16|494398.79|312.00 +Customer#000024341 | 24341| 1474818|1992-11-15|491348.26|302.00 +Customer#000137446 | 137446| 5489475|1997-05-23|487763.25|311.00 +Customer#000107590 | 107590| 4267751|1994-11-04|485141.38|301.00 +Customer#000050008 | 50008| 2366755|1996-12-09|483891.26|302.00 +Customer#000015619 | 15619| 3767271|1996-08-07|480083.96|318.00 +Customer#000077260 | 77260| 1436544|1992-09-12|479499.43|307.00 +Customer#000109379 | 109379| 5746311|1996-10-10|478064.11|302.00 +Customer#000054602 | 54602| 5832321|1997-02-09|471220.08|307.00 +Customer#000105995 | 105995| 2096705|1994-07-03|469692.58|307.00 +Customer#000148885 | 148885| 2942469|1992-05-31|469630.44|313.00 +Customer#000114586 | 114586| 551136|1993-05-19|469605.59|308.00 +Customer#000105260 | 105260| 5296167|1996-09-06|469360.57|303.00 +Customer#000147197 | 147197| 1263015|1997-02-02|467149.67|320.00 +Customer#000064483 | 64483| 2745894|1996-07-04|466991.35|304.00 +Customer#000136573 | 136573| 2761378|1996-05-31|461282.73|301.00 +Customer#000016384 | 16384| 502886|1994-04-12|458378.92|312.00 +Customer#000117919 | 117919| 2869152|1996-06-20|456815.92|317.00 +Customer#000012251 | 12251| 735366|1993-11-24|455107.26|309.00 +Customer#000120098 | 120098| 1971680|1995-06-14|453451.23|308.00 +Customer#000066098 | 66098| 5007490|1992-08-07|453436.16|304.00 +Customer#000117076 | 117076| 4290656|1997-02-05|449545.85|301.00 +Customer#000129379 | 129379| 4720454|1997-06-07|448665.79|303.00 +Customer#000126865 | 126865| 4702759|1994-11-07|447606.65|320.00 +Customer#000088876 | 88876| 983201|1993-12-30|446717.46|304.00 +Customer#000036619 | 36619| 4806726|1995-01-17|446704.09|328.00 +Customer#000141823 | 141823| 2806245|1996-12-29|446269.12|310.00 +Customer#000053029 | 53029| 2662214|1993-08-13|446144.49|302.00 +Customer#000018188 | 18188| 3037414|1995-01-25|443807.22|308.00 +Customer#000066533 | 66533| 29158|1995-10-21|443576.50|305.00 +Customer#000037729 | 37729| 4134341|1995-06-29|441082.97|309.00 +Customer#000003566 | 3566| 2329187|1998-01-04|439803.36|304.00 +Customer#000045538 | 45538| 4527553|1994-05-22|436275.31|305.00 +Customer#000081581 | 81581| 4739650|1995-11-04|435405.90|305.00 +Customer#000119989 | 119989| 1544643|1997-09-20|434568.25|320.00 +Customer#000003680 | 3680| 3861123|1998-07-03|433525.97|301.00 +Customer#000113131 | 113131| 967334|1995-12-15|432957.75|301.00 +Customer#000141098 | 141098| 565574|1995-09-24|430986.69|301.00 +Customer#000093392 | 93392| 5200102|1997-01-22|425487.51|304.00 +Customer#000015631 | 15631| 1845057|1994-05-12|419879.59|302.00 +Customer#000112987 | 112987| 4439686|1996-09-17|418161.49|305.00 +Customer#000012599 | 12599| 4259524|1998-02-12|415200.61|304.00 +Customer#000105410 | 105410| 4478371|1996-03-05|412754.51|302.00 +Customer#000149842 | 149842| 5156581|1994-05-30|411329.35|302.00 +Customer#000010129 | 10129| 5849444|1994-03-21|409129.85|309.00 +Customer#000069904 | 69904| 1742403|1996-10-19|408513.00|305.00 +Customer#000017746 | 17746| 6882|1997-04-09|408446.93|303.00 +Customer#000013072 | 13072| 1481925|1998-03-15|399195.47|301.00 +Customer#000082441 | 82441| 857959|1994-02-07|382579.74|305.00 +Customer#000088703 | 88703| 2995076|1994-01-30|363812.12|302.00 diff --git a/examples/tpch/answers_sf1/q19.tbl b/examples/tpch/answers_sf1/q19.tbl new file mode 100644 index 000000000..3711427de --- /dev/null +++ b/examples/tpch/answers_sf1/q19.tbl @@ -0,0 +1,2 @@ +revenue +3083843.06 diff --git a/examples/tpch/answers_sf1/q2.tbl b/examples/tpch/answers_sf1/q2.tbl new file mode 100644 index 000000000..487b9fbb9 --- /dev/null +++ b/examples/tpch/answers_sf1/q2.tbl @@ -0,0 +1,101 @@ +s_acctbal |s_name |n_name |p_partkey |p_mfgr |s_address |s_phone |s_comment +9938.53|Supplier#000005359 |UNITED KINGDOM | 185358|Manufacturer#4 |QKuHYh,vZGiwu2FWEJoLDx04 |33-429-790-6131|uriously regular requests hag +9937.84|Supplier#000005969 |ROMANIA | 108438|Manufacturer#1 |ANDENSOSmk,miq23Xfb5RWt6dvUcvt6Qa |29-520-692-3537|efully express instructions. regular requests against the slyly fin +9936.22|Supplier#000005250 |UNITED KINGDOM | 249|Manufacturer#4 |B3rqp0xbSEim4Mpy2RH J |33-320-228-2957|etect about the furiously final accounts. slyly ironic pinto beans sleep inside the furiously +9923.77|Supplier#000002324 |GERMANY | 29821|Manufacturer#4 |y3OD9UywSTOk |17-779-299-1839|ackages boost blithely. blithely regular deposits c +9871.22|Supplier#000006373 |GERMANY | 43868|Manufacturer#5 |J8fcXWsTqM |17-813-485-8637|etect blithely bold asymptotes. fluffily ironic platelets wake furiously; blit +9870.78|Supplier#000001286 |GERMANY | 81285|Manufacturer#2 |YKA,E2fjiVd7eUrzp2Ef8j1QxGo2DFnosaTEH |17-516-924-4574| regular accounts. furiously unusual courts above the fi +9870.78|Supplier#000001286 |GERMANY | 181285|Manufacturer#4 |YKA,E2fjiVd7eUrzp2Ef8j1QxGo2DFnosaTEH |17-516-924-4574| regular accounts. furiously unusual courts above the fi +9852.52|Supplier#000008973 |RUSSIA | 18972|Manufacturer#2 |t5L67YdBYYH6o,Vz24jpDyQ9 |32-188-594-7038|rns wake final foxes. carefully unusual depende +9847.83|Supplier#000008097 |RUSSIA | 130557|Manufacturer#2 |xMe97bpE69NzdwLoX |32-375-640-3593| the special excuses. silent sentiments serve carefully final ac +9847.57|Supplier#000006345 |FRANCE | 86344|Manufacturer#1 |VSt3rzk3qG698u6ld8HhOByvrTcSTSvQlDQDag |16-886-766-7945|ges. slyly regular requests are. ruthless, express excuses cajole blithely across the unu +9847.57|Supplier#000006345 |FRANCE | 173827|Manufacturer#2 |VSt3rzk3qG698u6ld8HhOByvrTcSTSvQlDQDag |16-886-766-7945|ges. slyly regular requests are. ruthless, express excuses cajole blithely across the unu +9836.93|Supplier#000007342 |RUSSIA | 4841|Manufacturer#4 |JOlK7C1,7xrEZSSOw |32-399-414-5385|blithely carefully bold theodolites. fur +9817.10|Supplier#000002352 |RUSSIA | 124815|Manufacturer#2 |4LfoHUZjgjEbAKw TgdKcgOc4D4uCYw |32-551-831-1437|wake carefully alongside of the carefully final ex +9817.10|Supplier#000002352 |RUSSIA | 152351|Manufacturer#3 |4LfoHUZjgjEbAKw TgdKcgOc4D4uCYw |32-551-831-1437|wake carefully alongside of the carefully final ex +9739.86|Supplier#000003384 |FRANCE | 138357|Manufacturer#2 |o,Z3v4POifevE k9U1b 6J1ucX,I |16-494-913-5925|s after the furiously bold packages sleep fluffily idly final requests: quickly final +9721.95|Supplier#000008757 |UNITED KINGDOM | 156241|Manufacturer#3 |Atg6GnM4dT2 |33-821-407-2995|eep furiously sauternes; quickl +9681.33|Supplier#000008406 |RUSSIA | 78405|Manufacturer#1 |,qUuXcftUl |32-139-873-8571|haggle slyly regular excuses. quic +9643.55|Supplier#000005148 |ROMANIA | 107617|Manufacturer#1 |kT4ciVFslx9z4s79p Js825 |29-252-617-4850|final excuses. final ideas boost quickly furiously speci +9624.82|Supplier#000001816 |FRANCE | 34306|Manufacturer#3 |e7vab91vLJPWxxZnewmnDBpDmxYHrb |16-392-237-6726|e packages are around the special ideas. special, pending foxes us +9624.78|Supplier#000009658 |ROMANIA | 189657|Manufacturer#1 |oE9uBgEfSS4opIcepXyAYM,x |29-748-876-2014|ronic asymptotes wake bravely final +9612.94|Supplier#000003228 |ROMANIA | 120715|Manufacturer#2 |KDdpNKN3cWu7ZSrbdqp7AfSLxx,qWB |29-325-784-8187|warhorses. quickly even deposits sublate daringly ironic instructions. slyly blithe t +9612.94|Supplier#000003228 |ROMANIA | 198189|Manufacturer#4 |KDdpNKN3cWu7ZSrbdqp7AfSLxx,qWB |29-325-784-8187|warhorses. quickly even deposits sublate daringly ironic instructions. slyly blithe t +9571.83|Supplier#000004305 |ROMANIA | 179270|Manufacturer#2 |qNHZ7WmCzygwMPRDO9Ps |29-973-481-1831|kly carefully express asymptotes. furiou +9558.10|Supplier#000003532 |UNITED KINGDOM | 88515|Manufacturer#4 |EOeuiiOn21OVpTlGguufFDFsbN1p0lhpxHp |33-152-301-2164| foxes. quickly even excuses use. slyly special foxes nag bl +9492.79|Supplier#000005975 |GERMANY | 25974|Manufacturer#5 |S6mIiCTx82z7lV |17-992-579-4839|arefully pending accounts. blithely regular excuses boost carefully carefully ironic p +9461.05|Supplier#000002536 |UNITED KINGDOM | 20033|Manufacturer#1 |8mmGbyzaU 7ZS2wJumTibypncu9pNkDc4FYA |33-556-973-5522|. slyly regular deposits wake slyly. furiously regular warthogs are. +9453.01|Supplier#000000802 |ROMANIA | 175767|Manufacturer#1 |,6HYXb4uaHITmtMBj4Ak57Pd |29-342-882-6463|gular frets. permanently special multipliers believe blithely alongs +9408.65|Supplier#000007772 |UNITED KINGDOM | 117771|Manufacturer#4 |AiC5YAH,gdu0i7 |33-152-491-1126|nag against the final requests. furiously unusual packages cajole blit +9359.61|Supplier#000004856 |ROMANIA | 62349|Manufacturer#5 |HYogcF3Jb yh1 |29-334-870-9731|y ironic theodolites. blithely sile +9357.45|Supplier#000006188 |UNITED KINGDOM | 138648|Manufacturer#1 |g801,ssP8wpTk4Hm |33-583-607-1633|ously always regular packages. fluffily even accounts beneath the furiously final pack +9352.04|Supplier#000003439 |GERMANY | 170921|Manufacturer#4 |qYPDgoiBGhCYxjgC |17-128-996-4650| according to the carefully bold ideas +9312.97|Supplier#000007807 |RUSSIA | 90279|Manufacturer#5 |oGYMPCk9XHGB2PBfKRnHA |32-673-872-5854|ecial packages among the pending, even requests use regula +9312.97|Supplier#000007807 |RUSSIA | 100276|Manufacturer#5 |oGYMPCk9XHGB2PBfKRnHA |32-673-872-5854|ecial packages among the pending, even requests use regula +9280.27|Supplier#000007194 |ROMANIA | 47193|Manufacturer#3 |zhRUQkBSrFYxIAXTfInj vyGRQjeK |29-318-454-2133|o beans haggle after the furiously unusual deposits. carefully silent dolphins cajole carefully +9274.80|Supplier#000008854 |RUSSIA | 76346|Manufacturer#3 |1xhLoOUM7I3mZ1mKnerw OSqdbb4QbGa |32-524-148-5221|y. courts do wake slyly. carefully ironic platelets haggle above the slyly regular the +9249.35|Supplier#000003973 |FRANCE | 26466|Manufacturer#1 |d18GiDsL6Wm2IsGXM,RZf1jCsgZAOjNYVThTRP4 |16-722-866-1658|uests are furiously. regular tithes through the regular, final accounts cajole furiously above the q +9249.35|Supplier#000003973 |FRANCE | 33972|Manufacturer#1 |d18GiDsL6Wm2IsGXM,RZf1jCsgZAOjNYVThTRP4 |16-722-866-1658|uests are furiously. regular tithes through the regular, final accounts cajole furiously above the q +9208.70|Supplier#000007769 |ROMANIA | 40256|Manufacturer#5 |rsimdze 5o9P Ht7xS |29-964-424-9649|lites was quickly above the furiously ironic requests. slyly even foxes against the blithely bold +9201.47|Supplier#000009690 |UNITED KINGDOM | 67183|Manufacturer#5 |CB BnUTlmi5zdeEl7R7 |33-121-267-9529|e even, even foxes. blithely ironic packages cajole regular packages. slyly final ide +9192.10|Supplier#000000115 |UNITED KINGDOM | 85098|Manufacturer#3 |nJ 2t0f7Ve,wL1,6WzGBJLNBUCKlsV |33-597-248-1220|es across the carefully express accounts boost caref +9189.98|Supplier#000001226 |GERMANY | 21225|Manufacturer#4 |qsLCqSvLyZfuXIpjz |17-725-903-1381| deposits. blithely bold excuses about the slyly bold forges wake +9128.97|Supplier#000004311 |RUSSIA | 146768|Manufacturer#5 |I8IjnXd7NSJRs594RxsRR0 |32-155-440-7120|refully. blithely unusual asymptotes haggle +9104.83|Supplier#000008520 |GERMANY | 150974|Manufacturer#4 |RqRVDgD0ER J9 b41vR2,3 |17-728-804-1793|ly about the blithely ironic depths. slyly final theodolites among the fluffily bold ideas print +9101.00|Supplier#000005791 |ROMANIA | 128254|Manufacturer#5 |zub2zCV,jhHPPQqi,P2INAjE1zI n66cOEoXFG |29-549-251-5384|ts. notornis detect blithely above the carefully bold requests. blithely even package +9094.57|Supplier#000004582 |RUSSIA | 39575|Manufacturer#1 |WB0XkCSG3r,mnQ n,h9VIxjjr9ARHFvKgMDf |32-587-577-1351|jole. regular accounts sleep blithely frets. final pinto beans play furiously past the +8996.87|Supplier#000004702 |FRANCE | 102191|Manufacturer#5 |8XVcQK23akp |16-811-269-8946|ickly final packages along the express plat +8996.14|Supplier#000009814 |ROMANIA | 139813|Manufacturer#2 |af0O5pg83lPU4IDVmEylXZVqYZQzSDlYLAmR |29-995-571-8781| dependencies boost quickly across the furiously pending requests! unusual dolphins play sl +8968.42|Supplier#000010000 |ROMANIA | 119999|Manufacturer#5 |aTGLEusCiL4F PDBdv665XBJhPyCOB0i |29-578-432-2146|ly regular foxes boost slyly. quickly special waters boost carefully ironi +8936.82|Supplier#000007043 |UNITED KINGDOM | 109512|Manufacturer#1 |FVajceZInZdbJE6Z9XsRUxrUEpiwHDrOXi,1Rz |33-784-177-8208|efully regular courts. furiousl +8929.42|Supplier#000008770 |FRANCE | 173735|Manufacturer#4 |R7cG26TtXrHAP9 HckhfRi |16-242-746-9248|cajole furiously unusual requests. quickly stealthy requests are. +8920.59|Supplier#000003967 |ROMANIA | 26460|Manufacturer#1 |eHoAXe62SY9 |29-194-731-3944|aters. express, pending instructions sleep. brave, r +8920.59|Supplier#000003967 |ROMANIA | 173966|Manufacturer#2 |eHoAXe62SY9 |29-194-731-3944|aters. express, pending instructions sleep. brave, r +8913.96|Supplier#000004603 |UNITED KINGDOM | 137063|Manufacturer#2 |OUzlvMUr7n,utLxmPNeYKSf3T24OXskxB5 |33-789-255-7342| haggle slyly above the furiously regular pinto beans. even +8877.82|Supplier#000007967 |FRANCE | 167966|Manufacturer#5 |A3pi1BARM4nx6R,qrwFoRPU |16-442-147-9345|ously foxes. express, ironic requests im +8862.24|Supplier#000003323 |ROMANIA | 73322|Manufacturer#3 |W9 lYcsC9FwBqk3ItL |29-736-951-3710|ly pending ideas sleep about the furiously unu +8841.59|Supplier#000005750 |ROMANIA | 100729|Manufacturer#5 |Erx3lAgu0g62iaHF9x50uMH4EgeN9hEG |29-344-502-5481|gainst the pinto beans. fluffily unusual dependencies affix slyly even deposits. +8781.71|Supplier#000003121 |ROMANIA | 13120|Manufacturer#5 |wNqTogx238ZYCamFb,50v,bj 4IbNFW9Bvw1xP |29-707-291-5144|s wake quickly ironic ideas +8754.24|Supplier#000009407 |UNITED KINGDOM | 179406|Manufacturer#4 |CHRCbkaWcf5B |33-903-970-9604|e ironic requests. carefully even foxes above the furious +8691.06|Supplier#000004429 |UNITED KINGDOM | 126892|Manufacturer#2 |k,BQms5UhoAF1B2Asi,fLib |33-964-337-5038|efully express deposits kindle after the deposits. final +8655.99|Supplier#000006330 |RUSSIA | 193810|Manufacturer#2 |UozlaENr0ytKe2w6CeIEWFWn iO3S8Rae7Ou |32-561-198-3705|symptotes use about the express dolphins. requests use after the express platelets. final, ex +8638.36|Supplier#000002920 |RUSSIA | 75398|Manufacturer#1 |Je2a8bszf3L |32-122-621-7549|ly quickly ironic requests. even requests whithout t +8638.36|Supplier#000002920 |RUSSIA | 170402|Manufacturer#3 |Je2a8bszf3L |32-122-621-7549|ly quickly ironic requests. even requests whithout t +8607.69|Supplier#000006003 |UNITED KINGDOM | 76002|Manufacturer#2 |EH9wADcEiuenM0NR08zDwMidw,52Y2RyILEiA |33-416-807-5206|ar, pending accounts. pending depende +8569.52|Supplier#000005936 |RUSSIA | 5935|Manufacturer#5 |jXaNZ6vwnEWJ2ksLZJpjtgt0bY2a3AU |32-644-251-7916|. regular foxes nag carefully atop the regular, silent deposits. quickly regular packages +8564.12|Supplier#000000033 |GERMANY | 110032|Manufacturer#1 |gfeKpYw3400L0SDywXA6Ya1Qmq1w6YB9f3R |17-138-897-9374|n sauternes along the regular asymptotes are regularly along the +8553.82|Supplier#000003979 |ROMANIA | 143978|Manufacturer#4 |BfmVhCAnCMY3jzpjUMy4CNWs9 HzpdQR7INJU |29-124-646-4897|ic requests wake against the blithely unusual accounts. fluffily r +8517.23|Supplier#000009529 |RUSSIA | 37025|Manufacturer#5 |e44R8o7JAIS9iMcr |32-565-297-8775|ove the even courts. furiously special platelets +8517.23|Supplier#000009529 |RUSSIA | 59528|Manufacturer#2 |e44R8o7JAIS9iMcr |32-565-297-8775|ove the even courts. furiously special platelets +8503.70|Supplier#000006830 |RUSSIA | 44325|Manufacturer#4 |BC4WFCYRUZyaIgchU 4S |32-147-878-5069|pades cajole. furious packages among the carefully express excuses boost furiously across th +8457.09|Supplier#000009456 |UNITED KINGDOM | 19455|Manufacturer#1 |7SBhZs8gP1cJjT0Qf433YBk |33-858-440-4349|cing requests along the furiously unusual deposits promise among the furiously unus +8441.40|Supplier#000003817 |FRANCE | 141302|Manufacturer#2 |hU3fz3xL78 |16-339-356-5115|ely even ideas. ideas wake slyly furiously unusual instructions. pinto beans sleep ag +8432.89|Supplier#000003990 |RUSSIA | 191470|Manufacturer#1 |wehBBp1RQbfxAYDASS75MsywmsKHRVdkrvNe6m |32-839-509-9301|ep furiously. packages should have to haggle slyly across the deposits. furiously regu +8431.40|Supplier#000002675 |ROMANIA | 5174|Manufacturer#1 |HJFStOu9R5NGPOegKhgbzBdyvrG2yh8w |29-474-643-1443|ithely express pinto beans. blithely even foxes haggle. furiously regular theodol +8407.04|Supplier#000005406 |RUSSIA | 162889|Manufacturer#4 |j7 gYF5RW8DC5UrjKC |32-626-152-4621|r the blithely regular packages. slyly ironic theodoli +8386.08|Supplier#000008518 |FRANCE | 36014|Manufacturer#3 |2jqzqqAVe9crMVGP,n9nTsQXulNLTUYoJjEDcqWV|16-618-780-7481|blithely bold pains are carefully platelets. finally regular pinto beans sleep carefully special +8376.52|Supplier#000005306 |UNITED KINGDOM | 190267|Manufacturer#5 |9t8Y8 QqSIsoADPt6NLdk,TP5zyRx41oBUlgoGc9|33-632-514-7931|ly final accounts sleep special, regular requests. furiously regular +8348.74|Supplier#000008851 |FRANCE | 66344|Manufacturer#4 |nWxi7GwEbjhw1 |16-796-240-2472| boldly final deposits. regular, even instructions detect slyly. fluffily unusual pinto bea +8338.58|Supplier#000007269 |FRANCE | 17268|Manufacturer#4 |ZwhJSwABUoiB04,3 |16-267-277-4365|iously final accounts. even pinto beans cajole slyly regular +8328.46|Supplier#000001744 |ROMANIA | 69237|Manufacturer#5 |oLo3fV64q2,FKHa3p,qHnS7Yzv,ps8 |29-330-728-5873|ep carefully-- even, careful packages are slyly along t +8307.93|Supplier#000003142 |GERMANY | 18139|Manufacturer#1 |dqblvV8dCNAorGlJ |17-595-447-6026|olites wake furiously regular decoys. final requests nod +8231.61|Supplier#000009558 |RUSSIA | 192000|Manufacturer#2 |mcdgen,yT1iJDHDS5fV |32-762-137-5858| foxes according to the furi +8152.61|Supplier#000002731 |ROMANIA | 15227|Manufacturer#4 | nluXJCuY1tu |29-805-463-2030| special requests. even, regular warhorses affix among the final gr +8109.09|Supplier#000009186 |FRANCE | 99185|Manufacturer#1 |wgfosrVPexl9pEXWywaqlBMDYYf |16-668-570-1402|tions haggle slyly about the sil +8102.62|Supplier#000003347 |UNITED KINGDOM | 18344|Manufacturer#5 |m CtXS2S16i |33-454-274-8532|egrate with the slyly bold instructions. special foxes haggle silently among the +8046.07|Supplier#000008780 |FRANCE | 191222|Manufacturer#3 |AczzuE0UK9osj ,Lx0Jmh |16-473-215-6395|onic platelets cajole after the regular instructions. permanently bold excuses +8042.09|Supplier#000003245 |RUSSIA | 135705|Manufacturer#4 |Dh8Ikg39onrbOL4DyTfGw8a9oKUX3d9Y |32-836-132-8872|osits. packages cajole slyly. furiously regular deposits cajole slyly. q +8042.09|Supplier#000003245 |RUSSIA | 150729|Manufacturer#1 |Dh8Ikg39onrbOL4DyTfGw8a9oKUX3d9Y |32-836-132-8872|osits. packages cajole slyly. furiously regular deposits cajole slyly. q +7992.40|Supplier#000006108 |FRANCE | 118574|Manufacturer#1 |8tBydnTDwUqfBfFV4l3 |16-974-998-8937| ironic ideas? fluffily even instructions wake. blithel +7980.65|Supplier#000001288 |FRANCE | 13784|Manufacturer#4 |zE,7HgVPrCn |16-646-464-8247|ully bold courts. escapades nag slyly. furiously fluffy theodo +7950.37|Supplier#000008101 |GERMANY | 33094|Manufacturer#5 |kkYvL6IuvojJgTNG IKkaXQDYgx8ILohj |17-627-663-8014|arefully unusual requests x-ray above the quickly final deposits. +7937.93|Supplier#000009012 |ROMANIA | 83995|Manufacturer#2 |iUiTziH,Ek3i4lwSgunXMgrcTzwdb |29-250-925-9690|to the blithely ironic deposits nag sly +7914.45|Supplier#000001013 |RUSSIA | 125988|Manufacturer#2 |riRcntps4KEDtYScjpMIWeYF6mNnR |32-194-698-3365| busily bold packages are dolphi +7912.91|Supplier#000004211 |GERMANY | 159180|Manufacturer#5 |2wQRVovHrm3,v03IKzfTd,1PYsFXQFFOG |17-266-947-7315|ay furiously regular platelets. cou +7912.91|Supplier#000004211 |GERMANY | 184210|Manufacturer#4 |2wQRVovHrm3,v03IKzfTd,1PYsFXQFFOG |17-266-947-7315|ay furiously regular platelets. cou +7894.56|Supplier#000007981 |GERMANY | 85472|Manufacturer#4 |NSJ96vMROAbeXP |17-963-404-3760|ic platelets affix after the furiously +7887.08|Supplier#000009792 |GERMANY | 164759|Manufacturer#3 |Y28ITVeYriT3kIGdV2K8fSZ V2UqT5H1Otz |17-988-938-4296|ckly around the carefully fluffy theodolites. slyly ironic pack +7871.50|Supplier#000007206 |RUSSIA | 104695|Manufacturer#1 |3w fNCnrVmvJjE95sgWZzvW |32-432-452-7731|ironic requests. furiously final theodolites cajole. final, express packages sleep. quickly reg +7852.45|Supplier#000005864 |RUSSIA | 8363|Manufacturer#4 |WCNfBPZeSXh3h,c |32-454-883-3821|usly unusual pinto beans. brave ideas sleep carefully quickly ironi +7850.66|Supplier#000001518 |UNITED KINGDOM | 86501|Manufacturer#1 |ONda3YJiHKJOC |33-730-383-3892|ifts haggle fluffily pending pai +7843.52|Supplier#000006683 |FRANCE | 11680|Manufacturer#4 |2Z0JGkiv01Y00oCFwUGfviIbhzCdy |16-464-517-8943| express, final pinto beans x-ray slyly asymptotes. unusual, unusual diff --git a/examples/tpch/answers_sf1/q20.tbl b/examples/tpch/answers_sf1/q20.tbl new file mode 100644 index 000000000..41e0dbc4c --- /dev/null +++ b/examples/tpch/answers_sf1/q20.tbl @@ -0,0 +1,187 @@ +s_name |s_address +Supplier#000000020 |iybAE,RmTymrZVYaFZva2SH,j +Supplier#000000091 |YV45D7TkfdQanOOZ7q9QxkyGUapU1oOWU6q3 +Supplier#000000205 |rF uV8d0JNEk +Supplier#000000285 |Br7e1nnt1yxrw6ImgpJ7YdhFDjuBf +Supplier#000000287 |7a9SP7qW5Yku5PvSg +Supplier#000000354 |w8fOo5W,aS +Supplier#000000378 |FfbhyCxWvcPrO8ltp9 +Supplier#000000402 |i9Sw4DoyMhzhKXCH9By,AYSgmD +Supplier#000000530 |0qwCMwobKY OcmLyfRXlagA8ukENJv, +Supplier#000000555 |TfB,a5bfl3Ah 3Z 74GqnNs6zKVGM +Supplier#000000640 |mvvtlQKsTOsJj5Ihk7,cq +Supplier#000000729 |pqck2ppy758TQpZCUAjPvlU55K3QjfL7Bi +Supplier#000000736 |l6i2nMwVuovfKnuVgaSGK2rDy65DlAFLegiL7 +Supplier#000000761 |zlSLelQUj2XrvTTFnv7WAcYZGvvMTx882d4 +Supplier#000000887 |urEaTejH5POADP2ARrf +Supplier#000000935 |ij98czM 2KzWe7dDTOxB8sq0UfCdvrX +Supplier#000000975 |,AC e,tBpNwKb5xMUzeohxlRn, hdZJo73gFQF8y +Supplier#000001263 |rQWr6nf8ZhB2TAiIDIvo5Io +Supplier#000001367 |42YSkFcAXMMcucsqeEefOE4HeCC +Supplier#000001426 |bPOCc086oFm8sLtS,fGrH +Supplier#000001446 |lch9HMNU1R7a0LIybsUodVknk6 +Supplier#000001500 |wDmF5xLxtQch9ctVu, +Supplier#000001602 |uKNWIeafaM644 +Supplier#000001626 |UhxNRzUu1dtFmp0 +Supplier#000001682 |pXTkGxrTQVyH1Rr +Supplier#000001700 |7hMlCof1Y5zLFg +Supplier#000001726 |TeRY7TtTH24sEword7yAaSkjx8 +Supplier#000001730 |Rc8e,1Pybn r6zo0VJIEiD0UD vhk +Supplier#000001746 |qWsendlOekQG1aW4uq06uQaCm51se8lirv7 hBRd +Supplier#000001806 |M934fuZSnLW +Supplier#000001855 |MWk6EAeozXb +Supplier#000001931 |FpJbMU2h6ZR2eBv8I9NIxF +Supplier#000002022 | dwebGX7Id2pc25YvY33 +Supplier#000002036 |20ytTtVObjKUUI2WCB0A +Supplier#000002096 |kuxseyLtq QPLXxm9ZUrnB6Kkh92JtK5cQzzXNU +Supplier#000002117 |MRtkgKolHJ9Wh X9J,urANHKDzvjr +Supplier#000002204 |uYmlr46C06udCqanj0KiRsoTQakZsEyssL +Supplier#000002218 |nODZw5q4dx kp0K5 +Supplier#000002243 |nSOEV3JeOU79 +Supplier#000002245 |hz2qWXWVjOyKhqPYMoEwz6zFkrTaDM +Supplier#000002282 |ES21K9dxoW1I1TzWCj7ekdlNwSWnv1Z 6mQ,BKn +Supplier#000002303 |nCoWfpB6YOymbgOht7ltfklpkHl +Supplier#000002331 |WRh2w5WFvRg7Z0S1AvSvHCL +Supplier#000002373 |RzHSxOTQmElCjxIBiVA52Z JB58rJhPRylR +Supplier#000002419 |qydBQd14I5l5mVXa4fYY +Supplier#000002571 |JZUugz04c iJFLrlGsz9O N,W 1rVHNIReyq +Supplier#000002585 |CsPoKpw2QuTY4AV1NkWuttneIa4SN +Supplier#000002629 |0Bw,q5Zp8su9XrzoCngZ3cAEXZwZ +Supplier#000002721 |HVdFAN2JHMQSpKm +Supplier#000002730 |lIFxR4fzm31C6,muzJwl84z +Supplier#000002775 |yDclaDaBD4ihH +Supplier#000002799 |lwr, 6L3gdfc79PQut,4XO6nQsTJY63cAyYO +Supplier#000002934 |m,trBENywSArwg3DhB +Supplier#000002941 |Naddba 8YTEKekZyP0 +Supplier#000003028 |jouzgX0WZjhNMWLaH4fy +Supplier#000003095 |HxON3jJhUi3zjt,r mTD +Supplier#000003143 |hdolgh608uTkHh7t6qfSqkifKaiFjnCH +Supplier#000003185 |hMa535Cbf2mj1Nw4OWOKWVrsK0VdDkJURrdjSIJe +Supplier#000003189 |DWdPxt7 RnkZv6VOByR0em +Supplier#000003201 |E87yws6I,t0qNs4QW7UzExKiJnJDZWue +Supplier#000003213 |pxrRP4irQ1VoyfQ,dTf3 +Supplier#000003275 |9xO4nyJ2QJcX6vGf +Supplier#000003288 |EDdfNt7E5Uc,xLTupoIgYL4yY7ujh, +Supplier#000003314 |jnisU8MzqO4iUB3zsPcrysMw3DDUojS4q7LD +Supplier#000003373 |iy8VM48ynpc3N2OsBwAvhYakO2us9R1bi +Supplier#000003421 |Sh3dt9W5oeofFWovnFhrg, +Supplier#000003422 |DJoCEapUeBXoV1iYiCcPFQvzsTv2ZI960 +Supplier#000003441 |zvFJIzS,oUuShHjpcX +Supplier#000003590 |sy79CMLxqb,Cbo +Supplier#000003607 |lNqFHQYjwSAkf +Supplier#000003625 |qY588W0Yk5iaUy1RXTgNrEKrMAjBYHcKs +Supplier#000003723 |jZEp0OEythCLcS OmJSrFtxJ66bMlzSp +Supplier#000003849 |KgbZEaRk,6Q3mWvwh6uptrs1KRUHg 0 +Supplier#000003894 |vvGC rameLOk +Supplier#000003941 |Pmb05mQfBMS618O7WKqZJ 9vyv +Supplier#000004059 |umEYZSq9RJ2WEzdsv9meU8rmqwzVLRgiZwC +Supplier#000004207 |tF64pwiOM4IkWjN3mS,e06WuAjLx +Supplier#000004236 |dl,HPtJmGipxYsSqn9wmqkuWjst,mCeJ8O6T +Supplier#000004278 |bBddbpBxIVp Di9 +Supplier#000004281 |1OwPHh Pgiyeus,iZS5eA23JDOipwk +Supplier#000004304 |hQCAz59k,HLlp2CKUrcBIL +Supplier#000004346 |S3076LEOwo +Supplier#000004406 |Ah0ZaLu6VwufPWUz,7kbXgYZhauEaHqGIg +Supplier#000004430 |yvSsKNSTL5HLXBET4luOsPNLxKzAMk +Supplier#000004527 |p pVXCnxgcklWF6A1o3OHY3qW6 +Supplier#000004655 |67NqBc4 t3PG3F8aO IsqWNq4kGaPowYL +Supplier#000004851 |Rj,x6IgLT7kBL99nqp +Supplier#000004871 |,phpt6AWEnUS8t4Avb50rFfdg7O9c6nU8xxv8eC5 +Supplier#000004884 |42Z1uLye9nsn6aTGBNd dI8 x +Supplier#000004975 |GPq5PMKY6Wy +Supplier#000005076 |Xl7h9ifgvIHmqxFLgWfHK4Gjav BkP +Supplier#000005195 |Woi3b2ZaicPh ZSfu1EfXhE +Supplier#000005256 |Onc3t57VAMchm,pmoVLaU8bONni9NsuaM PzMMFz +Supplier#000005257 |f9g8SEHB7obMj3QXAjXS2vfYY22 +Supplier#000005300 |gXG28YqpxU +Supplier#000005323 |tMCkdqbDoyNo8vMIkzjBqYexoRAuv,T6 qzcu +Supplier#000005386 |Ub6AAfHpWLWP +Supplier#000005426 |9Dz2OVT1q sb4BK71ljQ1XjPBYRPvO +Supplier#000005465 |63cYZenZBRZ613Q1FaoG0,smnC5zl9 +Supplier#000005484 |saFdOR qW7AFY,3asPqiiAa11Mo22pCoN0BtPrKo +Supplier#000005505 |d2sbjG43KwMPX +Supplier#000005506 |On f5ypzoWgB +Supplier#000005631 |14TVrjlzo2SJEBYCDgpMwTlvwSqC +Supplier#000005642 |ZwKxAv3V40tW E8P7Qwu,zlu,kPsL +Supplier#000005686 |f2RBKec2T1NIi7yS M +Supplier#000005730 |5rkb0PSews HvxkL8JaD41UpnSF2cg8H1 +Supplier#000005736 |2dq XTYhtYWSfp +Supplier#000005737 |dmEWcS32C3kx,d,B95 OmYn48 +Supplier#000005797 |,o,OebwRbSDmVl9gN9fpWPCiqB UogvlSR +Supplier#000005875 |lK,sYiGzB94hSyHy9xvSZFbVQNCZe2LXZuGbS +Supplier#000005974 |REhR5jE,lLusQXvf54SwYySgsSSVFhu +Supplier#000006059 |4m0cv8MwJ9yX2vlwI Z +Supplier#000006065 |UiI2Cy3W4Tu5sLk LuvXLRy6KihlGv +Supplier#000006093 |KJNUg1odUT2wtCS2s6PrH3D6fd +Supplier#000006099 |aZilwQKYDTVPoK +Supplier#000006109 |rY5gbfh3dKHnylcQUTPGCwnbe +Supplier#000006217 |RVN23SYT9jenUeaWGXUd +Supplier#000006297 |73VRDOO56GUCyvc40oYJ +Supplier#000006435 |xIgE69XszYbnO4Eon7cHHO8y +Supplier#000006463 |7 wkdj2EO49iotley2kmIM ADpLSszGV3RNWj +Supplier#000006478 |bQYPnj9lpmW3U +Supplier#000006521 |b9 2zjHzxR +Supplier#000006642 |N,CUclSqRLJcS8zQ +Supplier#000006659 |iTLsnvD8D2GzWNUv kRInwRjk5rDeEmfup1 +Supplier#000006669 |NQ4Yryj624p7K53 +Supplier#000006748 |rC,2rEn8gKDIS5Q0dJEoiF +Supplier#000006761 |n4jhxGMqB5prD1HhpLvwrWStOLlla +Supplier#000006808 |HGd2Xo 9nEcHJhZvXjXxWKIpApT +Supplier#000006858 |fnlINT885vBBhsWwTGiZ0o22thwGY16h GHJj21 +Supplier#000006946 |To6Slo0GJTqcIvD +Supplier#000006949 |mLxYUJhsGcLtKe ,GFirNu183AvT +Supplier#000007072 |2tRyX9M1a 4Rcm57s779F1ANG9jlpK +Supplier#000007098 |G3j8g0KC4OcbAu2OVoPHrXQWMCUdjq8wgCHOExu +Supplier#000007132 |xonvn0KAQIL3p8kYk HC1FSSDSUSTC +Supplier#000007135 |ls DoKV7V5ulfQy9V +Supplier#000007147 |Xzb16kC63wmLVYexUEgB0hXFvHkjT5iPpq +Supplier#000007160 |TqDGBULB3cTqIT6FKDvm9BS4e4v,zwYiQPb +Supplier#000007169 |tEc95D2moN9S84nd55O,dlnW +Supplier#000007278 |I2ae3rS7KVF8GVHtB +Supplier#000007365 |51xhROLvQMJ05DndtZWt +Supplier#000007398 |V8eE6oZ00OFNU, +Supplier#000007402 |4UVv58ery1rjmqSR5 +Supplier#000007448 |yhhpWiJi7EJ6Q5VCaQ +Supplier#000007458 |BYuucapYkptZl6fnd2QaDyZmI9gR1Ih16e +Supplier#000007477 |9m9j0wfhWzCvVHxkU,PpAxwSH0h +Supplier#000007509 |q8,V6LJRoHJjHcOuSG7aLTMg +Supplier#000007561 |rMcFg2530VC +Supplier#000007616 |R IovIqzDi3,QHnaqZk1xS4hGAgelhP4yj +Supplier#000007760 |JsPE18PvcdFTK +Supplier#000007801 |69fi,U1r6enUb +Supplier#000007865 |5cDGCS,T6N +Supplier#000007885 |u3sicchh5ZpyTUpN1cJKNcAoabIWgY +Supplier#000007926 |ErzCF80K9Uy +Supplier#000007998 |LnASFBfYRFOo9d6d,asBvVq9Lo2P +Supplier#000008090 |eonbJZvoDFYBNUinYfp6yERIg +Supplier#000008224 |TWxt9f,LVER +Supplier#000008231 |IK7eGw Yj90sTdpsP,vcqWxLB +Supplier#000008243 |2AyePMkDqmzVzjGTizXthFLo8h EiudCMxOmIIG +Supplier#000008323 |75I18sZmASwm POeheRMdj9tmpyeQ,BfCXN5BIAb +Supplier#000008366 |h778cEj14BuW9OEKlvPTWq4iwASR6EBBXN7zeS8 +Supplier#000008532 |Uc29q4,5xVdDOF87UZrxhr4xWS0ihEUXuh +Supplier#000008595 |MH0iB73GQ3z UW3O DbCbqmc +Supplier#000008610 |SgVgP90vP452sUNTgzL9zKwXHXAzV6tV +Supplier#000008683 |gLuGcugfpJSeGQARnaHNCaWnGaqsNnjyl20 +Supplier#000008705 |aE,trRNdPx,4yinTD9O3DebDIp +Supplier#000008742 |HmPlQEzKCPEcTUL14,kKq +Supplier#000008841 |I 85Lu1sekbg2xrSIzm0 +Supplier#000008872 |8D 45GgxJO2OwwYP9S4AaXJKvDwPfLM +Supplier#000008879 |rDSA,D9oPM,65NMWEFrmGKAu +Supplier#000008967 |2kwEHyMG 7FwozNImAUE6mH0hYtqYculJM +Supplier#000008972 |w2vF6 D5YZO3visPXsqVfLADTK +Supplier#000009032 |qK,trB6Sdy4Dz1BRUFNy +Supplier#000009043 |57OPvKH4qyXIZ7IzYeCaw11a5N1Ki9f1WWmVQ, +Supplier#000009278 |RqYTzgxj93CLX 0mcYfCENOefD +Supplier#000009326 |XmiC,uy36B9,fb0zhcjaagiXQutg +Supplier#000009430 |igRqmneFt +Supplier#000009549 |h3RVchUf8MzY46IzbZ0ng09 +Supplier#000009601 |51m637bO,Rw5DnHWFUvLacRx9 +Supplier#000009709 |rRnCbHYgDgl9PZYnyWKVYSUW0vKg +Supplier#000009753 |wLhVEcRmd7PkJF4FBnGK7Z +Supplier#000009799 | 4wNjXGa4OKWl +Supplier#000009811 |E3iuyq7UnZxU7oPZIe2Gu6 +Supplier#000009812 |APFRMy3lCbgFga53n5t9DxzFPQPgnjrGt32 +Supplier#000009846 |57sNwJJ3PtBDu,hMPP5QvpcOcSNRXn3PypJJrh +Supplier#000009899 |7XdpAHrzr1t,UQFZE +Supplier#000009974 |7wJ,J5DKcxSU4Kp1cQLpbcAvB5AsvKT diff --git a/examples/tpch/answers_sf1/q21.tbl b/examples/tpch/answers_sf1/q21.tbl new file mode 100644 index 000000000..f944ca656 --- /dev/null +++ b/examples/tpch/answers_sf1/q21.tbl @@ -0,0 +1,101 @@ +s_name |numwait +Supplier#000002829 | 20 +Supplier#000005808 | 18 +Supplier#000000262 | 17 +Supplier#000000496 | 17 +Supplier#000002160 | 17 +Supplier#000002301 | 17 +Supplier#000002540 | 17 +Supplier#000003063 | 17 +Supplier#000005178 | 17 +Supplier#000008331 | 17 +Supplier#000002005 | 16 +Supplier#000002095 | 16 +Supplier#000005799 | 16 +Supplier#000005842 | 16 +Supplier#000006450 | 16 +Supplier#000006939 | 16 +Supplier#000009200 | 16 +Supplier#000009727 | 16 +Supplier#000000486 | 15 +Supplier#000000565 | 15 +Supplier#000001046 | 15 +Supplier#000001047 | 15 +Supplier#000001161 | 15 +Supplier#000001336 | 15 +Supplier#000001435 | 15 +Supplier#000003075 | 15 +Supplier#000003335 | 15 +Supplier#000005649 | 15 +Supplier#000006027 | 15 +Supplier#000006795 | 15 +Supplier#000006800 | 15 +Supplier#000006824 | 15 +Supplier#000007131 | 15 +Supplier#000007382 | 15 +Supplier#000008913 | 15 +Supplier#000009787 | 15 +Supplier#000000633 | 14 +Supplier#000001960 | 14 +Supplier#000002323 | 14 +Supplier#000002490 | 14 +Supplier#000002993 | 14 +Supplier#000003101 | 14 +Supplier#000004489 | 14 +Supplier#000005435 | 14 +Supplier#000005583 | 14 +Supplier#000005774 | 14 +Supplier#000007579 | 14 +Supplier#000008180 | 14 +Supplier#000008695 | 14 +Supplier#000009224 | 14 +Supplier#000000357 | 13 +Supplier#000000436 | 13 +Supplier#000000610 | 13 +Supplier#000000788 | 13 +Supplier#000000889 | 13 +Supplier#000001062 | 13 +Supplier#000001498 | 13 +Supplier#000002056 | 13 +Supplier#000002312 | 13 +Supplier#000002344 | 13 +Supplier#000002596 | 13 +Supplier#000002615 | 13 +Supplier#000002978 | 13 +Supplier#000003048 | 13 +Supplier#000003234 | 13 +Supplier#000003727 | 13 +Supplier#000003806 | 13 +Supplier#000004472 | 13 +Supplier#000005236 | 13 +Supplier#000005906 | 13 +Supplier#000006241 | 13 +Supplier#000006326 | 13 +Supplier#000006384 | 13 +Supplier#000006394 | 13 +Supplier#000006624 | 13 +Supplier#000006629 | 13 +Supplier#000006682 | 13 +Supplier#000006737 | 13 +Supplier#000006825 | 13 +Supplier#000007021 | 13 +Supplier#000007417 | 13 +Supplier#000007497 | 13 +Supplier#000007602 | 13 +Supplier#000008134 | 13 +Supplier#000008234 | 13 +Supplier#000009435 | 13 +Supplier#000009436 | 13 +Supplier#000009564 | 13 +Supplier#000009896 | 13 +Supplier#000000379 | 12 +Supplier#000000673 | 12 +Supplier#000000762 | 12 +Supplier#000000811 | 12 +Supplier#000000821 | 12 +Supplier#000001337 | 12 +Supplier#000001916 | 12 +Supplier#000001925 | 12 +Supplier#000002039 | 12 +Supplier#000002357 | 12 +Supplier#000002483 | 12 diff --git a/examples/tpch/answers_sf1/q22.tbl b/examples/tpch/answers_sf1/q22.tbl new file mode 100644 index 000000000..de4bb472c --- /dev/null +++ b/examples/tpch/answers_sf1/q22.tbl @@ -0,0 +1,8 @@ +cntrycode |numcust |totacctbal +13 | 888|6737713.99 +17 | 861|6460573.72 +18 | 964|7236687.40 +23 | 892|6701457.95 +29 | 948|7158866.63 +30 | 909|6808436.13 +31 | 922|6806670.18 diff --git a/examples/tpch/answers_sf1/q3.tbl b/examples/tpch/answers_sf1/q3.tbl new file mode 100644 index 000000000..69771b8be --- /dev/null +++ b/examples/tpch/answers_sf1/q3.tbl @@ -0,0 +1,11 @@ +l_orderkey |revenue |o_orderdat|o_shippriority + 2456423|406181.01|1995-03-05| 0 + 3459808|405838.70|1995-03-04| 0 + 492164|390324.06|1995-02-19| 0 + 1188320|384537.94|1995-03-09| 0 + 2435712|378673.06|1995-02-26| 0 + 4878020|378376.80|1995-03-12| 0 + 5521732|375153.92|1995-03-13| 0 + 2628192|373133.31|1995-02-22| 0 + 993600|371407.46|1995-03-05| 0 + 2300070|367371.15|1995-03-13| 0 diff --git a/examples/tpch/answers_sf1/q4.tbl b/examples/tpch/answers_sf1/q4.tbl new file mode 100644 index 000000000..91bc1da24 --- /dev/null +++ b/examples/tpch/answers_sf1/q4.tbl @@ -0,0 +1,6 @@ +o_orderpriority|order_count +1-URGENT | 10594 +2-HIGH | 10476 +3-MEDIUM | 10410 +4-NOT SPECIFIED| 10556 +5-LOW | 10487 diff --git a/examples/tpch/answers_sf1/q5.tbl b/examples/tpch/answers_sf1/q5.tbl new file mode 100644 index 000000000..5dac463df --- /dev/null +++ b/examples/tpch/answers_sf1/q5.tbl @@ -0,0 +1,6 @@ +n_name |revenue +INDONESIA |55502041.17 +VIETNAM |55295087.00 +CHINA |53724494.26 +INDIA |52035512.00 +JAPAN |45410175.70 diff --git a/examples/tpch/answers_sf1/q6.tbl b/examples/tpch/answers_sf1/q6.tbl new file mode 100644 index 000000000..703a8084c --- /dev/null +++ b/examples/tpch/answers_sf1/q6.tbl @@ -0,0 +1,2 @@ +revenue +123141078.23 diff --git a/examples/tpch/answers_sf1/q7.tbl b/examples/tpch/answers_sf1/q7.tbl new file mode 100644 index 000000000..7164bfac2 --- /dev/null +++ b/examples/tpch/answers_sf1/q7.tbl @@ -0,0 +1,5 @@ +supp_nation |cust_nation |l_year |revenue +FRANCE |GERMANY | 1995|54639732.73 +FRANCE |GERMANY | 1996|54633083.31 +GERMANY |FRANCE | 1995|52531746.67 +GERMANY |FRANCE | 1996|52520549.02 diff --git a/examples/tpch/answers_sf1/q8.tbl b/examples/tpch/answers_sf1/q8.tbl new file mode 100644 index 000000000..618439d1a --- /dev/null +++ b/examples/tpch/answers_sf1/q8.tbl @@ -0,0 +1,3 @@ +o_year |mkt_share + 1995|0.03 + 1996|0.04 diff --git a/examples/tpch/answers_sf1/q9.tbl b/examples/tpch/answers_sf1/q9.tbl new file mode 100644 index 000000000..16ab17f13 --- /dev/null +++ b/examples/tpch/answers_sf1/q9.tbl @@ -0,0 +1,176 @@ +nation |o_year |sum_profit +ALGERIA | 1998|27136900.18 +ALGERIA | 1997|48611833.50 +ALGERIA | 1996|48285482.68 +ALGERIA | 1995|44402273.60 +ALGERIA | 1994|48694008.07 +ALGERIA | 1993|46044207.78 +ALGERIA | 1992|45636849.49 +ARGENTINA | 1998|28341663.78 +ARGENTINA | 1997|47143964.12 +ARGENTINA | 1996|45255278.60 +ARGENTINA | 1995|45631769.21 +ARGENTINA | 1994|48268856.35 +ARGENTINA | 1993|48605593.62 +ARGENTINA | 1992|46654240.75 +BRAZIL | 1998|26527736.40 +BRAZIL | 1997|45640660.77 +BRAZIL | 1996|45090647.16 +BRAZIL | 1995|44015888.51 +BRAZIL | 1994|44854218.89 +BRAZIL | 1993|45766603.74 +BRAZIL | 1992|45280216.80 +CANADA | 1998|26828985.39 +CANADA | 1997|44849954.32 +CANADA | 1996|46307936.11 +CANADA | 1995|47311993.04 +CANADA | 1994|46691491.96 +CANADA | 1993|46634791.11 +CANADA | 1992|45873849.69 +CHINA | 1998|27510180.17 +CHINA | 1997|46123865.41 +CHINA | 1996|49532807.06 +CHINA | 1995|46734651.48 +CHINA | 1994|46397896.61 +CHINA | 1993|49634673.95 +CHINA | 1992|46949457.64 +EGYPT | 1998|28401491.80 +EGYPT | 1997|47674857.68 +EGYPT | 1996|47745727.55 +EGYPT | 1995|45897160.68 +EGYPT | 1994|47194895.23 +EGYPT | 1993|49133627.65 +EGYPT | 1992|47000574.50 +ETHIOPIA | 1998|25135046.14 +ETHIOPIA | 1997|43010596.08 +ETHIOPIA | 1996|43636287.19 +ETHIOPIA | 1995|43575757.33 +ETHIOPIA | 1994|41597208.53 +ETHIOPIA | 1993|42622804.16 +ETHIOPIA | 1992|44385735.68 +FRANCE | 1998|26210392.28 +FRANCE | 1997|42392969.47 +FRANCE | 1996|43306317.97 +FRANCE | 1995|46377408.43 +FRANCE | 1994|43447352.99 +FRANCE | 1993|43729961.06 +FRANCE | 1992|44052308.43 +GERMANY | 1998|25991257.11 +GERMANY | 1997|43968355.81 +GERMANY | 1996|45882074.80 +GERMANY | 1995|43314338.31 +GERMANY | 1994|44616995.44 +GERMANY | 1993|45126645.91 +GERMANY | 1992|44361141.21 +INDIA | 1998|29626417.24 +INDIA | 1997|51386111.34 +INDIA | 1996|47571018.51 +INDIA | 1995|49344062.28 +INDIA | 1994|50106952.43 +INDIA | 1993|48112766.70 +INDIA | 1992|47914303.12 +INDONESIA | 1998|27734909.68 +INDONESIA | 1997|44593812.99 +INDONESIA | 1996|44746729.81 +INDONESIA | 1995|45593622.70 +INDONESIA | 1994|45988483.88 +INDONESIA | 1993|46147963.79 +INDONESIA | 1992|45185777.07 +IRAN | 1998|26661608.93 +IRAN | 1997|45019114.17 +IRAN | 1996|45891397.10 +IRAN | 1995|44414285.23 +IRAN | 1994|43696360.48 +IRAN | 1993|45362775.81 +IRAN | 1992|43052338.41 +IRAQ | 1998|31188498.19 +IRAQ | 1997|48585307.52 +IRAQ | 1996|50036593.84 +IRAQ | 1995|48774801.73 +IRAQ | 1994|48795847.23 +IRAQ | 1993|47435691.51 +IRAQ | 1992|47562355.66 +JAPAN | 1998|24694102.17 +JAPAN | 1997|42377052.35 +JAPAN | 1996|40267778.91 +JAPAN | 1995|40925317.47 +JAPAN | 1994|41159518.31 +JAPAN | 1993|39589074.28 +JAPAN | 1992|39113493.91 +JORDAN | 1998|23489867.79 +JORDAN | 1997|41615962.66 +JORDAN | 1996|41860855.47 +JORDAN | 1995|39931672.09 +JORDAN | 1994|40707555.46 +JORDAN | 1993|39060405.47 +JORDAN | 1992|41657604.27 +KENYA | 1998|25566337.43 +KENYA | 1997|43108847.90 +KENYA | 1996|43482953.54 +KENYA | 1995|42517988.98 +KENYA | 1994|43612479.45 +KENYA | 1993|42724038.76 +KENYA | 1992|43217106.21 +MOROCCO | 1998|24915496.88 +MOROCCO | 1997|42698382.85 +MOROCCO | 1996|42986113.50 +MOROCCO | 1995|42316089.16 +MOROCCO | 1994|43458604.60 +MOROCCO | 1993|42672288.07 +MOROCCO | 1992|42800781.64 +MOZAMBIQUE | 1998|28279876.03 +MOZAMBIQUE | 1997|51159216.23 +MOZAMBIQUE | 1996|48072525.06 +MOZAMBIQUE | 1995|48905200.60 +MOZAMBIQUE | 1994|46092076.28 +MOZAMBIQUE | 1993|48555926.27 +MOZAMBIQUE | 1992|47809075.12 +PERU | 1998|26713966.27 +PERU | 1997|48324008.60 +PERU | 1996|50310008.86 +PERU | 1995|49647080.96 +PERU | 1994|46420910.28 +PERU | 1993|51536906.25 +PERU | 1992|47711665.31 +ROMANIA | 1998|27271993.10 +ROMANIA | 1997|45063059.20 +ROMANIA | 1996|47492335.03 +ROMANIA | 1995|45710636.29 +ROMANIA | 1994|46088041.11 +ROMANIA | 1993|47515092.56 +ROMANIA | 1992|44111439.80 +RUSSIA | 1998|27935323.73 +RUSSIA | 1997|48222347.29 +RUSSIA | 1996|47553559.49 +RUSSIA | 1995|46755990.10 +RUSSIA | 1994|48000515.62 +RUSSIA | 1993|48569624.51 +RUSSIA | 1992|47672831.53 +SAUDI ARABIA | 1998|27113516.84 +SAUDI ARABIA | 1997|46690468.96 +SAUDI ARABIA | 1996|47775782.67 +SAUDI ARABIA | 1995|46657107.83 +SAUDI ARABIA | 1994|48181672.81 +SAUDI ARABIA | 1993|45692556.44 +SAUDI ARABIA | 1992|48924913.27 +UNITED KINGDOM | 1998|26366682.88 +UNITED KINGDOM | 1997|44518130.19 +UNITED KINGDOM | 1996|45539729.62 +UNITED KINGDOM | 1995|46845879.34 +UNITED KINGDOM | 1994|43081609.57 +UNITED KINGDOM | 1993|44770146.76 +UNITED KINGDOM | 1992|44123402.55 +UNITED STATES | 1998|27826593.68 +UNITED STATES | 1997|46638572.36 +UNITED STATES | 1996|46688280.55 +UNITED STATES | 1995|48951591.62 +UNITED STATES | 1994|45099092.06 +UNITED STATES | 1993|46181600.53 +UNITED STATES | 1992|46168214.09 +VIETNAM | 1998|27281931.00 +VIETNAM | 1997|48735914.18 +VIETNAM | 1996|47824595.90 +VIETNAM | 1995|48235135.80 +VIETNAM | 1994|47729256.33 +VIETNAM | 1993|45352676.87 +VIETNAM | 1992|47846355.65 diff --git a/examples/tpch/q21_suppliers_kept_orders_waiting.py b/examples/tpch/q21_suppliers_kept_orders_waiting.py index 619c4406b..4ee9d3733 100644 --- a/examples/tpch/q21_suppliers_kept_orders_waiting.py +++ b/examples/tpch/q21_suppliers_kept_orders_waiting.py @@ -81,16 +81,12 @@ [col("o_orderkey")], [ F.array_agg(col("l_suppkey"), distinct=True).alias("all_suppliers"), - F.array_agg(col("failed_supp"), distinct=True).alias("failed_suppliers"), + F.array_agg( + col("failed_supp"), filter=col("failed_supp").is_not_null(), distinct=True + ).alias("failed_suppliers"), ], ) -# Remove the null entries that will get returned by array_agg so we can test to see where we only -# have a single failed supplier in a multiple supplier order -df = df.with_column( - "failed_suppliers", F.array_remove(col("failed_suppliers"), lit(None)) -) - # This is the check described above which will identify single failed supplier in a multiple # supplier order. df = df.filter(F.array_length(col("failed_suppliers")) == lit(1)).filter( diff --git a/examples/tpch/util.py b/examples/tpch/util.py index ec53bcd15..0b52c4e09 100644 --- a/examples/tpch/util.py +++ b/examples/tpch/util.py @@ -31,4 +31,4 @@ def get_data_path(filename: str) -> Path: def get_answer_file(answer_file: str) -> Path: path = Path(__file__).resolve().parent - return path / "../../benchmarks/tpch/data/answers" / f"{answer_file}.out" + return path / "answers_sf1" / f"{answer_file}.tbl" diff --git a/pyproject.toml b/pyproject.toml index 117aeefc2..910fc3eb2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -172,7 +172,12 @@ extend-allowed-calls = ["datafusion.lit", "lit"] "docs/source/conf.py" = ["ANN001", "ERA001", "INP001"] [tool.codespell] -skip = ["./python/tests/test_functions.py", "./target", "uv.lock"] +skip = [ + "./python/tests/test_functions.py", + "./target", + "uv.lock", + "./examples/tpch/answers_sf1/*", +] count = true ignore-words-list = ["IST", "ans"] From 876646d67771261cfd9a57c721bece0d95b9740c Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Fri, 27 Mar 2026 06:08:57 -0700 Subject: [PATCH 25/56] docs: clarify DataFusion 52 FFI session-parameter requirement for provider hooks (#1439) * mention new session arg * flow better * smaller change --- docs/source/user-guide/upgrade-guides.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/source/user-guide/upgrade-guides.rst b/docs/source/user-guide/upgrade-guides.rst index e3d7c2d87..2ac7f7703 100644 --- a/docs/source/user-guide/upgrade-guides.rst +++ b/docs/source/user-guide/upgrade-guides.rst @@ -52,8 +52,10 @@ parameter, which is a Python object that can be used to extract the ``FFI_LogicalExtensionCodec`` that is necessary. A complete example can be found in the `FFI example `_. -Your methods need to be updated to take an additional parameter like in this -example. +Your FFI hook methods — ``__datafusion_catalog_provider__``, +``__datafusion_schema_provider__``, ``__datafusion_table_provider__``, and +``__datafusion_table_function__`` — need to be updated to accept an additional +``session: Bound`` parameter, as shown in this example. .. code-block:: rust From 6cea061abeca55bbe1a53e3c07ad62145d3ac809 Mon Sep 17 00:00:00 2001 From: Nick <24689722+ntjohnson1@users.noreply.github.com> Date: Fri, 27 Mar 2026 09:29:09 -0400 Subject: [PATCH 26/56] Update remaining existing examples to make testable/standalone executable (#1437) * Move example to doctestable examples for context.py * Add more standard dafusion namespaces to reduce clutter * Update project to use ruff compatible with pre-commit version * Resolve ruff errors for newer version but just ignore them * Convert dataframe examples to doctestable. Found bug in dropping A * Move expr.py to doctestable examples * Move user_defined.py to doctestable examples --- conftest.py | 5 + pyproject.toml | 3 +- python/datafusion/context.py | 30 ++-- python/datafusion/dataframe.py | 84 +++++++----- python/datafusion/expr.py | 24 ++-- python/datafusion/input/location.py | 4 +- python/datafusion/plan.py | 2 +- python/datafusion/user_defined.py | 205 +++++++++++++++++----------- python/tests/test_sql.py | 2 +- uv.lock | 46 +++---- 10 files changed, 238 insertions(+), 167 deletions(-) diff --git a/conftest.py b/conftest.py index 1c89f92bc..73e90077a 100644 --- a/conftest.py +++ b/conftest.py @@ -20,6 +20,8 @@ import datafusion as dfn import numpy as np import pytest +from datafusion import col, lit +from datafusion import functions as F @pytest.fixture(autouse=True) @@ -27,3 +29,6 @@ def _doctest_namespace(doctest_namespace: dict) -> None: """Add common imports to the doctest namespace.""" doctest_namespace["dfn"] = dfn doctest_namespace["np"] = np + doctest_namespace["col"] = col + doctest_namespace["lit"] = lit + doctest_namespace["F"] = F diff --git a/pyproject.toml b/pyproject.toml index 910fc3eb2..d05a64083 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,6 @@ ignore = [ "FIX002", # Allow TODO lines - consider removing at some point "ISC001", # Recommended to ignore these rules when using with ruff-format "N812", # Allow importing functions as `F` - "PD901", # Allow variable name df "PLR0913", # Allow many arguments in function definition "SLF001", # Allow accessing private members "TD002", # Do not require author names in TODO statements @@ -195,7 +194,7 @@ dev = [ "pytest-asyncio>=0.23.3", "pytest>=7.4.4", "pyyaml>=6.0.3", - "ruff>=0.9.1", + "ruff>=0.15.1", "toml>=0.10.2", ] docs = [ diff --git a/python/datafusion/context.py b/python/datafusion/context.py index ba9290a58..960a5fcde 100644 --- a/python/datafusion/context.py +++ b/python/datafusion/context.py @@ -371,9 +371,8 @@ def with_fair_spill_pool(self, size: int) -> RuntimeEnvBuilder: Returns: A new :py:class:`RuntimeEnvBuilder` object with the updated setting. - Examples usage:: - - config = RuntimeEnvBuilder().with_fair_spill_pool(1024) + Examples: + >>> config = dfn.RuntimeEnvBuilder().with_fair_spill_pool(1024) """ self.config_internal = self.config_internal.with_fair_spill_pool(size) return self @@ -391,9 +390,8 @@ def with_greedy_memory_pool(self, size: int) -> RuntimeEnvBuilder: Returns: A new :py:class:`RuntimeEnvBuilder` object with the updated setting. - Example usage:: - - config = RuntimeEnvBuilder().with_greedy_memory_pool(1024) + Examples: + >>> config = dfn.RuntimeEnvBuilder().with_greedy_memory_pool(1024) """ self.config_internal = self.config_internal.with_greedy_memory_pool(size) return self @@ -407,9 +405,8 @@ def with_temp_file_path(self, path: str | pathlib.Path) -> RuntimeEnvBuilder: Returns: A new :py:class:`RuntimeEnvBuilder` object with the updated setting. - Example usage:: - - config = RuntimeEnvBuilder().with_temp_file_path("/tmp") + Examples: + >>> config = dfn.RuntimeEnvBuilder().with_temp_file_path("/tmp") """ self.config_internal = self.config_internal.with_temp_file_path(str(path)) return self @@ -444,9 +441,8 @@ def with_allow_ddl(self, allow: bool = True) -> SQLOptions: Returns: A new :py:class:`SQLOptions` object with the updated setting. - Example usage:: - - options = SQLOptions().with_allow_ddl(True) + Examples: + >>> options = dfn.SQLOptions().with_allow_ddl(True) """ self.options_internal = self.options_internal.with_allow_ddl(allow) return self @@ -462,9 +458,8 @@ def with_allow_dml(self, allow: bool = True) -> SQLOptions: Returns: A new :py:class:`SQLOptions` object with the updated setting. - Example usage:: - - options = SQLOptions().with_allow_dml(True) + Examples: + >>> options = dfn.SQLOptions().with_allow_dml(True) """ self.options_internal = self.options_internal.with_allow_dml(allow) return self @@ -478,9 +473,8 @@ def with_allow_statements(self, allow: bool = True) -> SQLOptions: Returns: A new :py:class:SQLOptions` object with the updated setting. - Example usage:: - - options = SQLOptions().with_allow_statements(True) + Examples: + >>> options = dfn.SQLOptions().with_allow_statements(True) """ self.options_internal = self.options_internal.with_allow_statements(allow) return self diff --git a/python/datafusion/dataframe.py b/python/datafusion/dataframe.py index 214d44a42..10e2a913f 100644 --- a/python/datafusion/dataframe.py +++ b/python/datafusion/dataframe.py @@ -331,7 +331,7 @@ def into_view(self, temporary: bool = False) -> Table: >>> result[0].column("value").to_pylist() [1] """ - from datafusion.catalog import Table as _Table + from datafusion.catalog import Table as _Table # noqa: PLC0415 return _Table(self.df.into_view(temporary)) @@ -451,9 +451,20 @@ def drop(self, *columns: str) -> DataFrame: Returns: DataFrame with those columns removed in the projection. - Example Usage:: - df.drop('a') # To drop a lower-cased column 'a' - df.drop('"a"') # To drop an upper-cased column 'A' + Examples: + To drop a lower-cased column 'a' + + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2], "b": [3, 4]}) + >>> df.drop("a").schema().names + ['b'] + + Or to drop an upper-cased column 'A' + + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"A": [1, 2], "b": [3, 4]}) + >>> df.drop('"A"').schema().names + ['b'] """ return DataFrame(self.df.drop(*columns)) @@ -468,11 +479,13 @@ def filter(self, *predicates: Expr | str) -> DataFrame: that will be parsed against the DataFrame schema. If more complex logic is required, see the logical operations in :py:mod:`~datafusion.functions`. - Example:: - - from datafusion import col, lit - df.filter(col("a") > lit(1)) - df.filter("a > 1") + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2, 3]}) + >>> df.filter(col("a") > lit(1)).to_pydict() + {'a': [2, 3]} + >>> df.filter("a > 1").to_pydict() + {'a': [2, 3]} Args: predicates: Predicate expression(s) or SQL strings to filter the DataFrame. @@ -495,14 +508,12 @@ def parse_sql_expr(self, expr: str) -> Expr: The expression is created and processed against the current schema. - Example:: - - from datafusion import col, lit - df.parse_sql_expr("a > 1") - - should produce: - - col("a") > lit(1) + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2, 3]}) + >>> expr = df.parse_sql_expr("a > 1") + >>> df.filter(expr).to_pydict() + {'a': [2, 3]} Args: expr: Expression string to be converted to datafusion expression @@ -519,10 +530,11 @@ def with_column(self, name: str, expr: Expr | str) -> DataFrame: :func:`datafusion.col` or :func:`datafusion.lit`, or a SQL expression string that will be parsed against the DataFrame schema. - Example:: - - from datafusion import col, lit - df.with_column("b", col("a") + lit(1)) + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2]}) + >>> df.with_column("b", col("a") + lit(10)).to_pydict() + {'a': [1, 2], 'b': [11, 12]} Args: name: Name of the column to add. @@ -885,10 +897,14 @@ def join_on( built with :func:`datafusion.col`. On expressions are used to support in-equality predicates. Equality predicates are correctly optimized. - Example:: - - from datafusion import col - df.join_on(other_df, col("id") == col("other_id")) + Examples: + >>> ctx = dfn.SessionContext() + >>> left = ctx.from_pydict({"a": [1, 2], "x": ["a", "b"]}) + >>> right = ctx.from_pydict({"b": [1, 2], "y": ["c", "d"]}) + >>> left.join_on( + ... right, col("a") == col("b") + ... ).sort(col("x")).to_pydict() + {'a': [1, 2], 'x': ['a', 'b'], 'b': [1, 2], 'y': ['c', 'd']} Args: right: Other DataFrame to join with. @@ -1350,15 +1366,17 @@ def __aiter__(self) -> AsyncIterator[RecordBatch]: def transform(self, func: Callable[..., DataFrame], *args: Any) -> DataFrame: """Apply a function to the current DataFrame which returns another DataFrame. - This is useful for chaining together multiple functions. For example:: - - def add_3(df: DataFrame) -> DataFrame: - return df.with_column("modified", lit(3)) + This is useful for chaining together multiple functions. - def within_limit(df: DataFrame, limit: int) -> DataFrame: - return df.filter(col("a") < lit(limit)).distinct() - - df = df.transform(modify_df).transform(within_limit, 4) + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2, 3]}) + >>> def add_3(df): + ... return df.with_column("modified", dfn.lit(3)) + >>> def within_limit(df: DataFrame, limit: int) -> DataFrame: + ... return df.filter(col("a") < lit(limit)).distinct() + >>> df.transform(add_3).transform(within_limit, 4).sort("a").to_pydict() + {'a': [1, 2, 3], 'modified': [3, 3, 3]} Args: func: A callable function that takes a DataFrame as it's first argument diff --git a/python/datafusion/expr.py b/python/datafusion/expr.py index 5760b8948..14753a4f5 100644 --- a/python/datafusion/expr.py +++ b/python/datafusion/expr.py @@ -342,7 +342,7 @@ def sort_list_to_raw_sort_list( return raw_sort_list -class Expr: +class Expr: # noqa: PLW1641 """Expression object. Expressions are one of the core concepts in DataFusion. See @@ -1367,16 +1367,18 @@ def is_unbounded(self) -> bool: class CaseBuilder: """Builder class for constructing case statements. - An example usage would be as follows:: - - import datafusion.functions as f - from datafusion import lit, col - df.select( - f.case(col("column_a")) - .when(lit(1), lit("One")) - .when(lit(2), lit("Two")) - .otherwise(lit("Unknown")) - ) + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2, 3]}) + >>> result = df.select( + ... dfn.functions.case(dfn.col("a")) + ... .when(dfn.lit(1), dfn.lit("One")) + ... .when(dfn.lit(2), dfn.lit("Two")) + ... .otherwise(dfn.lit("Other")) + ... .alias("label") + ... ) + >>> result.to_pydict() + {'label': ['One', 'Two', 'Other']} """ def __init__(self, case_builder: expr_internal.CaseBuilder) -> None: diff --git a/python/datafusion/input/location.py b/python/datafusion/input/location.py index b804ac18b..779d94d23 100644 --- a/python/datafusion/input/location.py +++ b/python/datafusion/input/location.py @@ -46,7 +46,7 @@ def build_table( num_rows = 0 # Total number of rows in the file. Used for statistics columns = [] if file_format == "parquet": - import pyarrow.parquet as pq + import pyarrow.parquet as pq # noqa: PLC0415 # Read the Parquet metadata metadata = pq.read_metadata(input_item) @@ -61,7 +61,7 @@ def build_table( ] elif format == "csv": - import csv + import csv # noqa: PLC0415 # Consume header row and count number of rows for statistics. # TODO: Possibly makes sense to have the eager number of rows diff --git a/python/datafusion/plan.py b/python/datafusion/plan.py index fb54fd624..9c96a18fc 100644 --- a/python/datafusion/plan.py +++ b/python/datafusion/plan.py @@ -32,7 +32,7 @@ ] -class LogicalPlan: +class LogicalPlan: # noqa: PLW1641 """Logical Plan. A `LogicalPlan` is a node in a tree of relational operators (such as diff --git a/python/datafusion/user_defined.py b/python/datafusion/user_defined.py index eef23e741..3eaccdfa3 100644 --- a/python/datafusion/user_defined.py +++ b/python/datafusion/user_defined.py @@ -184,7 +184,8 @@ def udf(*args: Any, **kwargs: Any): # noqa: D417 This class can be used both as either a function or a decorator. Usage: - - As a function: ``udf(func, input_fields, return_field, volatility, name)``. + - As a function: ``udf(func, input_fields, return_field, + volatility, name)``. - As a decorator: ``@udf(input_fields, return_field, volatility, name)``. When used a decorator, do **not** pass ``func`` explicitly. @@ -209,19 +210,33 @@ def udf(*args: Any, **kwargs: Any): # noqa: D417 A user-defined function that can be used in SQL expressions, data aggregation, or window function calls. - Example: Using ``udf`` as a function:: + Examples: + Using ``udf`` as a function: - def double_func(x): - return x * 2 - double_udf = udf(double_func, [pa.int32()], pa.int32(), - "volatile", "double_it") + >>> import pyarrow as pa + >>> import pyarrow.compute as pc + >>> from datafusion.user_defined import ScalarUDF + >>> def double_func(x): + ... return pc.multiply(x, 2) + >>> double_udf = ScalarUDF.udf( + ... double_func, [pa.int64()], pa.int64(), + ... "volatile", "double_it") - Example: Using ``udf`` as a decorator:: + Using ``udf`` as a decorator: - @udf([pa.int32()], pa.int32(), "volatile", "double_it") - def double_udf(x): - return x * 2 - """ # noqa: W505 E501 + >>> @ScalarUDF.udf([pa.int64()], pa.int64(), "volatile") + ... def decorator_double_udf(x): + ... return pc.multiply(x, 3) + + Apply to a dataframe: + + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"x": [1, 2, 3]}) + >>> df.select(double_udf(col("x")).alias("result")).to_pydict() + {'result': [2, 4, 6]} + >>> df.select(decorator_double_udf(col("x")).alias("result")).to_pydict() + {'result': [3, 6, 9]} + """ def _function( func: Callable[..., _R], @@ -458,48 +473,72 @@ def udaf(*args: Any, **kwargs: Any): # noqa: D417, C901 - As a decorator: ``@udaf(input_types, return_type, state_type, volatility, name)``. When using ``udaf`` as a decorator, do not pass ``accum`` explicitly. - Function example: - If your :py:class:`Accumulator` can be instantiated with no arguments, you - can simply pass it's type as `accum`. If you need to pass additional - arguments to it's constructor, you can define a lambda or a factory method. + can simply pass its type as ``accum``. If you need to pass additional + arguments to its constructor, you can define a lambda or a factory method. During runtime the :py:class:`Accumulator` will be constructed for every - instance in which this UDAF is used. The following examples are all valid:: - - import pyarrow as pa - import pyarrow.compute as pc - - class Summarize(Accumulator): - def __init__(self, bias: float = 0.0): - self._sum = pa.scalar(bias) - - def state(self) -> list[pa.Scalar]: - return [self._sum] - - def update(self, values: pa.Array) -> None: - self._sum = pa.scalar(self._sum.as_py() + pc.sum(values).as_py()) - - def merge(self, states: list[pa.Array]) -> None: - self._sum = pa.scalar(self._sum.as_py() + pc.sum(states[0]).as_py()) - - def evaluate(self) -> pa.Scalar: - return self._sum - - def sum_bias_10() -> Summarize: - return Summarize(10.0) - - udaf1 = udaf(Summarize, pa.float64(), pa.float64(), [pa.float64()], - "immutable") - udaf2 = udaf(sum_bias_10, pa.float64(), pa.float64(), [pa.float64()], - "immutable") - udaf3 = udaf(lambda: Summarize(20.0), pa.float64(), pa.float64(), - [pa.float64()], "immutable") - - Decorator example::: - - @udaf(pa.float64(), pa.float64(), [pa.float64()], "immutable") - def udf4() -> Summarize: - return Summarize(10.0) + instance in which this UDAF is used. + + Examples: + >>> import pyarrow as pa + >>> import pyarrow.compute as pc + >>> from datafusion.user_defined import AggregateUDF, Accumulator, udaf + >>> class Summarize(Accumulator): + ... def __init__(self, bias: float = 0.0): + ... self._sum = pa.scalar(bias) + ... def state(self): + ... return [self._sum] + ... def update(self, values): + ... self._sum = pa.scalar( + ... self._sum.as_py() + pc.sum(values).as_py()) + ... def merge(self, states): + ... self._sum = pa.scalar( + ... self._sum.as_py() + pc.sum(states[0]).as_py()) + ... def evaluate(self): + ... return self._sum + + Using ``udaf`` as a function: + + >>> udaf1 = AggregateUDF.udaf( + ... Summarize, pa.float64(), pa.float64(), + ... [pa.float64()], "immutable") + + Wrapping ``udaf`` with a function: + + >>> def sum_bias_10() -> Summarize: + ... return Summarize(10.0) + >>> udaf2 = udaf(sum_bias_10, pa.float64(), pa.float64(), [pa.float64()], + ... "immutable") + + Using ``udaf`` with lambda: + + >>> udaf3 = udaf(lambda: Summarize(20.0), pa.float64(), pa.float64(), + ... [pa.float64()], "immutable") + + Using ``udaf`` as a decorator: + + >>> @AggregateUDF.udaf( + ... pa.float64(), pa.float64(), + ... [pa.float64()], "immutable") + ... def udaf4(): + ... return Summarize(10.0) + + Apply to a dataframe: + + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0]}) + >>> df.aggregate([], [udaf1(col("a")).alias("total")]).collect_column( + ... "total")[0].as_py() + 6.0 + >>> df.aggregate([], [udaf2(col("a")).alias("total")]).collect_column( + ... "total")[0].as_py() + 16.0 + >>> df.aggregate([], [udaf3(col("a")).alias("total")]).collect_column( + ... "total")[0].as_py() + 26.0 + >>> df.aggregate([], [udaf4(col("a")).alias("total")]).collect_column( + ... "total")[0].as_py() + 16.0 Args: accum: The accumulator python function. Only needed when calling as a @@ -834,31 +873,45 @@ def udwf(*args: Any, **kwargs: Any): # noqa: D417 - As a decorator: ``@udwf(input_types, return_type, volatility, name)``. When using ``udwf`` as a decorator, do not pass ``func`` explicitly. - Function example:: - - import pyarrow as pa - - class BiasedNumbers(WindowEvaluator): - def __init__(self, start: int = 0) -> None: - self.start = start - - def evaluate_all(self, values: list[pa.Array], - num_rows: int) -> pa.Array: - return pa.array([self.start + i for i in range(num_rows)]) - - def bias_10() -> BiasedNumbers: - return BiasedNumbers(10) - - udwf1 = udwf(BiasedNumbers, pa.int64(), pa.int64(), "immutable") - udwf2 = udwf(bias_10, pa.int64(), pa.int64(), "immutable") - udwf3 = udwf(lambda: BiasedNumbers(20), pa.int64(), pa.int64(), "immutable") - - - Decorator example:: - - @udwf(pa.int64(), pa.int64(), "immutable") - def biased_numbers() -> BiasedNumbers: - return BiasedNumbers(10) + Examples: + >>> import pyarrow as pa + >>> from datafusion.user_defined import WindowUDF, WindowEvaluator, udwf + >>> class BiasedNumbers(WindowEvaluator): + ... def __init__(self, start: int = 0): + ... self.start = start + ... def evaluate_all(self, values, num_rows): + ... return pa.array( + ... [self.start + i for i in range(num_rows)]) + + Using ``udwf`` as a function: + + >>> udwf1 = WindowUDF.udwf( + ... BiasedNumbers, pa.int64(), pa.int64(), "immutable") + >>> def bias_10() -> BiasedNumbers: + ... return BiasedNumbers(10) + >>> udwf2 = udwf(bias_10, pa.int64(), pa.int64(), "immutable") + >>> udwf3 = udwf( + ... lambda: BiasedNumbers(20), pa.int64(), pa.int64(), "immutable" + ... ) + + Using ``udwf`` as a decorator: + + >>> @WindowUDF.udwf(pa.int64(), pa.int64(), "immutable") + ... def biased_numbers(): + ... return BiasedNumbers(10) + + Apply to a dataframe: + + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [10, 20, 30]}) + >>> df.select(udwf1(col("a")).alias("result")).to_pydict() + {'result': [0, 1, 2]} + >>> df.select(udwf2(col("a")).alias("result")).to_pydict() + {'result': [10, 11, 12]} + >>> df.select(udwf3(col("a")).alias("result")).to_pydict() + {'result': [20, 21, 22]} + >>> df.select(biased_numbers(col("a")).alias("result")).to_pydict() + {'result': [10, 11, 12]} Args: func: Only needed when calling as a function. Skip this argument when diff --git a/python/tests/test_sql.py b/python/tests/test_sql.py index 92c311930..1ed1746e1 100644 --- a/python/tests/test_sql.py +++ b/python/tests/test_sql.py @@ -188,7 +188,7 @@ def test_register_parquet_partitioned(ctx, tmp_path, path_to_str, legacy_data_ty partition_data_type = "string" if legacy_data_type else pa.string() if legacy_data_type: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning): # noqa: PT030 ctx.register_parquet( "datapp", dir_root, diff --git a/uv.lock b/uv.lock index f4926521b..3b7135e32 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14'", @@ -370,7 +370,7 @@ dev = [ { name = "pytest", specifier = ">=7.4.4" }, { name = "pytest-asyncio", specifier = ">=0.23.3" }, { name = "pyyaml", specifier = ">=6.0.3" }, - { name = "ruff", specifier = ">=0.9.1" }, + { name = "ruff", specifier = ">=0.15.1" }, { name = "toml", specifier = ">=0.10.2" }, ] docs = [ @@ -1329,27 +1329,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.9.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/67/3e/e89f736f01aa9517a97e2e7e0ce8d34a4d8207087b3cfdec95133fee13b5/ruff-0.9.1.tar.gz", hash = "sha256:fd2b25ecaf907d6458fa842675382c8597b3c746a2dde6717fe3415425df0c17", size = 3498844, upload-time = "2025-01-10T18:57:53.896Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/05/c3a2e0feb3d5d394cdfd552de01df9d3ec8a3a3771bbff247fab7e668653/ruff-0.9.1-py3-none-linux_armv6l.whl", hash = "sha256:84330dda7abcc270e6055551aca93fdde1b0685fc4fd358f26410f9349cf1743", size = 10645241, upload-time = "2025-01-10T18:56:45.897Z" }, - { url = "https://files.pythonhosted.org/packages/dd/da/59f0a40e5f88ee5c054ad175caaa2319fc96571e1d29ab4730728f2aad4f/ruff-0.9.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3cae39ba5d137054b0e5b472aee3b78a7c884e61591b100aeb544bcd1fc38d4f", size = 10391066, upload-time = "2025-01-10T18:56:52.224Z" }, - { url = "https://files.pythonhosted.org/packages/b7/fe/85e1c1acf0ba04a3f2d54ae61073da030f7a5dc386194f96f3c6ca444a78/ruff-0.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:50c647ff96f4ba288db0ad87048257753733763b409b2faf2ea78b45c8bb7fcb", size = 10012308, upload-time = "2025-01-10T18:56:55.426Z" }, - { url = "https://files.pythonhosted.org/packages/6f/9b/780aa5d4bdca8dcea4309264b8faa304bac30e1ce0bcc910422bfcadd203/ruff-0.9.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0c8b149e9c7353cace7d698e1656ffcf1e36e50f8ea3b5d5f7f87ff9986a7ca", size = 10881960, upload-time = "2025-01-10T18:56:59.539Z" }, - { url = "https://files.pythonhosted.org/packages/12/f4/dac4361afbfe520afa7186439e8094e4884ae3b15c8fc75fb2e759c1f267/ruff-0.9.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:beb3298604540c884d8b282fe7625651378e1986c25df51dec5b2f60cafc31ce", size = 10414803, upload-time = "2025-01-10T18:57:04.919Z" }, - { url = "https://files.pythonhosted.org/packages/f0/a2/057a3cb7999513cb78d6cb33a7d1cc6401c82d7332583786e4dad9e38e44/ruff-0.9.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39d0174ccc45c439093971cc06ed3ac4dc545f5e8bdacf9f067adf879544d969", size = 11464929, upload-time = "2025-01-10T18:57:08.146Z" }, - { url = "https://files.pythonhosted.org/packages/eb/c6/1ccfcc209bee465ced4874dcfeaadc88aafcc1ea9c9f31ef66f063c187f0/ruff-0.9.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:69572926c0f0c9912288915214ca9b2809525ea263603370b9e00bed2ba56dbd", size = 12170717, upload-time = "2025-01-10T18:57:12.564Z" }, - { url = "https://files.pythonhosted.org/packages/84/97/4a524027518525c7cf6931e9fd3b2382be5e4b75b2b61bec02681a7685a5/ruff-0.9.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:937267afce0c9170d6d29f01fcd1f4378172dec6760a9f4dface48cdabf9610a", size = 11708921, upload-time = "2025-01-10T18:57:17.216Z" }, - { url = "https://files.pythonhosted.org/packages/a6/a4/4e77cf6065c700d5593b25fca6cf725b1ab6d70674904f876254d0112ed0/ruff-0.9.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:186c2313de946f2c22bdf5954b8dd083e124bcfb685732cfb0beae0c47233d9b", size = 13058074, upload-time = "2025-01-10T18:57:20.57Z" }, - { url = "https://files.pythonhosted.org/packages/f9/d6/fcb78e0531e863d0a952c4c5600cc5cd317437f0e5f031cd2288b117bb37/ruff-0.9.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f94942a3bb767675d9a051867c036655fe9f6c8a491539156a6f7e6b5f31831", size = 11281093, upload-time = "2025-01-10T18:57:25.526Z" }, - { url = "https://files.pythonhosted.org/packages/e4/3b/7235bbeff00c95dc2d073cfdbf2b871b5bbf476754c5d277815d286b4328/ruff-0.9.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:728d791b769cc28c05f12c280f99e8896932e9833fef1dd8756a6af2261fd1ab", size = 10882610, upload-time = "2025-01-10T18:57:28.855Z" }, - { url = "https://files.pythonhosted.org/packages/2a/66/5599d23257c61cf038137f82999ca8f9d0080d9d5134440a461bef85b461/ruff-0.9.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2f312c86fb40c5c02b44a29a750ee3b21002bd813b5233facdaf63a51d9a85e1", size = 10489273, upload-time = "2025-01-10T18:57:32.219Z" }, - { url = "https://files.pythonhosted.org/packages/78/85/de4aa057e2532db0f9761e2c2c13834991e087787b93e4aeb5f1cb10d2df/ruff-0.9.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ae017c3a29bee341ba584f3823f805abbe5fe9cd97f87ed07ecbf533c4c88366", size = 11003314, upload-time = "2025-01-10T18:57:35.431Z" }, - { url = "https://files.pythonhosted.org/packages/00/42/afedcaa089116d81447347f76041ff46025849fedb0ed2b187d24cf70fca/ruff-0.9.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5dc40a378a0e21b4cfe2b8a0f1812a6572fc7b230ef12cd9fac9161aa91d807f", size = 11342982, upload-time = "2025-01-10T18:57:38.642Z" }, - { url = "https://files.pythonhosted.org/packages/39/c6/fe45f3eb27e3948b41a305d8b768e949bf6a39310e9df73f6c576d7f1d9f/ruff-0.9.1-py3-none-win32.whl", hash = "sha256:46ebf5cc106cf7e7378ca3c28ce4293b61b449cd121b98699be727d40b79ba72", size = 8819750, upload-time = "2025-01-10T18:57:41.93Z" }, - { url = "https://files.pythonhosted.org/packages/38/8d/580db77c3b9d5c3d9479e55b0b832d279c30c8f00ab0190d4cd8fc67831c/ruff-0.9.1-py3-none-win_amd64.whl", hash = "sha256:342a824b46ddbcdddd3abfbb332fa7fcaac5488bf18073e841236aadf4ad5c19", size = 9701331, upload-time = "2025-01-10T18:57:46.334Z" }, - { url = "https://files.pythonhosted.org/packages/b2/94/0498cdb7316ed67a1928300dd87d659c933479f44dec51b4f62bfd1f8028/ruff-0.9.1-py3-none-win_arm64.whl", hash = "sha256:1cd76c7f9c679e6e8f2af8f778367dca82b95009bc7b1a85a47f1521ae524fa7", size = 9145708, upload-time = "2025-01-10T18:57:51.308Z" }, +version = "0.15.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/df/f8629c19c5318601d3121e230f74cbee7a3732339c52b21daa2b82ef9c7d/ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4", size = 4597916, upload-time = "2026-03-12T23:05:47.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/2f/4e03a7e5ce99b517e98d3b4951f411de2b0fa8348d39cf446671adcce9a2/ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff", size = 10508953, upload-time = "2026-03-12T23:05:17.246Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/55bcdc3e9f80bcf39edf0cd272da6fa511a3d94d5a0dd9e0adf76ceebdb4/ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3", size = 10942257, upload-time = "2026-03-12T23:05:23.076Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/005c29bd1726c0f492bfa215e95154cf480574140cb5f867c797c18c790b/ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb", size = 10322683, upload-time = "2026-03-12T23:05:33.738Z" }, + { url = "https://files.pythonhosted.org/packages/5f/74/2f861f5fd7cbb2146bddb5501450300ce41562da36d21868c69b7a828169/ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8", size = 10660986, upload-time = "2026-03-12T23:05:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a1/309f2364a424eccb763cdafc49df843c282609f47fe53aa83f38272389e0/ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e", size = 10332177, upload-time = "2026-03-12T23:05:56.145Z" }, + { url = "https://files.pythonhosted.org/packages/30/41/7ebf1d32658b4bab20f8ac80972fb19cd4e2c6b78552be263a680edc55ac/ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15", size = 11170783, upload-time = "2026-03-12T23:06:01.742Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/6d488f6adca047df82cd62c304638bcb00821c36bd4881cfca221561fdfc/ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9", size = 12044201, upload-time = "2026-03-12T23:05:28.697Z" }, + { url = "https://files.pythonhosted.org/packages/71/68/e6f125df4af7e6d0b498f8d373274794bc5156b324e8ab4bf5c1b4fc0ec7/ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab", size = 11421561, upload-time = "2026-03-12T23:05:31.236Z" }, + { url = "https://files.pythonhosted.org/packages/f1/9f/f85ef5fd01a52e0b472b26dc1b4bd228b8f6f0435975442ffa4741278703/ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e", size = 11310928, upload-time = "2026-03-12T23:05:45.288Z" }, + { url = "https://files.pythonhosted.org/packages/8c/26/b75f8c421f5654304b89471ed384ae8c7f42b4dff58fa6ce1626d7f2b59a/ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c", size = 11235186, upload-time = "2026-03-12T23:05:50.677Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/d5a6d065962ff7a68a86c9b4f5500f7d101a0792078de636526c0edd40da/ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512", size = 10635231, upload-time = "2026-03-12T23:05:37.044Z" }, + { url = "https://files.pythonhosted.org/packages/d6/56/7c3acf3d50910375349016cf33de24be021532042afbed87942858992491/ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0", size = 10340357, upload-time = "2026-03-12T23:06:04.748Z" }, + { url = "https://files.pythonhosted.org/packages/06/54/6faa39e9c1033ff6a3b6e76b5df536931cd30caf64988e112bbf91ef5ce5/ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb", size = 10860583, upload-time = "2026-03-12T23:05:58.978Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/509a201b843b4dfb0b32acdedf68d951d3377988cae43949ba4c4133a96a/ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0", size = 11410976, upload-time = "2026-03-12T23:05:39.955Z" }, + { url = "https://files.pythonhosted.org/packages/6c/25/3fc9114abf979a41673ce877c08016f8e660ad6cf508c3957f537d2e9fa9/ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c", size = 10616872, upload-time = "2026-03-12T23:05:42.451Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/09ece68445ceac348df06e08bf75db72d0e8427765b96c9c0ffabc1be1d9/ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406", size = 11787271, upload-time = "2026-03-12T23:05:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size = 11060497, upload-time = "2026-03-12T23:05:25.968Z" }, ] [[package]] From 207fc16d62e2f64b687798741b33964aad9b5b7e Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Fri, 27 Mar 2026 14:39:20 +0100 Subject: [PATCH 27/56] Remove validate_pycapsule (#1426) The Bound<'_, PyCapsule>::pointer_checked does the same validation and is already used across the codebase --- crates/core/src/array.rs | 6 +----- crates/core/src/catalog.rs | 11 +++-------- crates/core/src/context.rs | 21 ++++++--------------- crates/core/src/dataframe.rs | 7 ++----- crates/core/src/udaf.rs | 7 ++----- crates/core/src/udf.rs | 11 ++++------- crates/core/src/udtf.rs | 8 ++------ crates/core/src/udwf.rs | 11 ++++------- crates/util/src/lib.rs | 13 ++++--------- 9 files changed, 28 insertions(+), 67 deletions(-) diff --git a/crates/core/src/array.rs b/crates/core/src/array.rs index 99e63ef50..f284fa9de 100644 --- a/crates/core/src/array.rs +++ b/crates/core/src/array.rs @@ -22,8 +22,6 @@ use arrow::array::{Array, ArrayRef}; use arrow::datatypes::{Field, FieldRef}; use arrow::ffi::{FFI_ArrowArray, FFI_ArrowSchema}; use arrow::pyarrow::ToPyArrow; -use datafusion_python_util::validate_pycapsule; -use pyo3::ffi::c_str; use pyo3::prelude::{PyAnyMethods, PyCapsuleMethods}; use pyo3::types::PyCapsule; use pyo3::{Bound, PyAny, PyResult, Python, pyclass, pymethods}; @@ -53,10 +51,8 @@ impl PyArrowArrayExportable { requested_schema: Option>, ) -> PyDataFusionResult<(Bound<'py, PyCapsule>, Bound<'py, PyCapsule>)> { let field = if let Some(schema_capsule) = requested_schema { - validate_pycapsule(&schema_capsule, "arrow_schema")?; - let data: NonNull = schema_capsule - .pointer_checked(Some(c_str!("arrow_schema")))? + .pointer_checked(Some(c"arrow_schema"))? .cast(); let schema_ptr = unsafe { data.as_ref() }; let desired_field = Field::try_from(schema_ptr)?; diff --git a/crates/core/src/catalog.rs b/crates/core/src/catalog.rs index f707e7e5c..30ec4744c 100644 --- a/crates/core/src/catalog.rs +++ b/crates/core/src/catalog.rs @@ -31,12 +31,10 @@ use datafusion_ffi::catalog_provider::FFI_CatalogProvider; use datafusion_ffi::proto::logical_extension_codec::FFI_LogicalExtensionCodec; use datafusion_ffi::schema_provider::FFI_SchemaProvider; use datafusion_python_util::{ - create_logical_extension_capsule, ffi_logical_codec_from_pycapsule, validate_pycapsule, - wait_for_future, + create_logical_extension_capsule, ffi_logical_codec_from_pycapsule, wait_for_future, }; use pyo3::IntoPyObjectExt; use pyo3::exceptions::PyKeyError; -use pyo3::ffi::c_str; use pyo3::prelude::*; use pyo3::types::PyCapsule; @@ -659,9 +657,8 @@ fn extract_catalog_provider_from_pyobj( } let provider = if let Ok(capsule) = catalog_provider.cast::() { - validate_pycapsule(capsule, "datafusion_catalog_provider")?; let data: NonNull = capsule - .pointer_checked(Some(c_str!("datafusion_catalog_provider")))? + .pointer_checked(Some(c"datafusion_catalog_provider"))? .cast(); let provider = unsafe { data.as_ref() }; let provider: Arc = provider.into(); @@ -692,10 +689,8 @@ fn extract_schema_provider_from_pyobj( } let provider = if let Ok(capsule) = schema_provider.cast::() { - validate_pycapsule(capsule, "datafusion_schema_provider")?; - let data: NonNull = capsule - .pointer_checked(Some(c_str!("datafusion_schema_provider")))? + .pointer_checked(Some(c"datafusion_schema_provider"))? .cast(); let provider = unsafe { data.as_ref() }; let provider: Arc = provider.into(); diff --git a/crates/core/src/context.rs b/crates/core/src/context.rs index 200b6470b..00cab4be4 100644 --- a/crates/core/src/context.rs +++ b/crates/core/src/context.rs @@ -55,12 +55,11 @@ use datafusion_ffi::table_provider_factory::FFI_TableProviderFactory; use datafusion_proto::logical_plan::DefaultLogicalExtensionCodec; use datafusion_python_util::{ create_logical_extension_capsule, ffi_logical_codec_from_pycapsule, get_global_ctx, - get_tokio_runtime, spawn_future, validate_pycapsule, wait_for_future, + get_tokio_runtime, spawn_future, wait_for_future, }; use object_store::ObjectStore; use pyo3::IntoPyObjectExt; use pyo3::exceptions::{PyKeyError, PyValueError}; -use pyo3::ffi::c_str; use pyo3::prelude::*; use pyo3::types::{PyCapsule, PyDict, PyList, PyTuple}; use url::Url; @@ -675,10 +674,8 @@ impl PySessionContext { let factory: Arc = if let Ok(capsule) = factory.cast::().map_err(py_datafusion_err) { - validate_pycapsule(capsule, "datafusion_table_provider_factory")?; - let data: NonNull = capsule - .pointer_checked(Some(c_str!("datafusion_table_provider_factory")))? + .pointer_checked(Some(c"datafusion_table_provider_factory"))? .cast(); let factory = unsafe { data.as_ref() }; factory.into() @@ -709,12 +706,9 @@ impl PySessionContext { .call1((codec_capsule,))?; } - let provider = if let Ok(capsule) = provider.cast::().map_err(py_datafusion_err) - { - validate_pycapsule(capsule, "datafusion_catalog_provider_list")?; - + let provider = if let Ok(capsule) = provider.cast::() { let data: NonNull = capsule - .pointer_checked(Some(c_str!("datafusion_catalog_provider_list")))? + .pointer_checked(Some(c"datafusion_catalog_provider_list"))? .cast(); let provider = unsafe { data.as_ref() }; let provider: Arc = provider.into(); @@ -747,12 +741,9 @@ impl PySessionContext { .call1((codec_capsule,))?; } - let provider = if let Ok(capsule) = provider.cast::().map_err(py_datafusion_err) - { - validate_pycapsule(capsule, "datafusion_catalog_provider")?; - + let provider = if let Ok(capsule) = provider.cast::() { let data: NonNull = capsule - .pointer_checked(Some(c_str!("datafusion_catalog_provider")))? + .pointer_checked(Some(c"datafusion_catalog_provider"))? .cast(); let provider = unsafe { data.as_ref() }; let provider: Arc = provider.into(); diff --git a/crates/core/src/dataframe.rs b/crates/core/src/dataframe.rs index 29fc05ed3..72595ba81 100644 --- a/crates/core/src/dataframe.rs +++ b/crates/core/src/dataframe.rs @@ -41,12 +41,11 @@ use datafusion::logical_expr::SortExpr; use datafusion::logical_expr::dml::InsertOp; use datafusion::parquet::basic::{BrotliLevel, Compression, GzipLevel, ZstdLevel}; use datafusion::prelude::*; -use datafusion_python_util::{is_ipython_env, spawn_future, validate_pycapsule, wait_for_future}; +use datafusion_python_util::{is_ipython_env, spawn_future, wait_for_future}; use futures::{StreamExt, TryStreamExt}; use parking_lot::Mutex; use pyo3::PyErr; use pyo3::exceptions::PyValueError; -use pyo3::ffi::c_str; use pyo3::prelude::*; use pyo3::pybacked::PyBackedStr; use pyo3::types::{PyCapsule, PyList, PyTuple, PyTupleMethods}; @@ -1117,10 +1116,8 @@ impl PyDataFrame { let mut projection: Option = None; if let Some(schema_capsule) = requested_schema { - validate_pycapsule(&schema_capsule, "arrow_schema")?; - let data: NonNull = schema_capsule - .pointer_checked(Some(c_str!("arrow_schema")))? + .pointer_checked(Some(c"arrow_schema"))? .cast(); let schema_ptr = unsafe { data.as_ref() }; let desired_schema = Schema::try_from(schema_ptr)?; diff --git a/crates/core/src/udaf.rs b/crates/core/src/udaf.rs index ed26c79cc..80ef51716 100644 --- a/crates/core/src/udaf.rs +++ b/crates/core/src/udaf.rs @@ -27,8 +27,7 @@ use datafusion::logical_expr::{ Accumulator, AccumulatorFactoryFunction, AggregateUDF, AggregateUDFImpl, create_udaf, }; use datafusion_ffi::udaf::FFI_AggregateUDF; -use datafusion_python_util::{parse_volatility, validate_pycapsule}; -use pyo3::ffi::c_str; +use datafusion_python_util::parse_volatility; use pyo3::prelude::*; use pyo3::types::{PyCapsule, PyTuple}; @@ -157,10 +156,8 @@ pub fn to_rust_accumulator(accum: Py) -> AccumulatorFactoryFunction { } fn aggregate_udf_from_capsule(capsule: &Bound<'_, PyCapsule>) -> PyDataFusionResult { - validate_pycapsule(capsule, "datafusion_aggregate_udf")?; - let data: NonNull = capsule - .pointer_checked(Some(c_str!("datafusion_aggregate_udf")))? + .pointer_checked(Some(c"datafusion_aggregate_udf"))? .cast(); let udaf = unsafe { data.as_ref() }; let udaf: Arc = udaf.into(); diff --git a/crates/core/src/udf.rs b/crates/core/src/udf.rs index 7543f96d4..c0a39cb47 100644 --- a/crates/core/src/udf.rs +++ b/crates/core/src/udf.rs @@ -32,13 +32,12 @@ use datafusion::logical_expr::{ Volatility, }; use datafusion_ffi::udf::FFI_ScalarUDF; -use datafusion_python_util::{parse_volatility, validate_pycapsule}; -use pyo3::ffi::c_str; +use datafusion_python_util::parse_volatility; use pyo3::prelude::*; use pyo3::types::{PyCapsule, PyTuple}; use crate::array::PyArrowArrayExportable; -use crate::errors::{PyDataFusionResult, py_datafusion_err, to_datafusion_err}; +use crate::errors::{PyDataFusionResult, to_datafusion_err}; use crate::expr::PyExpr; /// This struct holds the Python written function that is a @@ -194,11 +193,9 @@ impl PyScalarUDF { pub fn from_pycapsule(func: Bound<'_, PyAny>) -> PyDataFusionResult { if func.hasattr("__datafusion_scalar_udf__")? { let capsule = func.getattr("__datafusion_scalar_udf__")?.call0()?; - let capsule = capsule.cast::().map_err(py_datafusion_err)?; - validate_pycapsule(capsule, "datafusion_scalar_udf")?; - + let capsule = capsule.cast::().map_err(to_datafusion_err)?; let data: NonNull = capsule - .pointer_checked(Some(c_str!("datafusion_scalar_udf")))? + .pointer_checked(Some(c"datafusion_scalar_udf"))? .cast(); let udf = unsafe { data.as_ref() }; let udf: Arc = udf.into(); diff --git a/crates/core/src/udtf.rs b/crates/core/src/udtf.rs index 77c5ffbbc..9371732dc 100644 --- a/crates/core/src/udtf.rs +++ b/crates/core/src/udtf.rs @@ -22,10 +22,8 @@ use datafusion::catalog::{TableFunctionImpl, TableProvider}; use datafusion::error::Result as DataFusionResult; use datafusion::logical_expr::Expr; use datafusion_ffi::udtf::FFI_TableFunction; -use datafusion_python_util::validate_pycapsule; use pyo3::IntoPyObjectExt; use pyo3::exceptions::{PyImportError, PyTypeError}; -use pyo3::ffi::c_str; use pyo3::prelude::*; use pyo3::types::{PyCapsule, PyTuple, PyType}; @@ -73,11 +71,9 @@ impl PyTableFunction { err } })?; - let capsule = capsule.cast::().map_err(py_datafusion_err)?; - validate_pycapsule(capsule, "datafusion_table_function")?; - + let capsule = capsule.cast::()?; let data: NonNull = capsule - .pointer_checked(Some(c_str!("datafusion_table_function")))? + .pointer_checked(Some(c"datafusion_table_function"))? .cast(); let ffi_func = unsafe { data.as_ref() }; let foreign_func: Arc = ffi_func.to_owned().into(); diff --git a/crates/core/src/udwf.rs b/crates/core/src/udwf.rs index ff7ab0352..1d3608ada 100644 --- a/crates/core/src/udwf.rs +++ b/crates/core/src/udwf.rs @@ -32,14 +32,13 @@ use datafusion::logical_expr::{ }; use datafusion::scalar::ScalarValue; use datafusion_ffi::udwf::FFI_WindowUDF; -use datafusion_python_util::{parse_volatility, validate_pycapsule}; +use datafusion_python_util::parse_volatility; use pyo3::exceptions::PyValueError; -use pyo3::ffi::c_str; use pyo3::prelude::*; use pyo3::types::{PyCapsule, PyList, PyTuple}; use crate::common::data_type::PyScalarValue; -use crate::errors::{PyDataFusionResult, py_datafusion_err, to_datafusion_err}; +use crate::errors::{PyDataFusionResult, to_datafusion_err}; use crate::expr::PyExpr; #[derive(Debug)] @@ -262,11 +261,9 @@ impl PyWindowUDF { func }; - let capsule = capsule.cast::().map_err(py_datafusion_err)?; - validate_pycapsule(capsule, "datafusion_window_udf")?; - + let capsule = capsule.cast::().map_err(to_datafusion_err)?; let data: NonNull = capsule - .pointer_checked(Some(c_str!("datafusion_window_udf")))? + .pointer_checked(Some(c"datafusion_window_udf"))? .cast(); let udwf = unsafe { data.as_ref() }; let udwf: Arc = udwf.into(); diff --git a/crates/util/src/lib.rs b/crates/util/src/lib.rs index 2678a6b9a..5b1c89936 100644 --- a/crates/util/src/lib.rs +++ b/crates/util/src/lib.rs @@ -26,14 +26,13 @@ use datafusion::logical_expr::Volatility; use datafusion_ffi::proto::logical_extension_codec::FFI_LogicalExtensionCodec; use datafusion_ffi::table_provider::FFI_TableProvider; use pyo3::exceptions::{PyImportError, PyTypeError, PyValueError}; -use pyo3::ffi::c_str; use pyo3::prelude::*; use pyo3::types::{PyCapsule, PyType}; use tokio::runtime::Runtime; use tokio::task::JoinHandle; use tokio::time::sleep; -use crate::errors::{PyDataFusionError, PyDataFusionResult, py_datafusion_err, to_datafusion_err}; +use crate::errors::{PyDataFusionError, PyDataFusionResult, to_datafusion_err}; pub mod errors; @@ -186,11 +185,9 @@ pub fn table_provider_from_pycapsule<'py>( })?; } - if let Ok(capsule) = obj.cast::().map_err(py_datafusion_err) { - validate_pycapsule(capsule, "datafusion_table_provider")?; - + if let Ok(capsule) = obj.cast::() { let data: NonNull = capsule - .pointer_checked(Some(c_str!("datafusion_table_provider")))? + .pointer_checked(Some(c"datafusion_table_provider"))? .cast(); let provider = unsafe { data.as_ref() }; let provider: Arc = provider.into(); @@ -220,10 +217,8 @@ pub fn ffi_logical_codec_from_pycapsule(obj: Bound) -> PyResult()?; - validate_pycapsule(capsule, "datafusion_logical_extension_codec")?; - let data: NonNull = capsule - .pointer_checked(Some(c_str!("datafusion_logical_extension_codec")))? + .pointer_checked(Some(c"datafusion_logical_extension_codec"))? .cast(); let codec = unsafe { data.as_ref() }; From 75d07ce706fcbda423ad90222aa3dacccb7a5766 Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Fri, 27 Mar 2026 10:29:23 -0400 Subject: [PATCH 28/56] Implement configuration extension support (#1391) * Implement config options * Update examples and tests * pyo3 update * Add docstring * rat * Update examples/datafusion-ffi-example/python/tests/_test_config.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update crates/core/src/context.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update crates/core/src/context.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- crates/core/src/context.rs | 28 ++++ crates/core/src/dataset_exec.rs | 2 +- .../python/tests/_test_config.py | 35 +++++ examples/datafusion-ffi-example/src/config.rs | 133 ++++++++++++++++++ examples/datafusion-ffi-example/src/lib.rs | 3 + python/datafusion/context.py | 13 ++ 6 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 examples/datafusion-ffi-example/python/tests/_test_config.py create mode 100644 examples/datafusion-ffi-example/src/config.rs diff --git a/crates/core/src/context.rs b/crates/core/src/context.rs index 00cab4be4..4880b92b3 100644 --- a/crates/core/src/context.rs +++ b/crates/core/src/context.rs @@ -49,6 +49,7 @@ use datafusion::prelude::{ }; use datafusion_ffi::catalog_provider::FFI_CatalogProvider; use datafusion_ffi::catalog_provider_list::FFI_CatalogProviderList; +use datafusion_ffi::config::extension_options::FFI_ExtensionOptions; use datafusion_ffi::execution::FFI_TaskContextProvider; use datafusion_ffi::proto::logical_extension_codec::FFI_LogicalExtensionCodec; use datafusion_ffi::table_provider_factory::FFI_TableProviderFactory; @@ -184,6 +185,33 @@ impl PySessionConfig { fn set(&self, key: &str, value: &str) -> Self { Self::from(self.config.clone().set_str(key, value)) } + + pub fn with_extension(&self, extension: Bound) -> PyResult { + if !extension.hasattr("__datafusion_extension_options__")? { + return Err(pyo3::exceptions::PyAttributeError::new_err( + "Expected extension object to define __datafusion_extension_options__()", + )); + } + let capsule = extension.call_method0("__datafusion_extension_options__")?; + let capsule = capsule.cast::()?; + + let extension: NonNull = capsule + .pointer_checked(Some(c_str!("datafusion_extension_options")))? + .cast(); + let mut extension = unsafe { extension.as_ref() }.clone(); + + let mut config = self.config.clone(); + let options = config.options_mut(); + if let Some(prior_extension) = options.extensions.get::() { + extension + .merge(prior_extension) + .map_err(py_datafusion_err)?; + } + + options.extensions.insert(extension); + + Ok(Self::from(config)) + } } /// Runtime options for a SessionContext diff --git a/crates/core/src/dataset_exec.rs b/crates/core/src/dataset_exec.rs index e3c058c07..a7dd1500d 100644 --- a/crates/core/src/dataset_exec.rs +++ b/crates/core/src/dataset_exec.rs @@ -111,7 +111,7 @@ impl DatasetExec { let scanner = dataset.call_method("scanner", (), Some(&kwargs))?; - let schema = Arc::new( + let schema: SchemaRef = Arc::new( scanner .getattr("projected_schema")? .extract::>()? diff --git a/examples/datafusion-ffi-example/python/tests/_test_config.py b/examples/datafusion-ffi-example/python/tests/_test_config.py new file mode 100644 index 000000000..24d527921 --- /dev/null +++ b/examples/datafusion-ffi-example/python/tests/_test_config.py @@ -0,0 +1,35 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from datafusion import SessionConfig, SessionContext +from datafusion_ffi_example import MyConfig + + +def test_config_extension_show_set(): + config = MyConfig() + config = SessionConfig( + {"datafusion.catalog.information_schema": "true"} + ).with_extension(config) + config.set("my_config.baz_count", "42") + ctx = SessionContext(config) + + result = ctx.sql("SHOW my_config.baz_count;").collect() + assert result[0][1][0].as_py() == "42" + + ctx.sql("SET my_config.baz_count=1;") + result = ctx.sql("SHOW my_config.baz_count;").collect() + assert result[0][1][0].as_py() == "1" diff --git a/examples/datafusion-ffi-example/src/config.rs b/examples/datafusion-ffi-example/src/config.rs new file mode 100644 index 000000000..6cdb8aa83 --- /dev/null +++ b/examples/datafusion-ffi-example/src/config.rs @@ -0,0 +1,133 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::any::Any; + +use datafusion_common::config::{ + ConfigEntry, ConfigExtension, ConfigField, ExtensionOptions, Visit, +}; +use datafusion_common::{DataFusionError, config_err}; +use datafusion_ffi::config::extension_options::FFI_ExtensionOptions; +use pyo3::exceptions::PyRuntimeError; +use pyo3::types::PyCapsule; +use pyo3::{Bound, PyResult, Python, pyclass, pymethods}; + +/// My own config options. +#[pyclass( + from_py_object, + name = "MyConfig", + module = "datafusion_ffi_example", + subclass +)] +#[derive(Clone, Debug)] +pub struct MyConfig { + /// Should "foo" be replaced by "bar"? + pub foo_to_bar: bool, + + /// How many "baz" should be created? + pub baz_count: usize, +} + +#[pymethods] +impl MyConfig { + #[new] + fn new() -> Self { + Self::default() + } + + fn __datafusion_extension_options__<'py>( + &self, + py: Python<'py>, + ) -> PyResult> { + let name = cr"datafusion_extension_options".into(); + + let mut config = FFI_ExtensionOptions::default(); + config + .add_config(self) + .map_err(|e| PyRuntimeError::new_err(e.to_string()))?; + + PyCapsule::new(py, config, Some(name)) + } +} + +impl Default for MyConfig { + fn default() -> Self { + Self { + foo_to_bar: true, + baz_count: 1337, + } + } +} + +impl ConfigExtension for MyConfig { + const PREFIX: &'static str = "my_config"; +} + +impl ExtensionOptions for MyConfig { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn cloned(&self) -> Box { + Box::new(self.clone()) + } + + fn set(&mut self, key: &str, value: &str) -> datafusion_common::Result<()> { + datafusion_common::config::ConfigField::set(self, key, value) + } + + fn entries(&self) -> Vec { + vec![ + ConfigEntry { + key: "foo_to_bar".to_owned(), + value: Some(format!("{}", self.foo_to_bar)), + description: "foo to bar", + }, + ConfigEntry { + key: "baz_count".to_owned(), + value: Some(format!("{}", self.baz_count)), + description: "baz count", + }, + ] + } +} + +impl ConfigField for MyConfig { + fn visit(&self, v: &mut V, _key: &str, _description: &'static str) { + let key = "foo_to_bar"; + let desc = "foo to bar"; + self.foo_to_bar.visit(v, key, desc); + + let key = "baz_count"; + let desc = "baz count"; + self.baz_count.visit(v, key, desc); + } + + fn set(&mut self, key: &str, value: &str) -> Result<(), DataFusionError> { + let (key, rem) = key.split_once('.').unwrap_or((key, "")); + match key { + "foo_to_bar" => self.foo_to_bar.set(rem, value.as_ref()), + "baz_count" => self.baz_count.set(rem, value.as_ref()), + + _ => config_err!("Config value \"{}\" not found on MyConfig", key), + } + } +} diff --git a/examples/datafusion-ffi-example/src/lib.rs b/examples/datafusion-ffi-example/src/lib.rs index 68120a4cd..e708c49cc 100644 --- a/examples/datafusion-ffi-example/src/lib.rs +++ b/examples/datafusion-ffi-example/src/lib.rs @@ -19,6 +19,7 @@ use pyo3::prelude::*; use crate::aggregate_udf::MySumUDF; use crate::catalog_provider::{FixedSchemaProvider, MyCatalogProvider, MyCatalogProviderList}; +use crate::config::MyConfig; use crate::scalar_udf::IsNullUDF; use crate::table_function::MyTableFunction; use crate::table_provider::MyTableProvider; @@ -27,6 +28,7 @@ use crate::window_udf::MyRankUDF; pub(crate) mod aggregate_udf; pub(crate) mod catalog_provider; +pub(crate) mod config; pub(crate) mod scalar_udf; pub(crate) mod table_function; pub(crate) mod table_provider; @@ -46,5 +48,6 @@ fn datafusion_ffi_example(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/python/datafusion/context.py b/python/datafusion/context.py index 960a5fcde..c8edc816f 100644 --- a/python/datafusion/context.py +++ b/python/datafusion/context.py @@ -296,6 +296,19 @@ def set(self, key: str, value: str) -> SessionConfig: self.config_internal = self.config_internal.set(key, value) return self + def with_extension(self, extension: Any) -> SessionConfig: + """Create a new configuration using an extension. + + Args: + extension: A custom configuration extension object. These are + shared from another DataFusion extension library. + + Returns: + A new :py:class:`SessionConfig` object with the updated setting. + """ + self.config_internal = self.config_internal.with_extension(extension) + return self + class RuntimeEnvBuilder: """Runtime configuration options.""" From 4b215724565cec4257ed9dfa25271c5481c9f7b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Topias=20Pyykk=C3=B6nen?= <43851547+toppyy@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:17:44 +0200 Subject: [PATCH 29/56] Add a working, more complete example of using a catalog (docs) (#1427) * Add a working, more complete example of using a catalog * the default schema is 'public', not 'default' * in-memory table instead of imaginary csv for standalone example * typo fix Co-authored-by: Kevin Liu * minor c string fix after merge --------- Co-authored-by: Kevin Liu Co-authored-by: Tim Saucer --- crates/core/src/context.rs | 2 +- docs/source/user-guide/data-sources.rst | 26 ++++++++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/crates/core/src/context.rs b/crates/core/src/context.rs index 4880b92b3..53994d2f5 100644 --- a/crates/core/src/context.rs +++ b/crates/core/src/context.rs @@ -196,7 +196,7 @@ impl PySessionConfig { let capsule = capsule.cast::()?; let extension: NonNull = capsule - .pointer_checked(Some(c_str!("datafusion_extension_options")))? + .pointer_checked(Some(c"datafusion_extension_options"))? .cast(); let mut extension = unsafe { extension.as_ref() }.clone(); diff --git a/docs/source/user-guide/data-sources.rst b/docs/source/user-guide/data-sources.rst index 26f1303c4..48ff4c014 100644 --- a/docs/source/user-guide/data-sources.rst +++ b/docs/source/user-guide/data-sources.rst @@ -224,25 +224,37 @@ A common technique for organizing tables is using a three level hierarchical app supports this form of organizing using the :py:class:`~datafusion.catalog.Catalog`, :py:class:`~datafusion.catalog.Schema`, and :py:class:`~datafusion.catalog.Table`. By default, a :py:class:`~datafusion.context.SessionContext` comes with a single Catalog and a single Schema -with the names ``datafusion`` and ``default``, respectively. +with the names ``datafusion`` and ``public``, respectively. The default implementation uses an in-memory approach to the catalog and schema. We have support -for adding additional in-memory catalogs and schemas. This can be done like in the following +for adding additional in-memory catalogs and schemas. You can access tables registered in a schema +either through the Dataframe API or via sql commands. This can be done like in the following example: .. code-block:: python + import pyarrow as pa from datafusion.catalog import Catalog, Schema + from datafusion import SessionContext + + ctx = SessionContext() my_catalog = Catalog.memory_catalog() - my_schema = Schema.memory_schema() + my_schema = Schema.memory_schema() + my_catalog.register_schema('my_schema_name', my_schema) + ctx.register_catalog_provider('my_catalog_name', my_catalog) - my_catalog.register_schema("my_schema_name", my_schema) + # Create an in-memory table + table = pa.table({ + 'name': ['Bulbasaur', 'Charmander', 'Squirtle'], + 'type': ['Grass', 'Fire', 'Water'], + 'hp': [45, 39, 44], + }) + df = ctx.create_dataframe([table.to_batches()], name='pokemon') - ctx.register_catalog("my_catalog_name", my_catalog) + my_schema.register_table('pokemon', df) -You could then register tables in ``my_schema`` and access them either through the DataFrame -API or via sql commands such as ``"SELECT * from my_catalog_name.my_schema_name.my_table"``. + ctx.sql('SELECT * FROM my_catalog_name.my_schema_name.pokemon').show() User Defined Catalog and Schema ------------------------------- From 8c6a481b43b322a80990ff6d793d1a921218f567 Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Fri, 27 Mar 2026 12:44:54 -0400 Subject: [PATCH 30/56] chore: update dependencies (#1447) * Cargo lock update * Update download-artifact * Update upload-artifact to v7 * Update cargo toml to latest deps --- .github/workflows/build.yml | 26 +-- .github/workflows/test.yml | 4 +- Cargo.lock | 338 ++++++++++++++++++++---------------- Cargo.toml | 4 +- 4 files changed, 209 insertions(+), 163 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 97ab2b2f0..77880fdfa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -124,7 +124,7 @@ jobs: - name: Generate license file run: uv run --no-project python ./dev/create_license.py - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 with: name: python-wheel-license path: LICENSE.txt @@ -141,7 +141,7 @@ jobs: - run: rm LICENSE.txt - name: Download LICENSE.txt - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: python-wheel-license path: . @@ -186,13 +186,13 @@ jobs: rustup-components: rust-std - name: Archive wheels - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: dist-manylinux-x86_64 path: dist/* - name: Archive FFI test wheel - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: test-ffi-manylinux-x86_64 path: examples/datafusion-ffi-example/dist/* @@ -209,7 +209,7 @@ jobs: - run: rm LICENSE.txt - name: Download LICENSE.txt - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: python-wheel-license path: . @@ -258,7 +258,7 @@ jobs: rustup-components: rust-std - name: Archive wheels - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: inputs.build_mode == 'release' with: name: dist-manylinux-aarch64 @@ -283,7 +283,7 @@ jobs: - run: rm LICENSE.txt - name: Download LICENSE.txt - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: python-wheel-license path: . @@ -333,7 +333,7 @@ jobs: run: find target/wheels/ - name: Archive wheels - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: inputs.build_mode == 'release' with: name: dist-${{ matrix.os }} @@ -357,7 +357,7 @@ jobs: - run: rm LICENSE.txt - name: Download LICENSE.txt - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: python-wheel-license path: . @@ -388,7 +388,7 @@ jobs: run: find target/wheels/ - name: Archive wheels - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: dist-macos-aarch64 path: target/wheels/* @@ -406,7 +406,7 @@ jobs: - uses: actions/checkout@v6 - run: rm LICENSE.txt - name: Download LICENSE.txt - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: python-wheel-license path: . @@ -444,7 +444,7 @@ jobs: - build-sdist steps: - name: Merge Build Artifacts - uses: actions/upload-artifact/merge@v6 + uses: actions/upload-artifact/merge@v7 with: name: dist pattern: dist-* @@ -497,7 +497,7 @@ jobs: # Download the Linux wheel built in the previous job - name: Download pre-built Linux wheel - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: dist-manylinux-x86_64 path: wheels/ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 881a1aca2..890072a0d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,14 +59,14 @@ jobs: # Download the Linux wheel built in the build workflow - name: Download pre-built Linux wheel - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: dist-manylinux-x86_64 path: wheels/ # Download the FFI test wheel - name: Download pre-built FFI test wheel - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: test-ffi-manylinux-x86_64 path: wheels/ diff --git a/Cargo.lock b/Cargo.lock index eb4b76d3f..ee89c8bda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,9 +111,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "apache-avro" @@ -131,7 +131,7 @@ dependencies = [ "miniz_oxide", "num-bigint", "quad-rand", - "rand", + "rand 0.9.2", "regex-lite", "serde", "serde_bytes", @@ -155,9 +155,9 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" dependencies = [ "rustversion", ] @@ -176,9 +176,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "arrow" -version = "58.0.0" +version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602268ce9f569f282cedb9a9f6bac569b680af47b9b077d515900c03c5d190da" +checksum = "d441fdda254b65f3e9025910eb2c2066b6295d9c8ed409522b8d2ace1ff8574c" dependencies = [ "arrow-arith", "arrow-array", @@ -198,9 +198,9 @@ dependencies = [ [[package]] name = "arrow-arith" -version = "58.0.0" +version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd53c6bf277dea91f136ae8e3a5d7041b44b5e489e244e637d00ae302051f56f" +checksum = "ced5406f8b720cc0bc3aa9cf5758f93e8593cda5490677aa194e4b4b383f9a59" dependencies = [ "arrow-array", "arrow-buffer", @@ -212,9 +212,9 @@ dependencies = [ [[package]] name = "arrow-array" -version = "58.0.0" +version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e53796e07a6525edaf7dc28b540d477a934aff14af97967ad1d5550878969b9e" +checksum = "772bd34cacdda8baec9418d80d23d0fb4d50ef0735685bd45158b83dfeb6e62d" dependencies = [ "ahash", "arrow-buffer", @@ -231,9 +231,9 @@ dependencies = [ [[package]] name = "arrow-buffer" -version = "58.0.0" +version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c1a85bb2e94ee10b76531d8bc3ce9b7b4c0d508cabfb17d477f63f2617bd20" +checksum = "898f4cf1e9598fdb77f356fdf2134feedfd0ee8d5a4e0a5f573e7d0aec16baa4" dependencies = [ "bytes", "half", @@ -243,9 +243,9 @@ dependencies = [ [[package]] name = "arrow-cast" -version = "58.0.0" +version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89fb245db6b0e234ed8e15b644edb8664673fefe630575e94e62cd9d489a8a26" +checksum = "b0127816c96533d20fc938729f48c52d3e48f99717e7a0b5ade77d742510736d" dependencies = [ "arrow-array", "arrow-buffer", @@ -265,9 +265,9 @@ dependencies = [ [[package]] name = "arrow-csv" -version = "58.0.0" +version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d374882fb465a194462527c0c15a93aa19a554cf690a6b77a26b2a02539937a7" +checksum = "ca025bd0f38eeecb57c2153c0123b960494138e6a957bbda10da2b25415209fe" dependencies = [ "arrow-array", "arrow-cast", @@ -280,9 +280,9 @@ dependencies = [ [[package]] name = "arrow-data" -version = "58.0.0" +version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "189d210bc4244c715fa3ed9e6e22864673cccb73d5da28c2723fb2e527329b33" +checksum = "42d10beeab2b1c3bb0b53a00f7c944a178b622173a5c7bcabc3cb45d90238df4" dependencies = [ "arrow-buffer", "arrow-schema", @@ -293,9 +293,9 @@ dependencies = [ [[package]] name = "arrow-ipc" -version = "58.0.0" +version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7968c2e5210c41f4909b2ef76f6e05e172b99021c2def5edf3cc48fdd39d1d6c" +checksum = "609a441080e338147a84e8e6904b6da482cefb957c5cdc0f3398872f69a315d0" dependencies = [ "arrow-array", "arrow-buffer", @@ -309,9 +309,9 @@ dependencies = [ [[package]] name = "arrow-json" -version = "58.0.0" +version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92111dba5bf900f443488e01f00d8c4ddc2f47f5c50039d18120287b580baa22" +checksum = "6ead0914e4861a531be48fe05858265cf854a4880b9ed12618b1d08cba9bebc8" dependencies = [ "arrow-array", "arrow-buffer", @@ -333,9 +333,9 @@ dependencies = [ [[package]] name = "arrow-ord" -version = "58.0.0" +version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "211136cb253577ee1a6665f741a13136d4e563f64f5093ffd6fb837af90b9495" +checksum = "763a7ba279b20b52dad300e68cfc37c17efa65e68623169076855b3a9e941ca5" dependencies = [ "arrow-array", "arrow-buffer", @@ -346,9 +346,9 @@ dependencies = [ [[package]] name = "arrow-pyarrow" -version = "58.0.0" +version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "205437da4c0877c756c81bfe847a621d0a740cd00a155109d65510a1a62ebcd9" +checksum = "e63351dc11981a316c828a6032a5021345bba882f68bc4a36c36825a50725089" dependencies = [ "arrow-array", "arrow-data", @@ -358,9 +358,9 @@ dependencies = [ [[package]] name = "arrow-row" -version = "58.0.0" +version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e0f20145f9f5ea3fe383e2ba7a7487bf19be36aa9dbf5dd6a1f92f657179663" +checksum = "e14fe367802f16d7668163ff647830258e6e0aeea9a4d79aaedf273af3bdcd3e" dependencies = [ "arrow-array", "arrow-buffer", @@ -371,9 +371,9 @@ dependencies = [ [[package]] name = "arrow-schema" -version = "58.0.0" +version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b47e0ca91cc438d2c7879fe95e0bca5329fff28649e30a88c6f760b1faeddcb" +checksum = "c30a1365d7a7dc50cc847e54154e6af49e4c4b0fddc9f607b687f29212082743" dependencies = [ "bitflags", "serde_core", @@ -382,9 +382,9 @@ dependencies = [ [[package]] name = "arrow-select" -version = "58.0.0" +version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "750a7d1dda177735f5e82a314485b6915c7cccdbb278262ac44090f4aba4a325" +checksum = "78694888660a9e8ac949853db393af2a8b8fc82c19ce333132dfa2e72cc1a7fe" dependencies = [ "ahash", "arrow-array", @@ -396,9 +396,9 @@ dependencies = [ [[package]] name = "arrow-string" -version = "58.0.0" +version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1eab1208bc4fe55d768cdc9b9f3d9df5a794cdb3ee2586bf89f9b30dc31ad8c" +checksum = "61e04a01f8bb73ce54437514c5fd3ee2aa3e8abe4c777ee5cc55853b1652f79e" dependencies = [ "arrow-array", "arrow-buffer", @@ -425,9 +425,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d67d43201f4d20c78bcda740c142ca52482d81da80681533d33bf3f0596c8e2" +checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" dependencies = [ "compression-codecs", "compression-core", @@ -533,7 +533,7 @@ dependencies = [ "cc", "cfg-if", "constant_time_eq", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -547,9 +547,9 @@ dependencies = [ [[package]] name = "bon" -version = "3.9.0" +version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d13a61f2963b88eef9c1be03df65d42f6996dfeac1054870d950fcf66686f83" +checksum = "f47dbe92550676ee653353c310dfb9cf6ba17ee70396e1f7cf0a2020ad49b2fe" dependencies = [ "bon-macros", "rustversion", @@ -557,9 +557,9 @@ dependencies = [ [[package]] name = "bon-macros" -version = "3.9.0" +version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d314cc62af2b6b0c65780555abb4d02a03dd3b799cd42419044f0c38d99738c0" +checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c" dependencies = [ "darling", "ident_case", @@ -593,9 +593,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6f81257d10a0f602a294ae4182251151ff97dbb504ef9afcdda4a64b24d9b4" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" @@ -620,9 +620,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.56" +version = "1.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" dependencies = [ "find-msvc-tools", "jobserver", @@ -642,6 +642,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", +] + [[package]] name = "chrono" version = "0.4.44" @@ -666,9 +677,9 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] @@ -779,6 +790,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -944,7 +964,7 @@ dependencies = [ "object_store", "parking_lot", "parquet", - "rand", + "rand 0.9.2", "regex", "sqlparser", "tempfile", @@ -1067,7 +1087,7 @@ dependencies = [ "liblzma", "log", "object_store", - "rand", + "rand 0.9.2", "tokio", "tokio-util", "url", @@ -1219,7 +1239,7 @@ dependencies = [ "log", "object_store", "parking_lot", - "rand", + "rand 0.9.2", "tempfile", "url", ] @@ -1335,7 +1355,7 @@ dependencies = [ "md-5", "memchr", "num-traits", - "rand", + "rand 0.9.2", "regex", "sha2", "unicode-segmentation", @@ -1609,7 +1629,7 @@ dependencies = [ "datafusion-proto-common", "object_store", "prost", - "rand", + "rand 0.9.2", ] [[package]] @@ -1980,20 +2000,21 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", + "rand_core 0.10.0", "wasip2", "wasip3", ] @@ -2338,15 +2359,15 @@ checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb" dependencies = [ "memchr", "serde", @@ -2363,9 +2384,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" @@ -2379,9 +2400,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -2458,9 +2479,9 @@ checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libloading" @@ -2510,9 +2531,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -2543,9 +2564,9 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "lz4_flex" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab6473172471198271ff72e9379150e9dfd70d8e533e0752a27e515b48dd375e" +checksum = "db9a0d582c2874f68138a16ce1867e0ffde6c0bb0a0df85e1f36d04146db488a" dependencies = [ "twox-hash", ] @@ -2587,9 +2608,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", @@ -2652,16 +2673,18 @@ dependencies = [ [[package]] name = "object_store" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2858065e55c148d294a9f3aae3b0fa9458edadb41a108397094566f4e3c0dfb" +checksum = "622acbc9100d3c10e2ee15804b0caa40e55c933d5aa53814cd520805b7958a49" dependencies = [ "async-trait", "base64", "bytes", "chrono", "form_urlencoded", - "futures", + "futures-channel", + "futures-core", + "futures-util", "http", "http-body-util", "httparse", @@ -2672,7 +2695,7 @@ dependencies = [ "parking_lot", "percent-encoding", "quick-xml", - "rand", + "rand 0.10.0", "reqwest", "ring", "rustls-pki-types", @@ -2690,9 +2713,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "openssl-probe" @@ -2734,9 +2757,9 @@ dependencies = [ [[package]] name = "parquet" -version = "58.0.0" +version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f491d0ef1b510194426ee67ddc18a9b747ef3c42050c19322a2cd2e1666c29b" +checksum = "7d3f9f2205199603564127932b89695f52b62322f541d0fc7179d57c2e1c9877" dependencies = [ "ahash", "arrow-array", @@ -2849,9 +2872,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -3069,9 +3092,9 @@ checksum = "5a651516ddc9168ebd67b24afd085a718be02f8858fe406591b013d101ce2f40" [[package]] name = "quick-xml" -version = "0.38.4" +version = "0.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" dependencies = [ "memchr", "serde", @@ -3099,14 +3122,14 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand", + "rand 0.9.2", "ring", "rustc-hash", "rustls", @@ -3134,9 +3157,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -3147,6 +3170,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.9.2" @@ -3154,7 +3183,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", - "rand_core", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.0", ] [[package]] @@ -3164,7 +3204,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.5", ] [[package]] @@ -3176,6 +3216,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + [[package]] name = "recursive" version = "0.1.1" @@ -3236,9 +3282,9 @@ checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "regress" @@ -3332,9 +3378,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", @@ -3345,9 +3391,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "once_cell", "ring", @@ -3381,9 +3427,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "ring", "rustls-pki-types", @@ -3413,9 +3459,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -3452,9 +3498,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "3.6.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", "core-foundation", @@ -3465,9 +3511,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.16.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -3555,9 +3601,9 @@ dependencies = [ [[package]] name = "serde_tokenstream" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64060d864397305347a78851c51588fd283767e7e7589829e8121d65512340f1" +checksum = "d7c49585c52c01f13c5c2ebb333f14f6885d76daa768d8a037d28017ec538c69" dependencies = [ "proc-macro2", "quote", @@ -3597,7 +3643,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -3609,9 +3655,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "simdutf8" @@ -3645,12 +3691,12 @@ checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3800,12 +3846,12 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "tempfile" -version = "3.25.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -3863,9 +3909,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -3878,9 +3924,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -3893,9 +3939,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -4113,9 +4159,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-width" @@ -4161,11 +4207,11 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.21.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ - "getrandom 0.4.1", + "getrandom 0.4.2", "js-sys", "serde_core", "wasm-bindgen", @@ -4222,9 +4268,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -4235,9 +4281,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", "futures-util", @@ -4249,9 +4295,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4259,9 +4305,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -4272,9 +4318,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -4328,9 +4374,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -4720,18 +4766,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", @@ -4800,9 +4846,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c745c48e1007337ed136dc99df34128b9faa6ed542d80a1c673cf55a6d7236c8" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" [[package]] name = "zmij" diff --git a/Cargo.toml b/Cargo.toml index fd8b7c7c7..346f6da3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ members = ["crates/core", "crates/util", "examples/datafusion-ffi-example"] resolver = "3" [workspace.dependencies] -tokio = { version = "1.49" } +tokio = { version = "1.50" } pyo3 = { version = "0.28" } pyo3-async-runtimes = { version = "0.28" } pyo3-log = "0.13.3" @@ -50,7 +50,7 @@ datafusion-functions-window = { version = "53" } datafusion-expr = { version = "53" } prost = "0.14.3" serde_json = "1" -uuid = { version = "1.21" } +uuid = { version = "1.23" } mimalloc = { version = "0.1", default-features = false } async-trait = "0.1.89" futures = "0.3" From acd9a8dcdd1015497835ae3c9a49e4bf5961d719 Mon Sep 17 00:00:00 2001 From: Nick <24689722+ntjohnson1@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:19:18 -0400 Subject: [PATCH 31/56] Complete doc string examples for functions.py (#1435) * Verify all non-alias functions have doc string * MNove all alias for statements to see also blocks and confirm no examples * Fix google doc style for all examples * Remove builtins use * Add coverage for optional filter * Cover optional argument examples for window and value functions * Cover optional arguments for scalar functions * Cover array and aggregation functions * Make examples different * Make format more consistent * Remove duplicated df definition --- python/datafusion/functions.py | 2048 +++++++++++++++++++++----------- 1 file changed, 1353 insertions(+), 695 deletions(-) diff --git a/python/datafusion/functions.py b/python/datafusion/functions.py index e85d710e7..f062cbfce 100644 --- a/python/datafusion/functions.py +++ b/python/datafusion/functions.py @@ -386,8 +386,6 @@ def list_to_string(expr: Expr, delimiter: Expr) -> Expr: def list_join(expr: Expr, delimiter: Expr) -> Expr: """Converts each element to its text representation. - This is an alias for :py:func:`array_to_string`. - See Also: This is an alias for :py:func:`array_to_string`. """ @@ -407,6 +405,15 @@ def in_list(arg: Expr, values: list[Expr], negated: bool = False) -> Expr: ... ) >>> result.collect_column("in").to_pylist() [True, False, True] + + >>> result = df.select( + ... dfn.functions.in_list( + ... dfn.col("a"), [dfn.lit(1), dfn.lit(3)], + ... negated=True, + ... ).alias("not_in") + ... ) + >>> result.collect_column("not_in").to_pylist() + [False, True, False] """ values = [v.expr for v in values] return Expr(f.in_list(arg.expr, values, negated)) @@ -468,9 +475,15 @@ def order_by(expr: Expr, ascending: bool = True, nulls_first: bool = True) -> So """Creates a new sort expression. Examples: - >>> sort_expr = dfn.functions.order_by(dfn.col("a"), ascending=False) + >>> sort_expr = dfn.functions.order_by( + ... dfn.col("a"), ascending=False) >>> sort_expr.ascending() False + + >>> sort_expr = dfn.functions.order_by( + ... dfn.col("a"), ascending=True, nulls_first=False) + >>> sort_expr.nulls_first() + False """ return SortExpr(expr, ascending=ascending, nulls_first=nulls_first) @@ -486,10 +499,23 @@ def alias(expr: Expr, name: str, metadata: dict[str, str] | None = None) -> Expr Examples: >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"a": [1, 2]}) - >>> df.select( - ... dfn.functions.alias(dfn.col("a"), "b") - ... ).collect_column("b")[0].as_py() + >>> result = df.select( + ... dfn.functions.alias( + ... dfn.col("a"), "b" + ... ) + ... ) + >>> result.collect_column("b")[0].as_py() 1 + + >>> result = df.select( + ... dfn.functions.alias( + ... dfn.col("a"), "b", metadata={"info": "test"} + ... ) + ... ) + >>> result.schema() + b: int64 + -- field metadata -- + info: 'test' """ return Expr(f.alias(expr.expr, name, metadata)) @@ -520,9 +546,18 @@ def count_star(filter: Expr | None = None) -> Expr: Examples: >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"a": [1, 2, 3]}) - >>> result = df.aggregate([], [dfn.functions.count_star().alias("cnt")]) + >>> result = df.aggregate( + ... [], [dfn.functions.count_star( + ... ).alias("cnt")]) >>> result.collect_column("cnt")[0].as_py() 3 + + >>> result = df.aggregate( + ... [], [dfn.functions.count_star( + ... filter=dfn.col("a") > dfn.lit(1) + ... ).alias("cnt")]) + >>> result.collect_column("cnt")[0].as_py() + 2 """ return count(Expr.literal(1), filter=filter) @@ -652,12 +687,11 @@ def ascii(arg: Expr) -> Expr: """Returns the numeric code of the first character of the argument. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["a","b","c"]}) - >>> ascii_df = df.select(dfn.functions.ascii(dfn.col("a")).alias("ascii")) - >>> ascii_df.collect_column("ascii")[0].as_py() - 97 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["a","b","c"]}) + >>> ascii_df = df.select(dfn.functions.ascii(dfn.col("a")).alias("ascii")) + >>> ascii_df.collect_column("ascii")[0].as_py() + 97 """ return Expr(f.ascii(arg.expr)) @@ -732,12 +766,11 @@ def bit_length(arg: Expr) -> Expr: """Returns the number of bits in the string argument. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["a","b","c"]}) - >>> bit_df = df.select(dfn.functions.bit_length(dfn.col("a")).alias("bit_len")) - >>> bit_df.collect_column("bit_len")[0].as_py() - 8 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["a","b","c"]}) + >>> bit_df = df.select(dfn.functions.bit_length(dfn.col("a")).alias("bit_len")) + >>> bit_df.collect_column("bit_len")[0].as_py() + 8 """ return Expr(f.bit_length(arg.expr)) @@ -746,12 +779,11 @@ def btrim(arg: Expr) -> Expr: """Removes all characters, spaces by default, from both sides of a string. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [" a "]}) - >>> trim_df = df.select(dfn.functions.btrim(dfn.col("a")).alias("trimmed")) - >>> trim_df.collect_column("trimmed")[0].as_py() - 'a' + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [" a "]}) + >>> trim_df = df.select(dfn.functions.btrim(dfn.col("a")).alias("trimmed")) + >>> trim_df.collect_column("trimmed")[0].as_py() + 'a' """ return Expr(f.btrim(arg.expr)) @@ -786,13 +818,12 @@ def character_length(arg: Expr) -> Expr: """Returns the number of characters in the argument. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["abc","b","c"]}) - >>> char_len_df = df.select( - ... dfn.functions.character_length(dfn.col("a")).alias("char_len")) - >>> char_len_df.collect_column("char_len")[0].as_py() - 3 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["abc","b","c"]}) + >>> char_len_df = df.select( + ... dfn.functions.character_length(dfn.col("a")).alias("char_len")) + >>> char_len_df.collect_column("char_len")[0].as_py() + 3 """ return Expr(f.character_length(arg.expr)) @@ -801,12 +832,11 @@ def length(string: Expr) -> Expr: """The number of characters in the ``string``. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["hello"]}) - >>> result = df.select(dfn.functions.length(dfn.col("a")).alias("len")) - >>> result.collect_column("len")[0].as_py() - 5 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"]}) + >>> result = df.select(dfn.functions.length(dfn.col("a")).alias("len")) + >>> result.collect_column("len")[0].as_py() + 5 """ return Expr(f.length(string.expr)) @@ -815,12 +845,11 @@ def char_length(string: Expr) -> Expr: """The number of characters in the ``string``. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["hello"]}) - >>> result = df.select(dfn.functions.char_length(dfn.col("a")).alias("len")) - >>> result.collect_column("len")[0].as_py() - 5 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"]}) + >>> result = df.select(dfn.functions.char_length(dfn.col("a")).alias("len")) + >>> result.collect_column("len")[0].as_py() + 5 """ return Expr(f.char_length(string.expr)) @@ -829,12 +858,11 @@ def chr(arg: Expr) -> Expr: """Converts the Unicode code point to a UTF8 character. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [65]}) - >>> result = df.select(dfn.functions.chr(dfn.col("a")).alias("chr")) - >>> result.collect_column("chr")[0].as_py() - 'A' + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [65]}) + >>> result = df.select(dfn.functions.chr(dfn.col("a")).alias("chr")) + >>> result.collect_column("chr")[0].as_py() + 'A' """ return Expr(f.chr(arg.expr)) @@ -914,13 +942,12 @@ def ends_with(arg: Expr, suffix: Expr) -> Expr: """Returns true if the ``string`` ends with the ``suffix``, false otherwise. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["abc","b","c"]}) - >>> ends_with_df = df.select( - ... dfn.functions.ends_with(dfn.col("a"), dfn.lit("c")).alias("ends_with")) - >>> ends_with_df.collect_column("ends_with")[0].as_py() - True + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["abc","b","c"]}) + >>> ends_with_df = df.select( + ... dfn.functions.ends_with(dfn.col("a"), dfn.lit("c")).alias("ends_with")) + >>> ends_with_df.collect_column("ends_with")[0].as_py() + True """ return Expr(f.ends_with(arg.expr, suffix.expr)) @@ -962,13 +989,12 @@ def find_in_set(string: Expr, string_list: Expr) -> Expr: The string list is a string composed of substrings separated by ``,`` characters. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["b"]}) - >>> result = df.select( - ... dfn.functions.find_in_set(dfn.col("a"), dfn.lit("a,b,c")).alias("pos")) - >>> result.collect_column("pos")[0].as_py() - 2 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["b"]}) + >>> result = df.select( + ... dfn.functions.find_in_set(dfn.col("a"), dfn.lit("a,b,c")).alias("pos")) + >>> result.collect_column("pos")[0].as_py() + 2 """ return Expr(f.find_in_set(string.expr, string_list.expr)) @@ -1008,12 +1034,11 @@ def initcap(string: Expr) -> Expr: characters to lowercase. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["the cat"]}) - >>> cap_df = df.select(dfn.functions.initcap(dfn.col("a")).alias("cap")) - >>> cap_df.collect_column("cap")[0].as_py() - 'The Cat' + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["the cat"]}) + >>> cap_df = df.select(dfn.functions.initcap(dfn.col("a")).alias("cap")) + >>> cap_df.collect_column("cap")[0].as_py() + 'The Cat' """ return Expr(f.initcap(string.expr)) @@ -1021,7 +1046,8 @@ def initcap(string: Expr) -> Expr: def instr(string: Expr, substring: Expr) -> Expr: """Finds the position from where the ``substring`` matches the ``string``. - This is an alias for :py:func:`strpos`. + See Also: + This is an alias for :py:func:`strpos`. """ return strpos(string, substring) @@ -1058,12 +1084,12 @@ def left(string: Expr, n: Expr) -> Expr: """Returns the first ``n`` characters in the ``string``. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["the cat"]}) - >>> left_df = df.select(dfn.functions.left(dfn.col("a"), dfn.lit(3)).alias("left")) - >>> left_df.collect_column("left")[0].as_py() - 'the' + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["the cat"]}) + >>> left_df = df.select( + ... dfn.functions.left(dfn.col("a"), dfn.lit(3)).alias("left")) + >>> left_df.collect_column("left")[0].as_py() + 'the' """ return Expr(f.left(string.expr, n.expr)) @@ -1072,13 +1098,12 @@ def levenshtein(string1: Expr, string2: Expr) -> Expr: """Returns the Levenshtein distance between the two given strings. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["kitten"]}) - >>> result = df.select( - ... dfn.functions.levenshtein(dfn.col("a"), dfn.lit("sitting")).alias("d")) - >>> result.collect_column("d")[0].as_py() - 3 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["kitten"]}) + >>> result = df.select( + ... dfn.functions.levenshtein(dfn.col("a"), dfn.lit("sitting")).alias("d")) + >>> result.collect_column("d")[0].as_py() + 3 """ return Expr(f.levenshtein(string1.expr, string2.expr)) @@ -1141,12 +1166,11 @@ def lower(arg: Expr) -> Expr: """Converts a string to lowercase. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["THE CaT"]}) - >>> lower_df = df.select(dfn.functions.lower(dfn.col("a")).alias("lower")) - >>> lower_df.collect_column("lower")[0].as_py() - 'the cat' + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["THE CaT"]}) + >>> lower_df = df.select(dfn.functions.lower(dfn.col("a")).alias("lower")) + >>> lower_df.collect_column("lower")[0].as_py() + 'the cat' """ return Expr(f.lower(arg.expr)) @@ -1159,14 +1183,23 @@ def lpad(string: Expr, count: Expr, characters: Expr | None = None) -> Expr: truncated (on the right). Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["the cat", "a hat"]}) - >>> lpad_df = df.select(dfn.functions.lpad(dfn.col("a"), dfn.lit(6)).alias("lpad")) - >>> lpad_df.collect_column("lpad")[0].as_py() - 'the ca' - >>> lpad_df.collect_column("lpad")[1].as_py() - ' a hat' + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["the cat", "a hat"]}) + >>> lpad_df = df.select( + ... dfn.functions.lpad( + ... dfn.col("a"), dfn.lit(6) + ... ).alias("lpad")) + >>> lpad_df.collect_column("lpad")[0].as_py() + 'the ca' + >>> lpad_df.collect_column("lpad")[1].as_py() + ' a hat' + + >>> result = df.select( + ... dfn.functions.lpad( + ... dfn.col("a"), dfn.lit(10), characters=dfn.lit(".") + ... ).alias("lpad")) + >>> result.collect_column("lpad")[0].as_py() + '...the cat' """ characters = characters if characters is not None else Expr.literal(" ") return Expr(f.lpad(string.expr, count.expr, characters.expr)) @@ -1176,12 +1209,11 @@ def ltrim(arg: Expr) -> Expr: """Removes all characters, spaces by default, from the beginning of a string. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [" a "]}) - >>> trim_df = df.select(dfn.functions.ltrim(dfn.col("a")).alias("trimmed")) - >>> trim_df.collect_column("trimmed")[0].as_py() - 'a ' + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [" a "]}) + >>> trim_df = df.select(dfn.functions.ltrim(dfn.col("a")).alias("trimmed")) + >>> trim_df.collect_column("trimmed")[0].as_py() + 'a ' """ return Expr(f.ltrim(arg.expr)) @@ -1236,12 +1268,11 @@ def octet_length(arg: Expr) -> Expr: """Returns the number of bytes of a string. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["hello"]}) - >>> result = df.select(dfn.functions.octet_length(dfn.col("a")).alias("len")) - >>> result.collect_column("len")[0].as_py() - 5 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"]}) + >>> result = df.select(dfn.functions.octet_length(dfn.col("a")).alias("len")) + >>> result.collect_column("len")[0].as_py() + 5 """ return Expr(f.octet_length(arg.expr)) @@ -1255,14 +1286,13 @@ def overlay( extends for ``length`` characters with new substring. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["abcdef"]}) - >>> result = df.select( - ... dfn.functions.overlay(dfn.col("a"), dfn.lit("XY"), dfn.lit(3), - ... dfn.lit(2)).alias("o")) - >>> result.collect_column("o")[0].as_py() - 'abXYef' + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["abcdef"]}) + >>> result = df.select( + ... dfn.functions.overlay(dfn.col("a"), dfn.lit("XY"), dfn.lit(3), + ... dfn.lit(2)).alias("o")) + >>> result.collect_column("o")[0].as_py() + 'abXYef' """ if length is None: return Expr(f.overlay(string.expr, substring.expr, start.expr)) @@ -1275,14 +1305,12 @@ def pi() -> Expr: Examples: >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"a": [1]}) - >>> import builtins + >>> from math import pi >>> result = df.select( ... dfn.functions.pi().alias("pi") ... ) - >>> builtins.round( - ... result.collect_column("pi")[0].as_py(), 5 - ... ) - 3.14159 + >>> result.collect_column("pi")[0].as_py() == pi + True """ return Expr(f.pi()) @@ -1290,7 +1318,8 @@ def pi() -> Expr: def position(string: Expr, substring: Expr) -> Expr: """Finds the position from where the ``substring`` matches the ``string``. - This is an alias for :py:func:`strpos`. + See Also: + This is an alias for :py:func:`strpos`. """ return strpos(string, substring) @@ -1313,7 +1342,8 @@ def power(base: Expr, exponent: Expr) -> Expr: def pow(base: Expr, exponent: Expr) -> Expr: """Returns ``base`` raised to the power of ``exponent``. - This is an alias of :py:func:`power`. + See Also: + This is an alias of :py:func:`power`. """ return power(base, exponent) @@ -1350,6 +1380,17 @@ def regexp_like(string: Expr, regex: Expr, flags: Expr | None = None) -> Expr: ... ) >>> result.collect_column("m")[0].as_py() True + + Use ``flags`` for case-insensitive matching: + + >>> result = df.select( + ... dfn.functions.regexp_like( + ... dfn.col("a"), dfn.lit("HELLO"), + ... flags=dfn.lit("i"), + ... ).alias("m") + ... ) + >>> result.collect_column("m")[0].as_py() + True """ if flags is not None: flags = flags.expr @@ -1372,6 +1413,17 @@ def regexp_match(string: Expr, regex: Expr, flags: Expr | None = None) -> Expr: ... ) >>> result.collect_column("m")[0].as_py() ['42'] + + Use ``flags`` for case-insensitive matching: + + >>> result = df.select( + ... dfn.functions.regexp_match( + ... dfn.col("a"), dfn.lit("(HELLO)"), + ... flags=dfn.lit("i"), + ... ).alias("m") + ... ) + >>> result.collect_column("m")[0].as_py() + ['hello'] """ if flags is not None: flags = flags.expr @@ -1400,6 +1452,18 @@ def regexp_replace( ... ) >>> result.collect_column("r")[0].as_py() 'hello XX' + + Use the ``g`` flag to replace all occurrences: + + >>> df = ctx.from_pydict({"a": ["a1 b2 c3"]}) + >>> result = df.select( + ... dfn.functions.regexp_replace( + ... dfn.col("a"), dfn.lit("\\d+"), + ... dfn.lit("X"), flags=dfn.lit("g"), + ... ).alias("r") + ... ) + >>> result.collect_column("r")[0].as_py() + 'aX bX cX' """ if flags is not None: flags = flags.expr @@ -1418,9 +1482,22 @@ def regexp_count( >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"a": ["abcabc"]}) >>> result = df.select( - ... dfn.functions.regexp_count(dfn.col("a"), dfn.lit("abc")).alias("c")) + ... dfn.functions.regexp_count( + ... dfn.col("a"), dfn.lit("abc") + ... ).alias("c")) >>> result.collect_column("c")[0].as_py() 2 + + Use ``start`` to begin searching from a position, and + ``flags`` for case-insensitive matching: + + >>> result = df.select( + ... dfn.functions.regexp_count( + ... dfn.col("a"), dfn.lit("ABC"), + ... start=dfn.lit(4), flags=dfn.lit("i"), + ... ).alias("c")) + >>> result.collect_column("c")[0].as_py() + 1 """ if flags is not None: flags = flags.expr @@ -1456,6 +1533,31 @@ def regexp_instr( ... ) >>> result.collect_column("pos")[0].as_py() 7 + + Use ``start`` to search from a position, ``n`` for the + nth occurrence, and ``flags`` for case-insensitive mode: + + >>> df = ctx.from_pydict({"a": ["abc ABC abc"]}) + >>> result = df.select( + ... dfn.functions.regexp_instr( + ... dfn.col("a"), dfn.lit("abc"), + ... start=dfn.lit(2), n=dfn.lit(1), + ... flags=dfn.lit("i"), + ... ).alias("pos") + ... ) + >>> result.collect_column("pos")[0].as_py() + 5 + + Use ``sub_expr`` to get the position of a capture group: + + >>> result = df.select( + ... dfn.functions.regexp_instr( + ... dfn.col("a"), dfn.lit("(abc)"), + ... sub_expr=dfn.lit(1), + ... ).alias("pos") + ... ) + >>> result.collect_column("pos")[0].as_py() + 1 """ start = start.expr if start is not None else None n = n.expr if n is not None else None @@ -1478,12 +1580,12 @@ def repeat(string: Expr, n: Expr) -> Expr: """Repeats the ``string`` to ``n`` times. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["ha"]}) - >>> result = df.select(dfn.functions.repeat(dfn.col("a"), dfn.lit(3)).alias("r")) - >>> result.collect_column("r")[0].as_py() - 'hahaha' + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["ha"]}) + >>> result = df.select( + ... dfn.functions.repeat(dfn.col("a"), dfn.lit(3)).alias("r")) + >>> result.collect_column("r")[0].as_py() + 'hahaha' """ return Expr(f.repeat(string.expr, n.expr)) @@ -1492,14 +1594,13 @@ def replace(string: Expr, from_val: Expr, to_val: Expr) -> Expr: """Replaces all occurrences of ``from_val`` with ``to_val`` in the ``string``. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["hello world"]}) - >>> result = df.select( - ... dfn.functions.replace(dfn.col("a"), dfn.lit("world"), - ... dfn.lit("there")).alias("r")) - >>> result.collect_column("r")[0].as_py() - 'hello there' + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello world"]}) + >>> result = df.select( + ... dfn.functions.replace(dfn.col("a"), dfn.lit("world"), + ... dfn.lit("there")).alias("r")) + >>> result.collect_column("r")[0].as_py() + 'hello there' """ return Expr(f.replace(string.expr, from_val.expr, to_val.expr)) @@ -1508,12 +1609,11 @@ def reverse(arg: Expr) -> Expr: """Reverse the string argument. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["hello"]}) - >>> result = df.select(dfn.functions.reverse(dfn.col("a")).alias("r")) - >>> result.collect_column("r")[0].as_py() - 'olleh' + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"]}) + >>> result = df.select(dfn.functions.reverse(dfn.col("a")).alias("r")) + >>> result.collect_column("r")[0].as_py() + 'olleh' """ return Expr(f.reverse(arg.expr)) @@ -1522,12 +1622,11 @@ def right(string: Expr, n: Expr) -> Expr: """Returns the last ``n`` characters in the ``string``. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["hello"]}) - >>> result = df.select(dfn.functions.right(dfn.col("a"), dfn.lit(3)).alias("r")) - >>> result.collect_column("r")[0].as_py() - 'llo' + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"]}) + >>> result = df.select(dfn.functions.right(dfn.col("a"), dfn.lit(3)).alias("r")) + >>> result.collect_column("r")[0].as_py() + 'llo' """ return Expr(f.right(string.expr, n.expr)) @@ -1558,13 +1657,12 @@ def rpad(string: Expr, count: Expr, characters: Expr | None = None) -> Expr: by default). If the string is already longer than length then it is truncated. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["hi"]}) - >>> result = df.select( - ... dfn.functions.rpad(dfn.col("a"), dfn.lit(5), dfn.lit("!")).alias("r")) - >>> result.collect_column("r")[0].as_py() - 'hi!!!' + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hi"]}) + >>> result = df.select( + ... dfn.functions.rpad(dfn.col("a"), dfn.lit(5), dfn.lit("!")).alias("r")) + >>> result.collect_column("r")[0].as_py() + 'hi!!!' """ characters = characters if characters is not None else Expr.literal(" ") return Expr(f.rpad(string.expr, count.expr, characters.expr)) @@ -1574,12 +1672,11 @@ def rtrim(arg: Expr) -> Expr: """Removes all characters, spaces by default, from the end of a string. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [" a "]}) - >>> trim_df = df.select(dfn.functions.rtrim(dfn.col("a")).alias("trimmed")) - >>> trim_df.collect_column("trimmed")[0].as_py() - ' a' + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [" a "]}) + >>> trim_df = df.select(dfn.functions.rtrim(dfn.col("a")).alias("trimmed")) + >>> trim_df.collect_column("trimmed")[0].as_py() + ' a' """ return Expr(f.rtrim(arg.expr)) @@ -1690,13 +1787,14 @@ def split_part(string: Expr, delimiter: Expr, index: Expr) -> Expr: on the index. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["a,b,c"]}) - >>> result = df.select( - ... dfn.functions.split_part(dfn.col("a"), dfn.lit(","), dfn.lit(2)).alias("s")) - >>> result.collect_column("s")[0].as_py() - 'b' + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["a,b,c"]}) + >>> result = df.select( + ... dfn.functions.split_part( + ... dfn.col("a"), dfn.lit(","), dfn.lit(2) + ... ).alias("s")) + >>> result.collect_column("s")[0].as_py() + 'b' """ return Expr(f.split_part(string.expr, delimiter.expr, index.expr)) @@ -1718,13 +1816,12 @@ def starts_with(string: Expr, prefix: Expr) -> Expr: """Returns true if string starts with prefix. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["hello_from_datafusion"]}) - >>> result = df.select( - ... dfn.functions.starts_with(dfn.col("a"), dfn.lit("hello")).alias("sw")) - >>> result.collect_column("sw")[0].as_py() - True + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello_from_datafusion"]}) + >>> result = df.select( + ... dfn.functions.starts_with(dfn.col("a"), dfn.lit("hello")).alias("sw")) + >>> result.collect_column("sw")[0].as_py() + True """ return Expr(f.starts_with(string.expr, prefix.expr)) @@ -1733,13 +1830,12 @@ def strpos(string: Expr, substring: Expr) -> Expr: """Finds the position from where the ``substring`` matches the ``string``. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["hello"]}) - >>> result = df.select( - ... dfn.functions.strpos(dfn.col("a"), dfn.lit("llo")).alias("pos")) - >>> result.collect_column("pos")[0].as_py() - 3 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"]}) + >>> result = df.select( + ... dfn.functions.strpos(dfn.col("a"), dfn.lit("llo")).alias("pos")) + >>> result.collect_column("pos")[0].as_py() + 3 """ return Expr(f.strpos(string.expr, substring.expr)) @@ -1748,12 +1844,12 @@ def substr(string: Expr, position: Expr) -> Expr: """Substring from the ``position`` to the end. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["hello"]}) - >>> result = df.select(dfn.functions.substr(dfn.col("a"), dfn.lit(3)).alias("s")) - >>> result.collect_column("s")[0].as_py() - 'llo' + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"]}) + >>> result = df.select( + ... dfn.functions.substr(dfn.col("a"), dfn.lit(3)).alias("s")) + >>> result.collect_column("s")[0].as_py() + 'llo' """ return Expr(f.substr(string.expr, position.expr)) @@ -1765,14 +1861,13 @@ def substr_index(string: Expr, delimiter: Expr, count: Expr) -> Expr: ``delimiter``. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["a.b.c"]}) - >>> result = df.select( - ... dfn.functions.substr_index(dfn.col("a"), dfn.lit("."), - ... dfn.lit(2)).alias("s")) - >>> result.collect_column("s")[0].as_py() - 'a.b' + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["a.b.c"]}) + >>> result = df.select( + ... dfn.functions.substr_index(dfn.col("a"), dfn.lit("."), + ... dfn.lit(2)).alias("s")) + >>> result.collect_column("s")[0].as_py() + 'a.b' """ return Expr(f.substr_index(string.expr, delimiter.expr, count.expr)) @@ -1781,13 +1876,14 @@ def substring(string: Expr, position: Expr, length: Expr) -> Expr: """Substring from the ``position`` with ``length`` characters. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["hello world"]}) - >>> result = df.select( - ... dfn.functions.substring(dfn.col("a"), dfn.lit(1), dfn.lit(5)).alias("s")) - >>> result.collect_column("s")[0].as_py() - 'hello' + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello world"]}) + >>> result = df.select( + ... dfn.functions.substring( + ... dfn.col("a"), dfn.lit(1), dfn.lit(5) + ... ).alias("s")) + >>> result.collect_column("s")[0].as_py() + 'hello' """ return Expr(f.substring(string.expr, position.expr, length.expr)) @@ -1822,12 +1918,11 @@ def to_hex(arg: Expr) -> Expr: """Converts an integer to a hexadecimal string. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [255]}) - >>> result = df.select(dfn.functions.to_hex(dfn.col("a")).alias("hex")) - >>> result.collect_column("hex")[0].as_py() - 'ff' + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [255]}) + >>> result = df.select(dfn.functions.to_hex(dfn.col("a")).alias("hex")) + >>> result.collect_column("hex")[0].as_py() + 'ff' """ return Expr(f.to_hex(arg.expr)) @@ -1859,6 +1954,18 @@ def to_char(arg: Expr, formatter: Expr) -> Expr: For usage of ``formatter`` see the rust chrono package ``strftime`` package. [Documentation here.](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["2021-01-01T00:00:00"]}) + >>> result = df.select( + ... dfn.functions.to_char( + ... dfn.functions.to_timestamp(dfn.col("a")), + ... dfn.lit("%Y/%m/%d"), + ... ).alias("formatted") + ... ) + >>> result.collect_column("formatted")[0].as_py() + '2021/01/01' """ return Expr(f.to_char(arg.expr, formatter.expr)) @@ -1878,6 +1985,14 @@ def to_date(arg: Expr, *formatters: Expr) -> Expr: For usage of ``formatters`` see the rust chrono package ``strftime`` package. [Documentation here.](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["2021-07-20"]}) + >>> result = df.select( + ... dfn.functions.to_date(dfn.col("a")).alias("dt")) + >>> str(result.collect_column("dt")[0].as_py()) + '2021-07-20' """ return Expr(f.to_date(arg.expr, *_unwrap_exprs(formatters))) @@ -1899,6 +2014,14 @@ def to_time(arg: Expr, *formatters: Expr) -> Expr: For usage of ``formatters`` see the rust chrono package ``strftime`` package. [Documentation here.](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["14:30:00"]}) + >>> result = df.select( + ... dfn.functions.to_time(dfn.col("a")).alias("t")) + >>> str(result.collect_column("t")[0].as_py()) + '14:30:00' """ return Expr(f.to_time(arg.expr, *_unwrap_exprs(formatters))) @@ -2053,7 +2176,8 @@ def current_time() -> Expr: def datepart(part: Expr, date: Expr) -> Expr: """Return a specified part of a date. - This is an alias for :py:func:`date_part`. + See Also: + This is an alias for :py:func:`date_part`. """ return date_part(part, date) @@ -2076,7 +2200,8 @@ def date_part(part: Expr, date: Expr) -> Expr: def extract(part: Expr, date: Expr) -> Expr: """Extracts a subfield from the date. - This is an alias for :py:func:`date_part`. + See Also: + This is an alias for :py:func:`date_part`. """ return date_part(part, date) @@ -2102,7 +2227,8 @@ def date_trunc(part: Expr, date: Expr) -> Expr: def datetrunc(part: Expr, date: Expr) -> Expr: """Truncates the date to a specified level of precision. - This is an alias for :py:func:`date_trunc`. + See Also: + This is an alias for :py:func:`date_trunc`. """ return date_trunc(part, date) @@ -2148,14 +2274,13 @@ def translate(string: Expr, from_val: Expr, to_val: Expr) -> Expr: """Replaces the characters in ``from_val`` with the counterpart in ``to_val``. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["hello"]}) - >>> result = df.select( - ... dfn.functions.translate(dfn.col("a"), dfn.lit("helo"), - ... dfn.lit("HELO")).alias("t")) - >>> result.collect_column("t")[0].as_py() - 'HELLO' + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"]}) + >>> result = df.select( + ... dfn.functions.translate(dfn.col("a"), dfn.lit("helo"), + ... dfn.lit("HELO")).alias("t")) + >>> result.collect_column("t")[0].as_py() + 'HELLO' """ return Expr(f.translate(string.expr, from_val.expr, to_val.expr)) @@ -2164,12 +2289,11 @@ def trim(arg: Expr) -> Expr: """Removes all characters, spaces by default, from both sides of a string. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [" hello "]}) - >>> result = df.select(dfn.functions.trim(dfn.col("a")).alias("t")) - >>> result.collect_column("t")[0].as_py() - 'hello' + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [" hello "]}) + >>> result = df.select(dfn.functions.trim(dfn.col("a")).alias("t")) + >>> result.collect_column("t")[0].as_py() + 'hello' """ return Expr(f.trim(arg.expr)) @@ -2180,9 +2304,19 @@ def trunc(num: Expr, precision: Expr | None = None) -> Expr: Examples: >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"a": [1.567]}) - >>> result = df.select(dfn.functions.trunc(dfn.col("a")).alias("t")) + >>> result = df.select( + ... dfn.functions.trunc( + ... dfn.col("a") + ... ).alias("t")) >>> result.collect_column("t")[0].as_py() 1.0 + + >>> result = df.select( + ... dfn.functions.trunc( + ... dfn.col("a"), precision=dfn.lit(2) + ... ).alias("t")) + >>> result.collect_column("t")[0].as_py() + 1.56 """ if precision is not None: return Expr(f.trunc(num.expr, precision.expr)) @@ -2193,12 +2327,11 @@ def upper(arg: Expr) -> Expr: """Converts a string to uppercase. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["hello"]}) - >>> result = df.select(dfn.functions.upper(dfn.col("a")).alias("u")) - >>> result.collect_column("u")[0].as_py() - 'HELLO' + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello"]}) + >>> result = df.select(dfn.functions.upper(dfn.col("a")).alias("u")) + >>> result.collect_column("u")[0].as_py() + 'HELLO' """ return Expr(f.upper(arg.expr)) @@ -2207,13 +2340,14 @@ def make_array(*args: Expr) -> Expr: """Returns an array using the specified input expressions. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [1]}) - >>> result = df.select( - ... dfn.functions.make_array(dfn.lit(1), dfn.lit(2), dfn.lit(3)).alias("arr")) - >>> result.collect_column("arr")[0].as_py() - [1, 2, 3] + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1]}) + >>> result = df.select( + ... dfn.functions.make_array( + ... dfn.lit(1), dfn.lit(2), dfn.lit(3) + ... ).alias("arr")) + >>> result.collect_column("arr")[0].as_py() + [1, 2, 3] """ args = [arg.expr for arg in args] return Expr(f.make_array(args)) @@ -2222,7 +2356,8 @@ def make_array(*args: Expr) -> Expr: def make_list(*args: Expr) -> Expr: """Returns an array using the specified input expressions. - This is an alias for :py:func:`make_array`. + See Also: + This is an alias for :py:func:`make_array`. """ return make_array(*args) @@ -2230,7 +2365,8 @@ def make_list(*args: Expr) -> Expr: def array(*args: Expr) -> Expr: """Returns an array using the specified input expressions. - This is an alias for :py:func:`make_array`. + See Also: + This is an alias for :py:func:`make_array`. """ return make_array(*args) @@ -2239,13 +2375,12 @@ def range(start: Expr, stop: Expr, step: Expr) -> Expr: """Create a list of values in the range between start and stop. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [1]}) - >>> result = df.select( - ... dfn.functions.range(dfn.lit(0), dfn.lit(5), dfn.lit(2)).alias("r")) - >>> result.collect_column("r")[0].as_py() - [0, 2, 4] + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1]}) + >>> result = df.select( + ... dfn.functions.range(dfn.lit(0), dfn.lit(5), dfn.lit(2)).alias("r")) + >>> result.collect_column("r")[0].as_py() + [0, 2, 4] """ return Expr(f.range(start.expr, stop.expr, step.expr)) @@ -2377,13 +2512,12 @@ def array_append(array: Expr, element: Expr) -> Expr: """Appends an element to the end of an array. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) - >>> result = df.select( - ... dfn.functions.array_append(dfn.col("a"), dfn.lit(4)).alias("result")) - >>> result.collect_column("result")[0].as_py() - [1, 2, 3, 4] + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) + >>> result = df.select( + ... dfn.functions.array_append(dfn.col("a"), dfn.lit(4)).alias("result")) + >>> result.collect_column("result")[0].as_py() + [1, 2, 3, 4] """ return Expr(f.array_append(array.expr, element.expr)) @@ -2391,7 +2525,8 @@ def array_append(array: Expr, element: Expr) -> Expr: def array_push_back(array: Expr, element: Expr) -> Expr: """Appends an element to the end of an array. - This is an alias for :py:func:`array_append`. + See Also: + This is an alias for :py:func:`array_append`. """ return array_append(array, element) @@ -2399,7 +2534,8 @@ def array_push_back(array: Expr, element: Expr) -> Expr: def list_append(array: Expr, element: Expr) -> Expr: """Appends an element to the end of an array. - This is an alias for :py:func:`array_append`. + See Also: + This is an alias for :py:func:`array_append`. """ return array_append(array, element) @@ -2407,7 +2543,8 @@ def list_append(array: Expr, element: Expr) -> Expr: def list_push_back(array: Expr, element: Expr) -> Expr: """Appends an element to the end of an array. - This is an alias for :py:func:`array_append`. + See Also: + This is an alias for :py:func:`array_append`. """ return array_append(array, element) @@ -2416,13 +2553,12 @@ def array_concat(*args: Expr) -> Expr: """Concatenates the input arrays. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 2]], "b": [[3, 4]]}) - >>> result = df.select( - ... dfn.functions.array_concat(dfn.col("a"), dfn.col("b")).alias("result")) - >>> result.collect_column("result")[0].as_py() - [1, 2, 3, 4] + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2]], "b": [[3, 4]]}) + >>> result = df.select( + ... dfn.functions.array_concat(dfn.col("a"), dfn.col("b")).alias("result")) + >>> result.collect_column("result")[0].as_py() + [1, 2, 3, 4] """ args = [arg.expr for arg in args] return Expr(f.array_concat(args)) @@ -2431,7 +2567,8 @@ def array_concat(*args: Expr) -> Expr: def array_cat(*args: Expr) -> Expr: """Concatenates the input arrays. - This is an alias for :py:func:`array_concat`. + See Also: + This is an alias for :py:func:`array_concat`. """ return array_concat(*args) @@ -2440,12 +2577,11 @@ def array_dims(array: Expr) -> Expr: """Returns an array of the array's dimensions. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) - >>> result = df.select(dfn.functions.array_dims(dfn.col("a")).alias("result")) - >>> result.collect_column("result")[0].as_py() - [3] + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) + >>> result = df.select(dfn.functions.array_dims(dfn.col("a")).alias("result")) + >>> result.collect_column("result")[0].as_py() + [3] """ return Expr(f.array_dims(array.expr)) @@ -2454,18 +2590,17 @@ def array_distinct(array: Expr) -> Expr: """Returns distinct values from the array after removing duplicates. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 1, 2, 3]]}) - >>> result = df.select( - ... dfn.functions.array_distinct( - ... dfn.col("a") - ... ).alias("result") - ... ) - >>> sorted( - ... result.collect_column("result")[0].as_py() - ... ) - [1, 2, 3] + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 1, 2, 3]]}) + >>> result = df.select( + ... dfn.functions.array_distinct( + ... dfn.col("a") + ... ).alias("result") + ... ) + >>> sorted( + ... result.collect_column("result")[0].as_py() + ... ) + [1, 2, 3] """ return Expr(f.array_distinct(array.expr)) @@ -2473,7 +2608,8 @@ def array_distinct(array: Expr) -> Expr: def list_cat(*args: Expr) -> Expr: """Concatenates the input arrays. - This is an alias for :py:func:`array_concat`, :py:func:`array_cat`. + See Also: + This is an alias for :py:func:`array_concat`, :py:func:`array_cat`. """ return array_concat(*args) @@ -2481,7 +2617,8 @@ def list_cat(*args: Expr) -> Expr: def list_concat(*args: Expr) -> Expr: """Concatenates the input arrays. - This is an alias for :py:func:`array_concat`, :py:func:`array_cat`. + See Also: + This is an alias for :py:func:`array_concat`, :py:func:`array_cat`. """ return array_concat(*args) @@ -2489,7 +2626,8 @@ def list_concat(*args: Expr) -> Expr: def list_distinct(array: Expr) -> Expr: """Returns distinct values from the array after removing duplicates. - This is an alias for :py:func:`array_distinct`. + See Also: + This is an alias for :py:func:`array_distinct`. """ return array_distinct(array) @@ -2497,7 +2635,8 @@ def list_distinct(array: Expr) -> Expr: def list_dims(array: Expr) -> Expr: """Returns an array of the array's dimensions. - This is an alias for :py:func:`array_dims`. + See Also: + This is an alias for :py:func:`array_dims`. """ return array_dims(array) @@ -2506,13 +2645,12 @@ def array_element(array: Expr, n: Expr) -> Expr: """Extracts the element with the index n from the array. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[10, 20, 30]]}) - >>> result = df.select( - ... dfn.functions.array_element(dfn.col("a"), dfn.lit(2)).alias("result")) - >>> result.collect_column("result")[0].as_py() - 20 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[10, 20, 30]]}) + >>> result = df.select( + ... dfn.functions.array_element(dfn.col("a"), dfn.lit(2)).alias("result")) + >>> result.collect_column("result")[0].as_py() + 20 """ return Expr(f.array_element(array.expr, n.expr)) @@ -2521,12 +2659,11 @@ def array_empty(array: Expr) -> Expr: """Returns a boolean indicating whether the array is empty. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 2]]}) - >>> result = df.select(dfn.functions.array_empty(dfn.col("a")).alias("result")) - >>> result.collect_column("result")[0].as_py() - False + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2]]}) + >>> result = df.select(dfn.functions.array_empty(dfn.col("a")).alias("result")) + >>> result.collect_column("result")[0].as_py() + False """ return Expr(f.array_empty(array.expr)) @@ -2534,7 +2671,8 @@ def array_empty(array: Expr) -> Expr: def array_extract(array: Expr, n: Expr) -> Expr: """Extracts the element with the index n from the array. - This is an alias for :py:func:`array_element`. + See Also: + This is an alias for :py:func:`array_element`. """ return array_element(array, n) @@ -2542,7 +2680,8 @@ def array_extract(array: Expr, n: Expr) -> Expr: def list_element(array: Expr, n: Expr) -> Expr: """Extracts the element with the index n from the array. - This is an alias for :py:func:`array_element`. + See Also: + This is an alias for :py:func:`array_element`. """ return array_element(array, n) @@ -2550,7 +2689,8 @@ def list_element(array: Expr, n: Expr) -> Expr: def list_extract(array: Expr, n: Expr) -> Expr: """Extracts the element with the index n from the array. - This is an alias for :py:func:`array_element`. + See Also: + This is an alias for :py:func:`array_element`. """ return array_element(array, n) @@ -2559,12 +2699,11 @@ def array_length(array: Expr) -> Expr: """Returns the length of the array. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) - >>> result = df.select(dfn.functions.array_length(dfn.col("a")).alias("result")) - >>> result.collect_column("result")[0].as_py() - 3 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) + >>> result = df.select(dfn.functions.array_length(dfn.col("a")).alias("result")) + >>> result.collect_column("result")[0].as_py() + 3 """ return Expr(f.array_length(array.expr)) @@ -2572,7 +2711,8 @@ def array_length(array: Expr) -> Expr: def list_length(array: Expr) -> Expr: """Returns the length of the array. - This is an alias for :py:func:`array_length`. + See Also: + This is an alias for :py:func:`array_length`. """ return array_length(array) @@ -2581,13 +2721,12 @@ def array_has(first_array: Expr, second_array: Expr) -> Expr: """Returns true if the element appears in the first array, otherwise false. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) - >>> result = df.select( - ... dfn.functions.array_has(dfn.col("a"), dfn.lit(2)).alias("result")) - >>> result.collect_column("result")[0].as_py() - True + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) + >>> result = df.select( + ... dfn.functions.array_has(dfn.col("a"), dfn.lit(2)).alias("result")) + >>> result.collect_column("result")[0].as_py() + True """ return Expr(f.array_has(first_array.expr, second_array.expr)) @@ -2599,13 +2738,12 @@ def array_has_all(first_array: Expr, second_array: Expr) -> Expr: Otherwise, it returns false. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 2, 3]], "b": [[1, 2]]}) - >>> result = df.select( - ... dfn.functions.array_has_all(dfn.col("a"), dfn.col("b")).alias("result")) - >>> result.collect_column("result")[0].as_py() - True + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]], "b": [[1, 2]]}) + >>> result = df.select( + ... dfn.functions.array_has_all(dfn.col("a"), dfn.col("b")).alias("result")) + >>> result.collect_column("result")[0].as_py() + True """ return Expr(f.array_has_all(first_array.expr, second_array.expr)) @@ -2617,13 +2755,12 @@ def array_has_any(first_array: Expr, second_array: Expr) -> Expr: array. Otherwise, it returns false. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 2, 3]], "b": [[2, 5]]}) - >>> result = df.select( - ... dfn.functions.array_has_any(dfn.col("a"), dfn.col("b")).alias("result")) - >>> result.collect_column("result")[0].as_py() - True + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]], "b": [[2, 5]]}) + >>> result = df.select( + ... dfn.functions.array_has_any(dfn.col("a"), dfn.col("b")).alias("result")) + >>> result.collect_column("result")[0].as_py() + True """ return Expr(f.array_has_any(first_array.expr, second_array.expr)) @@ -2632,13 +2769,24 @@ def array_position(array: Expr, element: Expr, index: int | None = 1) -> Expr: """Return the position of the first occurrence of ``element`` in ``array``. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[10, 20, 30]]}) - >>> result = df.select( - ... dfn.functions.array_position(dfn.col("a"), dfn.lit(20)).alias("result")) - >>> result.collect_column("result")[0].as_py() - 2 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[10, 20, 30]]}) + >>> result = df.select( + ... dfn.functions.array_position( + ... dfn.col("a"), dfn.lit(20) + ... ).alias("result")) + >>> result.collect_column("result")[0].as_py() + 2 + + Use ``index`` to start searching from a given position: + + >>> df = ctx.from_pydict({"a": [[10, 20, 10, 20]]}) + >>> result = df.select( + ... dfn.functions.array_position( + ... dfn.col("a"), dfn.lit(20), index=3, + ... ).alias("result")) + >>> result.collect_column("result")[0].as_py() + 4 """ return Expr(f.array_position(array.expr, element.expr, index)) @@ -2646,7 +2794,8 @@ def array_position(array: Expr, element: Expr, index: int | None = 1) -> Expr: def array_indexof(array: Expr, element: Expr, index: int | None = 1) -> Expr: """Return the position of the first occurrence of ``element`` in ``array``. - This is an alias for :py:func:`array_position`. + See Also: + This is an alias for :py:func:`array_position`. """ return array_position(array, element, index) @@ -2654,7 +2803,8 @@ def array_indexof(array: Expr, element: Expr, index: int | None = 1) -> Expr: def list_position(array: Expr, element: Expr, index: int | None = 1) -> Expr: """Return the position of the first occurrence of ``element`` in ``array``. - This is an alias for :py:func:`array_position`. + See Also: + This is an alias for :py:func:`array_position`. """ return array_position(array, element, index) @@ -2662,7 +2812,8 @@ def list_position(array: Expr, element: Expr, index: int | None = 1) -> Expr: def list_indexof(array: Expr, element: Expr, index: int | None = 1) -> Expr: """Return the position of the first occurrence of ``element`` in ``array``. - This is an alias for :py:func:`array_position`. + See Also: + This is an alias for :py:func:`array_position`. """ return array_position(array, element, index) @@ -2671,13 +2822,12 @@ def array_positions(array: Expr, element: Expr) -> Expr: """Searches for an element in the array and returns all occurrences. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 2, 1]]}) - >>> result = df.select( - ... dfn.functions.array_positions(dfn.col("a"), dfn.lit(1)).alias("result")) - >>> result.collect_column("result")[0].as_py() - [1, 3] + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 1]]}) + >>> result = df.select( + ... dfn.functions.array_positions(dfn.col("a"), dfn.lit(1)).alias("result")) + >>> result.collect_column("result")[0].as_py() + [1, 3] """ return Expr(f.array_positions(array.expr, element.expr)) @@ -2685,7 +2835,8 @@ def array_positions(array: Expr, element: Expr) -> Expr: def list_positions(array: Expr, element: Expr) -> Expr: """Searches for an element in the array and returns all occurrences. - This is an alias for :py:func:`array_positions`. + See Also: + This is an alias for :py:func:`array_positions`. """ return array_positions(array, element) @@ -2694,12 +2845,11 @@ def array_ndims(array: Expr) -> Expr: """Returns the number of dimensions of the array. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) - >>> result = df.select(dfn.functions.array_ndims(dfn.col("a")).alias("result")) - >>> result.collect_column("result")[0].as_py() - 1 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) + >>> result = df.select(dfn.functions.array_ndims(dfn.col("a")).alias("result")) + >>> result.collect_column("result")[0].as_py() + 1 """ return Expr(f.array_ndims(array.expr)) @@ -2707,7 +2857,8 @@ def array_ndims(array: Expr) -> Expr: def list_ndims(array: Expr) -> Expr: """Returns the number of dimensions of the array. - This is an alias for :py:func:`array_ndims`. + See Also: + This is an alias for :py:func:`array_ndims`. """ return array_ndims(array) @@ -2716,13 +2867,12 @@ def array_prepend(element: Expr, array: Expr) -> Expr: """Prepends an element to the beginning of an array. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 2]]}) - >>> result = df.select( - ... dfn.functions.array_prepend(dfn.lit(0), dfn.col("a")).alias("result")) - >>> result.collect_column("result")[0].as_py() - [0, 1, 2] + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2]]}) + >>> result = df.select( + ... dfn.functions.array_prepend(dfn.lit(0), dfn.col("a")).alias("result")) + >>> result.collect_column("result")[0].as_py() + [0, 1, 2] """ return Expr(f.array_prepend(element.expr, array.expr)) @@ -2730,7 +2880,8 @@ def array_prepend(element: Expr, array: Expr) -> Expr: def array_push_front(element: Expr, array: Expr) -> Expr: """Prepends an element to the beginning of an array. - This is an alias for :py:func:`array_prepend`. + See Also: + This is an alias for :py:func:`array_prepend`. """ return array_prepend(element, array) @@ -2738,7 +2889,8 @@ def array_push_front(element: Expr, array: Expr) -> Expr: def list_prepend(element: Expr, array: Expr) -> Expr: """Prepends an element to the beginning of an array. - This is an alias for :py:func:`array_prepend`. + See Also: + This is an alias for :py:func:`array_prepend`. """ return array_prepend(element, array) @@ -2746,7 +2898,8 @@ def list_prepend(element: Expr, array: Expr) -> Expr: def list_push_front(element: Expr, array: Expr) -> Expr: """Prepends an element to the beginning of an array. - This is an alias for :py:func:`array_prepend`. + See Also: + This is an alias for :py:func:`array_prepend`. """ return array_prepend(element, array) @@ -2755,12 +2908,12 @@ def array_pop_back(array: Expr) -> Expr: """Returns the array without the last element. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) - >>> result = df.select(dfn.functions.array_pop_back(dfn.col("a")).alias("result")) - >>> result.collect_column("result")[0].as_py() - [1, 2] + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) + >>> result = df.select( + ... dfn.functions.array_pop_back(dfn.col("a")).alias("result")) + >>> result.collect_column("result")[0].as_py() + [1, 2] """ return Expr(f.array_pop_back(array.expr)) @@ -2769,12 +2922,12 @@ def array_pop_front(array: Expr) -> Expr: """Returns the array without the first element. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) - >>> result = df.select(dfn.functions.array_pop_front(dfn.col("a")).alias("result")) - >>> result.collect_column("result")[0].as_py() - [2, 3] + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) + >>> result = df.select( + ... dfn.functions.array_pop_front(dfn.col("a")).alias("result")) + >>> result.collect_column("result")[0].as_py() + [2, 3] """ return Expr(f.array_pop_front(array.expr)) @@ -2783,13 +2936,12 @@ def array_remove(array: Expr, element: Expr) -> Expr: """Removes the first element from the array equal to the given value. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 2, 1]]}) - >>> result = df.select( - ... dfn.functions.array_remove(dfn.col("a"), dfn.lit(1)).alias("result")) - >>> result.collect_column("result")[0].as_py() - [2, 1] + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 1]]}) + >>> result = df.select( + ... dfn.functions.array_remove(dfn.col("a"), dfn.lit(1)).alias("result")) + >>> result.collect_column("result")[0].as_py() + [2, 1] """ return Expr(f.array_remove(array.expr, element.expr)) @@ -2797,7 +2949,8 @@ def array_remove(array: Expr, element: Expr) -> Expr: def list_remove(array: Expr, element: Expr) -> Expr: """Removes the first element from the array equal to the given value. - This is an alias for :py:func:`array_remove`. + See Also: + This is an alias for :py:func:`array_remove`. """ return array_remove(array, element) @@ -2806,14 +2959,13 @@ def array_remove_n(array: Expr, element: Expr, max: Expr) -> Expr: """Removes the first ``max`` elements from the array equal to the given value. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 2, 1, 1]]}) - >>> result = df.select( - ... dfn.functions.array_remove_n(dfn.col("a"), dfn.lit(1), - ... dfn.lit(2)).alias("result")) - >>> result.collect_column("result")[0].as_py() - [2, 1] + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 1, 1]]}) + >>> result = df.select( + ... dfn.functions.array_remove_n(dfn.col("a"), dfn.lit(1), + ... dfn.lit(2)).alias("result")) + >>> result.collect_column("result")[0].as_py() + [2, 1] """ return Expr(f.array_remove_n(array.expr, element.expr, max.expr)) @@ -2821,7 +2973,8 @@ def array_remove_n(array: Expr, element: Expr, max: Expr) -> Expr: def list_remove_n(array: Expr, element: Expr, max: Expr) -> Expr: """Removes the first ``max`` elements from the array equal to the given value. - This is an alias for :py:func:`array_remove_n`. + See Also: + This is an alias for :py:func:`array_remove_n`. """ return array_remove_n(array, element, max) @@ -2830,13 +2983,14 @@ def array_remove_all(array: Expr, element: Expr) -> Expr: """Removes all elements from the array equal to the given value. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 2, 1]]}) - >>> result = df.select( - ... dfn.functions.array_remove_all(dfn.col("a"), dfn.lit(1)).alias("result")) - >>> result.collect_column("result")[0].as_py() - [2] + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 1]]}) + >>> result = df.select( + ... dfn.functions.array_remove_all( + ... dfn.col("a"), dfn.lit(1) + ... ).alias("result")) + >>> result.collect_column("result")[0].as_py() + [2] """ return Expr(f.array_remove_all(array.expr, element.expr)) @@ -2844,7 +2998,8 @@ def array_remove_all(array: Expr, element: Expr) -> Expr: def list_remove_all(array: Expr, element: Expr) -> Expr: """Removes all elements from the array equal to the given value. - This is an alias for :py:func:`array_remove_all`. + See Also: + This is an alias for :py:func:`array_remove_all`. """ return array_remove_all(array, element) @@ -2853,13 +3008,12 @@ def array_repeat(element: Expr, count: Expr) -> Expr: """Returns an array containing ``element`` ``count`` times. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [1]}) - >>> result = df.select( - ... dfn.functions.array_repeat(dfn.lit(3), dfn.lit(3)).alias("result")) - >>> result.collect_column("result")[0].as_py() - [3, 3, 3] + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1]}) + >>> result = df.select( + ... dfn.functions.array_repeat(dfn.lit(3), dfn.lit(3)).alias("result")) + >>> result.collect_column("result")[0].as_py() + [3, 3, 3] """ return Expr(f.array_repeat(element.expr, count.expr)) @@ -2867,7 +3021,8 @@ def array_repeat(element: Expr, count: Expr) -> Expr: def list_repeat(element: Expr, count: Expr) -> Expr: """Returns an array containing ``element`` ``count`` times. - This is an alias for :py:func:`array_repeat`. + See Also: + This is an alias for :py:func:`array_repeat`. """ return array_repeat(element, count) @@ -2876,14 +3031,13 @@ def array_replace(array: Expr, from_val: Expr, to_val: Expr) -> Expr: """Replaces the first occurrence of ``from_val`` with ``to_val``. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 2, 1]]}) - >>> result = df.select( - ... dfn.functions.array_replace(dfn.col("a"), dfn.lit(1), - ... dfn.lit(9)).alias("result")) - >>> result.collect_column("result")[0].as_py() - [9, 2, 1] + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 1]]}) + >>> result = df.select( + ... dfn.functions.array_replace(dfn.col("a"), dfn.lit(1), + ... dfn.lit(9)).alias("result")) + >>> result.collect_column("result")[0].as_py() + [9, 2, 1] """ return Expr(f.array_replace(array.expr, from_val.expr, to_val.expr)) @@ -2891,7 +3045,8 @@ def array_replace(array: Expr, from_val: Expr, to_val: Expr) -> Expr: def list_replace(array: Expr, from_val: Expr, to_val: Expr) -> Expr: """Replaces the first occurrence of ``from_val`` with ``to_val``. - This is an alias for :py:func:`array_replace`. + See Also: + This is an alias for :py:func:`array_replace`. """ return array_replace(array, from_val, to_val) @@ -2903,14 +3058,13 @@ def array_replace_n(array: Expr, from_val: Expr, to_val: Expr, max: Expr) -> Exp specified element. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 2, 1, 1]]}) - >>> result = df.select( - ... dfn.functions.array_replace_n(dfn.col("a"), dfn.lit(1), dfn.lit(9), - ... dfn.lit(2)).alias("result")) - >>> result.collect_column("result")[0].as_py() - [9, 2, 9, 1] + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 1, 1]]}) + >>> result = df.select( + ... dfn.functions.array_replace_n(dfn.col("a"), dfn.lit(1), dfn.lit(9), + ... dfn.lit(2)).alias("result")) + >>> result.collect_column("result")[0].as_py() + [9, 2, 9, 1] """ return Expr(f.array_replace_n(array.expr, from_val.expr, to_val.expr, max.expr)) @@ -2921,7 +3075,8 @@ def list_replace_n(array: Expr, from_val: Expr, to_val: Expr, max: Expr) -> Expr Replaces the first ``max`` occurrences of the specified element with another specified element. - This is an alias for :py:func:`array_replace_n`. + See Also: + This is an alias for :py:func:`array_replace_n`. """ return array_replace_n(array, from_val, to_val, max) @@ -2930,14 +3085,13 @@ def array_replace_all(array: Expr, from_val: Expr, to_val: Expr) -> Expr: """Replaces all occurrences of ``from_val`` with ``to_val``. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 2, 1]]}) - >>> result = df.select( - ... dfn.functions.array_replace_all(dfn.col("a"), dfn.lit(1), - ... dfn.lit(9)).alias("result")) - >>> result.collect_column("result")[0].as_py() - [9, 2, 9] + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 1]]}) + >>> result = df.select( + ... dfn.functions.array_replace_all(dfn.col("a"), dfn.lit(1), + ... dfn.lit(9)).alias("result")) + >>> result.collect_column("result")[0].as_py() + [9, 2, 9] """ return Expr(f.array_replace_all(array.expr, from_val.expr, to_val.expr)) @@ -2945,7 +3099,8 @@ def array_replace_all(array: Expr, from_val: Expr, to_val: Expr) -> Expr: def list_replace_all(array: Expr, from_val: Expr, to_val: Expr) -> Expr: """Replaces all occurrences of ``from_val`` with ``to_val``. - This is an alias for :py:func:`array_replace_all`. + See Also: + This is an alias for :py:func:`array_replace_all`. """ return array_replace_all(array, from_val, to_val) @@ -2959,12 +3114,22 @@ def array_sort(array: Expr, descending: bool = False, null_first: bool = False) null_first: If True, nulls will be returned at the beginning of the array. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[3, 1, 2]]}) - >>> result = df.select(dfn.functions.array_sort(dfn.col("a")).alias("result")) - >>> result.collect_column("result")[0].as_py() - [1, 2, 3] + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[3, 1, 2]]}) + >>> result = df.select( + ... dfn.functions.array_sort( + ... dfn.col("a") + ... ).alias("result")) + >>> result.collect_column("result")[0].as_py() + [1, 2, 3] + + >>> df = ctx.from_pydict({"a": [[3, None, 1]]}) + >>> result = df.select( + ... dfn.functions.array_sort( + ... dfn.col("a"), descending=True, null_first=True, + ... ).alias("result")) + >>> result.collect_column("result")[0].as_py() + [None, 3, 1] """ desc = "DESC" if descending else "ASC" nulls_first = "NULLS FIRST" if null_first else "NULLS LAST" @@ -2978,7 +3143,11 @@ def array_sort(array: Expr, descending: bool = False, null_first: bool = False) def list_sort(array: Expr, descending: bool = False, null_first: bool = False) -> Expr: - """This is an alias for :py:func:`array_sort`.""" + """Sorts the array. + + See Also: + This is an alias for :py:func:`array_sort`. + """ return array_sort(array, descending=descending, null_first=null_first) @@ -2988,14 +3157,24 @@ def array_slice( """Returns a slice of the array. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 2, 3, 4]]}) - >>> result = df.select( - ... dfn.functions.array_slice(dfn.col("a"), dfn.lit(2), - ... dfn.lit(3)).alias("result")) - >>> result.collect_column("result")[0].as_py() - [2, 3] + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3, 4]]}) + >>> result = df.select( + ... dfn.functions.array_slice( + ... dfn.col("a"), dfn.lit(2), dfn.lit(3) + ... ).alias("result")) + >>> result.collect_column("result")[0].as_py() + [2, 3] + + Use ``stride`` to skip elements: + + >>> result = df.select( + ... dfn.functions.array_slice( + ... dfn.col("a"), dfn.lit(1), dfn.lit(4), + ... stride=dfn.lit(2), + ... ).alias("result")) + >>> result.collect_column("result")[0].as_py() + [1, 3] """ if stride is not None: stride = stride.expr @@ -3005,7 +3184,8 @@ def array_slice( def list_slice(array: Expr, begin: Expr, end: Expr, stride: Expr | None = None) -> Expr: """Returns a slice of the array. - This is an alias for :py:func:`array_slice`. + See Also: + This is an alias for :py:func:`array_slice`. """ return array_slice(array, begin, end, stride) @@ -3014,18 +3194,17 @@ def array_intersect(array1: Expr, array2: Expr) -> Expr: """Returns the intersection of ``array1`` and ``array2``. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 2, 3]], "b": [[2, 3, 4]]}) - >>> result = df.select( - ... dfn.functions.array_intersect( - ... dfn.col("a"), dfn.col("b") - ... ).alias("result") - ... ) - >>> sorted( - ... result.collect_column("result")[0].as_py() - ... ) - [2, 3] + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]], "b": [[2, 3, 4]]}) + >>> result = df.select( + ... dfn.functions.array_intersect( + ... dfn.col("a"), dfn.col("b") + ... ).alias("result") + ... ) + >>> sorted( + ... result.collect_column("result")[0].as_py() + ... ) + [2, 3] """ return Expr(f.array_intersect(array1.expr, array2.expr)) @@ -3033,7 +3212,8 @@ def array_intersect(array1: Expr, array2: Expr) -> Expr: def list_intersect(array1: Expr, array2: Expr) -> Expr: """Returns an the intersection of ``array1`` and ``array2``. - This is an alias for :py:func:`array_intersect`. + See Also: + This is an alias for :py:func:`array_intersect`. """ return array_intersect(array1, array2) @@ -3044,18 +3224,17 @@ def array_union(array1: Expr, array2: Expr) -> Expr: Duplicate rows will not be returned. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 2, 3]], "b": [[2, 3, 4]]}) - >>> result = df.select( - ... dfn.functions.array_union( - ... dfn.col("a"), dfn.col("b") - ... ).alias("result") - ... ) - >>> sorted( - ... result.collect_column("result")[0].as_py() - ... ) - [1, 2, 3, 4] + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]], "b": [[2, 3, 4]]}) + >>> result = df.select( + ... dfn.functions.array_union( + ... dfn.col("a"), dfn.col("b") + ... ).alias("result") + ... ) + >>> sorted( + ... result.collect_column("result")[0].as_py() + ... ) + [1, 2, 3, 4] """ return Expr(f.array_union(array1.expr, array2.expr)) @@ -3065,7 +3244,8 @@ def list_union(array1: Expr, array2: Expr) -> Expr: Duplicate rows will not be returned. - This is an alias for :py:func:`array_union`. + See Also: + This is an alias for :py:func:`array_union`. """ return array_union(array1, array2) @@ -3074,13 +3254,12 @@ def array_except(array1: Expr, array2: Expr) -> Expr: """Returns the elements that appear in ``array1`` but not in ``array2``. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 2, 3]], "b": [[2, 3, 4]]}) - >>> result = df.select( - ... dfn.functions.array_except(dfn.col("a"), dfn.col("b")).alias("result")) - >>> result.collect_column("result")[0].as_py() - [1] + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]], "b": [[2, 3, 4]]}) + >>> result = df.select( + ... dfn.functions.array_except(dfn.col("a"), dfn.col("b")).alias("result")) + >>> result.collect_column("result")[0].as_py() + [1] """ return Expr(f.array_except(array1.expr, array2.expr)) @@ -3088,7 +3267,8 @@ def array_except(array1: Expr, array2: Expr) -> Expr: def list_except(array1: Expr, array2: Expr) -> Expr: """Returns the elements that appear in ``array1`` but not in the ``array2``. - This is an alias for :py:func:`array_except`. + See Also: + This is an alias for :py:func:`array_except`. """ return array_except(array1, array2) @@ -3100,14 +3280,13 @@ def array_resize(array: Expr, size: Expr, value: Expr) -> Expr: be filled with the given ``value``. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 2]]}) - >>> result = df.select( - ... dfn.functions.array_resize(dfn.col("a"), dfn.lit(4), - ... dfn.lit(0)).alias("result")) - >>> result.collect_column("result")[0].as_py() - [1, 2, 0, 0] + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2]]}) + >>> result = df.select( + ... dfn.functions.array_resize(dfn.col("a"), dfn.lit(4), + ... dfn.lit(0)).alias("result")) + >>> result.collect_column("result")[0].as_py() + [1, 2, 0, 0] """ return Expr(f.array_resize(array.expr, size.expr, value.expr)) @@ -3116,7 +3295,10 @@ def list_resize(array: Expr, size: Expr, value: Expr) -> Expr: """Returns an array with the specified size filled. If ``size`` is greater than the ``array`` length, the additional entries will be - filled with the given ``value``. This is an alias for :py:func:`array_resize`. + filled with the given ``value``. + + See Also: + This is an alias for :py:func:`array_resize`. """ return array_resize(array, size, value) @@ -3125,12 +3307,11 @@ def flatten(array: Expr) -> Expr: """Flattens an array of arrays into a single array. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[[1, 2], [3, 4]]]}) - >>> result = df.select(dfn.functions.flatten(dfn.col("a")).alias("result")) - >>> result.collect_column("result")[0].as_py() - [1, 2, 3, 4] + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[[1, 2], [3, 4]]]}) + >>> result = df.select(dfn.functions.flatten(dfn.col("a")).alias("result")) + >>> result.collect_column("result")[0].as_py() + [1, 2, 3, 4] """ return Expr(f.flatten(array.expr)) @@ -3139,18 +3320,21 @@ def cardinality(array: Expr) -> Expr: """Returns the total number of elements in the array. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) - >>> result = df.select(dfn.functions.cardinality(dfn.col("a")).alias("result")) - >>> result.collect_column("result")[0].as_py() - 3 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) + >>> result = df.select(dfn.functions.cardinality(dfn.col("a")).alias("result")) + >>> result.collect_column("result")[0].as_py() + 3 """ return Expr(f.cardinality(array.expr)) def empty(array: Expr) -> Expr: - """This is an alias for :py:func:`array_empty`.""" + """Returns true if the array is empty. + + See Also: + This is an alias for :py:func:`array_empty`. + """ return array_empty(array) @@ -3173,13 +3357,22 @@ def approx_distinct( filter: If provided, only compute against rows for which the filter is True Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [1, 1, 2, 3]}) - >>> result = df.aggregate( - ... [], [dfn.functions.approx_distinct(dfn.col("a")).alias("v")]) - >>> result.collect_column("v")[0].as_py() == 3 - True + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 1, 2, 3]}) + >>> result = df.aggregate( + ... [], [dfn.functions.approx_distinct( + ... dfn.col("a") + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() == 3 + True + + >>> result = df.aggregate( + ... [], [dfn.functions.approx_distinct( + ... dfn.col("a"), + ... filter=dfn.col("a") > dfn.lit(1) + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() == 2 + True """ filter_raw = filter.expr if filter is not None else None @@ -3200,13 +3393,22 @@ def approx_median(expression: Expr, filter: Expr | None = None) -> Expr: filter: If provided, only compute against rows for which the filter is True Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0]}) - >>> result = df.aggregate( - ... [], [dfn.functions.approx_median(dfn.col("a")).alias("v")]) - >>> result.collect_column("v")[0].as_py() - 2.0 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.approx_median( + ... dfn.col("a") + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 2.0 + + >>> result = df.aggregate( + ... [], [dfn.functions.approx_median( + ... dfn.col("a"), + ... filter=dfn.col("a") > dfn.lit(1.0) + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 2.5 """ filter_raw = filter.expr if filter is not None else None return Expr(f.approx_median(expression.expr, filter=filter_raw)) @@ -3240,13 +3442,23 @@ def approx_percentile_cont( filter: If provided, only compute against rows for which the filter is True Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0, 4.0, 5.0]}) - >>> result = df.aggregate( - ... [], [dfn.functions.approx_percentile_cont(dfn.col("a"), 0.5).alias("v")]) - >>> result.collect_column("v")[0].as_py() - 3.0 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0, 4.0, 5.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.approx_percentile_cont( + ... dfn.col("a"), 0.5 + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 3.0 + + >>> result = df.aggregate( + ... [], [dfn.functions.approx_percentile_cont( + ... dfn.col("a"), 0.5, + ... num_centroids=10, + ... filter=dfn.col("a") > dfn.lit(1.0), + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 3.5 """ sort_expr_raw = sort_or_default(sort_expression) filter_raw = filter.expr if filter is not None else None @@ -3280,14 +3492,23 @@ def approx_percentile_cont_with_weight( filter: If provided, only compute against rows for which the filter is True Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0], "w": [1.0, 1.0, 1.0]}) - >>> result = df.aggregate( - ... [], [dfn.functions.approx_percentile_cont_with_weight(dfn.col("a"), - ... dfn.col("w"), 0.5).alias("v")]) - >>> result.collect_column("v")[0].as_py() - 2.0 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0], "w": [1.0, 1.0, 1.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.approx_percentile_cont_with_weight( + ... dfn.col("a"), dfn.col("w"), 0.5 + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 2.0 + + >>> result = df.aggregate( + ... [], [dfn.functions.approx_percentile_cont_with_weight( + ... dfn.col("a"), dfn.col("w"), 0.5, + ... num_centroids=10, + ... filter=dfn.col("a") > dfn.lit(1.0), + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 2.5 """ sort_expr_raw = sort_or_default(sort_expression) filter_raw = filter.expr if filter is not None else None @@ -3324,12 +3545,31 @@ def array_agg( order_by: Order the resultant array values. Accepts column names or expressions. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [1, 2, 3]}) - >>> result = df.aggregate([], [dfn.functions.array_agg(dfn.col("a")).alias("v")]) - >>> result.collect_column("v")[0].as_py() - [1, 2, 3] + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2, 3]}) + >>> result = df.aggregate( + ... [], [dfn.functions.array_agg( + ... dfn.col("a") + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + [1, 2, 3] + + >>> df = ctx.from_pydict({"a": [3, 1, 2, 1]}) + >>> result = df.aggregate( + ... [], [dfn.functions.array_agg( + ... dfn.col("a"), distinct=True, + ... ).alias("v")]) + >>> sorted(result.collect_column("v")[0].as_py()) + [1, 2, 3] + + >>> result = df.aggregate( + ... [], [dfn.functions.array_agg( + ... dfn.col("a"), + ... filter=dfn.col("a") > dfn.lit(1), + ... order_by="a", + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + [2, 3] """ order_by_raw = sort_list_to_raw_sort_list(order_by) filter_raw = filter.expr if filter is not None else None @@ -3357,12 +3597,22 @@ def avg( filter: If provided, only compute against rows for which the filter is True Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0]}) - >>> result = df.aggregate([], [dfn.functions.avg(dfn.col("a")).alias("v")]) - >>> result.collect_column("v")[0].as_py() - 2.0 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.avg( + ... dfn.col("a") + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 2.0 + + >>> result = df.aggregate( + ... [], [dfn.functions.avg( + ... dfn.col("a"), + ... filter=dfn.col("a") > dfn.lit(1.0) + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 2.5 """ filter_raw = filter.expr if filter is not None else None return Expr(f.avg(expression.expr, filter=filter_raw)) @@ -3385,7 +3635,17 @@ def corr(value_y: Expr, value_x: Expr, filter: Expr | None = None) -> Expr: >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0], "b": [1.0, 2.0, 3.0]}) >>> result = df.aggregate( - ... [], [dfn.functions.corr(dfn.col("a"), dfn.col("b")).alias("v")]) + ... [], [dfn.functions.corr( + ... dfn.col("a"), dfn.col("b") + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 1.0 + + >>> result = df.aggregate( + ... [], [dfn.functions.corr( + ... dfn.col("a"), dfn.col("b"), + ... filter=dfn.col("a") > dfn.lit(1.0) + ... ).alias("v")]) >>> result.collect_column("v")[0].as_py() 1.0 """ @@ -3411,12 +3671,23 @@ def count( filter: If provided, only compute against rows for which the filter is True Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [1, 2, 3]}) - >>> result = df.aggregate([], [dfn.functions.count(dfn.col("a")).alias("v")]) - >>> result.collect_column("v")[0].as_py() - 3 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2, 3]}) + >>> result = df.aggregate( + ... [], [dfn.functions.count( + ... dfn.col("a") + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 3 + + >>> df = ctx.from_pydict({"a": [1, 1, 2, 3]}) + >>> result = df.aggregate( + ... [], [dfn.functions.count( + ... dfn.col("a"), distinct=True, + ... filter=dfn.col("a") > dfn.lit(1), + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 2 """ filter_raw = filter.expr if filter is not None else None @@ -3454,6 +3725,18 @@ def covar_pop(value_y: Expr, value_x: Expr, filter: Expr | None = None) -> Expr: ... ) >>> result.collect_column("v")[0].as_py() 3.0 + + >>> df = ctx.from_pydict( + ... {"a": [0.0, 1.0, 3.0], "b": [0.0, 1.0, 3.0]}) + >>> result = df.aggregate( + ... [], + ... [dfn.functions.covar_pop( + ... dfn.col("a"), dfn.col("b"), + ... filter=dfn.col("a") > dfn.lit(0.0) + ... ).alias("v")] + ... ) + >>> result.collect_column("v")[0].as_py() + 1.0 """ filter_raw = filter.expr if filter is not None else None return Expr(f.covar_pop(value_y.expr, value_x.expr, filter=filter_raw)) @@ -3476,9 +3759,19 @@ def covar_samp(value_y: Expr, value_x: Expr, filter: Expr | None = None) -> Expr >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0], "b": [4.0, 5.0, 6.0]}) >>> result = df.aggregate( - ... [], [dfn.functions.covar_samp(dfn.col("a"), dfn.col("b")).alias("v")]) + ... [], [dfn.functions.covar_samp( + ... dfn.col("a"), dfn.col("b") + ... ).alias("v")]) >>> result.collect_column("v")[0].as_py() 1.0 + + >>> result = df.aggregate( + ... [], [dfn.functions.covar_samp( + ... dfn.col("a"), dfn.col("b"), + ... filter=dfn.col("a") > dfn.lit(1.0) + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 0.5 """ filter_raw = filter.expr if filter is not None else None return Expr(f.covar_samp(value_y.expr, value_x.expr, filter=filter_raw)) @@ -3504,12 +3797,22 @@ def max(expression: Expr, filter: Expr | None = None) -> Expr: filter: If provided, only compute against rows for which the filter is True Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [1, 2, 3]}) - >>> result = df.aggregate([], [dfn.functions.max(dfn.col("a")).alias("v")]) - >>> result.collect_column("v")[0].as_py() - 3 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2, 3]}) + >>> result = df.aggregate( + ... [], [dfn.functions.max( + ... dfn.col("a") + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 3 + + >>> result = df.aggregate( + ... [], [dfn.functions.max( + ... dfn.col("a"), + ... filter=dfn.col("a") < dfn.lit(3) + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 2 """ filter_raw = filter.expr if filter is not None else None return Expr(f.max(expression.expr, filter=filter_raw)) @@ -3518,15 +3821,8 @@ def max(expression: Expr, filter: Expr | None = None) -> Expr: def mean(expression: Expr, filter: Expr | None = None) -> Expr: """Returns the average (mean) value of the argument. - This is an alias for :py:func:`avg`. - - Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0]}) - >>> result = df.aggregate([], [dfn.functions.mean(dfn.col("a")).alias("v")]) - >>> result.collect_column("v")[0].as_py() - 2.0 + See Also: + This is an alias for :py:func:`avg`. """ return avg(expression, filter) @@ -3548,12 +3844,23 @@ def median( filter: If provided, only compute against rows for which the filter is True Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0]}) - >>> result = df.aggregate([], [dfn.functions.median(dfn.col("a")).alias("v")]) - >>> result.collect_column("v")[0].as_py() - 2.0 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.median( + ... dfn.col("a") + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 2.0 + + >>> df = ctx.from_pydict({"a": [1.0, 1.0, 2.0, 3.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.median( + ... dfn.col("a"), distinct=True, + ... filter=dfn.col("a") < dfn.lit(3.0), + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 1.5 """ filter_raw = filter.expr if filter is not None else None return Expr(f.median(expression.expr, distinct=distinct, filter=filter_raw)) @@ -3570,12 +3877,22 @@ def min(expression: Expr, filter: Expr | None = None) -> Expr: filter: If provided, only compute against rows for which the filter is True Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [1, 2, 3]}) - >>> result = df.aggregate([], [dfn.functions.min(dfn.col("a")).alias("v")]) - >>> result.collect_column("v")[0].as_py() - 1 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2, 3]}) + >>> result = df.aggregate( + ... [], [dfn.functions.min( + ... dfn.col("a") + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 1 + + >>> result = df.aggregate( + ... [], [dfn.functions.min( + ... dfn.col("a"), + ... filter=dfn.col("a") > dfn.lit(1) + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 2 """ filter_raw = filter.expr if filter is not None else None return Expr(f.min(expression.expr, filter=filter_raw)) @@ -3597,12 +3914,22 @@ def sum( filter: If provided, only compute against rows for which the filter is True Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [1, 2, 3]}) - >>> result = df.aggregate([], [dfn.functions.sum(dfn.col("a")).alias("v")]) - >>> result.collect_column("v")[0].as_py() - 6 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2, 3]}) + >>> result = df.aggregate( + ... [], [dfn.functions.sum( + ... dfn.col("a") + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 6 + + >>> result = df.aggregate( + ... [], [dfn.functions.sum( + ... dfn.col("a"), + ... filter=dfn.col("a") > dfn.lit(1) + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 5 """ filter_raw = filter.expr if filter is not None else None return Expr(f.sum(expression.expr, filter=filter_raw)) @@ -3621,9 +3948,20 @@ def stddev(expression: Expr, filter: Expr | None = None) -> Expr: Examples: >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"a": [2.0, 4.0, 6.0]}) - >>> result = df.aggregate([], [dfn.functions.stddev(dfn.col("a")).alias("v")]) + >>> result = df.aggregate( + ... [], [dfn.functions.stddev( + ... dfn.col("a") + ... ).alias("v")]) >>> result.collect_column("v")[0].as_py() 2.0 + + >>> result = df.aggregate( + ... [], [dfn.functions.stddev( + ... dfn.col("a"), + ... filter=dfn.col("a") > dfn.lit(2.0) + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 1.41... """ filter_raw = filter.expr if filter is not None else None return Expr(f.stddev(expression.expr, filter=filter_raw)) @@ -3641,9 +3979,21 @@ def stddev_pop(expression: Expr, filter: Expr | None = None) -> Expr: Examples: >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [1.0, 3.0]}) + >>> df = ctx.from_pydict({"a": [0.0, 1.0, 3.0]}) >>> result = df.aggregate( - ... [], [dfn.functions.stddev_pop(dfn.col("a")).alias("v")] + ... [], [dfn.functions.stddev_pop( + ... dfn.col("a") + ... ).alias("v")] + ... ) + >>> result.collect_column("v")[0].as_py() + 1.247... + + >>> df = ctx.from_pydict({"a": [0.0, 1.0, 3.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.stddev_pop( + ... dfn.col("a"), + ... filter=dfn.col("a") > dfn.lit(0.0) + ... ).alias("v")] ... ) >>> result.collect_column("v")[0].as_py() 1.0 @@ -3655,16 +4005,8 @@ def stddev_pop(expression: Expr, filter: Expr | None = None) -> Expr: def stddev_samp(arg: Expr, filter: Expr | None = None) -> Expr: """Computes the sample standard deviation of the argument. - This is an alias for :py:func:`stddev`. - - Examples: - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [2.0, 4.0, 6.0]}) - >>> result = df.aggregate( - ... [], [dfn.functions.stddev_samp(dfn.col("a")).alias("v")] - ... ) - >>> result.collect_column("v")[0].as_py() - 2.0 + See Also: + This is an alias for :py:func:`stddev`. """ return stddev(arg, filter=filter) @@ -3672,14 +4014,8 @@ def stddev_samp(arg: Expr, filter: Expr | None = None) -> Expr: def var(expression: Expr, filter: Expr | None = None) -> Expr: """Computes the sample variance of the argument. - This is an alias for :py:func:`var_samp`. - - Examples: - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0]}) - >>> result = df.aggregate([], [dfn.functions.var(dfn.col("a")).alias("v")]) - >>> result.collect_column("v")[0].as_py() - 1.0 + See Also: + This is an alias for :py:func:`var_samp`. """ return var_samp(expression, filter) @@ -3696,8 +4032,19 @@ def var_pop(expression: Expr, filter: Expr | None = None) -> Expr: Examples: >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [0.0, 2.0]}) - >>> result = df.aggregate([], [dfn.functions.var_pop(dfn.col("a")).alias("v")]) + >>> df = ctx.from_pydict({"a": [-1.0, 0.0, 2.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.var_pop( + ... dfn.col("a") + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 1.555... + + >>> result = df.aggregate( + ... [], [dfn.functions.var_pop( + ... dfn.col("a"), + ... filter=dfn.col("a") > dfn.lit(-1.0) + ... ).alias("v")]) >>> result.collect_column("v")[0].as_py() 1.0 """ @@ -3718,9 +4065,20 @@ def var_samp(expression: Expr, filter: Expr | None = None) -> Expr: Examples: >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0]}) - >>> result = df.aggregate([], [dfn.functions.var_samp(dfn.col("a")).alias("v")]) + >>> result = df.aggregate( + ... [], [dfn.functions.var_samp( + ... dfn.col("a") + ... ).alias("v")]) >>> result.collect_column("v")[0].as_py() 1.0 + + >>> result = df.aggregate( + ... [], [dfn.functions.var_samp( + ... dfn.col("a"), + ... filter=dfn.col("a") > dfn.lit(1.0) + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 0.5 """ filter_raw = filter.expr if filter is not None else None return Expr(f.var_sample(expression.expr, filter=filter_raw)) @@ -3729,16 +4087,8 @@ def var_samp(expression: Expr, filter: Expr | None = None) -> Expr: def var_sample(expression: Expr, filter: Expr | None = None) -> Expr: """Computes the sample variance of the argument. - This is an alias for :py:func:`var_samp`. - - Examples: - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0]}) - >>> result = df.aggregate( - ... [], [dfn.functions.var_sample(dfn.col("a")).alias("v")] - ... ) - >>> result.collect_column("v")[0].as_py() - 1.0 + See Also: + This is an alias for :py:func:`var_samp`. """ return var_samp(expression, filter) @@ -3765,9 +4115,19 @@ def regr_avgx( >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"y": [1.0, 2.0, 3.0], "x": [4.0, 5.0, 6.0]}) >>> result = df.aggregate( - ... [], [dfn.functions.regr_avgx(dfn.col("y"), dfn.col("x")).alias("v")]) + ... [], [dfn.functions.regr_avgx( + ... dfn.col("y"), dfn.col("x") + ... ).alias("v")]) >>> result.collect_column("v")[0].as_py() 5.0 + + >>> result = df.aggregate( + ... [], [dfn.functions.regr_avgx( + ... dfn.col("y"), dfn.col("x"), + ... filter=dfn.col("y") > dfn.lit(1.0) + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 5.5 """ filter_raw = filter.expr if filter is not None else None @@ -3796,9 +4156,19 @@ def regr_avgy( >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"y": [1.0, 2.0, 3.0], "x": [4.0, 5.0, 6.0]}) >>> result = df.aggregate( - ... [], [dfn.functions.regr_avgy(dfn.col("y"), dfn.col("x")).alias("v")]) + ... [], [dfn.functions.regr_avgy( + ... dfn.col("y"), dfn.col("x") + ... ).alias("v")]) >>> result.collect_column("v")[0].as_py() 2.0 + + >>> result = df.aggregate( + ... [], [dfn.functions.regr_avgy( + ... dfn.col("y"), dfn.col("x"), + ... filter=dfn.col("y") > dfn.lit(1.0) + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 2.5 """ filter_raw = filter.expr if filter is not None else None @@ -3827,9 +4197,19 @@ def regr_count( >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"y": [1.0, 2.0, 3.0], "x": [4.0, 5.0, 6.0]}) >>> result = df.aggregate( - ... [], [dfn.functions.regr_count(dfn.col("y"), dfn.col("x")).alias("v")]) + ... [], [dfn.functions.regr_count( + ... dfn.col("y"), dfn.col("x") + ... ).alias("v")]) >>> result.collect_column("v")[0].as_py() 3 + + >>> result = df.aggregate( + ... [], [dfn.functions.regr_count( + ... dfn.col("y"), dfn.col("x"), + ... filter=dfn.col("y") > dfn.lit(1.0) + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 2 """ filter_raw = filter.expr if filter is not None else None @@ -3856,12 +4236,23 @@ def regr_intercept( Examples: >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"y": [2.0, 4.0, 6.0], "x": [1.0, 2.0, 3.0]}) + >>> df = ctx.from_pydict({"y": [2.0, 4.0, 6.0], "x": [4.0, 16.0, 36.0]}) >>> result = df.aggregate( ... [], - ... [dfn.functions.regr_intercept(dfn.col("y"), dfn.col("x")).alias("v")]) + ... [dfn.functions.regr_intercept( + ... dfn.col("y"), dfn.col("x") + ... ).alias("v")]) >>> result.collect_column("v")[0].as_py() - 0.0 + 1.714... + + >>> result = df.aggregate( + ... [], + ... [dfn.functions.regr_intercept( + ... dfn.col("y"), dfn.col("x"), + ... filter=dfn.col("y") > dfn.lit(2.0) + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 2.4 """ filter_raw = filter.expr if filter is not None else None @@ -3888,9 +4279,19 @@ def regr_r2( Examples: >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"y": [2.0, 4.0, 6.0], "x": [1.0, 2.0, 3.0]}) + >>> df = ctx.from_pydict({"y": [2.0, 4.0, 6.0], "x": [4.0, 16.0, 36.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.regr_r2( + ... dfn.col("y"), dfn.col("x") + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 0.9795... + >>> result = df.aggregate( - ... [], [dfn.functions.regr_r2(dfn.col("y"), dfn.col("x")).alias("v")]) + ... [], [dfn.functions.regr_r2( + ... dfn.col("y"), dfn.col("x"), + ... filter=dfn.col("y") > dfn.lit(2.0) + ... ).alias("v")]) >>> result.collect_column("v")[0].as_py() 1.0 """ @@ -3919,11 +4320,21 @@ def regr_slope( Examples: >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"y": [2.0, 4.0, 6.0], "x": [1.0, 2.0, 3.0]}) + >>> df = ctx.from_pydict({"y": [2.0, 4.0, 6.0], "x": [4.0, 16.0, 36.0]}) >>> result = df.aggregate( - ... [], [dfn.functions.regr_slope(dfn.col("y"), dfn.col("x")).alias("v")]) + ... [], [dfn.functions.regr_slope( + ... dfn.col("y"), dfn.col("x") + ... ).alias("v")]) >>> result.collect_column("v")[0].as_py() - 2.0 + 0.122... + + >>> result = df.aggregate( + ... [], [dfn.functions.regr_slope( + ... dfn.col("y"), dfn.col("x"), + ... filter=dfn.col("y") > dfn.lit(2.0) + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 0.1 """ filter_raw = filter.expr if filter is not None else None @@ -3952,9 +4363,19 @@ def regr_sxx( >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"y": [1.0, 2.0, 3.0], "x": [1.0, 2.0, 3.0]}) >>> result = df.aggregate( - ... [], [dfn.functions.regr_sxx(dfn.col("y"), dfn.col("x")).alias("v")]) + ... [], [dfn.functions.regr_sxx( + ... dfn.col("y"), dfn.col("x") + ... ).alias("v")]) >>> result.collect_column("v")[0].as_py() 2.0 + + >>> result = df.aggregate( + ... [], [dfn.functions.regr_sxx( + ... dfn.col("y"), dfn.col("x"), + ... filter=dfn.col("y") > dfn.lit(1.0) + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 0.5 """ filter_raw = filter.expr if filter is not None else None @@ -3983,9 +4404,19 @@ def regr_sxy( >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"y": [1.0, 2.0, 3.0], "x": [1.0, 2.0, 3.0]}) >>> result = df.aggregate( - ... [], [dfn.functions.regr_sxy(dfn.col("y"), dfn.col("x")).alias("v")]) + ... [], [dfn.functions.regr_sxy( + ... dfn.col("y"), dfn.col("x") + ... ).alias("v")]) >>> result.collect_column("v")[0].as_py() 2.0 + + >>> result = df.aggregate( + ... [], [dfn.functions.regr_sxy( + ... dfn.col("y"), dfn.col("x"), + ... filter=dfn.col("y") > dfn.lit(1.0) + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 0.5 """ filter_raw = filter.expr if filter is not None else None @@ -4014,9 +4445,19 @@ def regr_syy( >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"y": [1.0, 2.0, 3.0], "x": [1.0, 2.0, 3.0]}) >>> result = df.aggregate( - ... [], [dfn.functions.regr_syy(dfn.col("y"), dfn.col("x")).alias("v")]) + ... [], [dfn.functions.regr_syy( + ... dfn.col("y"), dfn.col("x") + ... ).alias("v")]) >>> result.collect_column("v")[0].as_py() 2.0 + + >>> result = df.aggregate( + ... [], [dfn.functions.regr_syy( + ... dfn.col("y"), dfn.col("x"), + ... filter=dfn.col("y") > dfn.lit(1.0) + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 0.5 """ filter_raw = filter.expr if filter is not None else None @@ -4047,10 +4488,24 @@ def first_value( >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"a": [10, 20, 30]}) >>> result = df.aggregate( - ... [], [dfn.functions.first_value(dfn.col("a")).alias("v")] + ... [], [dfn.functions.first_value( + ... dfn.col("a") + ... ).alias("v")] ... ) >>> result.collect_column("v")[0].as_py() 10 + + >>> df = ctx.from_pydict({"a": [None, 20, 10]}) + >>> result = df.aggregate( + ... [], [dfn.functions.first_value( + ... dfn.col("a"), + ... filter=dfn.col("a") > dfn.lit(10), + ... order_by="a", + ... null_treatment=dfn.common.NullTreatment.IGNORE_NULLS, + ... ).alias("v")] + ... ) + >>> result.collect_column("v")[0].as_py() + 20 """ order_by_raw = sort_list_to_raw_sort_list(order_by) filter_raw = filter.expr if filter is not None else None @@ -4089,10 +4544,24 @@ def last_value( >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"a": [10, 20, 30]}) >>> result = df.aggregate( - ... [], [dfn.functions.last_value(dfn.col("a")).alias("v")] + ... [], [dfn.functions.last_value( + ... dfn.col("a") + ... ).alias("v")] ... ) >>> result.collect_column("v")[0].as_py() 30 + + >>> df = ctx.from_pydict({"a": [None, 20, 10]}) + >>> result = df.aggregate( + ... [], [dfn.functions.last_value( + ... dfn.col("a"), + ... filter=dfn.col("a") > dfn.lit(10), + ... order_by="a", + ... null_treatment=dfn.common.NullTreatment.IGNORE_NULLS, + ... ).alias("v")] + ... ) + >>> result.collect_column("v")[0].as_py() + 20 """ order_by_raw = sort_list_to_raw_sort_list(order_by) filter_raw = filter.expr if filter is not None else None @@ -4133,7 +4602,20 @@ def nth_value( >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"a": [10, 20, 30]}) >>> result = df.aggregate( - ... [], [dfn.functions.nth_value(dfn.col("a"), 2).alias("v")] + ... [], [dfn.functions.nth_value( + ... dfn.col("a"), 1 + ... ).alias("v")] + ... ) + >>> result.collect_column("v")[0].as_py() + 10 + + >>> result = df.aggregate( + ... [], [dfn.functions.nth_value( + ... dfn.col("a"), 1, + ... filter=dfn.col("a") > dfn.lit(10), + ... order_by="a", + ... null_treatment=dfn.common.NullTreatment.IGNORE_NULLS, + ... ).alias("v")] ... ) >>> result.collect_column("v")[0].as_py() 20 @@ -4165,12 +4647,23 @@ def bit_and(expression: Expr, filter: Expr | None = None) -> Expr: filter: If provided, only compute against rows for which the filter is True Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [7, 3]}) - >>> result = df.aggregate([], [dfn.functions.bit_and(dfn.col("a")).alias("v")]) - >>> result.collect_column("v")[0].as_py() - 3 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [7, 3]}) + >>> result = df.aggregate( + ... [], [dfn.functions.bit_and( + ... dfn.col("a") + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 3 + + >>> df = ctx.from_pydict({"a": [7, 5, 3]}) + >>> result = df.aggregate( + ... [], [dfn.functions.bit_and( + ... dfn.col("a"), + ... filter=dfn.col("a") > dfn.lit(3) + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 5 """ filter_raw = filter.expr if filter is not None else None return Expr(f.bit_and(expression.expr, filter=filter_raw)) @@ -4189,12 +4682,25 @@ def bit_or(expression: Expr, filter: Expr | None = None) -> Expr: filter: If provided, only compute against rows for which the filter is True Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [1, 2]}) - >>> result = df.aggregate([], [dfn.functions.bit_or(dfn.col("a")).alias("v")]) - >>> result.collect_column("v")[0].as_py() - 3 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2]}) + >>> result = df.aggregate( + ... [], [dfn.functions.bit_or( + ... dfn.col("a") + ... ).alias("v")] + ... ) + >>> result.collect_column("v")[0].as_py() + 3 + + >>> df = ctx.from_pydict({"a": [1, 2, 4]}) + >>> result = df.aggregate( + ... [], [dfn.functions.bit_or( + ... dfn.col("a"), + ... filter=dfn.col("a") > dfn.lit(1) + ... ).alias("v")] + ... ) + >>> result.collect_column("v")[0].as_py() + 6 """ filter_raw = filter.expr if filter is not None else None return Expr(f.bit_or(expression.expr, filter=filter_raw)) @@ -4216,12 +4722,25 @@ def bit_xor( filter: If provided, only compute against rows for which the filter is True Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [5, 3]}) - >>> result = df.aggregate([], [dfn.functions.bit_xor(dfn.col("a")).alias("v")]) - >>> result.collect_column("v")[0].as_py() - 6 + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [5, 3]}) + >>> result = df.aggregate( + ... [], [dfn.functions.bit_xor( + ... dfn.col("a") + ... ).alias("v")] + ... ) + >>> result.collect_column("v")[0].as_py() + 6 + + >>> df = ctx.from_pydict({"a": [5, 5, 3]}) + >>> result = df.aggregate( + ... [], [dfn.functions.bit_xor( + ... dfn.col("a"), distinct=True, + ... filter=dfn.col("a") > dfn.lit(3), + ... ).alias("v")] + ... ) + >>> result.collect_column("v")[0].as_py() + 5 """ filter_raw = filter.expr if filter is not None else None return Expr(f.bit_xor(expression.expr, distinct=distinct, filter=filter_raw)) @@ -4241,12 +4760,26 @@ def bool_and(expression: Expr, filter: Expr | None = None) -> Expr: filter: If provided, only compute against rows for which the filter is True Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [True, True, False]}) - >>> result = df.aggregate([], [dfn.functions.bool_and(dfn.col("a")).alias("v")]) - >>> result.collect_column("v")[0].as_py() - False + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [True, True, False]}) + >>> result = df.aggregate( + ... [], [dfn.functions.bool_and( + ... dfn.col("a") + ... ).alias("v")] + ... ) + >>> result.collect_column("v")[0].as_py() + False + + >>> df = ctx.from_pydict( + ... {"a": [True, True, False], "b": [1, 2, 3]}) + >>> result = df.aggregate( + ... [], [dfn.functions.bool_and( + ... dfn.col("a"), + ... filter=dfn.col("b") < dfn.lit(3) + ... ).alias("v")] + ... ) + >>> result.collect_column("v")[0].as_py() + True """ filter_raw = filter.expr if filter is not None else None return Expr(f.bool_and(expression.expr, filter=filter_raw)) @@ -4266,12 +4799,26 @@ def bool_or(expression: Expr, filter: Expr | None = None) -> Expr: filter: If provided, only compute against rows for which the filter is True Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": [False, False, True]}) - >>> result = df.aggregate([], [dfn.functions.bool_or(dfn.col("a")).alias("v")]) - >>> result.collect_column("v")[0].as_py() - True + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [False, False, True]}) + >>> result = df.aggregate( + ... [], [dfn.functions.bool_or( + ... dfn.col("a") + ... ).alias("v")] + ... ) + >>> result.collect_column("v")[0].as_py() + True + + >>> df = ctx.from_pydict( + ... {"a": [False, False, True], "b": [1, 2, 3]}) + >>> result = df.aggregate( + ... [], [dfn.functions.bool_or( + ... dfn.col("a"), + ... filter=dfn.col("b") < dfn.lit(3) + ... ).alias("v")] + ... ) + >>> result.collect_column("v")[0].as_py() + False """ filter_raw = filter.expr if filter is not None else None return Expr(f.bool_or(expression.expr, filter=filter_raw)) @@ -4318,10 +4865,23 @@ def lead( >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"a": [1, 2, 3]}) >>> result = df.select( - ... dfn.col("a"), dfn.functions.lead(dfn.col("a"), shift_offset=1, - ... default_value=0, order_by="a").alias("lead")) + ... dfn.col("a"), + ... dfn.functions.lead( + ... dfn.col("a"), shift_offset=1, + ... default_value=0, order_by="a" + ... ).alias("lead")) >>> result.sort(dfn.col("a")).collect_column("lead").to_pylist() [2, 3, 0] + + >>> df = ctx.from_pydict({"g": ["a", "a", "b"], "v": [1, 2, 3]}) + >>> result = df.select( + ... dfn.col("g"), dfn.col("v"), + ... dfn.functions.lead( + ... dfn.col("v"), shift_offset=1, default_value=0, + ... partition_by=dfn.col("g"), order_by="v", + ... ).alias("lead")) + >>> result.sort(dfn.col("g"), dfn.col("v")).collect_column("lead").to_pylist() + [2, 0, 0] """ if not isinstance(default_value, pa.Scalar) and default_value is not None: default_value = pa.scalar(default_value) @@ -4378,10 +4938,23 @@ def lag( >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"a": [1, 2, 3]}) >>> result = df.select( - ... dfn.col("a"), dfn.functions.lag(dfn.col("a"), shift_offset=1, - ... default_value=0, order_by="a").alias("lag")) + ... dfn.col("a"), + ... dfn.functions.lag( + ... dfn.col("a"), shift_offset=1, + ... default_value=0, order_by="a" + ... ).alias("lag")) >>> result.sort(dfn.col("a")).collect_column("lag").to_pylist() [0, 1, 2] + + >>> df = ctx.from_pydict({"g": ["a", "a", "b"], "v": [1, 2, 3]}) + >>> result = df.select( + ... dfn.col("g"), dfn.col("v"), + ... dfn.functions.lag( + ... dfn.col("v"), shift_offset=1, default_value=0, + ... partition_by=dfn.col("g"), order_by="v", + ... ).alias("lag")) + >>> result.sort(dfn.col("g"), dfn.col("v")).collect_column("lag").to_pylist() + [0, 1, 0] """ if not isinstance(default_value, pa.Scalar): default_value = pa.scalar(default_value) @@ -4428,9 +5001,22 @@ def row_number( >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"a": [10, 20, 30]}) >>> result = df.select( - ... dfn.col("a"), dfn.functions.row_number(order_by="a").alias("rn")) + ... dfn.col("a"), + ... dfn.functions.row_number( + ... order_by="a" + ... ).alias("rn")) >>> result.sort(dfn.col("a")).collect_column("rn").to_pylist() [1, 2, 3] + + >>> df = ctx.from_pydict( + ... {"g": ["a", "a", "b", "b"], "v": [1, 2, 3, 4]}) + >>> result = df.select( + ... dfn.col("g"), dfn.col("v"), + ... dfn.functions.row_number( + ... partition_by=dfn.col("g"), order_by="v", + ... ).alias("rn")) + >>> result.sort(dfn.col("g"), dfn.col("v")).collect_column("rn").to_pylist() + [1, 2, 1, 2] """ partition_by_raw = expr_list_to_raw_expr_list(partition_by) order_by_raw = sort_list_to_raw_sort_list(order_by) @@ -4476,10 +5062,23 @@ def rank( >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"a": [10, 10, 20]}) >>> result = df.select( - ... dfn.col("a"), dfn.functions.rank(order_by="a").alias("rnk") + ... dfn.col("a"), + ... dfn.functions.rank( + ... order_by="a" + ... ).alias("rnk") ... ) >>> result.sort(dfn.col("a")).collect_column("rnk").to_pylist() [1, 1, 3] + + >>> df = ctx.from_pydict( + ... {"g": ["a", "a", "b", "b"], "v": [1, 1, 2, 3]}) + >>> result = df.select( + ... dfn.col("g"), dfn.col("v"), + ... dfn.functions.rank( + ... partition_by=dfn.col("g"), order_by="v", + ... ).alias("rnk")) + >>> result.sort(dfn.col("g"), dfn.col("v")).collect_column("rnk").to_pylist() + [1, 1, 1, 2] """ partition_by_raw = expr_list_to_raw_expr_list(partition_by) order_by_raw = sort_list_to_raw_sort_list(order_by) @@ -4520,9 +5119,22 @@ def dense_rank( >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"a": [10, 10, 20]}) >>> result = df.select( - ... dfn.col("a"), dfn.functions.dense_rank(order_by="a").alias("dr")) + ... dfn.col("a"), + ... dfn.functions.dense_rank( + ... order_by="a" + ... ).alias("dr")) >>> result.sort(dfn.col("a")).collect_column("dr").to_pylist() [1, 1, 2] + + >>> df = ctx.from_pydict( + ... {"g": ["a", "a", "b", "b"], "v": [1, 1, 2, 3]}) + >>> result = df.select( + ... dfn.col("g"), dfn.col("v"), + ... dfn.functions.dense_rank( + ... partition_by=dfn.col("g"), order_by="v", + ... ).alias("dr")) + >>> result.sort(dfn.col("g"), dfn.col("v")).collect_column("dr").to_pylist() + [1, 1, 1, 2] """ partition_by_raw = expr_list_to_raw_expr_list(partition_by) order_by_raw = sort_list_to_raw_sort_list(order_by) @@ -4565,9 +5177,22 @@ def percent_rank( >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"a": [10, 20, 30]}) >>> result = df.select( - ... dfn.col("a"), dfn.functions.percent_rank(order_by="a").alias("pr")) + ... dfn.col("a"), + ... dfn.functions.percent_rank( + ... order_by="a" + ... ).alias("pr")) >>> result.sort(dfn.col("a")).collect_column("pr").to_pylist() [0.0, 0.5, 1.0] + + >>> df = ctx.from_pydict( + ... {"g": ["a", "a", "a", "b", "b"], "v": [1, 2, 3, 4, 5]}) + >>> result = df.select( + ... dfn.col("g"), dfn.col("v"), + ... dfn.functions.percent_rank( + ... partition_by=dfn.col("g"), order_by="v", + ... ).alias("pr")) + >>> result.sort(dfn.col("g"), dfn.col("v")).collect_column("pr").to_pylist() + [0.0, 0.5, 1.0, 0.0, 1.0] """ partition_by_raw = expr_list_to_raw_expr_list(partition_by) order_by_raw = sort_list_to_raw_sort_list(order_by) @@ -4616,6 +5241,16 @@ def cume_dist( ... ) >>> result.collect_column("cd").to_pylist() [0.25..., 0.75..., 0.75..., 1.0...] + + >>> df = ctx.from_pydict( + ... {"g": ["a", "a", "b", "b"], "v": [1, 2, 3, 4]}) + >>> result = df.select( + ... dfn.col("g"), dfn.col("v"), + ... dfn.functions.cume_dist( + ... partition_by=dfn.col("g"), order_by="v", + ... ).alias("cd")) + >>> result.sort(dfn.col("g"), dfn.col("v")).collect_column("cd").to_pylist() + [0.5, 1.0, 0.5, 1.0] """ partition_by_raw = expr_list_to_raw_expr_list(partition_by) order_by_raw = sort_list_to_raw_sort_list(order_by) @@ -4661,9 +5296,22 @@ def ntile( >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"a": [10, 20, 30, 40]}) >>> result = df.select( - ... dfn.col("a"), dfn.functions.ntile(2, order_by="a").alias("nt")) + ... dfn.col("a"), + ... dfn.functions.ntile( + ... 2, order_by="a" + ... ).alias("nt")) >>> result.sort(dfn.col("a")).collect_column("nt").to_pylist() [1, 1, 2, 2] + + >>> df = ctx.from_pydict( + ... {"g": ["a", "a", "b", "b"], "v": [1, 2, 3, 4]}) + >>> result = df.select( + ... dfn.col("g"), dfn.col("v"), + ... dfn.functions.ntile( + ... 2, partition_by=dfn.col("g"), order_by="v", + ... ).alias("nt")) + >>> result.sort(dfn.col("g"), dfn.col("v")).collect_column("nt").to_pylist() + [1, 2, 1, 2] """ partition_by_raw = expr_list_to_raw_expr_list(partition_by) order_by_raw = sort_list_to_raw_sort_list(order_by) @@ -4700,13 +5348,23 @@ def string_agg( column names or expressions. Examples: - --------- - >>> ctx = dfn.SessionContext() - >>> df = ctx.from_pydict({"a": ["x", "y", "z"]}) - >>> result = df.aggregate( - ... [], [dfn.functions.string_agg(dfn.col("a"), ",", order_by="a").alias("s")]) - >>> result.collect_column("s")[0].as_py() - 'x,y,z' + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["x", "y", "z"]}) + >>> result = df.aggregate( + ... [], [dfn.functions.string_agg( + ... dfn.col("a"), ",", order_by="a" + ... ).alias("s")]) + >>> result.collect_column("s")[0].as_py() + 'x,y,z' + + >>> result = df.aggregate( + ... [], [dfn.functions.string_agg( + ... dfn.col("a"), ",", + ... filter=dfn.col("a") > dfn.lit("x"), + ... order_by="a", + ... ).alias("s")]) + >>> result.collect_column("s")[0].as_py() + 'y,z' """ order_by_raw = sort_list_to_raw_sort_list(order_by) filter_raw = filter.expr if filter is not None else None From ad8d41f2b5faff9a35aeeb340a24480c8ccb6eff Mon Sep 17 00:00:00 2001 From: Daniel Mesejo Date: Sat, 28 Mar 2026 14:35:32 +0100 Subject: [PATCH 32/56] chore: enforce uv lockfile consistency in CI and pre-commit (#1398) * chore: enforce uv lockfile consistency in CI and pre-commit Add --locked flag to uv sync in CI to fail if uv.lock is out of sync, and add the uv-lock pre-commit hook to automatically keep uv.lock up to date when pyproject.toml changes. * chore: add missing --locked calls --- .github/workflows/build.yml | 1 + .github/workflows/test.yml | 3 +++ .pre-commit-config.yaml | 7 +++++++ 3 files changed, 11 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 77880fdfa..1ef951d77 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,6 +36,7 @@ on: env: CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 + UV_LOCKED: true jobs: # ============================================ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 890072a0d..706ccbc55 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,6 +23,9 @@ name: Test on: workflow_call: +env: + UV_LOCKED: true + jobs: test-matrix: runs-on: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8ae6a4e32..2d3c2bc59 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,5 +53,12 @@ repos: additional_dependencies: - tomli + - repo: https://github.com/astral-sh/uv-pre-commit + # uv version. + rev: 0.10.7 + hooks: + # Update the uv lockfile + - id: uv-lock + default_language_version: python: python3 From 5be412b6a691a57bb2246e6726751fe9e8916035 Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Sun, 29 Mar 2026 18:34:48 -0400 Subject: [PATCH 33/56] Pin rust toolchain to apache allowlist sha --- .github/workflows/build.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1ef951d77..4b37046ee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,7 +48,7 @@ jobs: - uses: actions/checkout@v6 - name: Setup Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 with: toolchain: "nightly" components: rustfmt @@ -148,7 +148,7 @@ jobs: path: . - name: Setup Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 - name: Cache Cargo uses: Swatinem/rust-cache@v2 @@ -216,7 +216,7 @@ jobs: path: . - name: Setup Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 - name: Cache Cargo uses: Swatinem/rust-cache@v2 @@ -280,7 +280,7 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 - run: rm LICENSE.txt - name: Download LICENSE.txt @@ -354,7 +354,7 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 - run: rm LICENSE.txt - name: Download LICENSE.txt From 73a9d53a37f6ce864b68dda1b07e92a0fed8c8ba Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Tue, 31 Mar 2026 01:57:32 -0700 Subject: [PATCH 34/56] CI: Add CodeQL workflow for GitHub Actions security scanning (#1408) * CI: Add CodeQL workflow for GitHub Actions security scanning * Update .github/workflows/codeql.yml --- .github/workflows/codeql.yml | 54 ++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..a9855cf48 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,54 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '16 4 * * 1' + +permissions: + contents: read + +jobs: + analyze: + name: Analyze Actions + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + + - name: Initialize CodeQL + uses: github/codeql-action/init@c793b717bc78562f491db7b0e93a3a178b099162 # v4 + with: + languages: actions + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@c793b717bc78562f491db7b0e93a3a178b099162 # v4 + with: + category: "/language:actions" From 24994099e41a4e933f883557e2bce1a963bac0ea Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Tue, 31 Mar 2026 14:09:16 -0400 Subject: [PATCH 35/56] ci: update codespell paths (#1469) * Update path so it works well with pre-commit * Prefix path with asterisk so we get matching in both CI and pre-commit * Update paths for codespell --- pyproject.toml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d05a64083..327199d1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -170,12 +170,15 @@ extend-allowed-calls = ["datafusion.lit", "lit"] "docs/*" = ["D"] "docs/source/conf.py" = ["ANN001", "ERA001", "INP001"] +# CI and pre-commit invoke codespell with different paths, so we have a little +# redundancy here, and we intentionally drop python in the path. [tool.codespell] skip = [ - "./python/tests/test_functions.py", - "./target", + "*/tests/test_functions.py", + "*/target", + "./uv.lock", "uv.lock", - "./examples/tpch/answers_sf1/*", + "*/tpch/answers_sf1/*", ] count = true ignore-words-list = ["IST", "ans"] From 0113a6ee55cc61f9ebd897ae8cfc9213f560e468 Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Thu, 2 Apr 2026 17:47:47 -0400 Subject: [PATCH 36/56] Add missing datetime functions (#1467) * Add missing datetime functions: make_time, current_timestamp, date_format Closes #1451. Adds make_time Rust binding and Python wrapper, and adds current_timestamp (alias for now) and date_format (alias for to_char) Python functions. Co-Authored-By: Claude Opus 4.6 (1M context) * Add unit tests for make_time, current_timestamp, and date_format Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- crates/core/src/functions.rs | 2 ++ python/datafusion/functions.py | 36 ++++++++++++++++++++++++++++++++++ python/tests/test_functions.py | 33 +++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/crates/core/src/functions.rs b/crates/core/src/functions.rs index c32134054..6996dca94 100644 --- a/crates/core/src/functions.rs +++ b/crates/core/src/functions.rs @@ -616,6 +616,7 @@ expr_fn!(date_part, part date); expr_fn!(date_trunc, part date); expr_fn!(date_bin, stride source origin); expr_fn!(make_date, year month day); +expr_fn!(make_time, hour minute second); expr_fn!(to_char, datetime format); expr_fn!(translate, string from to, "Replaces each character in string that matches a character in the from set with the corresponding character in the to set. If from is longer than to, occurrences of the extra characters in from are deleted."); @@ -974,6 +975,7 @@ pub(crate) fn init_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(date_part))?; m.add_wrapped(wrap_pyfunction!(date_trunc))?; m.add_wrapped(wrap_pyfunction!(make_date))?; + m.add_wrapped(wrap_pyfunction!(make_time))?; m.add_wrapped(wrap_pyfunction!(digest))?; m.add_wrapped(wrap_pyfunction!(ends_with))?; m.add_wrapped(wrap_pyfunction!(exp))?; diff --git a/python/datafusion/functions.py b/python/datafusion/functions.py index f062cbfce..3c8d2bcee 100644 --- a/python/datafusion/functions.py +++ b/python/datafusion/functions.py @@ -128,7 +128,9 @@ "cume_dist", "current_date", "current_time", + "current_timestamp", "date_bin", + "date_format", "date_part", "date_trunc", "datepart", @@ -200,6 +202,7 @@ "make_array", "make_date", "make_list", + "make_time", "max", "md5", "mean", @@ -1948,6 +1951,15 @@ def now() -> Expr: return Expr(f.now()) +def current_timestamp() -> Expr: + """Returns the current timestamp in nanoseconds. + + See Also: + This is an alias for :py:func:`now`. + """ + return now() + + def to_char(arg: Expr, formatter: Expr) -> Expr: """Returns a string representation of a date, time, timestamp or duration. @@ -1970,6 +1982,15 @@ def to_char(arg: Expr, formatter: Expr) -> Expr: return Expr(f.to_char(arg.expr, formatter.expr)) +def date_format(arg: Expr, formatter: Expr) -> Expr: + """Returns a string representation of a date, time, timestamp or duration. + + See Also: + This is an alias for :py:func:`to_char`. + """ + return to_char(arg, formatter) + + def _unwrap_exprs(args: tuple[Expr, ...]) -> list: return [arg.expr for arg in args] @@ -2270,6 +2291,21 @@ def make_date(year: Expr, month: Expr, day: Expr) -> Expr: return Expr(f.make_date(year.expr, month.expr, day.expr)) +def make_time(hour: Expr, minute: Expr, second: Expr) -> Expr: + """Make a time from hour, minute and second component parts. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"h": [12], "m": [30], "s": [0]}) + >>> result = df.select( + ... dfn.functions.make_time(dfn.col("h"), dfn.col("m"), + ... dfn.col("s")).alias("t")) + >>> result.collect_column("t")[0].as_py() + datetime.time(12, 30) + """ + return Expr(f.make_time(hour.expr, minute.expr, second.expr)) + + def translate(string: Expr, from_val: Expr, to_val: Expr) -> Expr: """Replaces the characters in ``from_val`` with the counterpart in ``to_val``. diff --git a/python/tests/test_functions.py b/python/tests/test_functions.py index 37d349c58..08420826d 100644 --- a/python/tests/test_functions.py +++ b/python/tests/test_functions.py @@ -1107,6 +1107,39 @@ def test_today_alias_matches_current_date(df): assert result.column(0) == result.column(1) +def test_current_timestamp_alias_matches_now(df): + result = df.select( + f.now().alias("now"), + f.current_timestamp().alias("current_timestamp"), + ).collect()[0] + + assert result.column(0) == result.column(1) + + +def test_date_format_alias_matches_to_char(df): + result = df.select( + f.to_char( + f.to_timestamp(literal("2021-01-01T00:00:00")), literal("%Y/%m/%d") + ).alias("to_char"), + f.date_format( + f.to_timestamp(literal("2021-01-01T00:00:00")), literal("%Y/%m/%d") + ).alias("date_format"), + ).collect()[0] + + assert result.column(0) == result.column(1) + assert result.column(0)[0].as_py() == "2021/01/01" + + +def test_make_time(df): + ctx = SessionContext() + df_time = ctx.from_pydict({"h": [12], "m": [30], "s": [0]}) + result = df_time.select( + f.make_time(column("h"), column("m"), column("s")).alias("t") + ).collect()[0] + + assert result.column(0)[0].as_py() == time(12, 30) + + def test_arrow_cast(df): df = df.select( # we use `string_literal` to return utf8 instead of `literal` which returns From be8dd9d08fd284cf1747a2c1b965d9c95fff117c Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Fri, 3 Apr 2026 09:37:00 -0400 Subject: [PATCH 37/56] Add AI skill to check current repository against upstream APIs (#1460) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial commit for skill to check upstream repo * Add instructions on using the check-upstream skill * Add FFI type coverage and implementation pattern to check-upstream skill Document the full FFI type pipeline (Rust PyO3 wrapper → Protocol type → Python wrapper → ABC base class → exports → example) and catalog which upstream datafusion-ffi types are supported, which have been evaluated as not needing direct exposure, and how to check for new gaps. Co-Authored-By: Claude Opus 4.6 (1M context) * Update check-upstream skill to include FFI types as a checkable area Add "ffi types" to the argument-hint and description so users can invoke the skill with `/check-upstream ffi types`. Also add pipeline verification step to ensure each supported FFI type has the full end-to-end chain (PyO3 wrapper, Protocol, Python wrapper with type hints, ABC, exports). Co-Authored-By: Claude Opus 4.6 (1M context) * Move FFI Types section alongside other areas to check Section 7 (FFI Types) was incorrectly placed after the Output Format and Implementation Pattern sections. Move it to sit after Section 6 (SessionContext Methods), consistent with the other checkable areas. Co-Authored-By: Claude Opus 4.6 (1M context) * Replace static FFI type list with dynamic discovery instruction The supported FFI types list would go stale as new types are added. Replace it with a grep instruction to discover them at check time, keeping only the "evaluated and not requiring exposure" list which captures rationale not derivable from code. Co-Authored-By: Claude Opus 4.6 (1M context) * Make Python API the source of truth for upstream coverage checks Functions exposed in Python (e.g., as aliases of other Rust bindings) were being falsely reported as missing because they lacked a dedicated #[pyfunction] in Rust. The user-facing API is the Python layer, so coverage should be measured there. Co-Authored-By: Claude Opus 4.6 (1M context) * Add exclusion list for DataFrame methods already covered by Python API show_limit is covered by DataFrame.show() and with_param_values is covered by SessionContext.sql(param_values=...), so neither needs separate exposure. Co-Authored-By: Claude Opus 4.6 (1M context) * Move skills to .ai/skills/ for tool-agnostic discoverability Moves the canonical skill definitions from .claude/skills/ to .ai/skills/ and replaces .claude/skills with a symlink, so Claude Code still discovers them while other AI agents can find them in a tool-neutral location. Co-Authored-By: Claude Opus 4.6 (1M context) * Add AGENTS.md for tool-agnostic agent instructions with CLAUDE.md symlink AGENTS.md points agents to .ai/skills/ for skill discovery. CLAUDE.md symlinks to it so Claude Code picks it up as project instructions. Co-Authored-By: Claude Opus 4.6 (1M context) * Make README upstream coverage section tool-agnostic Remove Claude Code references and update skill path from .claude/skills/ to .ai/skills/ to match the new tool-neutral directory structure. Co-Authored-By: Claude Opus 4.6 (1M context) * Add GitHub issue lookup step to check-upstream skill When gaps are identified, search open issues at apache/datafusion-python before reporting. Existing issues are linked in the report rather than duplicated. Co-Authored-By: Claude Opus 4.6 (1M context) * Require Python test coverage in issues created by check-upstream skill Co-Authored-By: Claude Opus 4.6 (1M context) * Add license text --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .ai/skills/check-upstream/SKILL.md | 382 +++++++++++++++++++++++++++++ .claude/skills | 1 + AGENTS.md | 27 ++ CLAUDE.md | 1 + README.md | 27 ++ 5 files changed, 438 insertions(+) create mode 100644 .ai/skills/check-upstream/SKILL.md create mode 120000 .claude/skills create mode 100644 AGENTS.md create mode 120000 CLAUDE.md diff --git a/.ai/skills/check-upstream/SKILL.md b/.ai/skills/check-upstream/SKILL.md new file mode 100644 index 000000000..f77210371 --- /dev/null +++ b/.ai/skills/check-upstream/SKILL.md @@ -0,0 +1,382 @@ + + +--- +name: check-upstream +description: Check if upstream Apache DataFusion features (functions, DataFrame ops, SessionContext methods, FFI types) are exposed in this Python project. Use when adding missing functions, auditing API coverage, or ensuring parity with upstream. +argument-hint: [area] (e.g., "scalar functions", "aggregate functions", "window functions", "dataframe", "session context", "ffi types", "all") +--- + +# Check Upstream DataFusion Feature Coverage + +You are auditing the datafusion-python project to find features from the upstream Apache DataFusion Rust library that are **not yet exposed** in this Python binding project. Your goal is to identify gaps and, if asked, implement the missing bindings. + +**IMPORTANT: The Python API is the source of truth for coverage.** A function or method is considered "exposed" if it exists in the Python API (e.g., `python/datafusion/functions.py`), even if there is no corresponding entry in the Rust bindings. Many upstream functions are aliases of other functions — the Python layer can expose these aliases by calling a different underlying Rust binding. Do NOT report a function as missing if it appears in the Python `__all__` list and has a working implementation, regardless of whether a matching `#[pyfunction]` exists in Rust. + +## Areas to Check + +The user may specify an area via `$ARGUMENTS`. If no area is specified or "all" is given, check all areas. + +### 1. Scalar Functions + +**Upstream source of truth:** +- Rust docs: https://docs.rs/datafusion/latest/datafusion/functions/index.html +- User docs: https://datafusion.apache.org/user-guide/sql/scalar_functions.html + +**Where they are exposed in this project:** +- Python API: `python/datafusion/functions.py` — each function wraps a call to `datafusion._internal.functions` +- Rust bindings: `crates/core/src/functions.rs` — `#[pyfunction]` definitions registered via `init_module()` + +**How to check:** +1. Fetch the upstream scalar function documentation page +2. Compare against functions listed in `python/datafusion/functions.py` (check the `__all__` list and function definitions) +3. A function is covered if it exists in the Python API — it does NOT need a dedicated Rust `#[pyfunction]`. Many functions are aliases that reuse another function's Rust binding. +4. Only report functions that are missing from the Python `__all__` list / function definitions + +### 2. Aggregate Functions + +**Upstream source of truth:** +- Rust docs: https://docs.rs/datafusion/latest/datafusion/functions_aggregate/index.html +- User docs: https://datafusion.apache.org/user-guide/sql/aggregate_functions.html + +**Where they are exposed in this project:** +- Python API: `python/datafusion/functions.py` (aggregate functions are mixed in with scalar functions) +- Rust bindings: `crates/core/src/functions.rs` + +**How to check:** +1. Fetch the upstream aggregate function documentation page +2. Compare against aggregate functions in `python/datafusion/functions.py` (check `__all__` list and function definitions) +3. A function is covered if it exists in the Python API, even if it aliases another function's Rust binding +4. Report only functions missing from the Python API + +### 3. Window Functions + +**Upstream source of truth:** +- Rust docs: https://docs.rs/datafusion/latest/datafusion/functions_window/index.html +- User docs: https://datafusion.apache.org/user-guide/sql/window_functions.html + +**Where they are exposed in this project:** +- Python API: `python/datafusion/functions.py` (window functions like `rank`, `dense_rank`, `lag`, `lead`, etc.) +- Rust bindings: `crates/core/src/functions.rs` + +**How to check:** +1. Fetch the upstream window function documentation page +2. Compare against window functions in `python/datafusion/functions.py` (check `__all__` list and function definitions) +3. A function is covered if it exists in the Python API, even if it aliases another function's Rust binding +4. Report only functions missing from the Python API + +### 4. Table Functions + +**Upstream source of truth:** +- Rust docs: https://docs.rs/datafusion/latest/datafusion/functions_table/index.html +- User docs: https://datafusion.apache.org/user-guide/sql/table_functions.html (if available) + +**Where they are exposed in this project:** +- Python API: `python/datafusion/functions.py` and `python/datafusion/user_defined.py` (TableFunction/udtf) +- Rust bindings: `crates/core/src/functions.rs` and `crates/core/src/udtf.rs` + +**How to check:** +1. Fetch the upstream table function documentation +2. Compare against what's available in the Python API +3. A function is covered if it exists in the Python API, even if it aliases another function's Rust binding +4. Report only functions missing from the Python API + +### 5. DataFrame Operations + +**Upstream source of truth:** +- Rust docs: https://docs.rs/datafusion/latest/datafusion/dataframe/struct.DataFrame.html + +**Where they are exposed in this project:** +- Python API: `python/datafusion/dataframe.py` — the `DataFrame` class +- Rust bindings: `crates/core/src/dataframe.rs` — `PyDataFrame` with `#[pymethods]` + +**Evaluated and not requiring separate Python exposure:** +- `show_limit` — already covered by `DataFrame.show()`, which provides the same functionality with a simpler API +- `with_param_values` — already covered by the `param_values` argument on `SessionContext.sql()`, which accomplishes the same thing more robustly + +**How to check:** +1. Fetch the upstream DataFrame documentation page listing all methods +2. Compare against methods in `python/datafusion/dataframe.py` — this is the source of truth for coverage +3. The Rust bindings (`crates/core/src/dataframe.rs`) may be consulted for context, but a method is covered if it exists in the Python API +4. Check against the "evaluated and not requiring exposure" list before flagging as a gap +5. Report only methods missing from the Python API + +### 6. SessionContext Methods + +**Upstream source of truth:** +- Rust docs: https://docs.rs/datafusion/latest/datafusion/execution/context/struct.SessionContext.html + +**Where they are exposed in this project:** +- Python API: `python/datafusion/context.py` — the `SessionContext` class +- Rust bindings: `crates/core/src/context.rs` — `PySessionContext` with `#[pymethods]` + +**How to check:** +1. Fetch the upstream SessionContext documentation page listing all methods +2. Compare against methods in `python/datafusion/context.py` — this is the source of truth for coverage +3. The Rust bindings (`crates/core/src/context.rs`) may be consulted for context, but a method is covered if it exists in the Python API +4. Report only methods missing from the Python API + +### 7. FFI Types (datafusion-ffi) + +**Upstream source of truth:** +- Crate source: https://github.com/apache/datafusion/tree/main/datafusion/ffi/src +- Rust docs: https://docs.rs/datafusion-ffi/latest/datafusion_ffi/ + +**Where they are exposed in this project:** +- Rust bindings: various files under `crates/core/src/` and `crates/util/src/` +- FFI example: `examples/datafusion-ffi-example/src/` +- Dependency declared in root `Cargo.toml` and `crates/core/Cargo.toml` + +**Discovering currently supported FFI types:** +Grep for `use datafusion_ffi::` in `crates/core/src/` and `crates/util/src/` to find all FFI types currently imported and used. + +**Evaluated and not requiring direct Python exposure:** +These upstream FFI types have been reviewed and do not need to be independently exposed to end users: +- `FFI_ExecutionPlan` — already used indirectly through table providers; no need for direct exposure +- `FFI_PhysicalExpr` / `FFI_PhysicalSortExpr` — internal physical planning types not expected to be needed by end users +- `FFI_RecordBatchStream` — one level deeper than FFI_ExecutionPlan, used internally when execution plans stream results +- `FFI_SessionRef` / `ForeignSession` — session sharing across FFI; Python manages sessions natively via SessionContext +- `FFI_SessionConfig` — Python can configure sessions natively without FFI +- `FFI_ConfigOptions` / `FFI_TableOptions` — internal configuration plumbing +- `FFI_PlanProperties` / `FFI_Boundedness` / `FFI_EmissionType` — read from existing plans, not user-facing +- `FFI_Partitioning` — supporting type for physical planning +- Supporting/utility types (`FFI_Option`, `FFI_Result`, `WrappedSchema`, `WrappedArray`, `FFI_ColumnarValue`, `FFI_Volatility`, `FFI_InsertOp`, `FFI_AccumulatorArgs`, `FFI_Accumulator`, `FFI_GroupsAccumulator`, `FFI_EmitTo`, `FFI_AggregateOrderSensitivity`, `FFI_PartitionEvaluator`, `FFI_PartitionEvaluatorArgs`, `FFI_Range`, `FFI_SortOptions`, `FFI_Distribution`, `FFI_ExprProperties`, `FFI_SortProperties`, `FFI_Interval`, `FFI_TableProviderFilterPushDown`, `FFI_TableType`) — used as building blocks within the types above, not independently exposed + +**How to check:** +1. Discover currently supported types by grepping for `use datafusion_ffi::` in `crates/core/src/` and `crates/util/src/`, then compare against the upstream `datafusion-ffi` crate's `lib.rs` exports +2. If new FFI types appear upstream, evaluate whether they represent a user-facing capability +3. Check against the "evaluated and not requiring exposure" list before flagging as a gap +4. Report any genuinely new types that enable user-facing functionality +5. For each currently supported FFI type, verify the full pipeline is present using the checklist from "Adding a New FFI Type": + - Rust PyO3 wrapper with `from_pycapsule()` method + - Python Protocol type (e.g., `ScalarUDFExportable`) for FFI objects + - Python wrapper class with full type hints on all public methods + - ABC base class (if the type can be user-implemented) + - Registered in Rust `init_module()` and Python `__init__.py` + - FFI example in `examples/datafusion-ffi-example/` + - Type appears in union type hints where accepted + +## Checking for Existing GitHub Issues + +After identifying missing APIs, search the open issues at https://github.com/apache/datafusion-python/issues for each gap to see if an issue already exists requesting that API be exposed. Search using the function or method name as the query. + +- If an existing issue is found, include a link to it in the report. Do NOT create a new issue. +- If no existing issue is found, note that no issue exists yet. If the user asks to create issues for missing APIs, each issue should specify that Python test coverage is required as part of the implementation. + +## Output Format + +For each area checked, produce a report like: + +``` +## [Area Name] Coverage Report + +### Currently Exposed (X functions/methods) +- list of what's already available + +### Missing from Upstream (Y functions/methods) +- function_name — brief description of what it does (existing issue: #123) +- function_name — brief description of what it does (no existing issue) + +### Notes +- Any relevant observations about partial implementations, naming differences, etc. +``` + +## Implementation Pattern + +If the user asks you to implement missing features, follow these patterns: + +### Adding a New Function (Scalar/Aggregate/Window) + +**Step 1: Rust binding** in `crates/core/src/functions.rs`: +```rust +#[pyfunction] +#[pyo3(signature = (arg1, arg2))] +fn new_function_name(arg1: PyExpr, arg2: PyExpr) -> PyResult { + Ok(datafusion::functions::module::expr_fn::new_function_name(arg1.expr, arg2.expr).into()) +} +``` +Then register in `init_module()`: +```rust +m.add_wrapped(wrap_pyfunction!(new_function_name))?; +``` + +**Step 2: Python wrapper** in `python/datafusion/functions.py`: +```python +def new_function_name(arg1: Expr, arg2: Expr) -> Expr: + """Description of what the function does. + + Args: + arg1: Description of first argument. + arg2: Description of second argument. + + Returns: + Description of return value. + """ + return Expr(f.new_function_name(arg1.expr, arg2.expr)) +``` +Add to `__all__` list. + +### Adding a New DataFrame Method + +**Step 1: Rust binding** in `crates/core/src/dataframe.rs`: +```rust +#[pymethods] +impl PyDataFrame { + fn new_method(&self, py: Python, param: PyExpr) -> PyDataFusionResult { + let df = self.df.as_ref().clone().new_method(param.into())?; + Ok(Self::new(df)) + } +} +``` + +**Step 2: Python wrapper** in `python/datafusion/dataframe.py`: +```python +def new_method(self, param: Expr) -> DataFrame: + """Description of the method.""" + return DataFrame(self.df.new_method(param.expr)) +``` + +### Adding a New SessionContext Method + +**Step 1: Rust binding** in `crates/core/src/context.rs`: +```rust +#[pymethods] +impl PySessionContext { + pub fn new_method(&self, py: Python, param: String) -> PyDataFusionResult { + let df = wait_for_future(py, self.ctx.new_method(¶m))?; + Ok(PyDataFrame::new(df)) + } +} +``` + +**Step 2: Python wrapper** in `python/datafusion/context.py`: +```python +def new_method(self, param: str) -> DataFrame: + """Description of the method.""" + return DataFrame(self.ctx.new_method(param)) +``` + +### Adding a New FFI Type + +FFI types require a full pipeline from C struct through to a typed Python wrapper. Each layer must be present. + +**Step 1: Rust PyO3 wrapper class** in a new or existing file under `crates/core/src/`: +```rust +use datafusion_ffi::new_type::FFI_NewType; + +#[pyclass(from_py_object, frozen, name = "RawNewType", module = "datafusion.module_name", subclass)] +pub struct PyNewType { + pub inner: Arc, +} + +#[pymethods] +impl PyNewType { + #[staticmethod] + fn from_pycapsule(obj: &Bound<'_, PyAny>) -> PyDataFusionResult { + let capsule = obj + .getattr("__datafusion_new_type__")? + .call0()? + .downcast::()?; + let ffi_ptr = unsafe { capsule.reference::() }; + let provider: Arc = ffi_ptr.into(); + Ok(Self { inner: provider }) + } + + fn some_method(&self) -> PyResult<...> { + // wrap inner trait method + } +} +``` +Register in the appropriate `init_module()`: +```rust +m.add_class::()?; +``` + +**Step 2: Python Protocol type** in the appropriate Python module (e.g., `python/datafusion/catalog.py`): +```python +class NewTypeExportable(Protocol): + """Type hint for objects providing a __datafusion_new_type__ PyCapsule.""" + + def __datafusion_new_type__(self) -> object: ... +``` + +**Step 3: Python wrapper class** in the same module: +```python +class NewType: + """Description of the type. + + This class wraps a DataFusion NewType, which can be created from a native + Python implementation or imported from an FFI-compatible library. + """ + + def __init__( + self, + new_type: df_internal.module_name.RawNewType | NewTypeExportable, + ) -> None: + if isinstance(new_type, df_internal.module_name.RawNewType): + self._raw = new_type + else: + self._raw = df_internal.module_name.RawNewType.from_pycapsule(new_type) + + def some_method(self) -> ReturnType: + """Description of the method.""" + return self._raw.some_method() +``` + +**Step 4: ABC base class** (if users should be able to subclass and provide custom implementations in Python): +```python +from abc import ABC, abstractmethod + +class NewTypeProvider(ABC): + """Abstract base class for implementing a custom NewType in Python.""" + + @abstractmethod + def some_method(self) -> ReturnType: + """Description of the method.""" + ... +``` + +**Step 5: Module exports** — add to the appropriate `__init__.py`: +- Add the wrapper class (`NewType`) to `python/datafusion/__init__.py` +- Add the ABC (`NewTypeProvider`) if applicable +- Add the Protocol type (`NewTypeExportable`) if it should be public + +**Step 6: FFI example** — add an example implementation under `examples/datafusion-ffi-example/src/`: +```rust +// examples/datafusion-ffi-example/src/new_type.rs +use datafusion_ffi::new_type::FFI_NewType; +// ... example showing how an external Rust library exposes this type via PyCapsule +``` + +**Checklist for each FFI type:** +- [ ] Rust PyO3 wrapper with `from_pycapsule()` method +- [ ] Python Protocol type (e.g., `NewTypeExportable`) for FFI objects +- [ ] Python wrapper class with full type hints on all public methods +- [ ] ABC base class (if the type can be user-implemented) +- [ ] Registered in Rust `init_module()` and Python `__init__.py` +- [ ] FFI example in `examples/datafusion-ffi-example/` +- [ ] Type appears in union type hints where accepted (e.g., `Table | TableProviderExportable`) + +## Important Notes + +- The upstream DataFusion version used by this project is specified in `crates/core/Cargo.toml` — check the `datafusion` dependency version to ensure you're comparing against the right upstream version. +- Some upstream features may intentionally not be exposed (e.g., internal-only APIs). Use judgment about what's user-facing. +- When fetching upstream docs, prefer the published docs.rs documentation as it matches the crate version. +- Function aliases (e.g., `array_append` / `list_append`) should both be exposed if upstream supports them. +- Check the `__all__` list in `functions.py` to see what's publicly exported vs just defined. diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 000000000..6838a1160 --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.ai/skills \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..1853a84cd --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,27 @@ + + +# Agent Instructions + +This project uses AI agent skills stored in `.ai/skills/`. Each skill is a directory containing a `SKILL.md` file with instructions for performing a specific task. + +Skills follow the [Agent Skills](https://agentskills.io) open standard. Each skill directory contains: + +- `SKILL.md` — The skill definition with YAML frontmatter (name, description, argument-hint) and detailed instructions. +- Additional supporting files as needed. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 000000000..47dc3e3d8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/README.md b/README.md index c24257876..7c1c71281 100644 --- a/README.md +++ b/README.md @@ -312,6 +312,33 @@ There are scripts in `ci/scripts` for running Rust and Python linters. ./ci/scripts/rust_toml_fmt.sh ``` +## Checking Upstream DataFusion Coverage + +This project includes an [AI agent skill](.ai/skills/check-upstream/SKILL.md) for auditing which +features from the upstream Apache DataFusion Rust library are not yet exposed in these Python +bindings. This is useful when adding missing functions, auditing API coverage, or ensuring parity +with upstream. + +The skill accepts an optional area argument: + +``` +scalar functions +aggregate functions +window functions +dataframe +session context +ffi types +all +``` + +If no argument is provided, it defaults to checking all areas. The skill will fetch the upstream +DataFusion documentation, compare it against the functions and methods exposed in this project, and +produce a coverage report listing what is currently exposed and what is missing. + +The skill definition lives in `.ai/skills/check-upstream/SKILL.md` and follows the +[Agent Skills](https://agentskills.io) open standard. It can be used by any AI coding agent that +supports skill discovery, or followed manually. + ## How to update dependencies To change test dependencies, change the `pyproject.toml` and run From 645d261ce3bc0b3b610c8d82422042b3e573e793 Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Fri, 3 Apr 2026 13:51:43 -0400 Subject: [PATCH 38/56] Add missing string function `contains` (#1465) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add missing `contains` string function Expose the upstream DataFusion `contains(string, search_str)` function which returns true if search_str is found within string (case-sensitive). Note: the other functions from #1450 (instr, position, substring_index) already exist — instr and position are aliases for strpos, and substring_index is exposed as substr_index. Closes #1450 Co-Authored-By: Claude Opus 4.6 (1M context) * Add unit test for contains string function Co-Authored-By: Claude Opus 4.6 (1M context) * Update python/datafusion/functions.py Co-authored-by: Nuno Faria --------- Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Nuno Faria --- crates/core/src/functions.rs | 6 ++++++ python/datafusion/functions.py | 15 +++++++++++++++ python/tests/test_functions.py | 1 + 3 files changed, 22 insertions(+) diff --git a/crates/core/src/functions.rs b/crates/core/src/functions.rs index 6996dca94..fefe14b3e 100644 --- a/crates/core/src/functions.rs +++ b/crates/core/src/functions.rs @@ -494,6 +494,11 @@ expr_fn!(length, string); expr_fn!(char_length, string); expr_fn!(chr, arg, "Returns the character with the given code."); expr_fn_vec!(coalesce); +expr_fn!( + contains, + string search_str, + "Return true if search_str is found within string (case-sensitive)." +); expr_fn!(cos, num); expr_fn!(cosh, num); expr_fn!(cot, num); @@ -961,6 +966,7 @@ pub(crate) fn init_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(col))?; m.add_wrapped(wrap_pyfunction!(concat_ws))?; m.add_wrapped(wrap_pyfunction!(concat))?; + m.add_wrapped(wrap_pyfunction!(contains))?; m.add_wrapped(wrap_pyfunction!(corr))?; m.add_wrapped(wrap_pyfunction!(cos))?; m.add_wrapped(wrap_pyfunction!(cosh))?; diff --git a/python/datafusion/functions.py b/python/datafusion/functions.py index 3c8d2bcee..2ef2f0473 100644 --- a/python/datafusion/functions.py +++ b/python/datafusion/functions.py @@ -116,6 +116,7 @@ "col", "concat", "concat_ws", + "contains", "corr", "cos", "cosh", @@ -439,6 +440,20 @@ def digest(value: Expr, method: Expr) -> Expr: return Expr(f.digest(value.expr, method.expr)) +def contains(string: Expr, search_str: Expr) -> Expr: + """Returns true if ``search_str`` is found within ``string`` (case-sensitive). + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["the quick brown fox"]}) + >>> result = df.select( + ... dfn.functions.contains(dfn.col("a"), dfn.lit("brown")).alias("c")) + >>> result.collect_column("c")[0].as_py() + True + """ + return Expr(f.contains(string.expr, search_str.expr)) + + def concat(*args: Expr) -> Expr: """Concatenates the text representations of all the arguments. diff --git a/python/tests/test_functions.py b/python/tests/test_functions.py index 08420826d..db141fbe0 100644 --- a/python/tests/test_functions.py +++ b/python/tests/test_functions.py @@ -745,6 +745,7 @@ def test_array_function_obj_tests(stmt, py_expr): f.split_part(column("a"), literal("l"), literal(1)), pa.array(["He", "Wor", "!"]), ), + (f.contains(column("a"), literal("ell")), pa.array([True, False, False])), (f.starts_with(column("a"), literal("Wor")), pa.array([False, True, False])), (f.strpos(column("a"), literal("o")), pa.array([5, 2, 0], type=pa.int32())), ( From 0b6ea95a3d304a774bbe512bb70fbca332aa5426 Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Fri, 3 Apr 2026 15:43:28 -0400 Subject: [PATCH 39/56] Add missing conditional functions (#1464) * Add missing conditional functions: greatest, least, nvl2, ifnull (#1449) Expose four conditional functions from upstream DataFusion that were not yet available in the Python bindings. Co-Authored-By: Claude Opus 4.6 (1M context) * Add unit tests for greatest, least, nvl2, and ifnull functions Tests cover multiple data types (integers, strings), null handling (all-null, partial-null), multiple arguments, and ifnull/nvl equivalence. Co-Authored-By: Claude Opus 4.6 (1M context) * Use standard alias docstring pattern for ifnull Co-Authored-By: Claude Opus 4.6 (1M context) * remove unused df fixture and fix parameter shadowing * Refactor conditional function tests into parametrized test suite Replace separate test functions for coalesce, greatest, least, nvl, nvl2, ifnull with a single parametrized test using a shared fixture. Adds coverage for nvl, nullif (previously untested), datetime and boolean types, literal fallbacks, and variadic calls. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- crates/core/src/functions.rs | 10 ++ python/datafusion/functions.py | 69 ++++++++ python/tests/test_functions.py | 289 +++++++++++++++++++++++++++------ 3 files changed, 319 insertions(+), 49 deletions(-) diff --git a/crates/core/src/functions.rs b/crates/core/src/functions.rs index fefe14b3e..3f07da95b 100644 --- a/crates/core/src/functions.rs +++ b/crates/core/src/functions.rs @@ -494,6 +494,8 @@ expr_fn!(length, string); expr_fn!(char_length, string); expr_fn!(chr, arg, "Returns the character with the given code."); expr_fn_vec!(coalesce); +expr_fn_vec!(greatest); +expr_fn_vec!(least); expr_fn!( contains, string search_str, @@ -548,6 +550,11 @@ expr_fn!( x y, "Returns x if x is not NULL otherwise returns y." ); +expr_fn!( + nvl2, + x y z, + "Returns y if x is not NULL; otherwise returns z." +); expr_fn!(nullif, arg_1 arg_2); expr_fn!( octet_length, @@ -989,6 +996,7 @@ pub(crate) fn init_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(floor))?; m.add_wrapped(wrap_pyfunction!(from_unixtime))?; m.add_wrapped(wrap_pyfunction!(gcd))?; + m.add_wrapped(wrap_pyfunction!(greatest))?; // m.add_wrapped(wrap_pyfunction!(grouping))?; m.add_wrapped(wrap_pyfunction!(in_list))?; m.add_wrapped(wrap_pyfunction!(initcap))?; @@ -996,6 +1004,7 @@ pub(crate) fn init_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(iszero))?; m.add_wrapped(wrap_pyfunction!(levenshtein))?; m.add_wrapped(wrap_pyfunction!(lcm))?; + m.add_wrapped(wrap_pyfunction!(least))?; m.add_wrapped(wrap_pyfunction!(left))?; m.add_wrapped(wrap_pyfunction!(length))?; m.add_wrapped(wrap_pyfunction!(ln))?; @@ -1013,6 +1022,7 @@ pub(crate) fn init_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(named_struct))?; m.add_wrapped(wrap_pyfunction!(nanvl))?; m.add_wrapped(wrap_pyfunction!(nvl))?; + m.add_wrapped(wrap_pyfunction!(nvl2))?; m.add_wrapped(wrap_pyfunction!(now))?; m.add_wrapped(wrap_pyfunction!(nullif))?; m.add_wrapped(wrap_pyfunction!(octet_length))?; diff --git a/python/datafusion/functions.py b/python/datafusion/functions.py index 2ef2f0473..f1ea3d256 100644 --- a/python/datafusion/functions.py +++ b/python/datafusion/functions.py @@ -152,6 +152,8 @@ "floor", "from_unixtime", "gcd", + "greatest", + "ifnull", "in_list", "initcap", "isnan", @@ -160,6 +162,7 @@ "last_value", "lcm", "lead", + "least", "left", "length", "levenshtein", @@ -216,6 +219,7 @@ "ntile", "nullif", "nvl", + "nvl2", "octet_length", "order_by", "overlay", @@ -1045,6 +1049,34 @@ def gcd(x: Expr, y: Expr) -> Expr: return Expr(f.gcd(x.expr, y.expr)) +def greatest(*args: Expr) -> Expr: + """Returns the greatest value from a list of expressions. + + Returns NULL if all expressions are NULL. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 3], "b": [2, 1]}) + >>> result = df.select( + ... dfn.functions.greatest(dfn.col("a"), dfn.col("b")).alias("greatest")) + >>> result.collect_column("greatest")[0].as_py() + 2 + >>> result.collect_column("greatest")[1].as_py() + 3 + """ + exprs = [arg.expr for arg in args] + return Expr(f.greatest(*exprs)) + + +def ifnull(x: Expr, y: Expr) -> Expr: + """Returns ``x`` if ``x`` is not NULL. Otherwise returns ``y``. + + See Also: + This is an alias for :py:func:`nvl`. + """ + return nvl(x, y) + + def initcap(string: Expr) -> Expr: """Set the initial letter of each word to capital. @@ -1098,6 +1130,25 @@ def lcm(x: Expr, y: Expr) -> Expr: return Expr(f.lcm(x.expr, y.expr)) +def least(*args: Expr) -> Expr: + """Returns the least value from a list of expressions. + + Returns NULL if all expressions are NULL. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 3], "b": [2, 1]}) + >>> result = df.select( + ... dfn.functions.least(dfn.col("a"), dfn.col("b")).alias("least")) + >>> result.collect_column("least")[0].as_py() + 1 + >>> result.collect_column("least")[1].as_py() + 1 + """ + exprs = [arg.expr for arg in args] + return Expr(f.least(*exprs)) + + def left(string: Expr, n: Expr) -> Expr: """Returns the first ``n`` characters in the ``string``. @@ -1282,6 +1333,24 @@ def nvl(x: Expr, y: Expr) -> Expr: return Expr(f.nvl(x.expr, y.expr)) +def nvl2(x: Expr, y: Expr, z: Expr) -> Expr: + """Returns ``y`` if ``x`` is not NULL. Otherwise returns ``z``. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [None, 1], "b": [10, 20], "c": [30, 40]}) + >>> result = df.select( + ... dfn.functions.nvl2( + ... dfn.col("a"), dfn.col("b"), dfn.col("c")).alias("nvl2") + ... ) + >>> result.collect_column("nvl2")[0].as_py() + 30 + >>> result.collect_column("nvl2")[1].as_py() + 20 + """ + return Expr(f.nvl2(x.expr, y.expr, z.expr)) + + def octet_length(arg: Expr) -> Expr: """Returns the number of bytes of a string. diff --git a/python/tests/test_functions.py b/python/tests/test_functions.py index db141fbe0..74fcbffb4 100644 --- a/python/tests/test_functions.py +++ b/python/tests/test_functions.py @@ -1410,62 +1410,253 @@ def test_alias_with_metadata(df): assert df.schema().field("b").metadata == {b"key": b"value"} -def test_coalesce(df): - # Create a DataFrame with null values +@pytest.fixture +def df_with_nulls(): ctx = SessionContext() + # Rows: + # 0: both values present + # 1: a/d/h/k null, b/e/i/l present + # 2: a/d/h/k present, b/e/i/l null + # 3: all null batch = pa.RecordBatch.from_arrays( [ - pa.array(["Hello", None, "!"]), # string column with null - pa.array([4, None, 6]), # integer column with null - pa.array(["hello ", None, " !"]), # string column with null + pa.array([1, None, 3, None], type=pa.int64()), + pa.array([5, 10, None, None], type=pa.int64()), + pa.array([20, 30, 40, None], type=pa.int64()), + pa.array(["apple", None, "cherry", None], type=pa.utf8()), + pa.array(["banana", "date", None, None], type=pa.utf8()), + pa.array(["x", "y", "z", None], type=pa.utf8()), pa.array( [ - datetime(2022, 12, 31, tzinfo=DEFAULT_TZ), + datetime(2020, 1, 1, tzinfo=DEFAULT_TZ), None, - datetime(2020, 7, 2, tzinfo=DEFAULT_TZ), - ] - ), # datetime with null - pa.array([False, None, True]), # boolean column with null + datetime(2025, 6, 15, tzinfo=DEFAULT_TZ), + None, + ], + type=pa.timestamp("us", tz="UTC"), + ), + pa.array( + [ + datetime(2022, 7, 4, tzinfo=DEFAULT_TZ), + datetime(2023, 12, 25, tzinfo=DEFAULT_TZ), + None, + None, + ], + type=pa.timestamp("us", tz="UTC"), + ), + pa.array([True, None, False, None], type=pa.bool_()), + pa.array([False, True, None, None], type=pa.bool_()), ], - names=["a", "b", "c", "d", "e"], - ) - df_with_nulls = ctx.create_dataframe([[batch]]) - - # Test coalesce with different data types - result_df = df_with_nulls.select( - f.coalesce(column("a"), literal("default")).alias("a_coalesced"), - f.coalesce(column("b"), literal(0)).alias("b_coalesced"), - f.coalesce(column("c"), literal("default")).alias("c_coalesced"), - f.coalesce(column("d"), literal(datetime(2000, 1, 1, tzinfo=DEFAULT_TZ))).alias( - "d_coalesced" - ), - f.coalesce(column("e"), literal(value=False)).alias("e_coalesced"), + names=["a", "b", "c", "d", "e", "g", "h", "i", "k", "l"], ) + return ctx.create_dataframe([[batch]]) - result = result_df.collect()[0] - # Verify results - assert result.column(0) == pa.array( - ["Hello", "default", "!"], type=pa.string_view() - ) - assert result.column(1) == pa.array([4, 0, 6], type=pa.int64()) - assert result.column(2) == pa.array( - ["hello ", "default", " !"], type=pa.string_view() - ) - assert result.column(3).to_pylist() == [ - datetime(2022, 12, 31, tzinfo=DEFAULT_TZ), - datetime(2000, 1, 1, tzinfo=DEFAULT_TZ), - datetime(2020, 7, 2, tzinfo=DEFAULT_TZ), - ] - assert result.column(4) == pa.array([False, False, True], type=pa.bool_()) - - # Test multiple arguments - result_df = df_with_nulls.select( - f.coalesce(column("a"), literal(None), literal("fallback")).alias( - "multi_coalesce" - ) - ) - result = result_df.collect()[0] - assert result.column(0) == pa.array( - ["Hello", "fallback", "!"], type=pa.string_view() - ) +@pytest.mark.parametrize( + ("expr", "expected"), + [ + pytest.param( + f.greatest(column("a"), column("b")), + pa.array([5, 10, 3, None], type=pa.int64()), + id="greatest_int", + ), + pytest.param( + f.greatest(column("d"), column("e")), + pa.array(["banana", "date", "cherry", None], type=pa.utf8()), + id="greatest_str", + ), + pytest.param( + f.least(column("a"), column("b")), + pa.array([1, 10, 3, None], type=pa.int64()), + id="least_int", + ), + pytest.param( + f.least(column("d"), column("e")), + pa.array(["apple", "date", "cherry", None], type=pa.utf8()), + id="least_str", + ), + pytest.param( + f.coalesce(column("a"), column("b"), column("c")), + pa.array([1, 10, 3, None], type=pa.int64()), + id="coalesce_int", + ), + pytest.param( + f.coalesce(column("d"), column("e"), column("g")), + pa.array(["apple", "date", "cherry", None], type=pa.utf8()), + id="coalesce_str", + ), + pytest.param( + f.nvl(column("a"), column("c")), + pa.array([1, 30, 3, None], type=pa.int64()), + id="nvl_int", + ), + pytest.param( + f.nvl(column("d"), column("g")), + pa.array(["apple", "y", "cherry", None], type=pa.utf8()), + id="nvl_str", + ), + pytest.param( + f.ifnull(column("a"), column("c")), + pa.array([1, 30, 3, None], type=pa.int64()), + id="ifnull_int", + ), + pytest.param( + f.ifnull(column("d"), column("g")), + pa.array(["apple", "y", "cherry", None], type=pa.utf8()), + id="ifnull_str", + ), + pytest.param( + f.nvl2(column("a"), column("b"), column("c")), + pa.array([5, 30, None, None], type=pa.int64()), + id="nvl2_int", + ), + pytest.param( + f.nvl2(column("d"), column("e"), column("g")), + pa.array(["banana", "y", None, None], type=pa.utf8()), + id="nvl2_str", + ), + pytest.param( + f.nullif(column("a"), column("b")), + pa.array([1, None, 3, None], type=pa.int64()), + id="nullif_int", + ), + pytest.param( + f.nullif(column("d"), column("e")), + pa.array(["apple", None, "cherry", None], type=pa.utf8()), + id="nullif_str", + ), + pytest.param( + f.nullif(column("a"), literal(1)), + pa.array([None, None, 3, None], type=pa.int64()), + id="nullif_equal_values", + ), + pytest.param( + f.greatest(column("a"), column("b"), column("c")), + pa.array([20, 30, 40, None], type=pa.int64()), + id="greatest_variadic", + ), + pytest.param( + f.least(column("a"), column("b"), column("c")), + pa.array([1, 10, 3, None], type=pa.int64()), + id="least_variadic", + ), + pytest.param( + f.greatest(column("a"), literal(2)), + pa.array([2, 2, 3, 2], type=pa.int64()), + id="greatest_literal", + ), + pytest.param( + f.least(column("a"), literal(2)), + pa.array([1, 2, 2, 2], type=pa.int64()), + id="least_literal", + ), + pytest.param( + f.coalesce(column("a"), literal(0)), + pa.array([1, 0, 3, 0], type=pa.int64()), + id="coalesce_literal_int", + ), + pytest.param( + f.coalesce(column("d"), literal("default")), + pa.array(["apple", "default", "cherry", "default"], type=pa.string_view()), + id="coalesce_literal_str", + ), + pytest.param( + f.nvl(column("a"), literal(99)), + pa.array([1, 99, 3, 99], type=pa.int64()), + id="nvl_literal", + ), + pytest.param( + f.ifnull(column("d"), literal("unknown")), + pa.array(["apple", "unknown", "cherry", "unknown"], type=pa.string_view()), + id="ifnull_literal", + ), + pytest.param( + f.nvl2(column("a"), literal(1), literal(0)), + pa.array([1, 0, 1, 0], type=pa.int64()), + id="nvl2_literal", + ), + pytest.param( + f.greatest(column("h"), column("i")), + pa.array( + [ + datetime(2022, 7, 4, tzinfo=DEFAULT_TZ), + datetime(2023, 12, 25, tzinfo=DEFAULT_TZ), + datetime(2025, 6, 15, tzinfo=DEFAULT_TZ), + None, + ], + type=pa.timestamp("us", tz="UTC"), + ), + id="greatest_datetime", + ), + pytest.param( + f.least(column("h"), column("i")), + pa.array( + [ + datetime(2020, 1, 1, tzinfo=DEFAULT_TZ), + datetime(2023, 12, 25, tzinfo=DEFAULT_TZ), + datetime(2025, 6, 15, tzinfo=DEFAULT_TZ), + None, + ], + type=pa.timestamp("us", tz="UTC"), + ), + id="least_datetime", + ), + pytest.param( + f.coalesce(column("h"), column("i")), + pa.array( + [ + datetime(2020, 1, 1, tzinfo=DEFAULT_TZ), + datetime(2023, 12, 25, tzinfo=DEFAULT_TZ), + datetime(2025, 6, 15, tzinfo=DEFAULT_TZ), + None, + ], + type=pa.timestamp("us", tz="UTC"), + ), + id="coalesce_datetime", + ), + pytest.param( + f.nvl(column("k"), column("l")), + pa.array([True, True, False, None], type=pa.bool_()), + id="nvl_bool", + ), + pytest.param( + f.coalesce(column("k"), column("l")), + pa.array([True, True, False, None], type=pa.bool_()), + id="coalesce_bool", + ), + pytest.param( + f.nvl2(column("k"), column("k"), column("l")), + pa.array([True, True, False, None], type=pa.bool_()), + id="nvl2_bool", + ), + pytest.param( + f.coalesce( + column("h"), + literal(datetime(2000, 1, 1, tzinfo=DEFAULT_TZ)), + ), + pa.array( + [ + datetime(2020, 1, 1, tzinfo=DEFAULT_TZ), + datetime(2000, 1, 1, tzinfo=DEFAULT_TZ), + datetime(2025, 6, 15, tzinfo=DEFAULT_TZ), + datetime(2000, 1, 1, tzinfo=DEFAULT_TZ), + ], + type=pa.timestamp("us", tz="UTC"), + ), + id="coalesce_literal_datetime", + ), + pytest.param( + f.coalesce(column("k"), literal(value=False)), + pa.array([True, False, False, False], type=pa.bool_()), + id="coalesce_literal_bool", + ), + pytest.param( + f.coalesce(column("a"), literal(None), literal(99)), + pa.array([1, 99, 3, 99], type=pa.int64()), + id="coalesce_skip_null_literal", + ), + ], +) +def test_conditional_functions(df_with_nulls, expr, expected): + result = df_with_nulls.select(expr.alias("result")).collect()[0] + assert result.column(0) == expected From 16feeb136737ae45fac39f7a82cca2d88fd6224b Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Fri, 3 Apr 2026 12:47:31 -0700 Subject: [PATCH 40/56] Reduce peak memory usage during release builds to fix OOM on manylinux runners (#1445) * adjust swap to 8gb * modify profile.release --- .github/workflows/build.yml | 15 ++++++++++++++- Cargo.toml | 4 ++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4b37046ee..7682d6cb0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -159,6 +159,19 @@ jobs: with: enable-cache: true + - name: Add extra swap for release build + if: inputs.build_mode == 'release' + run: | + set -euxo pipefail + sudo swapoff -a || true + sudo rm -f /swapfile + sudo fallocate -l 8G /swapfile || sudo dd if=/dev/zero of=/swapfile bs=1M count=8192 + sudo chmod 600 /swapfile + sudo mkswap /swapfile + sudo swapon /swapfile + free -h + swapon --show + - name: Build (release mode) uses: PyO3/maturin-action@v1 if: inputs.build_mode == 'release' @@ -233,7 +246,7 @@ jobs: set -euxo pipefail sudo swapoff -a || true sudo rm -f /swapfile - sudo fallocate -l 16G /swapfile || sudo dd if=/dev/zero of=/swapfile bs=1M count=16384 + sudo fallocate -l 8G /swapfile || sudo dd if=/dev/zero of=/swapfile bs=1M count=8192 sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile diff --git a/Cargo.toml b/Cargo.toml index 346f6da3e..3a34e204c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,8 +64,8 @@ pyo3-build-config = "0.28" datafusion-python-util = { path = "crates/util" } [profile.release] -lto = true -codegen-units = 1 +lto = "thin" +codegen-units = 2 # We cannot publish to crates.io with any patches in the below section. Developers # must remove any entries in this section before creating a release candidate. From 8a35caea9ed01492742738f161fa5b4459d69402 Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Sat, 4 Apr 2026 12:20:31 -0400 Subject: [PATCH 41/56] Add missing map functions (#1461) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add map functions (make_map, map_keys, map_values, map_extract, map_entries, element_at) Closes #1448 Co-Authored-By: Claude Opus 4.6 (1M context) * Add unit tests for map functions Co-Authored-By: Claude Opus 4.6 (1M context) * Remove redundant pyo3 element_at function element_at is already a Python-only alias for map_extract, so the Rust binding is unnecessary. Co-Authored-By: Claude Opus 4.6 (1M context) * Change make_map to accept a Python dictionary make_map now takes a dict for the common case and also supports separate keys/values lists for column expressions. Non-Expr keys and values are automatically converted to literals. Co-Authored-By: Claude Opus 4.6 (1M context) * Make map the primary function with make_map as alias map() now supports three calling conventions matching upstream: - map({"a": 1, "b": 2}) — from a Python dictionary - map([keys], [values]) — two lists that get zipped - map(k1, v1, k2, v2, ...) — variadic key-value pairs Non-Expr keys and values are automatically converted to literals. Co-Authored-By: Claude Opus 4.6 (1M context) * Improve map function docstrings - Add examples for all three map() calling conventions - Use clearer descriptions instead of jargon (no "zipped" or "variadic") - Break map_keys/map_values/map_extract/map_entries examples into two steps: create the map column first, then call the function Co-Authored-By: Claude Opus 4.6 (1M context) * Remove map() in favor of make_map(), fix docstrings, add validation - Remove map() function that shadowed Python builtin; make_map() is now the sole entry point for creating map expressions - Fix map_extract/element_at docstrings: missing keys return [None], not an empty list (matches actual upstream behavior) - Add length validation for the two-list calling convention - Update all tests and docstring examples accordingly Co-Authored-By: Claude Opus 4.6 (1M context) * Consolidate map function tests into parametrized groups Reduce boilerplate by combining make_map construction tests and map accessor function tests into two @pytest.mark.parametrize groups. Co-Authored-By: Claude Opus 4.6 (1M context) * Docstring update Co-authored-by: Nuno Faria * Docstring update Co-authored-by: Nuno Faria * Simplify test for readability Co-authored-by: Nuno Faria * Simplify test for readability Co-authored-by: Nuno Faria --------- Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Nuno Faria --- crates/core/src/functions.rs | 20 +++++ python/datafusion/functions.py | 158 +++++++++++++++++++++++++++++++++ python/tests/test_functions.py | 100 +++++++++++++++++++++ 3 files changed, 278 insertions(+) diff --git a/crates/core/src/functions.rs b/crates/core/src/functions.rs index 3f07da95b..5e61b71be 100644 --- a/crates/core/src/functions.rs +++ b/crates/core/src/functions.rs @@ -93,6 +93,13 @@ fn array_cat(exprs: Vec) -> PyExpr { array_concat(exprs) } +#[pyfunction] +fn make_map(keys: Vec, values: Vec) -> PyExpr { + let keys = keys.into_iter().map(|x| x.into()).collect(); + let values = values.into_iter().map(|x| x.into()).collect(); + datafusion::functions_nested::map::map(keys, values).into() +} + #[pyfunction] #[pyo3(signature = (array, element, index=None))] fn array_position(array: PyExpr, element: PyExpr, index: Option) -> PyExpr { @@ -678,6 +685,12 @@ array_fn!(cardinality, array); array_fn!(flatten, array); array_fn!(range, start stop step); +// Map Functions +array_fn!(map_keys, map); +array_fn!(map_values, map); +array_fn!(map_extract, map key); +array_fn!(map_entries, map); + aggregate_function!(array_agg); aggregate_function!(max); aggregate_function!(min); @@ -1142,6 +1155,13 @@ pub(crate) fn init_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(flatten))?; m.add_wrapped(wrap_pyfunction!(cardinality))?; + // Map Functions + m.add_wrapped(wrap_pyfunction!(make_map))?; + m.add_wrapped(wrap_pyfunction!(map_keys))?; + m.add_wrapped(wrap_pyfunction!(map_values))?; + m.add_wrapped(wrap_pyfunction!(map_extract))?; + m.add_wrapped(wrap_pyfunction!(map_entries))?; + // Window Functions m.add_wrapped(wrap_pyfunction!(lead))?; m.add_wrapped(wrap_pyfunction!(lag))?; diff --git a/python/datafusion/functions.py b/python/datafusion/functions.py index f1ea3d256..3febb44e3 100644 --- a/python/datafusion/functions.py +++ b/python/datafusion/functions.py @@ -140,6 +140,7 @@ "degrees", "dense_rank", "digest", + "element_at", "empty", "encode", "ends_with", @@ -206,7 +207,12 @@ "make_array", "make_date", "make_list", + "make_map", "make_time", + "map_entries", + "map_extract", + "map_keys", + "map_values", "max", "md5", "mean", @@ -3458,6 +3464,158 @@ def empty(array: Expr) -> Expr: return array_empty(array) +# map functions + + +def make_map(*args: Any) -> Expr: + """Returns a map expression. + + Supports three calling conventions: + + - ``make_map({"a": 1, "b": 2})`` — from a Python dictionary. + - ``make_map([keys], [values])`` — from a list of keys and a list of + their associated values. Both lists must be the same length. + - ``make_map(k1, v1, k2, v2, ...)`` — from alternating keys and their + associated values. + + Keys and values that are not already :py:class:`~datafusion.expr.Expr` + are automatically converted to literal expressions. + + Examples: + From a dictionary: + + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1]}) + >>> result = df.select( + ... dfn.functions.make_map({"a": 1, "b": 2}).alias("m")) + >>> result.collect_column("m")[0].as_py() + [('a', 1), ('b', 2)] + + From two lists: + + >>> df = ctx.from_pydict({"key": ["x", "y"], "val": [10, 20]}) + >>> df = df.select( + ... dfn.functions.make_map( + ... [dfn.col("key")], [dfn.col("val")] + ... ).alias("m")) + >>> df.collect_column("m")[0].as_py() + [('x', 10)] + + From alternating keys and values: + + >>> df = ctx.from_pydict({"a": [1]}) + >>> result = df.select( + ... dfn.functions.make_map("x", 1, "y", 2).alias("m")) + >>> result.collect_column("m")[0].as_py() + [('x', 1), ('y', 2)] + """ + if len(args) == 1 and isinstance(args[0], dict): + key_list = list(args[0].keys()) + value_list = list(args[0].values()) + elif ( + len(args) == 2 # noqa: PLR2004 + and isinstance(args[0], list) + and isinstance(args[1], list) + ): + if len(args[0]) != len(args[1]): + msg = "make_map requires key and value lists to be the same length" + raise ValueError(msg) + key_list = args[0] + value_list = args[1] + elif len(args) >= 2 and len(args) % 2 == 0: # noqa: PLR2004 + key_list = list(args[0::2]) + value_list = list(args[1::2]) + else: + msg = ( + "make_map expects a dict, two lists, or an even number of " + "key-value arguments" + ) + raise ValueError(msg) + + key_exprs = [k if isinstance(k, Expr) else Expr.literal(k) for k in key_list] + val_exprs = [v if isinstance(v, Expr) else Expr.literal(v) for v in value_list] + return Expr(f.make_map([k.expr for k in key_exprs], [v.expr for v in val_exprs])) + + +def map_keys(map: Expr) -> Expr: + """Returns a list of all keys in the map. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1]}) + >>> df = df.select( + ... dfn.functions.make_map({"x": 1, "y": 2}).alias("m")) + >>> result = df.select( + ... dfn.functions.map_keys(dfn.col("m")).alias("keys")) + >>> result.collect_column("keys")[0].as_py() + ['x', 'y'] + """ + return Expr(f.map_keys(map.expr)) + + +def map_values(map: Expr) -> Expr: + """Returns a list of all values in the map. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1]}) + >>> df = df.select( + ... dfn.functions.make_map({"x": 1, "y": 2}).alias("m")) + >>> result = df.select( + ... dfn.functions.map_values(dfn.col("m")).alias("vals")) + >>> result.collect_column("vals")[0].as_py() + [1, 2] + """ + return Expr(f.map_values(map.expr)) + + +def map_extract(map: Expr, key: Expr) -> Expr: + """Returns the value for a given key in the map. + + Returns ``[None]`` if the key is absent. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1]}) + >>> df = df.select( + ... dfn.functions.make_map({"x": 1, "y": 2}).alias("m")) + >>> result = df.select( + ... dfn.functions.map_extract( + ... dfn.col("m"), dfn.lit("x") + ... ).alias("val")) + >>> result.collect_column("val")[0].as_py() + [1] + """ + return Expr(f.map_extract(map.expr, key.expr)) + + +def map_entries(map: Expr) -> Expr: + """Returns a list of all entries (key-value struct pairs) in the map. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1]}) + >>> df = df.select( + ... dfn.functions.make_map({"x": 1, "y": 2}).alias("m")) + >>> result = df.select( + ... dfn.functions.map_entries(dfn.col("m")).alias("entries")) + >>> result.collect_column("entries")[0].as_py() + [{'key': 'x', 'value': 1}, {'key': 'y', 'value': 2}] + """ + return Expr(f.map_entries(map.expr)) + + +def element_at(map: Expr, key: Expr) -> Expr: + """Returns the value for a given key in the map. + + Returns ``[None]`` if the key is absent. + + See Also: + This is an alias for :py:func:`map_extract`. + """ + return map_extract(map, key) + + # aggregate functions def approx_distinct( expression: Expr, diff --git a/python/tests/test_functions.py b/python/tests/test_functions.py index 74fcbffb4..f25c6e78c 100644 --- a/python/tests/test_functions.py +++ b/python/tests/test_functions.py @@ -668,6 +668,106 @@ def test_array_function_obj_tests(stmt, py_expr): assert a == b +@pytest.mark.parametrize( + ("args", "expected"), + [ + pytest.param( + ({"x": 1, "y": 2},), + [("x", 1), ("y", 2)], + id="dict", + ), + pytest.param( + ({"x": literal(1), "y": literal(2)},), + [("x", 1), ("y", 2)], + id="dict_with_exprs", + ), + pytest.param( + ("x", 1, "y", 2), + [("x", 1), ("y", 2)], + id="variadic_pairs", + ), + pytest.param( + (literal("x"), literal(1), literal("y"), literal(2)), + [("x", 1), ("y", 2)], + id="variadic_with_exprs", + ), + ], +) +def test_make_map(args, expected): + ctx = SessionContext() + batch = pa.RecordBatch.from_arrays([pa.array([1])], names=["a"]) + df = ctx.create_dataframe([[batch]]) + + result = df.select(f.make_map(*args).alias("m")).collect()[0].column(0) + assert result[0].as_py() == expected + + +def test_make_map_from_two_lists(): + ctx = SessionContext() + batch = pa.RecordBatch.from_arrays( + [ + pa.array(["k1", "k2", "k3"]), + pa.array([10, 20, 30]), + ], + names=["keys", "vals"], + ) + df = ctx.create_dataframe([[batch]]) + + m = f.make_map([column("keys")], [column("vals")]) + result = df.select(f.map_keys(m).alias("k")).collect()[0].column(0) + assert result.to_pylist() == [["k1"], ["k2"], ["k3"]] + + result = df.select(f.map_values(m).alias("v")).collect()[0].column(0) + assert result.to_pylist() == [[10], [20], [30]] + + +def test_make_map_odd_args_raises(): + with pytest.raises(ValueError, match="make_map expects"): + f.make_map("x", 1, "y") + + +def test_make_map_mismatched_lengths(): + with pytest.raises(ValueError, match="same length"): + f.make_map(["a", "b"], [1]) + + +@pytest.mark.parametrize( + ("func", "expected"), + [ + pytest.param(f.map_keys, ["x", "y"], id="map_keys"), + pytest.param(f.map_values, [1, 2], id="map_values"), + pytest.param( + lambda m: f.map_extract(m, literal("x")), + [1], + id="map_extract", + ), + pytest.param( + lambda m: f.map_extract(m, literal("z")), + [None], + id="map_extract_missing_key", + ), + pytest.param( + f.map_entries, + [{"key": "x", "value": 1}, {"key": "y", "value": 2}], + id="map_entries", + ), + pytest.param( + lambda m: f.element_at(m, literal("y")), + [2], + id="element_at", + ), + ], +) +def test_map_functions(func, expected): + ctx = SessionContext() + batch = pa.RecordBatch.from_arrays([pa.array([1])], names=["a"]) + df = ctx.create_dataframe([[batch]]) + + m = f.make_map({"x": 1, "y": 2}) + result = df.select(func(m).alias("out")).collect()[0].column(0) + assert result[0].as_py() == expected + + @pytest.mark.parametrize( ("function", "expected_result"), [ From ff15648c5dca6b41d3f6146c6c36c97e605f8561 Mon Sep 17 00:00:00 2001 From: Nuno Faria Date: Sun, 5 Apr 2026 13:29:32 +0100 Subject: [PATCH 42/56] minor: Fix pytest instructions in README (#1477) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7c1c71281..7849e7a02 100644 --- a/README.md +++ b/README.md @@ -275,7 +275,7 @@ needing to activate the virtual environment: ```bash uv run --no-project maturin develop --uv -uv run --no-project pytest . +uv run --no-project pytest ``` To run the FFI tests within the examples folder, after you have built From 99bc9602dd077c924685f1fc6e54e6feb3429302 Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Mon, 6 Apr 2026 07:47:13 -0400 Subject: [PATCH 43/56] Add missing array functions (#1468) * Add missing array/list functions and aliases (#1452) Add new array functions from upstream DataFusion v53: array_any_value, array_distance, array_max, array_min, array_reverse, arrays_zip, string_to_array, and gen_series. Add corresponding list_* aliases and missing list_* aliases for existing functions (list_empty, list_pop_back, list_pop_front, list_has, list_has_all, list_has_any). Also add array_contains/list_contains as aliases for array_has, generate_series as alias for gen_series, and string_to_list as alias for string_to_array. Co-Authored-By: Claude Opus 4.6 (1M context) * Add unit tests for new array/list functions and aliases Tests cover all functions and aliases added in the previous commit: array_any_value, array_distance, array_max, array_min, array_reverse, arrays_zip, string_to_array, gen_series, generate_series, array_contains, list_contains, list_empty, list_pop_back, list_pop_front, list_has, list_has_all, list_has_any, and list_* aliases for the new functions. Co-Authored-By: Claude Opus 4.6 (1M context) * Improve array function APIs: optional params, better naming, restore comment - Make null_string optional in string_to_array/string_to_list - Make step optional in gen_series/generate_series - Rename second_array to element in array_contains/list_has/list_contains - Restore # Window Functions section comment in __all__ - Add tests for optional parameter variants Co-Authored-By: Claude Opus 4.6 (1M context) * Consolidate array/list function tests using pytest parametrize Reduce 26 individual tests to 14 test functions with parametrized cases, eliminating boilerplate while maintaining full coverage. Co-Authored-By: Claude Opus 4.6 (1M context) * Move list alias tests into existing test_array_functions parametrize block Merge standalone tests for list_empty, list_pop_back, list_pop_front, list_has, array_contains, list_contains, list_has_all, and list_has_any into the existing parametrized test_array_functions block alongside their array_* counterparts. Co-Authored-By: Claude Opus 4.6 (1M context) * Merge test_array_any_value into parametrized test_any_value_aliases Use the richer multi-row dataset (including all-nulls case) for both array_any_value and list_any_value via the parametrized test. Co-Authored-By: Claude Opus 4.6 (1M context) * Add arrays_overlap and list_overlap as aliases for array_has_any These aliases match the upstream DataFusion SQL-level aliases, completing the set of missing array functions from issue #1452. Co-Authored-By: Claude Opus 4.6 (1M context) * Add docstring examples for optional params in string_to_array and gen_series Co-Authored-By: Claude Opus 4.6 (1M context) * Update AGENTS file to demonstrate preferred method of documenting python functions --------- Co-authored-by: Claude Opus 4.6 (1M context) --- AGENTS.md | 17 ++ crates/core/src/functions.rs | 56 ++++++ python/datafusion/functions.py | 337 +++++++++++++++++++++++++++++++++ python/tests/test_functions.py | 137 ++++++++++++++ 4 files changed, 547 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 1853a84cd..f6fdfbd90 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,3 +25,20 @@ Skills follow the [Agent Skills](https://agentskills.io) open standard. Each ski - `SKILL.md` — The skill definition with YAML frontmatter (name, description, argument-hint) and detailed instructions. - Additional supporting files as needed. + +## Python Function Docstrings + +Every Python function must include a docstring with usage examples. + +- **Examples are required**: Each function needs at least one doctest-style example + demonstrating basic usage. +- **Optional parameters**: If a function has optional parameters, include separate + examples that show usage both without and with the optional arguments. Pass + optional arguments using their keyword name (e.g., `step=dfn.lit(3)`) so readers + can immediately see which parameter is being demonstrated. +- **Reuse input data**: Use the same input data across examples wherever possible. + The examples should demonstrate how different optional arguments change the output + for the same input, making the effect of each option easy to understand. +- **Alias functions**: Functions that are simple aliases (e.g., `list_sort` aliasing + `array_sort`) only need a one-line description and a `See Also` reference to the + primary function. They do not need their own examples. diff --git a/crates/core/src/functions.rs b/crates/core/src/functions.rs index 5e61b71be..8bb927718 100644 --- a/crates/core/src/functions.rs +++ b/crates/core/src/functions.rs @@ -93,6 +93,50 @@ fn array_cat(exprs: Vec) -> PyExpr { array_concat(exprs) } +#[pyfunction] +fn array_distance(array1: PyExpr, array2: PyExpr) -> PyExpr { + let args = vec![array1.into(), array2.into()]; + Expr::ScalarFunction(datafusion::logical_expr::expr::ScalarFunction::new_udf( + datafusion::functions_nested::distance::array_distance_udf(), + args, + )) + .into() +} + +#[pyfunction] +fn arrays_zip(exprs: Vec) -> PyExpr { + let exprs = exprs.into_iter().map(|x| x.into()).collect(); + datafusion::functions_nested::expr_fn::arrays_zip(exprs).into() +} + +#[pyfunction] +#[pyo3(signature = (string, delimiter, null_string=None))] +fn string_to_array(string: PyExpr, delimiter: PyExpr, null_string: Option) -> PyExpr { + let mut args = vec![string.into(), delimiter.into()]; + if let Some(null_string) = null_string { + args.push(null_string.into()); + } + Expr::ScalarFunction(datafusion::logical_expr::expr::ScalarFunction::new_udf( + datafusion::functions_nested::string::string_to_array_udf(), + args, + )) + .into() +} + +#[pyfunction] +#[pyo3(signature = (start, stop, step=None))] +fn gen_series(start: PyExpr, stop: PyExpr, step: Option) -> PyExpr { + let mut args = vec![start.into(), stop.into()]; + if let Some(step) = step { + args.push(step.into()); + } + Expr::ScalarFunction(datafusion::logical_expr::expr::ScalarFunction::new_udf( + datafusion::functions_nested::range::gen_series_udf(), + args, + )) + .into() +} + #[pyfunction] fn make_map(keys: Vec, values: Vec) -> PyExpr { let keys = keys.into_iter().map(|x| x.into()).collect(); @@ -681,6 +725,10 @@ array_fn!(array_intersect, first_array second_array); array_fn!(array_union, array1 array2); array_fn!(array_except, first_array second_array); array_fn!(array_resize, array size value); +array_fn!(array_any_value, array); +array_fn!(array_max, array); +array_fn!(array_min, array); +array_fn!(array_reverse, array); array_fn!(cardinality, array); array_fn!(flatten, array); array_fn!(range, start stop step); @@ -1152,6 +1200,14 @@ pub(crate) fn init_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(array_replace_all))?; m.add_wrapped(wrap_pyfunction!(array_sort))?; m.add_wrapped(wrap_pyfunction!(array_slice))?; + m.add_wrapped(wrap_pyfunction!(array_any_value))?; + m.add_wrapped(wrap_pyfunction!(array_distance))?; + m.add_wrapped(wrap_pyfunction!(array_max))?; + m.add_wrapped(wrap_pyfunction!(array_min))?; + m.add_wrapped(wrap_pyfunction!(array_reverse))?; + m.add_wrapped(wrap_pyfunction!(arrays_zip))?; + m.add_wrapped(wrap_pyfunction!(string_to_array))?; + m.add_wrapped(wrap_pyfunction!(gen_series))?; m.add_wrapped(wrap_pyfunction!(flatten))?; m.add_wrapped(wrap_pyfunction!(cardinality))?; diff --git a/python/datafusion/functions.py b/python/datafusion/functions.py index 3febb44e3..1b267731e 100644 --- a/python/datafusion/functions.py +++ b/python/datafusion/functions.py @@ -53,10 +53,13 @@ "approx_percentile_cont_with_weight", "array", "array_agg", + "array_any_value", "array_append", "array_cat", "array_concat", + "array_contains", "array_dims", + "array_distance", "array_distinct", "array_element", "array_empty", @@ -69,6 +72,8 @@ "array_intersect", "array_join", "array_length", + "array_max", + "array_min", "array_ndims", "array_pop_back", "array_pop_front", @@ -85,10 +90,13 @@ "array_replace_all", "array_replace_n", "array_resize", + "array_reverse", "array_slice", "array_sort", "array_to_string", "array_union", + "arrays_overlap", + "arrays_zip", "arrow_cast", "arrow_typeof", "ascii", @@ -153,6 +161,8 @@ "floor", "from_unixtime", "gcd", + "gen_series", + "generate_series", "greatest", "ifnull", "in_list", @@ -167,19 +177,31 @@ "left", "length", "levenshtein", + "list_any_value", "list_append", "list_cat", "list_concat", + "list_contains", "list_dims", + "list_distance", "list_distinct", "list_element", + "list_empty", "list_except", "list_extract", + "list_has", + "list_has_all", + "list_has_any", "list_indexof", "list_intersect", "list_join", "list_length", + "list_max", + "list_min", "list_ndims", + "list_overlap", + "list_pop_back", + "list_pop_front", "list_position", "list_positions", "list_prepend", @@ -193,10 +215,12 @@ "list_replace_all", "list_replace_n", "list_resize", + "list_reverse", "list_slice", "list_sort", "list_to_string", "list_union", + "list_zip", "ln", "log", "log2", @@ -273,6 +297,8 @@ "stddev_pop", "stddev_samp", "string_agg", + "string_to_array", + "string_to_list", "strpos", "struct", "substr", @@ -2794,6 +2820,15 @@ def array_empty(array: Expr) -> Expr: return Expr(f.array_empty(array.expr)) +def list_empty(array: Expr) -> Expr: + """Returns a boolean indicating whether the array is empty. + + See Also: + This is an alias for :py:func:`array_empty`. + """ + return array_empty(array) + + def array_extract(array: Expr, n: Expr) -> Expr: """Extracts the element with the index n from the array. @@ -2891,6 +2926,69 @@ def array_has_any(first_array: Expr, second_array: Expr) -> Expr: return Expr(f.array_has_any(first_array.expr, second_array.expr)) +def array_contains(array: Expr, element: Expr) -> Expr: + """Returns true if the element appears in the array, otherwise false. + + See Also: + This is an alias for :py:func:`array_has`. + """ + return array_has(array, element) + + +def list_has(array: Expr, element: Expr) -> Expr: + """Returns true if the element appears in the array, otherwise false. + + See Also: + This is an alias for :py:func:`array_has`. + """ + return array_has(array, element) + + +def list_has_all(first_array: Expr, second_array: Expr) -> Expr: + """Determines if there is complete overlap ``second_array`` in ``first_array``. + + See Also: + This is an alias for :py:func:`array_has_all`. + """ + return array_has_all(first_array, second_array) + + +def list_has_any(first_array: Expr, second_array: Expr) -> Expr: + """Determine if there is an overlap between ``first_array`` and ``second_array``. + + See Also: + This is an alias for :py:func:`array_has_any`. + """ + return array_has_any(first_array, second_array) + + +def arrays_overlap(first_array: Expr, second_array: Expr) -> Expr: + """Returns true if any element appears in both arrays. + + See Also: + This is an alias for :py:func:`array_has_any`. + """ + return array_has_any(first_array, second_array) + + +def list_overlap(first_array: Expr, second_array: Expr) -> Expr: + """Returns true if any element appears in both arrays. + + See Also: + This is an alias for :py:func:`array_has_any`. + """ + return array_has_any(first_array, second_array) + + +def list_contains(array: Expr, element: Expr) -> Expr: + """Returns true if the element appears in the array, otherwise false. + + See Also: + This is an alias for :py:func:`array_has`. + """ + return array_has(array, element) + + def array_position(array: Expr, element: Expr, index: int | None = 1) -> Expr: """Return the position of the first occurrence of ``element`` in ``array``. @@ -3058,6 +3156,24 @@ def array_pop_front(array: Expr) -> Expr: return Expr(f.array_pop_front(array.expr)) +def list_pop_back(array: Expr) -> Expr: + """Returns the array without the last element. + + See Also: + This is an alias for :py:func:`array_pop_back`. + """ + return array_pop_back(array) + + +def list_pop_front(array: Expr) -> Expr: + """Returns the array without the first element. + + See Also: + This is an alias for :py:func:`array_pop_front`. + """ + return array_pop_front(array) + + def array_remove(array: Expr, element: Expr) -> Expr: """Removes the first element from the array equal to the given value. @@ -3429,6 +3545,227 @@ def list_resize(array: Expr, size: Expr, value: Expr) -> Expr: return array_resize(array, size, value) +def array_any_value(array: Expr) -> Expr: + """Returns the first non-null element in the array. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[None, 2, 3]]}) + >>> result = df.select( + ... dfn.functions.array_any_value(dfn.col("a")).alias("result")) + >>> result.collect_column("result")[0].as_py() + 2 + """ + return Expr(f.array_any_value(array.expr)) + + +def list_any_value(array: Expr) -> Expr: + """Returns the first non-null element in the array. + + See Also: + This is an alias for :py:func:`array_any_value`. + """ + return array_any_value(array) + + +def array_distance(array1: Expr, array2: Expr) -> Expr: + """Returns the Euclidean distance between two numeric arrays. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1.0, 2.0]], "b": [[1.0, 4.0]]}) + >>> result = df.select( + ... dfn.functions.array_distance( + ... dfn.col("a"), dfn.col("b"), + ... ).alias("result")) + >>> result.collect_column("result")[0].as_py() + 2.0 + """ + return Expr(f.array_distance(array1.expr, array2.expr)) + + +def list_distance(array1: Expr, array2: Expr) -> Expr: + """Returns the Euclidean distance between two numeric arrays. + + See Also: + This is an alias for :py:func:`array_distance`. + """ + return array_distance(array1, array2) + + +def array_max(array: Expr) -> Expr: + """Returns the maximum value in the array. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) + >>> result = df.select( + ... dfn.functions.array_max(dfn.col("a")).alias("result")) + >>> result.collect_column("result")[0].as_py() + 3 + """ + return Expr(f.array_max(array.expr)) + + +def list_max(array: Expr) -> Expr: + """Returns the maximum value in the array. + + See Also: + This is an alias for :py:func:`array_max`. + """ + return array_max(array) + + +def array_min(array: Expr) -> Expr: + """Returns the minimum value in the array. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) + >>> result = df.select( + ... dfn.functions.array_min(dfn.col("a")).alias("result")) + >>> result.collect_column("result")[0].as_py() + 1 + """ + return Expr(f.array_min(array.expr)) + + +def list_min(array: Expr) -> Expr: + """Returns the minimum value in the array. + + See Also: + This is an alias for :py:func:`array_min`. + """ + return array_min(array) + + +def array_reverse(array: Expr) -> Expr: + """Reverses the order of elements in the array. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2, 3]]}) + >>> result = df.select( + ... dfn.functions.array_reverse(dfn.col("a")).alias("result")) + >>> result.collect_column("result")[0].as_py() + [3, 2, 1] + """ + return Expr(f.array_reverse(array.expr)) + + +def list_reverse(array: Expr) -> Expr: + """Reverses the order of elements in the array. + + See Also: + This is an alias for :py:func:`array_reverse`. + """ + return array_reverse(array) + + +def arrays_zip(*arrays: Expr) -> Expr: + """Combines multiple arrays into a single array of structs. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2]], "b": [[3, 4]]}) + >>> result = df.select( + ... dfn.functions.arrays_zip(dfn.col("a"), dfn.col("b")).alias("result")) + >>> result.collect_column("result")[0].as_py() + [{'c0': 1, 'c1': 3}, {'c0': 2, 'c1': 4}] + """ + args = [a.expr for a in arrays] + return Expr(f.arrays_zip(args)) + + +def list_zip(*arrays: Expr) -> Expr: + """Combines multiple arrays into a single array of structs. + + See Also: + This is an alias for :py:func:`arrays_zip`. + """ + return arrays_zip(*arrays) + + +def string_to_array( + string: Expr, delimiter: Expr, null_string: Expr | None = None +) -> Expr: + """Splits a string based on a delimiter and returns an array of parts. + + Any parts matching the optional ``null_string`` will be replaced with ``NULL``. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": ["hello,world"]}) + >>> result = df.select( + ... dfn.functions.string_to_array( + ... dfn.col("a"), dfn.lit(","), + ... ).alias("result")) + >>> result.collect_column("result")[0].as_py() + ['hello', 'world'] + + Replace parts matching a ``null_string`` with ``NULL``: + + >>> result = df.select( + ... dfn.functions.string_to_array( + ... dfn.col("a"), dfn.lit(","), null_string=dfn.lit("world"), + ... ).alias("result")) + >>> result.collect_column("result")[0].as_py() + ['hello', None] + """ + null_expr = null_string.expr if null_string is not None else None + return Expr(f.string_to_array(string.expr, delimiter.expr, null_expr)) + + +def string_to_list( + string: Expr, delimiter: Expr, null_string: Expr | None = None +) -> Expr: + """Splits a string based on a delimiter and returns an array of parts. + + See Also: + This is an alias for :py:func:`string_to_array`. + """ + return string_to_array(string, delimiter, null_string) + + +def gen_series(start: Expr, stop: Expr, step: Expr | None = None) -> Expr: + """Creates a list of values in the range between start and stop. + + Unlike :py:func:`range`, this includes the upper bound. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [0]}) + >>> result = df.select( + ... dfn.functions.gen_series( + ... dfn.lit(1), dfn.lit(5), + ... ).alias("result")) + >>> result.collect_column("result")[0].as_py() + [1, 2, 3, 4, 5] + + Specify a custom ``step``: + + >>> result = df.select( + ... dfn.functions.gen_series( + ... dfn.lit(1), dfn.lit(10), step=dfn.lit(3), + ... ).alias("result")) + >>> result.collect_column("result")[0].as_py() + [1, 4, 7, 10] + """ + step_expr = step.expr if step is not None else None + return Expr(f.gen_series(start.expr, stop.expr, step_expr)) + + +def generate_series(start: Expr, stop: Expr, step: Expr | None = None) -> Expr: + """Creates a list of values in the range between start and stop. + + Unlike :py:func:`range`, this includes the upper bound. + + See Also: + This is an alias for :py:func:`gen_series`. + """ + return gen_series(start, stop, step) + + def flatten(array: Expr) -> Expr: """Flattens an array of arrays into a single array. diff --git a/python/tests/test_functions.py b/python/tests/test_functions.py index f25c6e78c..2100da9ae 100644 --- a/python/tests/test_functions.py +++ b/python/tests/test_functions.py @@ -330,6 +330,10 @@ def py_flatten(arr): f.empty, lambda data: [len(r) == 0 for r in data], ), + ( + f.list_empty, + lambda data: [len(r) == 0 for r in data], + ), ( lambda col: f.array_extract(col, literal(1)), lambda data: [r[0] for r in data], @@ -354,18 +358,54 @@ def py_flatten(arr): lambda col: f.array_has(col, literal(1.0)), lambda data: [1.0 in r for r in data], ), + ( + lambda col: f.list_has(col, literal(1.0)), + lambda data: [1.0 in r for r in data], + ), + ( + lambda col: f.array_contains(col, literal(1.0)), + lambda data: [1.0 in r for r in data], + ), + ( + lambda col: f.list_contains(col, literal(1.0)), + lambda data: [1.0 in r for r in data], + ), ( lambda col: f.array_has_all( col, f.make_array(*[literal(v) for v in [1.0, 3.0, 5.0]]) ), lambda data: [np.all([v in r for v in [1.0, 3.0, 5.0]]) for r in data], ), + ( + lambda col: f.list_has_all( + col, f.make_array(*[literal(v) for v in [1.0, 3.0, 5.0]]) + ), + lambda data: [np.all([v in r for v in [1.0, 3.0, 5.0]]) for r in data], + ), ( lambda col: f.array_has_any( col, f.make_array(*[literal(v) for v in [1.0, 3.0, 5.0]]) ), lambda data: [np.any([v in r for v in [1.0, 3.0, 5.0]]) for r in data], ), + ( + lambda col: f.list_has_any( + col, f.make_array(*[literal(v) for v in [1.0, 3.0, 5.0]]) + ), + lambda data: [np.any([v in r for v in [1.0, 3.0, 5.0]]) for r in data], + ), + ( + lambda col: f.arrays_overlap( + col, f.make_array(*[literal(v) for v in [1.0, 3.0, 5.0]]) + ), + lambda data: [np.any([v in r for v in [1.0, 3.0, 5.0]]) for r in data], + ), + ( + lambda col: f.list_overlap( + col, f.make_array(*[literal(v) for v in [1.0, 3.0, 5.0]]) + ), + lambda data: [np.any([v in r for v in [1.0, 3.0, 5.0]]) for r in data], + ), ( lambda col: f.array_position(col, literal(1.0)), lambda data: [py_indexof(r, 1.0) for r in data], @@ -418,10 +458,18 @@ def py_flatten(arr): f.array_pop_back, lambda data: [arr[:-1] for arr in data], ), + ( + f.list_pop_back, + lambda data: [arr[:-1] for arr in data], + ), ( f.array_pop_front, lambda data: [arr[1:] for arr in data], ), + ( + f.list_pop_front, + lambda data: [arr[1:] for arr in data], + ), ( lambda col: f.array_remove(col, literal(3.0)), lambda data: [py_arr_remove(arr, 3.0, 1) for arr in data], @@ -1760,3 +1808,92 @@ def df_with_nulls(): def test_conditional_functions(df_with_nulls, expr, expected): result = df_with_nulls.select(expr.alias("result")).collect()[0] assert result.column(0) == expected + + +@pytest.mark.parametrize("func", [f.array_any_value, f.list_any_value]) +def test_any_value_aliases(func): + ctx = SessionContext() + df = ctx.from_pydict({"a": [[None, 2, 3], [None, None, None], [1, 2, 3]]}) + result = df.select(func(column("a")).alias("v")).collect() + values = [row.as_py() for row in result[0].column(0)] + assert values[0] == 2 + assert values[1] is None + assert values[2] == 1 + + +@pytest.mark.parametrize("func", [f.array_distance, f.list_distance]) +def test_array_distance_aliases(func): + ctx = SessionContext() + df = ctx.from_pydict({"a": [[1.0, 2.0]], "b": [[1.0, 4.0]]}) + result = df.select(func(column("a"), column("b")).alias("v")).collect() + assert result[0].column(0)[0].as_py() == pytest.approx(2.0) + + +@pytest.mark.parametrize( + ("func", "expected"), + [ + (f.array_max, [5, 10]), + (f.list_max, [5, 10]), + (f.array_min, [1, 2]), + (f.list_min, [1, 2]), + ], +) +def test_array_min_max(func, expected): + ctx = SessionContext() + df = ctx.from_pydict({"a": [[1, 5, 3], [10, 2]]}) + result = df.select(func(column("a")).alias("v")).collect() + values = [row.as_py() for row in result[0].column(0)] + assert values == expected + + +@pytest.mark.parametrize("func", [f.array_reverse, f.list_reverse]) +def test_array_reverse_aliases(func): + ctx = SessionContext() + df = ctx.from_pydict({"a": [[1, 2, 3], [4, 5]]}) + result = df.select(func(column("a")).alias("v")).collect() + values = [row.as_py() for row in result[0].column(0)] + assert values == [[3, 2, 1], [5, 4]] + + +@pytest.mark.parametrize("func", [f.arrays_zip, f.list_zip]) +def test_arrays_zip_aliases(func): + ctx = SessionContext() + df = ctx.from_pydict({"a": [[1, 2]], "b": [[3, 4]]}) + result = df.select(func(column("a"), column("b")).alias("v")).collect() + values = result[0].column(0)[0].as_py() + assert values == [{"c0": 1, "c1": 3}, {"c0": 2, "c1": 4}] + + +@pytest.mark.parametrize("func", [f.string_to_array, f.string_to_list]) +def test_string_to_array_aliases(func): + ctx = SessionContext() + df = ctx.from_pydict({"a": ["hello,world,foo"]}) + result = df.select(func(column("a"), literal(",")).alias("v")).collect() + assert result[0].column(0)[0].as_py() == ["hello", "world", "foo"] + + +def test_string_to_array_with_null_string(): + ctx = SessionContext() + df = ctx.from_pydict({"a": ["hello,NA,world"]}) + result = df.select( + f.string_to_array(column("a"), literal(","), literal("NA")).alias("v") + ).collect() + values = result[0].column(0)[0].as_py() + assert values == ["hello", None, "world"] + + +@pytest.mark.parametrize("func", [f.gen_series, f.generate_series]) +def test_gen_series_aliases(func): + ctx = SessionContext() + df = ctx.from_pydict({"a": [0]}) + result = df.select(func(literal(1), literal(5)).alias("v")).collect() + assert result[0].column(0)[0].as_py() == [1, 2, 3, 4, 5] + + +def test_gen_series_with_step(): + ctx = SessionContext() + df = ctx.from_pydict({"a": [0]}) + result = df.select( + f.gen_series(literal(1), literal(10), literal(3)).alias("v") + ).collect() + assert result[0].column(0)[0].as_py() == [1, 4, 7, 10] From d07fdb3ef7d211920f40d0106fa50161c0bf20ce Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Mon, 6 Apr 2026 08:54:30 -0400 Subject: [PATCH 44/56] Add missing scalar functions (#1470) * Add missing scalar functions: get_field, union_extract, union_tag, arrow_metadata, version, row Expose upstream DataFusion scalar functions that were not yet available in the Python API. Closes #1453. - get_field: extracts a field from a struct or map by name - union_extract: extracts a value from a union type by field name - union_tag: returns the active field name of a union type - arrow_metadata: returns Arrow field metadata (all or by key) - version: returns the DataFusion version string - row: alias for the struct constructor Note: arrow_try_cast was listed in the issue but does not exist in DataFusion 53, so it is not included. Co-Authored-By: Claude Opus 4.6 (1M context) * Add tests for new scalar functions Tests for get_field, arrow_metadata, version, row, union_tag, and union_extract. Co-Authored-By: Claude Opus 4.6 (1M context) * Accept str for field name and type parameters in scalar functions Allow arrow_cast, get_field, and union_extract to accept plain str arguments instead of requiring Expr wrappers. Also improve arrow_metadata test coverage and fix parameter shadowing. Co-Authored-By: Claude Opus 4.6 (1M context) * Accept str for key parameter in arrow_metadata for consistency Co-Authored-By: Claude Opus 4.6 (1M context) * Add doctest examples and fix docstring style for new scalar functions Replace Args/Returns sections with doctest Examples blocks for arrow_metadata, get_field, union_extract, union_tag, and version to match existing codebase conventions. Simplify row to alias-style docstring with See Also reference. Document that arrow_cast accepts both str and Expr for data_type. Co-Authored-By: Claude Opus 4.6 (1M context) * Support pyarrow DataType in arrow_cast Allow arrow_cast to accept a pyarrow DataType in addition to str and Expr. The DataType is converted to its string representation before being passed to DataFusion. Adds test coverage for the new input type. Co-Authored-By: Claude Opus 4.6 (1M context) * Document bracket syntax shorthand in get_field docstring Note that expr["field"] is a convenient alternative when the field name is a static string, and get_field is needed for dynamic expressions. Add a second doctest example showing the bracket syntax. Co-Authored-By: Claude Opus 4.6 (1M context) * Fix arrow_cast with pyarrow DataType by delegating to Expr.cast Use the existing Rust-side PyArrowType conversion via Expr.cast() instead of str() which produces pyarrow type names that DataFusion does not recognize. Co-Authored-By: Claude Opus 4.6 (1M context) * Clarify when to use arrow_cast vs Expr.cast in docstring Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- crates/core/src/functions.rs | 26 +++++ python/datafusion/functions.py | 174 ++++++++++++++++++++++++++++++++- python/tests/test_functions.py | 105 ++++++++++++++++++-- 3 files changed, 296 insertions(+), 9 deletions(-) diff --git a/crates/core/src/functions.rs b/crates/core/src/functions.rs index 8bb927718..74654ce46 100644 --- a/crates/core/src/functions.rs +++ b/crates/core/src/functions.rs @@ -695,8 +695,29 @@ expr_fn_vec!(named_struct); expr_fn!(from_unixtime, unixtime); expr_fn!(arrow_typeof, arg_1); expr_fn!(arrow_cast, arg_1 datatype); +expr_fn_vec!(arrow_metadata); +expr_fn!(union_tag, arg1); expr_fn!(random); +#[pyfunction] +fn get_field(expr: PyExpr, name: PyExpr) -> PyExpr { + functions::core::get_field() + .call(vec![expr.into(), name.into()]) + .into() +} + +#[pyfunction] +fn union_extract(union_expr: PyExpr, field_name: PyExpr) -> PyExpr { + functions::core::union_extract() + .call(vec![union_expr.into(), field_name.into()]) + .into() +} + +#[pyfunction] +fn version() -> PyExpr { + functions::core::version().call(vec![]).into() +} + // Array Functions array_fn!(array_append, array element); array_fn!(array_to_string, array delimiter); @@ -1014,6 +1035,7 @@ pub(crate) fn init_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(array_agg))?; m.add_wrapped(wrap_pyfunction!(arrow_typeof))?; m.add_wrapped(wrap_pyfunction!(arrow_cast))?; + m.add_wrapped(wrap_pyfunction!(arrow_metadata))?; m.add_wrapped(wrap_pyfunction!(ascii))?; m.add_wrapped(wrap_pyfunction!(asin))?; m.add_wrapped(wrap_pyfunction!(asinh))?; @@ -1142,6 +1164,10 @@ pub(crate) fn init_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(trim))?; m.add_wrapped(wrap_pyfunction!(trunc))?; m.add_wrapped(wrap_pyfunction!(upper))?; + m.add_wrapped(wrap_pyfunction!(get_field))?; + m.add_wrapped(wrap_pyfunction!(union_extract))?; + m.add_wrapped(wrap_pyfunction!(union_tag))?; + m.add_wrapped(wrap_pyfunction!(version))?; m.add_wrapped(wrap_pyfunction!(self::uuid))?; // Use self to avoid name collision m.add_wrapped(wrap_pyfunction!(var_pop))?; m.add_wrapped(wrap_pyfunction!(var_sample))?; diff --git a/python/datafusion/functions.py b/python/datafusion/functions.py index 1b267731e..aa7f28746 100644 --- a/python/datafusion/functions.py +++ b/python/datafusion/functions.py @@ -98,6 +98,7 @@ "arrays_overlap", "arrays_zip", "arrow_cast", + "arrow_metadata", "arrow_typeof", "ascii", "asin", @@ -163,6 +164,7 @@ "gcd", "gen_series", "generate_series", + "get_field", "greatest", "ifnull", "in_list", @@ -280,6 +282,7 @@ "reverse", "right", "round", + "row", "row_number", "rpad", "rtrim", @@ -322,12 +325,15 @@ "translate", "trim", "trunc", + "union_extract", + "union_tag", "upper", "uuid", "var", "var_pop", "var_samp", "var_sample", + "version", "when", # Window Functions "window", @@ -2628,22 +2634,184 @@ def arrow_typeof(arg: Expr) -> Expr: return Expr(f.arrow_typeof(arg.expr)) -def arrow_cast(expr: Expr, data_type: Expr) -> Expr: +def arrow_cast(expr: Expr, data_type: Expr | str | pa.DataType) -> Expr: """Casts an expression to a specified data type. + The ``data_type`` can be a string, a ``pyarrow.DataType``, or an + ``Expr``. For simple types, :py:meth:`Expr.cast() + ` is more concise + (e.g., ``col("a").cast(pa.float64())``). Use ``arrow_cast`` when + you want to specify the target type as a string using DataFusion's + type syntax, which can be more readable for complex types like + ``"Timestamp(Nanosecond, None)"``. + Examples: >>> ctx = dfn.SessionContext() >>> df = ctx.from_pydict({"a": [1]}) - >>> data_type = dfn.string_literal("Float64") >>> result = df.select( - ... dfn.functions.arrow_cast(dfn.col("a"), data_type).alias("c") + ... dfn.functions.arrow_cast(dfn.col("a"), "Float64").alias("c") + ... ) + >>> result.collect_column("c")[0].as_py() + 1.0 + + >>> import pyarrow as pa + >>> result = df.select( + ... dfn.functions.arrow_cast( + ... dfn.col("a"), data_type=pa.float64() + ... ).alias("c") ... ) >>> result.collect_column("c")[0].as_py() 1.0 """ + if isinstance(data_type, pa.DataType): + return expr.cast(data_type) + if isinstance(data_type, str): + data_type = Expr.string_literal(data_type) return Expr(f.arrow_cast(expr.expr, data_type.expr)) +def arrow_metadata(expr: Expr, key: Expr | str | None = None) -> Expr: + """Returns the metadata of the input expression. + + If called with one argument, returns a Map of all metadata key-value pairs. + If called with two arguments, returns the value for the specified metadata key. + + Examples: + >>> import pyarrow as pa + >>> field = pa.field("val", pa.int64(), metadata={"k": "v"}) + >>> schema = pa.schema([field]) + >>> batch = pa.RecordBatch.from_arrays([pa.array([1])], schema=schema) + >>> ctx = dfn.SessionContext() + >>> df = ctx.create_dataframe([[batch]]) + >>> result = df.select( + ... dfn.functions.arrow_metadata(dfn.col("val")).alias("meta") + ... ) + >>> ("k", "v") in result.collect_column("meta")[0].as_py() + True + + >>> result = df.select( + ... dfn.functions.arrow_metadata( + ... dfn.col("val"), key="k" + ... ).alias("meta_val") + ... ) + >>> result.collect_column("meta_val")[0].as_py() + 'v' + """ + if key is None: + return Expr(f.arrow_metadata(expr.expr)) + if isinstance(key, str): + key = Expr.string_literal(key) + return Expr(f.arrow_metadata(expr.expr, key.expr)) + + +def get_field(expr: Expr, name: Expr | str) -> Expr: + """Extracts a field from a struct or map by name. + + When the field name is a static string, the bracket operator + ``expr["field"]`` is a convenient shorthand. Use ``get_field`` + when the field name is a dynamic expression. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1], "b": [2]}) + >>> df = df.with_column( + ... "s", + ... dfn.functions.named_struct( + ... [("x", dfn.col("a")), ("y", dfn.col("b"))] + ... ), + ... ) + >>> result = df.select( + ... dfn.functions.get_field(dfn.col("s"), "x").alias("x_val") + ... ) + >>> result.collect_column("x_val")[0].as_py() + 1 + + Equivalent using bracket syntax: + + >>> result = df.select( + ... dfn.col("s")["x"].alias("x_val") + ... ) + >>> result.collect_column("x_val")[0].as_py() + 1 + """ + if isinstance(name, str): + name = Expr.string_literal(name) + return Expr(f.get_field(expr.expr, name.expr)) + + +def union_extract(union_expr: Expr, field_name: Expr | str) -> Expr: + """Extracts a value from a union type by field name. + + Returns the value of the named field if it is the currently selected + variant, otherwise returns NULL. + + Examples: + >>> import pyarrow as pa + >>> ctx = dfn.SessionContext() + >>> types = pa.array([0, 1, 0], type=pa.int8()) + >>> offsets = pa.array([0, 0, 1], type=pa.int32()) + >>> arr = pa.UnionArray.from_dense( + ... types, offsets, [pa.array([1, 2]), pa.array(["hi"])], + ... ["int", "str"], [0, 1], + ... ) + >>> batch = pa.RecordBatch.from_arrays([arr], names=["u"]) + >>> df = ctx.create_dataframe([[batch]]) + >>> result = df.select( + ... dfn.functions.union_extract(dfn.col("u"), "int").alias("val") + ... ) + >>> result.collect_column("val").to_pylist() + [1, None, 2] + """ + if isinstance(field_name, str): + field_name = Expr.string_literal(field_name) + return Expr(f.union_extract(union_expr.expr, field_name.expr)) + + +def union_tag(union_expr: Expr) -> Expr: + """Returns the tag (active field name) of a union type. + + Examples: + >>> import pyarrow as pa + >>> ctx = dfn.SessionContext() + >>> types = pa.array([0, 1, 0], type=pa.int8()) + >>> offsets = pa.array([0, 0, 1], type=pa.int32()) + >>> arr = pa.UnionArray.from_dense( + ... types, offsets, [pa.array([1, 2]), pa.array(["hi"])], + ... ["int", "str"], [0, 1], + ... ) + >>> batch = pa.RecordBatch.from_arrays([arr], names=["u"]) + >>> df = ctx.create_dataframe([[batch]]) + >>> result = df.select( + ... dfn.functions.union_tag(dfn.col("u")).alias("tag") + ... ) + >>> result.collect_column("tag").to_pylist() + ['int', 'str', 'int'] + """ + return Expr(f.union_tag(union_expr.expr)) + + +def version() -> Expr: + """Returns the DataFusion version string. + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.empty_table() + >>> result = df.select(dfn.functions.version().alias("v")) + >>> "Apache DataFusion" in result.collect_column("v")[0].as_py() + True + """ + return Expr(f.version()) + + +def row(*args: Expr) -> Expr: + """Returns a struct with the given arguments. + + See Also: + This is an alias for :py:func:`struct`. + """ + return struct(*args) + + def random() -> Expr: """Returns a random value in the range ``0.0 <= x < 1.0``. diff --git a/python/tests/test_functions.py b/python/tests/test_functions.py index 2100da9ae..4e99fa9e3 100644 --- a/python/tests/test_functions.py +++ b/python/tests/test_functions.py @@ -20,7 +20,7 @@ import numpy as np import pyarrow as pa import pytest -from datafusion import SessionContext, column, literal, string_literal +from datafusion import SessionContext, column, literal from datafusion import functions as f np.seterr(invalid="ignore") @@ -1291,11 +1291,8 @@ def test_make_time(df): def test_arrow_cast(df): df = df.select( - # we use `string_literal` to return utf8 instead of `literal` which returns - # utf8view because datafusion.arrow_cast expects a utf8 instead of utf8view - # https://github.com/apache/datafusion/blob/86740bfd3d9831d6b7c1d0e1bf4a21d91598a0ac/datafusion/functions/src/core/arrow_cast.rs#L179 - f.arrow_cast(column("b"), string_literal("Float64")).alias("b_as_float"), - f.arrow_cast(column("b"), string_literal("Int32")).alias("b_as_int"), + f.arrow_cast(column("b"), "Float64").alias("b_as_float"), + f.arrow_cast(column("b"), "Int32").alias("b_as_int"), ) result = df.collect() assert len(result) == 1 @@ -1305,6 +1302,19 @@ def test_arrow_cast(df): assert result.column(1) == pa.array([4, 5, 6], type=pa.int32()) +def test_arrow_cast_with_pyarrow_type(df): + df = df.select( + f.arrow_cast(column("b"), pa.float64()).alias("b_as_float"), + f.arrow_cast(column("b"), pa.int32()).alias("b_as_int"), + f.arrow_cast(column("b"), pa.string()).alias("b_as_str"), + ) + result = df.collect()[0] + + assert result.column(0) == pa.array([4.0, 5.0, 6.0], type=pa.float64()) + assert result.column(1) == pa.array([4, 5, 6], type=pa.int32()) + assert result.column(2) == pa.array(["4", "5", "6"], type=pa.string()) + + def test_case(df): df = df.select( f.case(column("b")).when(literal(4), literal(10)).otherwise(literal(8)), @@ -1810,6 +1820,89 @@ def test_conditional_functions(df_with_nulls, expr, expected): assert result.column(0) == expected +def test_get_field(df): + df = df.with_column( + "s", + f.named_struct( + [ + ("x", column("a")), + ("y", column("b")), + ] + ), + ) + result = df.select( + f.get_field(column("s"), "x").alias("x_val"), + f.get_field(column("s"), "y").alias("y_val"), + ).collect()[0] + + assert result.column(0) == pa.array(["Hello", "World", "!"], type=pa.string_view()) + assert result.column(1) == pa.array([4, 5, 6]) + + +def test_arrow_metadata(): + ctx = SessionContext() + field = pa.field("val", pa.int64(), metadata={"key1": "value1", "key2": "value2"}) + schema = pa.schema([field]) + batch = pa.RecordBatch.from_arrays([pa.array([1, 2, 3])], schema=schema) + df = ctx.create_dataframe([[batch]]) + + # One-argument form: returns a Map of all metadata key-value pairs + result = df.select( + f.arrow_metadata(column("val")).alias("meta"), + ).collect()[0] + assert result.column(0).type == pa.map_(pa.utf8(), pa.utf8()) + meta = result.column(0)[0].as_py() + assert ("key1", "value1") in meta + assert ("key2", "value2") in meta + + # Two-argument form: returns the value for a specific metadata key + result = df.select( + f.arrow_metadata(column("val"), "key1").alias("meta_val"), + ).collect()[0] + assert result.column(0)[0].as_py() == "value1" + + +def test_version(): + ctx = SessionContext() + df = ctx.from_pydict({"a": [1]}) + result = df.select(f.version().alias("v")).collect()[0] + version_str = result.column(0)[0].as_py() + assert "Apache DataFusion" in version_str + + +def test_row(df): + result = df.select( + f.row(column("a"), column("b")).alias("r"), + f.struct(column("a"), column("b")).alias("s"), + ).collect()[0] + # row is an alias for struct, so they should produce the same output + assert result.column(0) == result.column(1) + + +def test_union_tag(): + ctx = SessionContext() + types = pa.array([0, 1, 0], type=pa.int8()) + offsets = pa.array([0, 0, 1], type=pa.int32()) + children = [pa.array([1, 2]), pa.array(["hello"])] + arr = pa.UnionArray.from_dense(types, offsets, children, ["int", "str"], [0, 1]) + df = ctx.create_dataframe([[pa.RecordBatch.from_arrays([arr], names=["u"])]]) + + result = df.select(f.union_tag(column("u")).alias("tag")).collect()[0] + assert result.column(0).to_pylist() == ["int", "str", "int"] + + +def test_union_extract(): + ctx = SessionContext() + types = pa.array([0, 1, 0], type=pa.int8()) + offsets = pa.array([0, 0, 1], type=pa.int32()) + children = [pa.array([1, 2]), pa.array(["hello"])] + arr = pa.UnionArray.from_dense(types, offsets, children, ["int", "str"], [0, 1]) + df = ctx.create_dataframe([[pa.RecordBatch.from_arrays([arr], names=["u"])]]) + + result = df.select(f.union_extract(column("u"), "int").alias("val")).collect()[0] + assert result.column(0).to_pylist() == [1, None, 2] + + @pytest.mark.parametrize("func", [f.array_any_value, f.list_any_value]) def test_any_value_aliases(func): ctx = SessionContext() From 898d73de20346bba7241907bb18cba47da53e9a9 Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Tue, 7 Apr 2026 09:01:36 -0400 Subject: [PATCH 45/56] Add missing aggregate functions (#1471) * Add missing aggregate functions: grouping, percentile_cont, var_population Expose upstream DataFusion aggregate functions that were not yet available in the Python API. Closes #1454. - grouping: returns grouping set membership indicator (rewritten by the ResolveGroupingFunction analyzer rule before physical planning) - percentile_cont: computes exact percentile using continuous interpolation (unlike approx_percentile_cont which uses t-digest) - var_population: alias for var_pop Co-Authored-By: Claude Opus 4.6 (1M context) * Fix grouping() distinct parameter type for API consistency Co-Authored-By: Claude Opus 4.6 (1M context) * Improve aggregate function tests and docstrings per review feedback Add docstring example to grouping(), parametrize percentile_cont tests, and add multi-column grouping test case. Co-Authored-By: Claude Opus 4.6 (1M context) * Add GroupingSet.rollup, .cube, and .grouping_sets factory methods Expose ROLLUP, CUBE, and GROUPING SETS via the DataFrame API by adding static methods on GroupingSet that construct the corresponding Expr variants. Update grouping() docstring and tests to use the new API. Co-Authored-By: Claude Opus 4.6 (1M context) * Remove _GroupingSetInternal alias, use expr_internal.GroupingSet directly Co-Authored-By: Claude Opus 4.6 (1M context) * Parametrize grouping set tests for rollup and cube Co-Authored-By: Claude Opus 4.6 (1M context) * Add grouping sets documentation and note grouping() alias limitation Add user documentation for GroupingSet.rollup, .cube, and .grouping_sets with Pokemon dataset examples. Document the upstream alias limitation (apache/datafusion#21411) in both the grouping() docstring and the aggregation user guide. Co-Authored-By: Claude Opus 4.6 (1M context) * Add grouping sets note to DataFrame.aggregate() docstring Co-Authored-By: Claude Opus 4.6 (1M context) * Address PR review feedback: add quantile_cont alias and simplify examples - Add quantile_cont as alias for percentile_cont (matches upstream) - Replace pa.concat_arrays batch pattern with collect_column() in docstrings - Add percentile_cont, quantile_cont, var_population to docs function list Co-Authored-By: Claude Opus 4.6 (1M context) * Accept string column names in GroupingSet factory methods GroupingSet.rollup(), .cube(), and .grouping_sets() now accept both Expr objects and string column names, consistent with DataFrame.aggregate(). Co-Authored-By: Claude Opus 4.6 (1M context) * Add agent instructions to keep aggregation/window docs in sync Co-Authored-By: Claude Opus 4.6 (1M context) * dfn is already available globally * Remove unnecessary import on doctest --------- Co-authored-by: Claude Opus 4.6 (1M context) --- AGENTS.md | 12 ++ crates/core/src/expr/grouping_set.rs | 37 +++- crates/core/src/functions.rs | 23 ++- .../common-operations/aggregations.rst | 172 +++++++++++++++++- python/datafusion/dataframe.py | 16 +- python/datafusion/expr.py | 127 ++++++++++++- python/datafusion/functions.py | 130 ++++++++++++- python/tests/test_functions.py | 109 +++++++++++ 8 files changed, 614 insertions(+), 12 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f6fdfbd90..86c2e9c3b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,3 +42,15 @@ Every Python function must include a docstring with usage examples. - **Alias functions**: Functions that are simple aliases (e.g., `list_sort` aliasing `array_sort`) only need a one-line description and a `See Also` reference to the primary function. They do not need their own examples. + +## Aggregate and Window Function Documentation + +When adding or updating an aggregate or window function, ensure the corresponding +site documentation is kept in sync: + +- **Aggregations**: `docs/source/user-guide/common-operations/aggregations.rst` — + add new aggregate functions to the "Aggregate Functions" list and include usage + examples if appropriate. +- **Window functions**: `docs/source/user-guide/common-operations/windows.rst` — + add new window functions to the "Available Functions" list and include usage + examples if appropriate. diff --git a/crates/core/src/expr/grouping_set.rs b/crates/core/src/expr/grouping_set.rs index 549a866ed..11d8f4fcd 100644 --- a/crates/core/src/expr/grouping_set.rs +++ b/crates/core/src/expr/grouping_set.rs @@ -15,9 +15,11 @@ // specific language governing permissions and limitations // under the License. -use datafusion::logical_expr::GroupingSet; +use datafusion::logical_expr::{Expr, GroupingSet}; use pyo3::prelude::*; +use crate::expr::PyExpr; + #[pyclass( from_py_object, frozen, @@ -30,6 +32,39 @@ pub struct PyGroupingSet { grouping_set: GroupingSet, } +#[pymethods] +impl PyGroupingSet { + #[staticmethod] + #[pyo3(signature = (*exprs))] + fn rollup(exprs: Vec) -> PyExpr { + Expr::GroupingSet(GroupingSet::Rollup( + exprs.into_iter().map(|e| e.expr).collect(), + )) + .into() + } + + #[staticmethod] + #[pyo3(signature = (*exprs))] + fn cube(exprs: Vec) -> PyExpr { + Expr::GroupingSet(GroupingSet::Cube( + exprs.into_iter().map(|e| e.expr).collect(), + )) + .into() + } + + #[staticmethod] + #[pyo3(signature = (*expr_lists))] + fn grouping_sets(expr_lists: Vec>) -> PyExpr { + Expr::GroupingSet(GroupingSet::GroupingSets( + expr_lists + .into_iter() + .map(|list| list.into_iter().map(|e| e.expr).collect()) + .collect(), + )) + .into() + } +} + impl From for GroupingSet { fn from(grouping_set: PyGroupingSet) -> Self { grouping_set.grouping_set diff --git a/crates/core/src/functions.rs b/crates/core/src/functions.rs index 74654ce46..f173aaa51 100644 --- a/crates/core/src/functions.rs +++ b/crates/core/src/functions.rs @@ -791,9 +791,10 @@ aggregate_function!(var_pop); aggregate_function!(approx_distinct); aggregate_function!(approx_median); -// Code is commented out since grouping is not yet implemented -// https://github.com/apache/datafusion-python/issues/861 -// aggregate_function!(grouping); +// The grouping function's physical plan is not implemented, but the +// ResolveGroupingFunction analyzer rule rewrites it before the physical +// planner sees it, so it works correctly at runtime. +aggregate_function!(grouping); #[pyfunction] #[pyo3(signature = (sort_expression, percentile, num_centroids=None, filter=None))] @@ -831,6 +832,19 @@ pub fn approx_percentile_cont_with_weight( add_builder_fns_to_aggregate(agg_fn, None, filter, None, None) } +#[pyfunction] +#[pyo3(signature = (sort_expression, percentile, filter=None))] +pub fn percentile_cont( + sort_expression: PySortExpr, + percentile: f64, + filter: Option, +) -> PyDataFusionResult { + let agg_fn = + functions_aggregate::expr_fn::percentile_cont(sort_expression.sort, lit(percentile)); + + add_builder_fns_to_aggregate(agg_fn, None, filter, None, None) +} + // We handle last_value explicitly because the signature expects an order_by // https://github.com/apache/datafusion/issues/12376 #[pyfunction] @@ -1031,6 +1045,7 @@ pub(crate) fn init_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(approx_median))?; m.add_wrapped(wrap_pyfunction!(approx_percentile_cont))?; m.add_wrapped(wrap_pyfunction!(approx_percentile_cont_with_weight))?; + m.add_wrapped(wrap_pyfunction!(percentile_cont))?; m.add_wrapped(wrap_pyfunction!(range))?; m.add_wrapped(wrap_pyfunction!(array_agg))?; m.add_wrapped(wrap_pyfunction!(arrow_typeof))?; @@ -1080,7 +1095,7 @@ pub(crate) fn init_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(from_unixtime))?; m.add_wrapped(wrap_pyfunction!(gcd))?; m.add_wrapped(wrap_pyfunction!(greatest))?; - // m.add_wrapped(wrap_pyfunction!(grouping))?; + m.add_wrapped(wrap_pyfunction!(grouping))?; m.add_wrapped(wrap_pyfunction!(in_list))?; m.add_wrapped(wrap_pyfunction!(initcap))?; m.add_wrapped(wrap_pyfunction!(isnan))?; diff --git a/docs/source/user-guide/common-operations/aggregations.rst b/docs/source/user-guide/common-operations/aggregations.rst index e458e5fcb..de24a2ba5 100644 --- a/docs/source/user-guide/common-operations/aggregations.rst +++ b/docs/source/user-guide/common-operations/aggregations.rst @@ -163,6 +163,168 @@ Suppose we want to find the speed values for only Pokemon that have low Attack v f.avg(col_speed, filter=col_attack < lit(50)).alias("Avg Speed Low Attack")]) +Grouping Sets +------------- + +The default style of aggregation produces one row per group. Sometimes you want a single query to +produce rows at multiple levels of detail — for example, totals per type *and* an overall grand +total, or subtotals for every combination of two columns plus the individual column totals. Writing +separate queries and concatenating them is tedious and runs the data multiple times. Grouping sets +solve this by letting you specify several grouping levels in one pass. + +DataFusion supports three grouping set styles through the +:py:class:`~datafusion.expr.GroupingSet` class: + +- :py:meth:`~datafusion.expr.GroupingSet.rollup` — hierarchical subtotals, like a drill-down report +- :py:meth:`~datafusion.expr.GroupingSet.cube` — every possible subtotal combination, like a pivot table +- :py:meth:`~datafusion.expr.GroupingSet.grouping_sets` — explicitly list exactly which grouping levels you want + +Because result rows come from different grouping levels, a column that is *not* part of a +particular level will be ``null`` in that row. Use :py:func:`~datafusion.functions.grouping` to +distinguish a real ``null`` in the data from one that means "this column was aggregated across." +It returns ``0`` when the column is a grouping key for that row, and ``1`` when it is not. + +Rollup +^^^^^^ + +:py:meth:`~datafusion.expr.GroupingSet.rollup` creates a hierarchy. ``rollup(a, b)`` produces +grouping sets ``(a, b)``, ``(a)``, and ``()`` — like nested subtotals in a report. This is useful +when your columns have a natural hierarchy, such as region → city or type → subtype. + +Suppose we want to summarize Pokemon stats by ``Type 1`` with subtotals and a grand total. With +the default aggregation style we would need two separate queries. With ``rollup`` we get it all at +once: + +.. ipython:: python + + from datafusion.expr import GroupingSet + + df.aggregate( + [GroupingSet.rollup(col_type_1)], + [f.count(col_speed).alias("Count"), + f.avg(col_speed).alias("Avg Speed"), + f.max(col_speed).alias("Max Speed")] + ).sort(col_type_1.sort(ascending=True, nulls_first=True)) + +The first row — where ``Type 1`` is ``null`` — is the grand total across all types. But how do you +tell a grand-total ``null`` apart from a Pokemon that genuinely has no type? The +:py:func:`~datafusion.functions.grouping` function returns ``0`` when the column is a grouping key +for that row and ``1`` when it is aggregated across. + +.. note:: + + Due to an upstream DataFusion limitation + (`apache/datafusion#21411 `_), + ``.alias()`` cannot be applied directly to a ``grouping()`` expression — it will raise an + error at execution time. Instead, use + :py:meth:`~datafusion.dataframe.DataFrame.with_column_renamed` on the result DataFrame to + give the column a readable name. Once the upstream issue is resolved, you will be able to + use ``.alias()`` directly and the workaround below will no longer be necessary. + +The raw column name generated by ``grouping()`` contains internal identifiers, so we use +:py:meth:`~datafusion.dataframe.DataFrame.with_column_renamed` to clean it up: + +.. ipython:: python + + result = df.aggregate( + [GroupingSet.rollup(col_type_1)], + [f.count(col_speed).alias("Count"), + f.avg(col_speed).alias("Avg Speed"), + f.grouping(col_type_1)] + ) + for field in result.schema(): + if field.name.startswith("grouping("): + result = result.with_column_renamed(field.name, "Is Total") + result.sort(col_type_1.sort(ascending=True, nulls_first=True)) + +With two columns the hierarchy becomes more apparent. ``rollup(Type 1, Type 2)`` produces: + +- one row per ``(Type 1, Type 2)`` pair — the most detailed level +- one row per ``Type 1`` — subtotals +- one grand total row + +.. ipython:: python + + df.aggregate( + [GroupingSet.rollup(col_type_1, col_type_2)], + [f.count(col_speed).alias("Count"), + f.avg(col_speed).alias("Avg Speed")] + ).sort( + col_type_1.sort(ascending=True, nulls_first=True), + col_type_2.sort(ascending=True, nulls_first=True) + ) + +Cube +^^^^ + +:py:meth:`~datafusion.expr.GroupingSet.cube` produces every possible subset. ``cube(a, b)`` +produces grouping sets ``(a, b)``, ``(a)``, ``(b)``, and ``()`` — one more than ``rollup`` because +it also includes ``(b)`` alone. This is useful when neither column is "above" the other in a +hierarchy and you want all cross-tabulations. + +For our Pokemon data, ``cube(Type 1, Type 2)`` gives us stats broken down by the type pair, +by ``Type 1`` alone, by ``Type 2`` alone, and a grand total — all in one query: + +.. ipython:: python + + df.aggregate( + [GroupingSet.cube(col_type_1, col_type_2)], + [f.count(col_speed).alias("Count"), + f.avg(col_speed).alias("Avg Speed")] + ).sort( + col_type_1.sort(ascending=True, nulls_first=True), + col_type_2.sort(ascending=True, nulls_first=True) + ) + +Compared to the ``rollup`` example above, notice the extra rows where ``Type 1`` is ``null`` but +``Type 2`` has a value — those are the per-``Type 2`` subtotals that ``rollup`` does not include. + +Explicit Grouping Sets +^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`~datafusion.expr.GroupingSet.grouping_sets` lets you list exactly which grouping levels +you need when ``rollup`` or ``cube`` would produce too many or too few. Each argument is a list of +columns forming one grouping set. + +For example, if we want only the per-``Type 1`` totals and per-``Type 2`` totals — but *not* the +full ``(Type 1, Type 2)`` detail rows or the grand total — we can ask for exactly that: + +.. ipython:: python + + df.aggregate( + [GroupingSet.grouping_sets([col_type_1], [col_type_2])], + [f.count(col_speed).alias("Count"), + f.avg(col_speed).alias("Avg Speed")] + ).sort( + col_type_1.sort(ascending=True, nulls_first=True), + col_type_2.sort(ascending=True, nulls_first=True) + ) + +Each row belongs to exactly one grouping level. The :py:func:`~datafusion.functions.grouping` +function tells you which level each row comes from: + +.. ipython:: python + + result = df.aggregate( + [GroupingSet.grouping_sets([col_type_1], [col_type_2])], + [f.count(col_speed).alias("Count"), + f.avg(col_speed).alias("Avg Speed"), + f.grouping(col_type_1), + f.grouping(col_type_2)] + ) + for field in result.schema(): + if field.name.startswith("grouping("): + clean = field.name.split(".")[-1].rstrip(")") + result = result.with_column_renamed(field.name, f"grouping({clean})") + result.sort( + col_type_1.sort(ascending=True, nulls_first=True), + col_type_2.sort(ascending=True, nulls_first=True) + ) + +Where ``grouping(Type 1)`` is ``0`` the row is a per-``Type 1`` total (and ``Type 2`` is ``null``). +Where ``grouping(Type 2)`` is ``0`` the row is a per-``Type 2`` total (and ``Type 1`` is ``null``). + + Aggregate Functions ------------------- @@ -192,6 +354,7 @@ The available aggregate functions are: - :py:func:`datafusion.functions.stddev_pop` - :py:func:`datafusion.functions.var_samp` - :py:func:`datafusion.functions.var_pop` + - :py:func:`datafusion.functions.var_population` 6. Linear Regression Functions - :py:func:`datafusion.functions.regr_count` - :py:func:`datafusion.functions.regr_slope` @@ -208,9 +371,16 @@ The available aggregate functions are: - :py:func:`datafusion.functions.nth_value` 8. String Functions - :py:func:`datafusion.functions.string_agg` -9. Approximation Functions +9. Percentile Functions + - :py:func:`datafusion.functions.percentile_cont` + - :py:func:`datafusion.functions.quantile_cont` - :py:func:`datafusion.functions.approx_distinct` - :py:func:`datafusion.functions.approx_median` - :py:func:`datafusion.functions.approx_percentile_cont` - :py:func:`datafusion.functions.approx_percentile_cont_with_weight` +10. Grouping Set Functions + - :py:func:`datafusion.functions.grouping` + - :py:meth:`datafusion.expr.GroupingSet.rollup` + - :py:meth:`datafusion.expr.GroupingSet.cube` + - :py:meth:`datafusion.expr.GroupingSet.grouping_sets` diff --git a/python/datafusion/dataframe.py b/python/datafusion/dataframe.py index 10e2a913f..9907eae8b 100644 --- a/python/datafusion/dataframe.py +++ b/python/datafusion/dataframe.py @@ -633,8 +633,22 @@ def aggregate( ) -> DataFrame: """Aggregates the rows of the current DataFrame. + By default each unique combination of the ``group_by`` columns + produces one row. To get multiple levels of subtotals in a + single pass, pass a + :py:class:`~datafusion.expr.GroupingSet` expression + (created via + :py:meth:`~datafusion.expr.GroupingSet.rollup`, + :py:meth:`~datafusion.expr.GroupingSet.cube`, or + :py:meth:`~datafusion.expr.GroupingSet.grouping_sets`) + as the ``group_by`` argument. See the + :ref:`aggregation` user guide for detailed examples. + Args: - group_by: Sequence of expressions or column names to group by. + group_by: Sequence of expressions or column names to group + by. A :py:class:`~datafusion.expr.GroupingSet` + expression may be included to produce multiple grouping + levels (rollup, cube, or explicit grouping sets). aggs: Sequence of expressions to aggregate. Returns: diff --git a/python/datafusion/expr.py b/python/datafusion/expr.py index 14753a4f5..35388468c 100644 --- a/python/datafusion/expr.py +++ b/python/datafusion/expr.py @@ -91,7 +91,6 @@ Extension = expr_internal.Extension FileType = expr_internal.FileType Filter = expr_internal.Filter -GroupingSet = expr_internal.GroupingSet Join = expr_internal.Join ILike = expr_internal.ILike InList = expr_internal.InList @@ -1430,3 +1429,129 @@ def __repr__(self) -> str: SortKey = Expr | SortExpr | str + + +class GroupingSet: + """Factory for creating grouping set expressions. + + Grouping sets control how + :py:meth:`~datafusion.dataframe.DataFrame.aggregate` groups rows. + Instead of a single ``GROUP BY``, they produce multiple grouping + levels in one pass — subtotals, cross-tabulations, or arbitrary + column subsets. + + Use :py:func:`~datafusion.functions.grouping` in the aggregate list + to tell which columns are aggregated across in each result row. + """ + + @staticmethod + def rollup(*exprs: Expr | str) -> Expr: + """Create a ``ROLLUP`` grouping set for use with ``aggregate()``. + + ``ROLLUP`` generates all prefixes of the given column list as + grouping sets. For example, ``rollup(a, b)`` produces grouping + sets ``(a, b)``, ``(a)``, and ``()`` (grand total). + + This is equivalent to ``GROUP BY ROLLUP(a, b)`` in SQL. + + Args: + *exprs: Column expressions or column name strings to + include in the rollup. + + Examples: + >>> from datafusion.expr import GroupingSet + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 1, 2], "b": [10, 20, 30]}) + >>> result = df.aggregate( + ... [GroupingSet.rollup(dfn.col("a"))], + ... [dfn.functions.sum(dfn.col("b")).alias("s"), + ... dfn.functions.grouping(dfn.col("a"))], + ... ).sort(dfn.col("a").sort(nulls_first=False)) + >>> result.collect_column("s").to_pylist() + [30, 30, 60] + + See Also: + :py:meth:`cube`, :py:meth:`grouping_sets`, + :py:func:`~datafusion.functions.grouping` + """ + args = [_to_raw_expr(e) for e in exprs] + return Expr(expr_internal.GroupingSet.rollup(*args)) + + @staticmethod + def cube(*exprs: Expr | str) -> Expr: + """Create a ``CUBE`` grouping set for use with ``aggregate()``. + + ``CUBE`` generates all possible subsets of the given column list + as grouping sets. For example, ``cube(a, b)`` produces grouping + sets ``(a, b)``, ``(a)``, ``(b)``, and ``()`` (grand total). + + This is equivalent to ``GROUP BY CUBE(a, b)`` in SQL. + + Args: + *exprs: Column expressions or column name strings to + include in the cube. + + Examples: + With a single column, ``cube`` behaves identically to + :py:meth:`rollup`: + + >>> from datafusion.expr import GroupingSet + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 1, 2], "b": [10, 20, 30]}) + >>> result = df.aggregate( + ... [GroupingSet.cube(dfn.col("a"))], + ... [dfn.functions.sum(dfn.col("b")).alias("s"), + ... dfn.functions.grouping(dfn.col("a"))], + ... ).sort(dfn.col("a").sort(nulls_first=False)) + >>> result.collect_column("s").to_pylist() + [30, 30, 60] + + See Also: + :py:meth:`rollup`, :py:meth:`grouping_sets`, + :py:func:`~datafusion.functions.grouping` + """ + args = [_to_raw_expr(e) for e in exprs] + return Expr(expr_internal.GroupingSet.cube(*args)) + + @staticmethod + def grouping_sets(*expr_lists: list[Expr | str]) -> Expr: + """Create explicit grouping sets for use with ``aggregate()``. + + Each argument is a list of column expressions or column name + strings representing one grouping set. For example, + ``grouping_sets([a], [b])`` groups by ``a`` alone and by ``b`` + alone in a single query. + + This is equivalent to ``GROUP BY GROUPING SETS ((a), (b))`` in + SQL. + + Args: + *expr_lists: Each positional argument is a list of + expressions or column name strings forming one + grouping set. + + Examples: + >>> from datafusion.expr import GroupingSet + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict( + ... {"a": ["x", "x", "y"], "b": ["m", "n", "m"], + ... "c": [1, 2, 3]}) + >>> result = df.aggregate( + ... [GroupingSet.grouping_sets( + ... [dfn.col("a")], [dfn.col("b")])], + ... [dfn.functions.sum(dfn.col("c")).alias("s"), + ... dfn.functions.grouping(dfn.col("a")), + ... dfn.functions.grouping(dfn.col("b"))], + ... ).sort( + ... dfn.col("a").sort(nulls_first=False), + ... dfn.col("b").sort(nulls_first=False), + ... ) + >>> result.collect_column("s").to_pylist() + [3, 3, 4, 2] + + See Also: + :py:meth:`rollup`, :py:meth:`cube`, + :py:func:`~datafusion.functions.grouping` + """ + raw_lists = [[_to_raw_expr(e) for e in lst] for lst in expr_lists] + return Expr(expr_internal.GroupingSet.grouping_sets(*raw_lists)) diff --git a/python/datafusion/functions.py b/python/datafusion/functions.py index aa7f28746..9dfabb62d 100644 --- a/python/datafusion/functions.py +++ b/python/datafusion/functions.py @@ -166,6 +166,7 @@ "generate_series", "get_field", "greatest", + "grouping", "ifnull", "in_list", "initcap", @@ -256,9 +257,11 @@ "order_by", "overlay", "percent_rank", + "percentile_cont", "pi", "pow", "power", + "quantile_cont", "radians", "random", "range", @@ -331,6 +334,7 @@ "uuid", "var", "var_pop", + "var_population", "var_samp", "var_sample", "version", @@ -2654,7 +2658,6 @@ def arrow_cast(expr: Expr, data_type: Expr | str | pa.DataType) -> Expr: >>> result.collect_column("c")[0].as_py() 1.0 - >>> import pyarrow as pa >>> result = df.select( ... dfn.functions.arrow_cast( ... dfn.col("a"), data_type=pa.float64() @@ -2677,7 +2680,6 @@ def arrow_metadata(expr: Expr, key: Expr | str | None = None) -> Expr: If called with two arguments, returns the value for the specified metadata key. Examples: - >>> import pyarrow as pa >>> field = pa.field("val", pa.int64(), metadata={"k": "v"}) >>> schema = pa.schema([field]) >>> batch = pa.RecordBatch.from_arrays([pa.array([1])], schema=schema) @@ -2746,7 +2748,6 @@ def union_extract(union_expr: Expr, field_name: Expr | str) -> Expr: variant, otherwise returns NULL. Examples: - >>> import pyarrow as pa >>> ctx = dfn.SessionContext() >>> types = pa.array([0, 1, 0], type=pa.int8()) >>> offsets = pa.array([0, 0, 1], type=pa.int32()) @@ -2771,7 +2772,6 @@ def union_tag(union_expr: Expr) -> Expr: """Returns the tag (active field name) of a union type. Examples: - >>> import pyarrow as pa >>> ctx = dfn.SessionContext() >>> types = pa.array([0, 1, 0], type=pa.int8()) >>> offsets = pa.array([0, 0, 1], type=pa.int32()) @@ -4306,6 +4306,60 @@ def approx_percentile_cont_with_weight( ) +def percentile_cont( + sort_expression: Expr | SortExpr, + percentile: float, + filter: Expr | None = None, +) -> Expr: + """Computes the exact percentile of input values using continuous interpolation. + + Unlike :py:func:`approx_percentile_cont`, this function computes the exact + percentile value rather than an approximation. + + If using the builder functions described in ref:`_aggregation` this function ignores + the options ``order_by``, ``null_treatment``, and ``distinct``. + + Args: + sort_expression: Values for which to find the percentile + percentile: This must be between 0.0 and 1.0, inclusive + filter: If provided, only compute against rows for which the filter is True + + Examples: + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1.0, 2.0, 3.0, 4.0, 5.0]}) + >>> result = df.aggregate( + ... [], [dfn.functions.percentile_cont( + ... dfn.col("a"), 0.5 + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 3.0 + + >>> result = df.aggregate( + ... [], [dfn.functions.percentile_cont( + ... dfn.col("a"), 0.5, + ... filter=dfn.col("a") > dfn.lit(1.0), + ... ).alias("v")]) + >>> result.collect_column("v")[0].as_py() + 3.5 + """ + sort_expr_raw = sort_or_default(sort_expression) + filter_raw = filter.expr if filter is not None else None + return Expr(f.percentile_cont(sort_expr_raw, percentile, filter=filter_raw)) + + +def quantile_cont( + sort_expression: Expr | SortExpr, + percentile: float, + filter: Expr | None = None, +) -> Expr: + """Computes the exact percentile of input values using continuous interpolation. + + See Also: + This is an alias for :py:func:`percentile_cont`. + """ + return percentile_cont(sort_expression, percentile, filter) + + def array_agg( expression: Expr, distinct: bool = False, @@ -4364,6 +4418,65 @@ def array_agg( ) +def grouping( + expression: Expr, + distinct: bool = False, + filter: Expr | None = None, +) -> Expr: + """Indicates whether a column is aggregated across in the current row. + + Returns 0 when the column is part of the grouping key for that row + (i.e., the row contains per-group results for that column). Returns 1 + when the column is *not* part of the grouping key (i.e., the row's + aggregate spans all values of that column). + + This function is meaningful with + :py:meth:`GroupingSet.rollup `, + :py:meth:`GroupingSet.cube `, or + :py:meth:`GroupingSet.grouping_sets `, + where different rows are grouped by different subsets of columns. In a + default aggregation without grouping sets every column is always part + of the key, so ``grouping()`` always returns 0. + + .. warning:: + + Due to an upstream DataFusion limitation + (`#21411 `_), + ``.alias()`` cannot be applied directly to a ``grouping()`` + expression. Doing so will raise an error at execution time. To + rename the column, use + :py:meth:`~datafusion.dataframe.DataFrame.with_column_renamed` + on the result DataFrame instead. + + Args: + expression: The column to check grouping status for + distinct: If True, compute on distinct values only + filter: If provided, only compute against rows for which the filter is True + + Examples: + With :py:meth:`~datafusion.expr.GroupingSet.rollup`, the result + includes both per-group rows (``grouping(a) = 0``) and a + grand-total row where ``a`` is aggregated across + (``grouping(a) = 1``): + + >>> from datafusion.expr import GroupingSet + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 1, 2], "b": [10, 20, 30]}) + >>> result = df.aggregate( + ... [GroupingSet.rollup(dfn.col("a"))], + ... [dfn.functions.sum(dfn.col("b")).alias("s"), + ... dfn.functions.grouping(dfn.col("a"))], + ... ).sort(dfn.col("a").sort(nulls_first=False)) + >>> result.collect_column("s").to_pylist() + [30, 30, 60] + + See Also: + :py:class:`~datafusion.expr.GroupingSet` + """ + filter_raw = filter.expr if filter is not None else None + return Expr(f.grouping(expression.expr, distinct=distinct, filter=filter_raw)) + + def avg( expression: Expr, filter: Expr | None = None, @@ -4835,6 +4948,15 @@ def var_pop(expression: Expr, filter: Expr | None = None) -> Expr: return Expr(f.var_pop(expression.expr, filter=filter_raw)) +def var_population(expression: Expr, filter: Expr | None = None) -> Expr: + """Computes the population variance of the argument. + + See Also: + This is an alias for :py:func:`var_pop`. + """ + return var_pop(expression, filter) + + def var_samp(expression: Expr, filter: Expr | None = None) -> Expr: """Computes the sample variance of the argument. diff --git a/python/tests/test_functions.py b/python/tests/test_functions.py index 4e99fa9e3..11e94af1c 100644 --- a/python/tests/test_functions.py +++ b/python/tests/test_functions.py @@ -22,6 +22,7 @@ import pytest from datafusion import SessionContext, column, literal from datafusion import functions as f +from datafusion.expr import GroupingSet np.seterr(invalid="ignore") @@ -1820,6 +1821,114 @@ def test_conditional_functions(df_with_nulls, expr, expected): assert result.column(0) == expected +@pytest.mark.parametrize( + ("func", "filter_expr", "expected"), + [ + (f.percentile_cont, None, 3.0), + (f.percentile_cont, column("a") > literal(1.0), 3.5), + (f.quantile_cont, None, 3.0), + ], + ids=["no_filter", "with_filter", "quantile_cont_alias"], +) +def test_percentile_cont(func, filter_expr, expected): + ctx = SessionContext() + df = ctx.from_pydict({"a": [1.0, 2.0, 3.0, 4.0, 5.0]}) + result = df.aggregate( + [], [func(column("a"), 0.5, filter=filter_expr).alias("v")] + ).collect()[0] + assert result.column(0)[0].as_py() == expected + + +@pytest.mark.parametrize( + ("grouping_set_expr", "expected_grouping", "expected_sums"), + [ + (GroupingSet.rollup(column("a")), [0, 0, 1], [30, 30, 60]), + (GroupingSet.cube(column("a")), [0, 0, 1], [30, 30, 60]), + (GroupingSet.rollup("a"), [0, 0, 1], [30, 30, 60]), + (GroupingSet.cube("a"), [0, 0, 1], [30, 30, 60]), + ], + ids=["rollup", "cube", "rollup_str", "cube_str"], +) +def test_grouping_set_single_column( + grouping_set_expr, expected_grouping, expected_sums +): + ctx = SessionContext() + df = ctx.from_pydict({"a": [1, 1, 2], "b": [10, 20, 30]}) + result = df.aggregate( + [grouping_set_expr], + [f.sum(column("b")).alias("s"), f.grouping(column("a"))], + ).sort(column("a").sort(ascending=True, nulls_first=False)) + batches = result.collect() + g = pa.concat_arrays([b.column(2) for b in batches]).to_pylist() + s = pa.concat_arrays([b.column("s") for b in batches]).to_pylist() + assert g == expected_grouping + assert s == expected_sums + + +@pytest.mark.parametrize( + ("grouping_set_expr", "expected_rows"), + [ + # rollup(a, b) => (a,b), (a), () => 3 + 2 + 1 = 6 + (GroupingSet.rollup(column("a"), column("b")), 6), + # cube(a, b) => (a,b), (a), (b), () => 3 + 2 + 2 + 1 = 8 + (GroupingSet.cube(column("a"), column("b")), 8), + (GroupingSet.rollup("a", "b"), 6), + (GroupingSet.cube("a", "b"), 8), + ], + ids=["rollup", "cube", "rollup_str", "cube_str"], +) +def test_grouping_set_multi_column(grouping_set_expr, expected_rows): + ctx = SessionContext() + df = ctx.from_pydict({"a": [1, 1, 2], "b": ["x", "y", "x"], "c": [10, 20, 30]}) + result = df.aggregate( + [grouping_set_expr], + [f.sum(column("c")).alias("s")], + ) + total_rows = sum(b.num_rows for b in result.collect()) + assert total_rows == expected_rows + + +@pytest.mark.parametrize( + "grouping_set_expr", + [ + GroupingSet.grouping_sets([column("a")], [column("b")]), + GroupingSet.grouping_sets(["a"], ["b"]), + ], + ids=["expr", "str"], +) +def test_grouping_sets_explicit(grouping_set_expr): + # Each row's grouping() value tells you which columns are aggregated across. + ctx = SessionContext() + df = ctx.from_pydict({"a": ["x", "x", "y"], "b": ["m", "n", "m"], "c": [1, 2, 3]}) + result = df.aggregate( + [grouping_set_expr], + [ + f.sum(column("c")).alias("s"), + f.grouping(column("a")), + f.grouping(column("b")), + ], + ).sort( + column("a").sort(ascending=True, nulls_first=False), + column("b").sort(ascending=True, nulls_first=False), + ) + batches = result.collect() + ga = pa.concat_arrays([b.column(3) for b in batches]).to_pylist() + gb = pa.concat_arrays([b.column(4) for b in batches]).to_pylist() + # Rows grouped by (a): ga=0 (a is a key), gb=1 (b is aggregated across) + # Rows grouped by (b): ga=1 (a is aggregated across), gb=0 (b is a key) + assert ga == [0, 0, 1, 1] + assert gb == [1, 1, 0, 0] + + +def test_var_population(): + ctx = SessionContext() + df = ctx.from_pydict({"a": [-1.0, 0.0, 2.0]}) + result = df.aggregate([], [f.var_population(column("a")).alias("v")]).collect()[0] + # var_population is an alias for var_pop + expected = df.aggregate([], [f.var_pop(column("a")).alias("v")]).collect()[0] + assert abs(result.column(0)[0].as_py() - expected.column(0)[0].as_py()) < 1e-10 + + def test_get_field(df): df = df.with_column( "s", From 52932128d353e417ddae2c5ff3f14135cb806f7e Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Tue, 7 Apr 2026 14:58:09 -0400 Subject: [PATCH 46/56] Add missing Dataframe functions (#1472) * Add missing DataFrame methods for set operations and query Expose upstream DataFusion DataFrame methods that were not yet available in the Python API. Closes #1455. Set operations: - except_distinct: set difference with deduplication - intersect_distinct: set intersection with deduplication - union_by_name: union matching columns by name instead of position - union_by_name_distinct: union by name with deduplication Query: - distinct_on: deduplicate rows based on specific columns - sort_by: sort by expressions with ascending order and nulls last Note: show_limit is already covered by the existing show(num) method. explain_with_options and with_param_values are deferred as they require exposing additional types (ExplainOption, ParamValues). Co-Authored-By: Claude Opus 4.6 (1M context) * Add ExplainFormat enum and format option to DataFrame.explain() Extend the existing explain() method with an optional format parameter instead of adding a separate explain_with_options() method. This keeps the API simple while exposing all upstream ExplainOption functionality. Available formats: indent (default), tree, pgjson, graphviz. The ExplainFormat enum is exported from the top-level datafusion module. Co-Authored-By: Claude Opus 4.6 (1M context) * Add DataFrame.window() and unnest recursion options Expose remaining DataFrame methods from upstream DataFusion. Closes #1456. - window(*exprs): apply window function expressions and append results as new columns - unnest_column/unnest_columns: add optional recursions parameter for controlling unnest depth via (input_column, output_column, depth) tuples Note: drop_columns is already exposed as the existing drop() method. Co-Authored-By: Claude Opus 4.6 (1M context) * Update docstring Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Improve docstrings and test robustness for new DataFrame methods Clarify except_distinct/intersect_distinct docstrings, add deterministic sort to test_window, add sort_by ascending verification test, and add smoke tests for PGJSON and GRAPHVIZ explain formats. Co-Authored-By: Claude Opus 4.6 (1M context) * Consolidate new DataFrame tests into parametrized tests Combine set operation tests (except_distinct, intersect_distinct, union_by_name, union_by_name_distinct) into a single parametrized test_set_operations_distinct. Merge sort_by tests and convert explain format tests to parametrized form. Co-Authored-By: Claude Opus 4.6 (1M context) * Add doctest examples to new DataFrame method docstrings Add >>> style usage examples for window, explain, except_distinct, intersect_distinct, union_by_name, union_by_name_distinct, distinct_on, sort_by, and unnest_columns to match existing docstring conventions. Co-Authored-By: Claude Opus 4.6 (1M context) * Improve error messages, tests, and API hygiene from PR review - Provide actionable error message for invalid explain format strings - Remove recursions param from deprecated unnest_column (use unnest_columns) - Add null-handling test case for sort_by to verify nulls-last behavior - Add format-specific assertions to explain tests (TREE, PGJSON, GRAPHVIZ) - Add deep recursion test for unnest_columns with depth > 1 - Add multi-expression window test to verify variadic *exprs Co-Authored-By: Claude Opus 4.6 (1M context) * Consolidate window and unnest tests into parametrized tests Combine test_window and test_window_multiple_expressions into a single parametrized test. Merge unnest recursion tests into one parametrized test covering basic, explicit depth 1, and deep recursion cases. Co-Authored-By: Claude Opus 4.6 (1M context) * Address PR review feedback for DataFrame operations - Use upstream parse error for explain format instead of hardcoded options - Fix sort_by to use column name resolution consistent with sort() - Use ExplainFormat enum members directly in tests instead of string lookup - Merge union_by_name_distinct into union_by_name(distinct=False) for a more Pythonic API - Update check-upstream skill to note union_by_name_distinct coverage Co-Authored-By: Claude Opus 4.6 (1M context) * Add DataFrame.column(), col(), and find_qualified_columns() methods Expose upstream find_qualified_columns to resolve unqualified column names into fully qualified column expressions. This is especially useful for disambiguating columns after joins. - find_qualified_columns(*names) on Rust side calls upstream directly - DataFrame.column(name) and col(name) alias on Python side - Update join and join_on docstrings to reference DataFrame.col() - Add "Disambiguating Columns with DataFrame.col()" section to joins docs - Add tests for qualified column resolution, ambiguity, and join usage Co-Authored-By: Claude Opus 4.6 (1M context) * Merge union_by_name and union_by_name_distinct into a single method with distinct flag Co-Authored-By: Claude Opus 4.6 (1M context) * converting into a python dict loses a column when the names are identical * Consolidate except_all/except_distinct and intersect/intersect_distinct into single methods with distinct flag Follows the same pattern as union(distinct=) and union_by_name(distinct=). Also deprecates union_distinct() in favor of union(distinct=True). Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .ai/skills/check-upstream/SKILL.md | 1 + crates/core/src/dataframe.rs | 157 ++++++-- .../user-guide/common-operations/joins.rst | 33 ++ python/datafusion/__init__.py | 2 + python/datafusion/dataframe.py | 365 +++++++++++++++++- python/tests/test_dataframe.py | 261 +++++++++++++ 6 files changed, 767 insertions(+), 52 deletions(-) diff --git a/.ai/skills/check-upstream/SKILL.md b/.ai/skills/check-upstream/SKILL.md index f77210371..ac4835a4e 100644 --- a/.ai/skills/check-upstream/SKILL.md +++ b/.ai/skills/check-upstream/SKILL.md @@ -109,6 +109,7 @@ The user may specify an area via `$ARGUMENTS`. If no area is specified or "all" **Evaluated and not requiring separate Python exposure:** - `show_limit` — already covered by `DataFrame.show()`, which provides the same functionality with a simpler API - `with_param_values` — already covered by the `param_values` argument on `SessionContext.sql()`, which accomplishes the same thing more robustly +- `union_by_name_distinct` — already covered by `DataFrame.union_by_name(distinct=True)`, which provides a more Pythonic API **How to check:** 1. Fetch the upstream DataFrame documentation page listing all methods diff --git a/crates/core/src/dataframe.rs b/crates/core/src/dataframe.rs index 72595ba81..fff5118d5 100644 --- a/crates/core/src/dataframe.rs +++ b/crates/core/src/dataframe.rs @@ -582,6 +582,14 @@ impl PyDataFrame { Ok(Self::new(df)) } + /// Apply window function expressions to the DataFrame + #[pyo3(signature = (*exprs))] + fn window(&self, exprs: Vec) -> PyDataFusionResult { + let window_exprs = exprs.into_iter().map(|e| e.into()).collect(); + let df = self.df.as_ref().clone().window(window_exprs)?; + Ok(Self::new(df)) + } + fn filter(&self, predicate: PyExpr) -> PyDataFusionResult { let df = self.df.as_ref().clone().filter(predicate.into())?; Ok(Self::new(df)) @@ -804,9 +812,27 @@ impl PyDataFrame { } /// Print the query plan - #[pyo3(signature = (verbose=false, analyze=false))] - fn explain(&self, py: Python, verbose: bool, analyze: bool) -> PyDataFusionResult<()> { - let df = self.df.as_ref().clone().explain(verbose, analyze)?; + #[pyo3(signature = (verbose=false, analyze=false, format=None))] + fn explain( + &self, + py: Python, + verbose: bool, + analyze: bool, + format: Option<&str>, + ) -> PyDataFusionResult<()> { + let explain_format = match format { + Some(f) => f + .parse::() + .map_err(|e| { + PyDataFusionError::Common(format!("Invalid explain format '{}': {}", f, e)) + })?, + None => datafusion::common::format::ExplainFormat::Indent, + }; + let opts = datafusion::logical_expr::ExplainOption::default() + .with_verbose(verbose) + .with_analyze(analyze) + .with_format(explain_format); + let df = self.df.as_ref().clone().explain_with_options(opts)?; print_dataframe(py, df) } @@ -864,22 +890,14 @@ impl PyDataFrame { Ok(Self::new(new_df)) } - /// Calculate the distinct union of two `DataFrame`s. The - /// two `DataFrame`s must have exactly the same schema - fn union_distinct(&self, py_df: PyDataFrame) -> PyDataFusionResult { - let new_df = self - .df - .as_ref() - .clone() - .union_distinct(py_df.df.as_ref().clone())?; - Ok(Self::new(new_df)) - } - - #[pyo3(signature = (column, preserve_nulls=true))] - fn unnest_column(&self, column: &str, preserve_nulls: bool) -> PyDataFusionResult { - // TODO: expose RecursionUnnestOptions - // REF: https://github.com/apache/datafusion/pull/11577 - let unnest_options = UnnestOptions::default().with_preserve_nulls(preserve_nulls); + #[pyo3(signature = (column, preserve_nulls=true, recursions=None))] + fn unnest_column( + &self, + column: &str, + preserve_nulls: bool, + recursions: Option>, + ) -> PyDataFusionResult { + let unnest_options = build_unnest_options(preserve_nulls, recursions); let df = self .df .as_ref() @@ -888,15 +906,14 @@ impl PyDataFrame { Ok(Self::new(df)) } - #[pyo3(signature = (columns, preserve_nulls=true))] + #[pyo3(signature = (columns, preserve_nulls=true, recursions=None))] fn unnest_columns( &self, columns: Vec, preserve_nulls: bool, + recursions: Option>, ) -> PyDataFusionResult { - // TODO: expose RecursionUnnestOptions - // REF: https://github.com/apache/datafusion/pull/11577 - let unnest_options = UnnestOptions::default().with_preserve_nulls(preserve_nulls); + let unnest_options = build_unnest_options(preserve_nulls, recursions); let cols = columns.iter().map(|s| s.as_ref()).collect::>(); let df = self .df @@ -907,21 +924,79 @@ impl PyDataFrame { } /// Calculate the intersection of two `DataFrame`s. The two `DataFrame`s must have exactly the same schema - fn intersect(&self, py_df: PyDataFrame) -> PyDataFusionResult { - let new_df = self - .df - .as_ref() - .clone() - .intersect(py_df.df.as_ref().clone())?; + #[pyo3(signature = (py_df, distinct=false))] + fn intersect(&self, py_df: PyDataFrame, distinct: bool) -> PyDataFusionResult { + let base = self.df.as_ref().clone(); + let other = py_df.df.as_ref().clone(); + let new_df = if distinct { + base.intersect_distinct(other)? + } else { + base.intersect(other)? + }; Ok(Self::new(new_df)) } /// Calculate the exception of two `DataFrame`s. The two `DataFrame`s must have exactly the same schema - fn except_all(&self, py_df: PyDataFrame) -> PyDataFusionResult { - let new_df = self.df.as_ref().clone().except(py_df.df.as_ref().clone())?; + #[pyo3(signature = (py_df, distinct=false))] + fn except_all(&self, py_df: PyDataFrame, distinct: bool) -> PyDataFusionResult { + let base = self.df.as_ref().clone(); + let other = py_df.df.as_ref().clone(); + let new_df = if distinct { + base.except_distinct(other)? + } else { + base.except(other)? + }; Ok(Self::new(new_df)) } + /// Union two DataFrames matching columns by name + #[pyo3(signature = (py_df, distinct=false))] + fn union_by_name(&self, py_df: PyDataFrame, distinct: bool) -> PyDataFusionResult { + let base = self.df.as_ref().clone(); + let other = py_df.df.as_ref().clone(); + let new_df = if distinct { + base.union_by_name_distinct(other)? + } else { + base.union_by_name(other)? + }; + Ok(Self::new(new_df)) + } + + /// Deduplicate rows based on specific columns, keeping the first row per group + fn distinct_on( + &self, + on_expr: Vec, + select_expr: Vec, + sort_expr: Option>, + ) -> PyDataFusionResult { + let on_expr = on_expr.into_iter().map(|e| e.into()).collect(); + let select_expr = select_expr.into_iter().map(|e| e.into()).collect(); + let sort_expr = sort_expr.map(to_sort_expressions); + let df = self + .df + .as_ref() + .clone() + .distinct_on(on_expr, select_expr, sort_expr)?; + Ok(Self::new(df)) + } + + /// Sort by column expressions with ascending order and nulls last + fn sort_by(&self, exprs: Vec) -> PyDataFusionResult { + let exprs = exprs.into_iter().map(|e| e.into()).collect(); + let df = self.df.as_ref().clone().sort_by(exprs)?; + Ok(Self::new(df)) + } + + /// Return fully qualified column expressions for the given column names + fn find_qualified_columns(&self, names: Vec) -> PyDataFusionResult> { + let name_refs: Vec<&str> = names.iter().map(|s| s.as_str()).collect(); + let qualified = self.df.find_qualified_columns(&name_refs)?; + Ok(qualified + .into_iter() + .map(|q| Expr::Column(Column::from(q)).into()) + .collect()) + } + /// Write a `DataFrame` to a CSV file. fn write_csv( &self, @@ -1295,6 +1370,26 @@ impl PyDataFrameWriteOptions { } } +fn build_unnest_options( + preserve_nulls: bool, + recursions: Option>, +) -> UnnestOptions { + let mut opts = UnnestOptions::default().with_preserve_nulls(preserve_nulls); + if let Some(recs) = recursions { + opts.recursions = recs + .into_iter() + .map( + |(input, output, depth)| datafusion::common::RecursionUnnestOption { + input_column: datafusion::common::Column::from(input.as_str()), + output_column: datafusion::common::Column::from(output.as_str()), + depth, + }, + ) + .collect(); + } + opts +} + /// Print DataFrame fn print_dataframe(py: Python, df: DataFrame) -> PyDataFusionResult<()> { // Get string representation of record batches diff --git a/docs/source/user-guide/common-operations/joins.rst b/docs/source/user-guide/common-operations/joins.rst index 1d9d70385..a289c9377 100644 --- a/docs/source/user-guide/common-operations/joins.rst +++ b/docs/source/user-guide/common-operations/joins.rst @@ -134,3 +134,36 @@ In contrast to the above example, if we wish to get both columns: .. ipython:: python left.join(right, "id", how="inner", coalesce_duplicate_keys=False) + +Disambiguating Columns with ``DataFrame.col()`` +------------------------------------------------ + +When both DataFrames contain non-key columns with the same name, you can use +:py:meth:`~datafusion.dataframe.DataFrame.col` on each DataFrame **before** the +join to create fully qualified column references. These references can then be +used in the join predicate and when selecting from the result. + +This is especially useful with :py:meth:`~datafusion.dataframe.DataFrame.join_on`, +which accepts expression-based predicates. + +.. ipython:: python + + left = ctx.from_pydict( + { + "id": [1, 2, 3], + "val": [10, 20, 30], + } + ) + + right = ctx.from_pydict( + { + "id": [1, 2, 3], + "val": [40, 50, 60], + } + ) + + joined = left.join_on( + right, left.col("id") == right.col("id"), how="inner" + ) + + joined.select(left.col("id"), left.col("val"), right.col("val")) diff --git a/python/datafusion/__init__.py b/python/datafusion/__init__.py index 2e6f81166..a736c3966 100644 --- a/python/datafusion/__init__.py +++ b/python/datafusion/__init__.py @@ -47,6 +47,7 @@ from .dataframe import ( DataFrame, DataFrameWriteOptions, + ExplainFormat, InsertOp, ParquetColumnOptions, ParquetWriterOptions, @@ -82,6 +83,7 @@ "DataFrameWriteOptions", "Database", "ExecutionPlan", + "ExplainFormat", "Expr", "InsertOp", "LogicalPlan", diff --git a/python/datafusion/dataframe.py b/python/datafusion/dataframe.py index 9907eae8b..9dc5f0e7d 100644 --- a/python/datafusion/dataframe.py +++ b/python/datafusion/dataframe.py @@ -44,6 +44,7 @@ Expr, SortExpr, SortKey, + _to_raw_expr, ensure_expr, ensure_expr_list, expr_list_to_raw_expr_list, @@ -65,6 +66,25 @@ from enum import Enum +class ExplainFormat(Enum): + """Output format for explain plans. + + Controls how the query plan is rendered in :py:meth:`DataFrame.explain`. + """ + + INDENT = "indent" + """Default indented text format.""" + + TREE = "tree" + """Tree-style visual format with box-drawing characters.""" + + PGJSON = "pgjson" + """PostgreSQL-compatible JSON format for use with visualization tools.""" + + GRAPHVIZ = "graphviz" + """Graphviz DOT format for graph rendering.""" + + # excerpt from deltalake # https://github.com/apache/datafusion-python/pull/981#discussion_r1905619163 class Compression(Enum): @@ -395,6 +415,80 @@ def schema(self) -> pa.Schema: """ return self.df.schema() + def column(self, name: str) -> Expr: + """Return a fully qualified column expression for ``name``. + + Resolves an unqualified column name against this DataFrame's schema + and returns an :py:class:`Expr` whose underlying column reference + includes the table qualifier. This is especially useful after joins, + where the same column name may appear in multiple relations. + + Args: + name: Unqualified column name to look up. + + Returns: + A fully qualified column expression. + + Raises: + Exception: If the column is not found or is ambiguous (exists in + multiple relations). + + Examples: + Resolve a column from a simple DataFrame: + + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2], "b": [3, 4]}) + >>> expr = df.column("a") + >>> df.select(expr).to_pydict() + {'a': [1, 2]} + + Resolve qualified columns after a join: + + >>> left = ctx.from_pydict({"id": [1, 2], "x": [10, 20]}) + >>> right = ctx.from_pydict({"id": [1, 2], "y": [30, 40]}) + >>> joined = left.join(right, on="id", how="inner") + >>> expr = joined.column("y") + >>> joined.select("id", expr).sort("id").to_pydict() + {'id': [1, 2], 'y': [30, 40]} + """ + return self.find_qualified_columns(name)[0] + + def col(self, name: str) -> Expr: + """Alias for :py:meth:`column`. + + See Also: + :py:meth:`column` + """ + return self.column(name) + + def find_qualified_columns(self, *names: str) -> list[Expr]: + """Return fully qualified column expressions for the given names. + + This is a batch version of :py:meth:`column` — it resolves each + unqualified name against the DataFrame's schema and returns a list + of qualified column expressions. + + Args: + names: Unqualified column names to look up. + + Returns: + List of fully qualified column expressions, one per name. + + Raises: + Exception: If any column is not found or is ambiguous. + + Examples: + Resolve multiple columns at once: + + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2], "b": [3, 4], "c": [5, 6]}) + >>> exprs = df.find_qualified_columns("a", "c") + >>> df.select(*exprs).to_pydict() + {'a': [1, 2], 'c': [5, 6]} + """ + raw_exprs = self.df.find_qualified_columns(list(names)) + return [Expr(e) for e in raw_exprs] + @deprecated( "select_columns() is deprecated. Use :py:meth:`~DataFrame.select` instead" ) @@ -468,6 +562,36 @@ def drop(self, *columns: str) -> DataFrame: """ return DataFrame(self.df.drop(*columns)) + def window(self, *exprs: Expr) -> DataFrame: + """Add window function columns to the DataFrame. + + Applies the given window function expressions and appends the results + as new columns. + + Args: + exprs: Window function expressions to evaluate. + + Returns: + DataFrame with new window function columns appended. + + Examples: + Add a row number within each group: + + >>> import datafusion.functions as f + >>> from datafusion import col + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2, 3], "b": ["x", "x", "y"]}) + >>> df = df.window( + ... f.row_number( + ... partition_by=[col("b")], order_by=[col("a")] + ... ).alias("rn") + ... ) + >>> "rn" in df.schema().names + True + """ + raw = expr_list_to_raw_expr_list(exprs) + return DataFrame(self.df.window(*raw)) + def filter(self, *predicates: Expr | str) -> DataFrame: """Return a DataFrame for which ``predicate`` evaluates to ``True``. @@ -837,7 +961,13 @@ def join( ) -> DataFrame: """Join this :py:class:`DataFrame` with another :py:class:`DataFrame`. - `on` has to be provided or both `left_on` and `right_on` in conjunction. + ``on`` has to be provided or both ``left_on`` and ``right_on`` in + conjunction. + + When non-key columns share the same name in both DataFrames, use + :py:meth:`DataFrame.col` on each DataFrame **before** the join to + obtain fully qualified column references that can disambiguate them. + See :py:meth:`join_on` for an example. Args: right: Other DataFrame to join with. @@ -911,7 +1041,14 @@ def join_on( built with :func:`datafusion.col`. On expressions are used to support in-equality predicates. Equality predicates are correctly optimized. + Use :py:meth:`DataFrame.col` on each DataFrame **before** the join to + obtain fully qualified column references. These qualified references + can then be used in the join predicate and to disambiguate columns + with the same name when selecting from the result. + Examples: + Join with unique column names: + >>> ctx = dfn.SessionContext() >>> left = ctx.from_pydict({"a": [1, 2], "x": ["a", "b"]}) >>> right = ctx.from_pydict({"b": [1, 2], "y": ["c", "d"]}) @@ -920,6 +1057,18 @@ def join_on( ... ).sort(col("x")).to_pydict() {'a': [1, 2], 'x': ['a', 'b'], 'b': [1, 2], 'y': ['c', 'd']} + Use :py:meth:`col` to disambiguate shared column names: + + >>> left = ctx.from_pydict({"id": [1, 2], "val": [10, 20]}) + >>> right = ctx.from_pydict({"id": [1, 2], "val": [30, 40]}) + >>> joined = left.join_on( + ... right, left.col("id") == right.col("id"), how="inner" + ... ) + >>> joined.select( + ... left.col("id"), left.col("val"), right.col("val").alias("rval") + ... ).sort(left.col("id")).to_pydict() + {'id': [1, 2], 'val': [10, 20], 'rval': [30, 40]} + Args: right: Other DataFrame to join with. on_exprs: single or multiple (in)-equality predicates. @@ -932,7 +1081,12 @@ def join_on( exprs = [ensure_expr(expr) for expr in on_exprs] return DataFrame(self.df.join_on(right.df, exprs, how)) - def explain(self, verbose: bool = False, analyze: bool = False) -> None: + def explain( + self, + verbose: bool = False, + analyze: bool = False, + format: ExplainFormat | None = None, + ) -> None: """Print an explanation of the DataFrame's plan so far. If ``analyze`` is specified, runs the plan and reports metrics. @@ -940,8 +1094,23 @@ def explain(self, verbose: bool = False, analyze: bool = False) -> None: Args: verbose: If ``True``, more details will be included. analyze: If ``True``, the plan will run and metrics reported. + format: Output format for the plan. Defaults to + :py:attr:`ExplainFormat.INDENT`. + + Examples: + Show the plan in tree format: + + >>> from datafusion import ExplainFormat + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2, 3]}) + >>> df.explain(format=ExplainFormat.TREE) # doctest: +SKIP + + Show plan with runtime metrics: + + >>> df.explain(analyze=True) # doctest: +SKIP """ - self.df.explain(verbose, analyze) + fmt = format.value if format is not None else None + self.df.explain(verbose, analyze, fmt) def logical_plan(self) -> LogicalPlan: """Return the unoptimized ``LogicalPlan``. @@ -1010,45 +1179,170 @@ def union(self, other: DataFrame, distinct: bool = False) -> DataFrame: """ return DataFrame(self.df.union(other.df, distinct)) + @deprecated( + "union_distinct() is deprecated. Use union(other, distinct=True) instead." + ) def union_distinct(self, other: DataFrame) -> DataFrame: """Calculate the distinct union of two :py:class:`DataFrame`. + See Also: + :py:meth:`union` + """ + return self.union(other, distinct=True) + + def intersect(self, other: DataFrame, distinct: bool = False) -> DataFrame: + """Calculate the intersection of two :py:class:`DataFrame`. + The two :py:class:`DataFrame` must have exactly the same schema. - Any duplicate rows are discarded. Args: - other: DataFrame to union with. + other: DataFrame to intersect with. + distinct: If ``True``, duplicate rows are removed from the result. Returns: - DataFrame after union. + DataFrame after intersection. + + Examples: + Find rows common to both DataFrames: + + >>> ctx = dfn.SessionContext() + >>> df1 = ctx.from_pydict({"a": [1, 2, 3], "b": [10, 20, 30]}) + >>> df2 = ctx.from_pydict({"a": [1, 4], "b": [10, 40]}) + >>> df1.intersect(df2).to_pydict() + {'a': [1], 'b': [10]} + + Intersect with deduplication: + + >>> df1 = ctx.from_pydict({"a": [1, 1, 2], "b": [10, 10, 20]}) + >>> df2 = ctx.from_pydict({"a": [1, 1], "b": [10, 10]}) + >>> df1.intersect(df2, distinct=True).to_pydict() + {'a': [1], 'b': [10]} """ - return DataFrame(self.df.union_distinct(other.df)) + return DataFrame(self.df.intersect(other.df, distinct)) - def intersect(self, other: DataFrame) -> DataFrame: - """Calculate the intersection of two :py:class:`DataFrame`. + def except_all(self, other: DataFrame, distinct: bool = False) -> DataFrame: + """Calculate the set difference of two :py:class:`DataFrame`. + + Returns rows that are in this DataFrame but not in ``other``. The two :py:class:`DataFrame` must have exactly the same schema. Args: - other: DataFrame to intersect with. + other: DataFrame to calculate exception with. + distinct: If ``True``, duplicate rows are removed from the result. Returns: - DataFrame after intersection. + DataFrame after set difference. + + Examples: + Remove rows present in ``df2``: + + >>> ctx = dfn.SessionContext() + >>> df1 = ctx.from_pydict({"a": [1, 2, 3], "b": [10, 20, 30]}) + >>> df2 = ctx.from_pydict({"a": [1, 2], "b": [10, 20]}) + >>> df1.except_all(df2).sort("a").to_pydict() + {'a': [3], 'b': [30]} + + Remove rows present in ``df2`` and deduplicate: + + >>> df1.except_all(df2, distinct=True).sort("a").to_pydict() + {'a': [3], 'b': [30]} """ - return DataFrame(self.df.intersect(other.df)) + return DataFrame(self.df.except_all(other.df, distinct)) - def except_all(self, other: DataFrame) -> DataFrame: - """Calculate the exception of two :py:class:`DataFrame`. + def union_by_name(self, other: DataFrame, distinct: bool = False) -> DataFrame: + """Union two :py:class:`DataFrame` matching columns by name. - The two :py:class:`DataFrame` must have exactly the same schema. + Unlike :py:meth:`union` which matches columns by position, this method + matches columns by their names, allowing DataFrames with different + column orders to be combined. Args: - other: DataFrame to calculate exception with. + other: DataFrame to union with. + distinct: If ``True``, duplicate rows are removed from the result. Returns: - DataFrame after exception. + DataFrame after union by name. + + Examples: + Combine DataFrames with different column orders: + + >>> ctx = dfn.SessionContext() + >>> df1 = ctx.from_pydict({"a": [1], "b": [10]}) + >>> df2 = ctx.from_pydict({"b": [20], "a": [2]}) + >>> df1.union_by_name(df2).sort("a").to_pydict() + {'a': [1, 2], 'b': [10, 20]} + + Union by name with deduplication: + + >>> df1 = ctx.from_pydict({"a": [1, 1], "b": [10, 10]}) + >>> df2 = ctx.from_pydict({"b": [10], "a": [1]}) + >>> df1.union_by_name(df2, distinct=True).to_pydict() + {'a': [1], 'b': [10]} """ - return DataFrame(self.df.except_all(other.df)) + return DataFrame(self.df.union_by_name(other.df, distinct)) + + def distinct_on( + self, + on_expr: list[Expr], + select_expr: list[Expr], + sort_expr: list[SortKey] | None = None, + ) -> DataFrame: + """Deduplicate rows based on specific columns. + + Returns a new DataFrame with one row per unique combination of the + ``on_expr`` columns, keeping the first row per group as determined by + ``sort_expr``. + + Args: + on_expr: Expressions that determine uniqueness. + select_expr: Expressions to include in the output. + sort_expr: Optional sort expressions to determine which row to keep. + + Returns: + DataFrame after deduplication. + + Examples: + Keep the row with the smallest ``b`` for each unique ``a``: + + >>> from datafusion import col + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [1, 1, 2, 2], "b": [10, 20, 30, 40]}) + >>> df.distinct_on( + ... [col("a")], + ... [col("a"), col("b")], + ... [col("a").sort(ascending=True), col("b").sort(ascending=True)], + ... ).sort("a").to_pydict() + {'a': [1, 2], 'b': [10, 30]} + """ + on_raw = expr_list_to_raw_expr_list(on_expr) + select_raw = expr_list_to_raw_expr_list(select_expr) + sort_raw = sort_list_to_raw_sort_list(sort_expr) if sort_expr else None + return DataFrame(self.df.distinct_on(on_raw, select_raw, sort_raw)) + + def sort_by(self, *exprs: Expr | str) -> DataFrame: + """Sort the DataFrame by column expressions in ascending order. + + This is a convenience method that sorts the DataFrame by the given + expressions in ascending order with nulls last. For more control over + sort direction and null ordering, use :py:meth:`sort` instead. + + Args: + exprs: Expressions or column names to sort by. + + Returns: + DataFrame after sorting. + + Examples: + Sort by a single column: + + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [3, 1, 2]}) + >>> df.sort_by("a").to_pydict() + {'a': [1, 2, 3]} + """ + raw = [_to_raw_expr(e) for e in exprs] + return DataFrame(self.df.sort_by(raw)) def write_csv( self, @@ -1310,23 +1604,52 @@ def count(self) -> int: return self.df.count() @deprecated("Use :py:func:`unnest_columns` instead.") - def unnest_column(self, column: str, preserve_nulls: bool = True) -> DataFrame: + def unnest_column( + self, + column: str, + preserve_nulls: bool = True, + ) -> DataFrame: """See :py:func:`unnest_columns`.""" return DataFrame(self.df.unnest_column(column, preserve_nulls=preserve_nulls)) - def unnest_columns(self, *columns: str, preserve_nulls: bool = True) -> DataFrame: + def unnest_columns( + self, + *columns: str, + preserve_nulls: bool = True, + recursions: list[tuple[str, str, int]] | None = None, + ) -> DataFrame: """Expand columns of arrays into a single row per array element. Args: columns: Column names to perform unnest operation on. preserve_nulls: If False, rows with null entries will not be returned. + recursions: Optional list of ``(input_column, output_column, depth)`` + tuples that control how deeply nested columns are unnested. Any + column not mentioned here is unnested with depth 1. Returns: A DataFrame with the columns expanded. + + Examples: + Unnest an array column: + + >>> ctx = dfn.SessionContext() + >>> df = ctx.from_pydict({"a": [[1, 2], [3]], "b": ["x", "y"]}) + >>> df.unnest_columns("a").to_pydict() + {'a': [1, 2, 3], 'b': ['x', 'x', 'y']} + + With explicit recursion depth: + + >>> df.unnest_columns("a", recursions=[("a", "a", 1)]).to_pydict() + {'a': [1, 2, 3], 'b': ['x', 'x', 'y']} """ columns = list(columns) - return DataFrame(self.df.unnest_columns(columns, preserve_nulls=preserve_nulls)) + return DataFrame( + self.df.unnest_columns( + columns, preserve_nulls=preserve_nulls, recursions=recursions + ) + ) def __arrow_c_stream__(self, requested_schema: object | None = None) -> object: """Export the DataFrame as an Arrow C Stream. diff --git a/python/tests/test_dataframe.py b/python/tests/test_dataframe.py index 759d6278c..bb8e9685c 100644 --- a/python/tests/test_dataframe.py +++ b/python/tests/test_dataframe.py @@ -29,6 +29,7 @@ import pytest from datafusion import ( DataFrame, + ExplainFormat, InsertOp, ParquetColumnOptions, ParquetWriterOptions, @@ -3569,3 +3570,263 @@ def test_read_parquet_file_sort_order(tmp_path, file_sort_order): pa.parquet.write_table(table, path) df = ctx.read_parquet(path, file_sort_order=file_sort_order) assert df.collect()[0].column(0).to_pylist() == [1, 2] + + +@pytest.mark.parametrize( + ("df1_data", "df2_data", "method", "kwargs", "expected_a", "expected_b"), + [ + pytest.param( + {"a": [1, 2, 3, 1], "b": [10, 20, 30, 10]}, + {"a": [1, 2], "b": [10, 20]}, + "except_all", + {"distinct": True}, + [3], + [30], + id="except_all(distinct=True): removes matching rows and deduplicates", + ), + pytest.param( + {"a": [1, 2, 3, 1], "b": [10, 20, 30, 10]}, + {"a": [1, 4], "b": [10, 40]}, + "intersect", + {"distinct": True}, + [1], + [10], + id="intersect(distinct=True): keeps common rows and deduplicates", + ), + pytest.param( + {"a": [1], "b": [10]}, + {"b": [20], "a": [2]}, # reversed column order tests matching by name + "union_by_name", + {}, + [1, 2], + [10, 20], + id="union_by_name: matches columns by name not position", + ), + ], +) +def test_set_operations_distinct( + df1_data, df2_data, method, kwargs, expected_a, expected_b +): + ctx = SessionContext() + df1 = ctx.from_pydict(df1_data) + df2 = ctx.from_pydict(df2_data) + result = ( + getattr(df1, method)(df2, **kwargs) + .sort(column("a").sort(ascending=True)) + .collect()[0] + ) + assert result.column(0).to_pylist() == expected_a + assert result.column(1).to_pylist() == expected_b + + +def test_union_by_name_distinct(): + ctx = SessionContext() + df1 = ctx.from_pydict({"a": [1, 1], "b": [10, 10]}) + df2 = ctx.from_pydict({"b": [10], "a": [1]}) + result = df1.union_by_name(df2, distinct=True).collect()[0] + assert result.column(0).to_pylist() == [1] + assert result.column(1).to_pylist() == [10] + + +def test_column_qualified(): + """DataFrame.column() returns a qualified column expression.""" + ctx = SessionContext() + df = ctx.from_pydict({"a": [1, 2], "b": [3, 4]}) + expr = df.column("a") + result = df.select(expr).collect()[0] + assert result.column(0).to_pylist() == [1, 2] + + +def test_column_not_found(): + ctx = SessionContext() + df = ctx.from_pydict({"a": [1]}) + with pytest.raises(Exception, match="not found"): + df.column("z") + + +def test_column_ambiguous(): + """After a join, duplicate column names that cannot be resolved raise an error.""" + ctx = SessionContext() + left = ctx.from_pydict({"id": [1, 2], "val": [10, 20]}) + right = ctx.from_pydict({"id": [1, 2], "val": [30, 40]}) + joined = left.join(right, on="id", how="inner") + with pytest.raises(Exception, match="not found"): + joined.column("val") + + +def test_column_after_join(): + """Qualified column works for non-ambiguous columns after a join.""" + ctx = SessionContext() + left = ctx.from_pydict({"id": [1, 2], "x": [10, 20]}) + right = ctx.from_pydict({"id": [1, 2], "y": [30, 40]}) + joined = left.join(right, on="id", how="inner") + expr = joined.column("y") + result = joined.select("id", expr).sort("id").collect()[0] + assert result.column(0).to_pylist() == [1, 2] + assert result.column(1).to_pylist() == [30, 40] + + +def test_col_join_disambiguate(): + """Use col() to disambiguate and select columns after a join.""" + ctx = SessionContext() + df1 = ctx.from_pydict({"foo": [1, 2, 3], "bar": [5, 6, 7]}) + df2 = ctx.from_pydict({"foo": [1, 2, 3], "baz": [8, 9, 10]}) + joined = df1.join_on(df2, df1.col("foo") == df2.col("foo"), how="inner") + result = ( + joined.select(df1.col("foo"), df1.col("bar"), df2.col("baz")) + .sort(df1.col("foo")) + .to_pydict() + ) + assert result["bar"] == [5, 6, 7] + assert result["baz"] == [8, 9, 10] + + +def test_find_qualified_columns(): + ctx = SessionContext() + df = ctx.from_pydict({"a": [1, 2], "b": [3, 4], "c": [5, 6]}) + exprs = df.find_qualified_columns("a", "c") + assert len(exprs) == 2 + result = df.select(*exprs).collect()[0] + assert result.column(0).to_pylist() == [1, 2] + assert result.column(1).to_pylist() == [5, 6] + + +def test_find_qualified_columns_not_found(): + ctx = SessionContext() + df = ctx.from_pydict({"a": [1]}) + with pytest.raises(Exception, match="not found"): + df.find_qualified_columns("a", "z") + + +def test_distinct_on(): + ctx = SessionContext() + df = ctx.from_pydict({"a": [1, 1, 2, 2], "b": [10, 20, 30, 40]}) + result = ( + df.distinct_on( + [column("a")], + [column("a"), column("b")], + [column("a").sort(ascending=True), column("b").sort(ascending=True)], + ) + .sort(column("a").sort(ascending=True)) + .collect()[0] + ) + # Keeps the first row per group (smallest b per a) + assert result.column(0).to_pylist() == [1, 2] + assert result.column(1).to_pylist() == [10, 30] + + +@pytest.mark.parametrize( + ("input_values", "expected"), + [ + ([3, 1, 2], [1, 2, 3]), + ([1, 2, 3], [1, 2, 3]), + ([3, None, 1, 2], [1, 2, 3, None]), + ], +) +def test_sort_by(input_values, expected): + """sort_by always sorts ascending with nulls last regardless of input order.""" + ctx = SessionContext() + df = ctx.from_pydict({"a": input_values}) + result = df.sort_by(column("a")).collect()[0] + assert result.column(0).to_pylist() == expected + + +@pytest.mark.parametrize( + ("fmt", "verbose", "analyze", "expected_substring"), + [ + pytest.param(None, False, False, None, id="default format"), + pytest.param(ExplainFormat.TREE, False, False, "---", id="tree format"), + pytest.param( + ExplainFormat.INDENT, True, True, None, id="indent verbose+analyze" + ), + pytest.param(ExplainFormat.PGJSON, False, False, '"Plan"', id="pgjson format"), + pytest.param( + ExplainFormat.GRAPHVIZ, False, False, "digraph", id="graphviz format" + ), + ], +) +def test_explain_with_format(capsys, fmt, verbose, analyze, expected_substring): + ctx = SessionContext() + df = ctx.from_pydict({"a": [1]}) + df.explain(verbose=verbose, analyze=analyze, format=fmt) + captured = capsys.readouterr() + assert "plan_type" in captured.out + if expected_substring is not None: + assert expected_substring in captured.out + + +@pytest.mark.parametrize( + ("window_exprs", "expected_columns"), + [ + pytest.param( + lambda: [ + f.row_number(partition_by=[column("b")], order_by=[column("a")]).alias( + "rn" + ), + ], + {"rn": [1, 2, 1]}, + id="single window expression", + ), + pytest.param( + lambda: [ + f.row_number(partition_by=[column("b")], order_by=[column("a")]).alias( + "rn" + ), + f.rank(partition_by=[column("b")], order_by=[column("a")]).alias("rnk"), + ], + {"rn": [1, 2, 1], "rnk": [1, 2, 1]}, + id="multiple window expressions", + ), + ], +) +def test_window(window_exprs, expected_columns): + ctx = SessionContext() + df = ctx.from_pydict({"a": [1, 2, 3], "b": ["x", "x", "y"]}) + result = ( + df.window(*window_exprs()).sort(column("a").sort(ascending=True)).collect()[0] + ) + for col_name, expected_values in expected_columns.items(): + assert col_name in result.schema.names + assert ( + result.column(result.schema.get_field_index(col_name)).to_pylist() + == expected_values + ) + + +@pytest.mark.parametrize( + ("input_data", "recursions", "expected_a"), + [ + pytest.param( + {"a": [[1, 2], [3]], "b": ["x", "y"]}, + None, + [1, 2, 3], + id="basic unnest without recursions", + ), + pytest.param( + {"a": [[1, 2], [3]], "b": ["x", "y"]}, + [("a", "a", 1)], + [1, 2, 3], + id="explicit depth 1 matches basic unnest", + ), + pytest.param( + {"a": [[[1, 2], [3]], [[4]]], "b": ["x", "y"]}, + [("a", "a", 1)], + [[1, 2], [3], [4]], + id="depth 1 on nested lists keeps inner lists", + ), + pytest.param( + {"a": [[[1, 2], [3]], [[4]]], "b": ["x", "y"]}, + [("a", "a", 2)], + [1, 2, 3, 4], + id="depth 2 fully flattens nested lists", + ), + ], +) +def test_unnest_columns_with_recursions(input_data, recursions, expected_a): + ctx = SessionContext() + df = ctx.from_pydict(input_data) + kwargs = {} + if recursions is not None: + kwargs["recursions"] = recursions + result = df.unnest_columns("a", **kwargs).collect()[0] + assert result.column(0).to_pylist() == expected_a From 46f9ab8fcad03913234ce29e5075644c1ecdb9b7 Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Tue, 7 Apr 2026 15:03:38 -0400 Subject: [PATCH 47/56] Add missing deregister methods to SessionContext (#1473) * Add deregister methods to SessionContext for UDFs and object stores Expose upstream DataFusion deregister methods (deregister_udf, deregister_udaf, deregister_udwf, deregister_udtf, deregister_object_store) in both the Rust PyO3 bindings and Python wrappers, closing the gap identified in #1457. Co-Authored-By: Claude Opus 4.6 (1M context) * Fix deregister tests to expect ValueError instead of RuntimeError DataFusion raises ValueError for planning errors when a deregistered function is used in a query. Co-Authored-By: Claude Opus 4.6 (1M context) * Replace .unwrap() with proper error propagation in object store methods Url::parse() can fail on invalid input. Use .map_err() to convert the error into a Python exception instead of panicking. Co-Authored-By: Claude Opus 4.6 (1M context) * Minor move of import statement --------- Co-authored-by: Claude Opus 4.6 (1M context) --- crates/core/src/context.rs | 32 +++++++++- python/datafusion/context.py | 41 ++++++++++++ python/tests/test_context.py | 120 +++++++++++++++++++++++++++++++++++ 3 files changed, 192 insertions(+), 1 deletion(-) diff --git a/crates/core/src/context.rs b/crates/core/src/context.rs index 53994d2f5..1300a1595 100644 --- a/crates/core/src/context.rs +++ b/crates/core/src/context.rs @@ -434,11 +434,25 @@ impl PySessionContext { &upstream_host }; let url_string = format!("{scheme}{derived_host}"); - let url = Url::parse(&url_string).unwrap(); + let url = Url::parse(&url_string).map_err(|e| PyValueError::new_err(e.to_string()))?; self.ctx.runtime_env().register_object_store(&url, store); Ok(()) } + /// Deregister an object store with the given url + #[pyo3(signature = (scheme, host=None))] + pub fn deregister_object_store( + &self, + scheme: &str, + host: Option<&str>, + ) -> PyDataFusionResult<()> { + let host = host.unwrap_or(""); + let url_string = format!("{scheme}{host}"); + let url = Url::parse(&url_string).map_err(|e| PyDataFusionError::Common(e.to_string()))?; + self.ctx.runtime_env().deregister_object_store(&url)?; + Ok(()) + } + #[allow(clippy::too_many_arguments)] #[pyo3(signature = (name, path, table_partition_cols=vec![], file_extension=".parquet", @@ -492,6 +506,10 @@ impl PySessionContext { self.ctx.register_udtf(&name, func); } + pub fn deregister_udtf(&self, name: &str) { + self.ctx.deregister_udtf(name); + } + #[pyo3(signature = (query, options=None, param_values=HashMap::default(), param_strings=HashMap::default()))] pub fn sql_with_options( &self, @@ -975,16 +993,28 @@ impl PySessionContext { Ok(()) } + pub fn deregister_udf(&self, name: &str) { + self.ctx.deregister_udf(name); + } + pub fn register_udaf(&self, udaf: PyAggregateUDF) -> PyResult<()> { self.ctx.register_udaf(udaf.function); Ok(()) } + pub fn deregister_udaf(&self, name: &str) { + self.ctx.deregister_udaf(name); + } + pub fn register_udwf(&self, udwf: PyWindowUDF) -> PyResult<()> { self.ctx.register_udwf(udwf.function); Ok(()) } + pub fn deregister_udwf(&self, name: &str) { + self.ctx.deregister_udwf(name); + } + #[pyo3(signature = (name="datafusion"))] pub fn catalog(&self, py: Python, name: &str) -> PyResult> { let catalog = self.ctx.catalog(name).ok_or(PyKeyError::new_err(format!( diff --git a/python/datafusion/context.py b/python/datafusion/context.py index c8edc816f..f190e3ca1 100644 --- a/python/datafusion/context.py +++ b/python/datafusion/context.py @@ -568,6 +568,15 @@ def register_object_store( """ self.ctx.register_object_store(schema, store, host) + def deregister_object_store(self, schema: str, host: str | None = None) -> None: + """Remove an object store from the session. + + Args: + schema: The data source schema (e.g. ``"s3://"``). + host: URL for the host (e.g. bucket name). + """ + self.ctx.deregister_object_store(schema, host) + def register_listing_table( self, name: str, @@ -894,6 +903,14 @@ def register_udtf(self, func: TableFunction) -> None: """Register a user defined table function.""" self.ctx.register_udtf(func._udtf) + def deregister_udtf(self, name: str) -> None: + """Remove a user-defined table function from the session. + + Args: + name: Name of the UDTF to deregister. + """ + self.ctx.deregister_udtf(name) + def register_record_batches( self, name: str, partitions: list[list[pa.RecordBatch]] ) -> None: @@ -1105,14 +1122,38 @@ def register_udf(self, udf: ScalarUDF) -> None: """Register a user-defined function (UDF) with the context.""" self.ctx.register_udf(udf._udf) + def deregister_udf(self, name: str) -> None: + """Remove a user-defined scalar function from the session. + + Args: + name: Name of the UDF to deregister. + """ + self.ctx.deregister_udf(name) + def register_udaf(self, udaf: AggregateUDF) -> None: """Register a user-defined aggregation function (UDAF) with the context.""" self.ctx.register_udaf(udaf._udaf) + def deregister_udaf(self, name: str) -> None: + """Remove a user-defined aggregate function from the session. + + Args: + name: Name of the UDAF to deregister. + """ + self.ctx.deregister_udaf(name) + def register_udwf(self, udwf: WindowUDF) -> None: """Register a user-defined window function (UDWF) with the context.""" self.ctx.register_udwf(udwf._udwf) + def deregister_udwf(self, name: str) -> None: + """Remove a user-defined window function from the session. + + Args: + name: Name of the UDWF to deregister. + """ + self.ctx.deregister_udwf(name) + def catalog(self, name: str = "datafusion") -> Catalog: """Retrieve a catalog by name.""" return Catalog(self.ctx.catalog(name)) diff --git a/python/tests/test_context.py b/python/tests/test_context.py index 5df6ed20f..8491cc3a5 100644 --- a/python/tests/test_context.py +++ b/python/tests/test_context.py @@ -31,6 +31,7 @@ Table, column, literal, + udf, ) @@ -351,6 +352,125 @@ def test_deregister_table(ctx, database): assert public.names() == {"csv1", "csv2"} +def test_deregister_udf(): + ctx = SessionContext() + + is_null = udf( + lambda x: x.is_null(), + [pa.float64()], + pa.bool_(), + volatility="immutable", + name="my_is_null", + ) + ctx.register_udf(is_null) + + # Verify it works + df = ctx.from_pydict({"a": [1.0, None]}) + ctx.register_table("t", df.into_view()) + result = ctx.sql("SELECT my_is_null(a) FROM t").collect() + assert result[0].column(0) == pa.array([False, True]) + + # Deregister and verify it's gone + ctx.deregister_udf("my_is_null") + with pytest.raises(ValueError): + ctx.sql("SELECT my_is_null(a) FROM t").collect() + + +def test_deregister_udaf(): + import pyarrow.compute as pc + + ctx = SessionContext() + from datafusion import Accumulator, udaf + + class MySum(Accumulator): + def __init__(self): + self._sum = 0.0 + + def update(self, values: pa.Array) -> None: + self._sum += pc.sum(values).as_py() + + def merge(self, states: list[pa.Array]) -> None: + self._sum += pc.sum(states[0]).as_py() + + def state(self) -> list: + return [self._sum] + + def evaluate(self) -> pa.Scalar: + return self._sum + + my_sum = udaf( + MySum, + [pa.float64()], + pa.float64(), + [pa.float64()], + volatility="immutable", + name="my_sum", + ) + ctx.register_udaf(my_sum) + df = ctx.from_pydict({"a": [1.0, 2.0, 3.0]}) + ctx.register_table("t", df.into_view()) + + result = ctx.sql("SELECT my_sum(a) FROM t").collect() + assert result[0].column(0) == pa.array([6.0]) + + ctx.deregister_udaf("my_sum") + with pytest.raises(ValueError): + ctx.sql("SELECT my_sum(a) FROM t").collect() + + +def test_deregister_udwf(): + ctx = SessionContext() + from datafusion import udwf + from datafusion.user_defined import WindowEvaluator + + class MyRowNumber(WindowEvaluator): + def __init__(self): + self._row = 0 + + def evaluate_all(self, values, num_rows): + return pa.array(list(range(1, num_rows + 1)), type=pa.uint64()) + + my_row_number = udwf( + MyRowNumber, + [pa.float64()], + pa.uint64(), + volatility="immutable", + name="my_row_number", + ) + ctx.register_udwf(my_row_number) + df = ctx.from_pydict({"a": [1.0, 2.0, 3.0]}) + ctx.register_table("t", df.into_view()) + + result = ctx.sql("SELECT my_row_number(a) OVER () FROM t").collect() + assert result[0].column(0) == pa.array([1, 2, 3], type=pa.uint64()) + + ctx.deregister_udwf("my_row_number") + with pytest.raises(ValueError): + ctx.sql("SELECT my_row_number(a) OVER () FROM t").collect() + + +def test_deregister_udtf(): + import pyarrow.dataset as ds + + ctx = SessionContext() + from datafusion import Table, udtf + + class MyTable: + def __call__(self): + batch = pa.RecordBatch.from_pydict({"x": [1, 2, 3]}) + return Table(ds.dataset([batch])) + + my_table = udtf(MyTable(), "my_table") + ctx.register_udtf(my_table) + + result = ctx.sql("SELECT * FROM my_table()").collect() + assert result[0].column(0) == pa.array([1, 2, 3]) + + ctx.deregister_udtf("my_table") + with pytest.raises(ValueError): + ctx.sql("SELECT * FROM my_table()").collect() + + def test_register_table_from_dataframe(ctx): df = ctx.from_pydict({"a": [1, 2]}) ctx.register_table("df_tbl", df) From aa3b1948c3a49d14395093287a6e93354229c539 Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Wed, 8 Apr 2026 09:22:28 -0400 Subject: [PATCH 48/56] Add missing registration methods (#1474) * Add missing SessionContext read/register methods for Arrow IPC and batches Add read_arrow, read_empty, register_arrow, and register_batch methods to SessionContext, exposing upstream DataFusion v53 functionality. The write_* methods and read_batch/read_batches are already covered by DataFrame.write_* and SessionContext.from_arrow respectively. Closes #1458. Co-Authored-By: Claude Opus 4.6 (1M context) * Remove redundant read_empty Rust binding, make Python read_empty an alias for empty_table Co-Authored-By: Claude Opus 4.6 (1M context) * Add pathlib.Path and empty batch tests for Arrow IPC and register_batch Co-Authored-By: Claude Opus 4.6 (1M context) * Make test_read_empty more robust with length and num_rows checks Co-Authored-By: Claude Opus 4.6 (1M context) * Add examples to docstrings for new register/read methods Co-Authored-By: Claude Opus 4.6 (1M context) * Empty table actually returns record batch of length one but there are no columns * Add optional argument examples to register_arrow and read_arrow docstrings Demonstrate schema= and file_extension= keyword arguments in the docstring examples for register_arrow and read_arrow, following project guidelines for optional parameter documentation. Co-Authored-By: Claude Opus 4.6 (1M context) * Simplify read_empty docstring to use alias pattern Follow the same See Also alias convention used in functions.py since read_empty is a simple alias for empty_table. Co-Authored-By: Claude Opus 4.6 (1M context) * Remove shared ctx from doctest namespace, use inline SessionContext Avoid shared SessionContext state across doctests by having each docstring example create its own ctx instance, matching the pattern used throughout the rest of the codebase. Co-Authored-By: Claude Opus 4.6 (1M context) * Remove redundant import pyarrow as pa from docstrings The pa alias is already provided by the doctest namespace in conftest.py, so inline imports are unnecessary. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- conftest.py | 2 + crates/core/src/context.rs | 58 +++++++++- python/datafusion/context.py | 181 ++++++++++++++++++++++++++++++ python/datafusion/user_defined.py | 3 - python/tests/test_context.py | 62 ++++++++++ 5 files changed, 302 insertions(+), 4 deletions(-) diff --git a/conftest.py b/conftest.py index 73e90077a..0c9410636 100644 --- a/conftest.py +++ b/conftest.py @@ -19,6 +19,7 @@ import datafusion as dfn import numpy as np +import pyarrow as pa import pytest from datafusion import col, lit from datafusion import functions as F @@ -29,6 +30,7 @@ def _doctest_namespace(doctest_namespace: dict) -> None: """Add common imports to the doctest namespace.""" doctest_namespace["dfn"] = dfn doctest_namespace["np"] = np + doctest_namespace["pa"] = pa doctest_namespace["col"] = col doctest_namespace["lit"] = lit doctest_namespace["F"] = F diff --git a/crates/core/src/context.rs b/crates/core/src/context.rs index 1300a1595..ce11ef04e 100644 --- a/crates/core/src/context.rs +++ b/crates/core/src/context.rs @@ -41,7 +41,7 @@ use datafusion::execution::context::{ }; use datafusion::execution::disk_manager::DiskManagerMode; use datafusion::execution::memory_pool::{FairSpillPool, GreedyMemoryPool, UnboundedMemoryPool}; -use datafusion::execution::options::ReadOptions; +use datafusion::execution::options::{ArrowReadOptions, ReadOptions}; use datafusion::execution::runtime_env::RuntimeEnvBuilder; use datafusion::execution::session_state::SessionStateBuilder; use datafusion::prelude::{ @@ -974,6 +974,39 @@ impl PySessionContext { Ok(()) } + #[pyo3(signature = (name, path, schema=None, file_extension=".arrow", table_partition_cols=vec![]))] + pub fn register_arrow( + &self, + name: &str, + path: &str, + schema: Option>, + file_extension: &str, + table_partition_cols: Vec<(String, PyArrowType)>, + py: Python, + ) -> PyDataFusionResult<()> { + let mut options = ArrowReadOptions::default().table_partition_cols( + table_partition_cols + .into_iter() + .map(|(name, ty)| (name, ty.0)) + .collect::>(), + ); + options.file_extension = file_extension; + options.schema = schema.as_ref().map(|x| &x.0); + + let result = self.ctx.register_arrow(name, path, options); + wait_for_future(py, result)??; + Ok(()) + } + + pub fn register_batch( + &self, + name: &str, + batch: PyArrowType, + ) -> PyDataFusionResult<()> { + self.ctx.register_batch(name, batch.0)?; + Ok(()) + } + // Registers a PyArrow.Dataset pub fn register_dataset( &self, @@ -1214,6 +1247,29 @@ impl PySessionContext { Ok(PyDataFrame::new(df)) } + #[pyo3(signature = (path, schema=None, file_extension=".arrow", table_partition_cols=vec![]))] + pub fn read_arrow( + &self, + path: &str, + schema: Option>, + file_extension: &str, + table_partition_cols: Vec<(String, PyArrowType)>, + py: Python, + ) -> PyDataFusionResult { + let mut options = ArrowReadOptions::default().table_partition_cols( + table_partition_cols + .into_iter() + .map(|(name, ty)| (name, ty.0)) + .collect::>(), + ); + options.file_extension = file_extension; + options.schema = schema.as_ref().map(|x| &x.0); + + let result = self.ctx.read_arrow(path, options); + let df = wait_for_future(py, result)??; + Ok(PyDataFrame::new(df)) + } + pub fn read_table(&self, table: Bound<'_, PyAny>) -> PyDataFusionResult { let session = self.clone().into_bound_py_any(table.py())?; let table = PyTable::new(table, Some(session))?; diff --git a/python/datafusion/context.py b/python/datafusion/context.py index f190e3ca1..7a306f04c 100644 --- a/python/datafusion/context.py +++ b/python/datafusion/context.py @@ -903,6 +903,27 @@ def register_udtf(self, func: TableFunction) -> None: """Register a user defined table function.""" self.ctx.register_udtf(func._udtf) + def register_batch(self, name: str, batch: pa.RecordBatch) -> None: + """Register a single :py:class:`pa.RecordBatch` as a table. + + Args: + name: Name of the resultant table. + batch: Record batch to register as a table. + + Examples: + >>> ctx = dfn.SessionContext() + >>> batch = pa.RecordBatch.from_pydict({"a": [1, 2, 3]}) + >>> ctx.register_batch("batch_tbl", batch) + >>> ctx.sql("SELECT * FROM batch_tbl").collect()[0].column(0) + + [ + 1, + 2, + 3 + ] + """ + self.ctx.register_batch(name, batch) + def deregister_udtf(self, name: str) -> None: """Remove a user-defined table function from the session. @@ -1109,6 +1130,86 @@ def register_avro( name, str(path), schema, file_extension, table_partition_cols ) + def register_arrow( + self, + name: str, + path: str | pathlib.Path, + schema: pa.Schema | None = None, + file_extension: str = ".arrow", + table_partition_cols: list[tuple[str, str | pa.DataType]] | None = None, + ) -> None: + """Register an Arrow IPC file as a table. + + The registered table can be referenced from SQL statements executed + against this context. + + Args: + name: Name of the table to register. + path: Path to the Arrow IPC file. + schema: The data source schema. + file_extension: File extension to select. + table_partition_cols: Partition columns. + + Examples: + >>> import tempfile, os + >>> ctx = dfn.SessionContext() + >>> table = pa.table({"x": [10, 20, 30]}) + >>> with tempfile.TemporaryDirectory() as tmpdir: + ... path = os.path.join(tmpdir, "data.arrow") + ... with pa.ipc.new_file(path, table.schema) as writer: + ... writer.write_table(table) + ... ctx.register_arrow("arrow_tbl", path) + ... ctx.sql("SELECT * FROM arrow_tbl").collect()[0].column(0) + + [ + 10, + 20, + 30 + ] + + Provide an explicit ``schema`` to override schema inference: + + >>> with tempfile.TemporaryDirectory() as tmpdir: + ... path = os.path.join(tmpdir, "data.arrow") + ... with pa.ipc.new_file(path, table.schema) as writer: + ... writer.write_table(table) + ... ctx.register_arrow( + ... "arrow_schema", + ... path, + ... schema=pa.schema([("x", pa.int64())]), + ... ) + ... ctx.sql("SELECT * FROM arrow_schema").collect()[0].column(0) + + [ + 10, + 20, + 30 + ] + + Use ``file_extension`` to read files with a non-default extension: + + >>> with tempfile.TemporaryDirectory() as tmpdir: + ... path = os.path.join(tmpdir, "data.ipc") + ... with pa.ipc.new_file(path, table.schema) as writer: + ... writer.write_table(table) + ... ctx.register_arrow( + ... "arrow_ipc", path, file_extension=".ipc" + ... ) + ... ctx.sql("SELECT * FROM arrow_ipc").collect()[0].column(0) + + [ + 10, + 20, + 30 + ] + """ + if table_partition_cols is None: + table_partition_cols = [] + table_partition_cols = _convert_table_partition_cols(table_partition_cols) + self.ctx.register_arrow( + name, str(path), schema, file_extension, table_partition_cols + ) + def register_dataset(self, name: str, dataset: pa.dataset.Dataset) -> None: """Register a :py:class:`pa.dataset.Dataset` as a table. @@ -1369,6 +1470,86 @@ def read_avro( self.ctx.read_avro(str(path), schema, file_partition_cols, file_extension) ) + def read_arrow( + self, + path: str | pathlib.Path, + schema: pa.Schema | None = None, + file_extension: str = ".arrow", + file_partition_cols: list[tuple[str, str | pa.DataType]] | None = None, + ) -> DataFrame: + """Create a :py:class:`DataFrame` for reading an Arrow IPC data source. + + Args: + path: Path to the Arrow IPC file. + schema: The data source schema. + file_extension: File extension to select. + file_partition_cols: Partition columns. + + Returns: + DataFrame representation of the read Arrow IPC file. + + Examples: + >>> import tempfile, os + >>> ctx = dfn.SessionContext() + >>> table = pa.table({"a": [1, 2, 3]}) + >>> with tempfile.TemporaryDirectory() as tmpdir: + ... path = os.path.join(tmpdir, "data.arrow") + ... with pa.ipc.new_file(path, table.schema) as writer: + ... writer.write_table(table) + ... df = ctx.read_arrow(path) + ... df.collect()[0].column(0) + + [ + 1, + 2, + 3 + ] + + Provide an explicit ``schema`` to override schema inference: + + >>> with tempfile.TemporaryDirectory() as tmpdir: + ... path = os.path.join(tmpdir, "data.arrow") + ... with pa.ipc.new_file(path, table.schema) as writer: + ... writer.write_table(table) + ... df = ctx.read_arrow(path, schema=pa.schema([("a", pa.int64())])) + ... df.collect()[0].column(0) + + [ + 1, + 2, + 3 + ] + + Use ``file_extension`` to read files with a non-default extension: + + >>> with tempfile.TemporaryDirectory() as tmpdir: + ... path = os.path.join(tmpdir, "data.ipc") + ... with pa.ipc.new_file(path, table.schema) as writer: + ... writer.write_table(table) + ... df = ctx.read_arrow(path, file_extension=".ipc") + ... df.collect()[0].column(0) + + [ + 1, + 2, + 3 + ] + """ + if file_partition_cols is None: + file_partition_cols = [] + file_partition_cols = _convert_table_partition_cols(file_partition_cols) + return DataFrame( + self.ctx.read_arrow(str(path), schema, file_extension, file_partition_cols) + ) + + def read_empty(self) -> DataFrame: + """Create an empty :py:class:`DataFrame` with no columns or rows. + + See Also: + This is an alias for :meth:`empty_table`. + """ + return self.empty_table() + def read_table( self, table: Table | TableProviderExportable | DataFrame | pa.dataset.Dataset ) -> DataFrame: diff --git a/python/datafusion/user_defined.py b/python/datafusion/user_defined.py index 3eaccdfa3..848ab4cee 100644 --- a/python/datafusion/user_defined.py +++ b/python/datafusion/user_defined.py @@ -213,7 +213,6 @@ def udf(*args: Any, **kwargs: Any): # noqa: D417 Examples: Using ``udf`` as a function: - >>> import pyarrow as pa >>> import pyarrow.compute as pc >>> from datafusion.user_defined import ScalarUDF >>> def double_func(x): @@ -480,7 +479,6 @@ def udaf(*args: Any, **kwargs: Any): # noqa: D417, C901 instance in which this UDAF is used. Examples: - >>> import pyarrow as pa >>> import pyarrow.compute as pc >>> from datafusion.user_defined import AggregateUDF, Accumulator, udaf >>> class Summarize(Accumulator): @@ -874,7 +872,6 @@ def udwf(*args: Any, **kwargs: Any): # noqa: D417 When using ``udwf`` as a decorator, do not pass ``func`` explicitly. Examples: - >>> import pyarrow as pa >>> from datafusion.user_defined import WindowUDF, WindowEvaluator, udwf >>> class BiasedNumbers(WindowEvaluator): ... def __init__(self, start: int = 0): diff --git a/python/tests/test_context.py b/python/tests/test_context.py index 8491cc3a5..25f66a647 100644 --- a/python/tests/test_context.py +++ b/python/tests/test_context.py @@ -788,6 +788,68 @@ def test_read_avro(ctx): assert avro_df is not None +def test_read_arrow(ctx, tmp_path): + # Write an Arrow IPC file, then read it back + table = pa.table({"a": [1, 2, 3], "b": ["x", "y", "z"]}) + arrow_path = tmp_path / "test.arrow" + with pa.ipc.new_file(str(arrow_path), table.schema) as writer: + writer.write_table(table) + + df = ctx.read_arrow(str(arrow_path)) + result = df.collect() + assert result[0].column(0) == pa.array([1, 2, 3]) + assert result[0].column(1) == pa.array(["x", "y", "z"]) + + # Also verify pathlib.Path works + df = ctx.read_arrow(arrow_path) + result = df.collect() + assert result[0].column(0) == pa.array([1, 2, 3]) + + +def test_read_empty(ctx): + df = ctx.read_empty() + result = df.collect() + assert len(result) == 1 + assert result[0].num_columns == 0 + + df = ctx.empty_table() + result = df.collect() + assert len(result) == 1 + assert result[0].num_columns == 0 + + +def test_register_arrow(ctx, tmp_path): + # Write an Arrow IPC file, then register and query it + table = pa.table({"x": [10, 20, 30]}) + arrow_path = tmp_path / "test.arrow" + with pa.ipc.new_file(str(arrow_path), table.schema) as writer: + writer.write_table(table) + + ctx.register_arrow("arrow_tbl", str(arrow_path)) + result = ctx.sql("SELECT * FROM arrow_tbl").collect() + assert result[0].column(0) == pa.array([10, 20, 30]) + + # Also verify pathlib.Path works + ctx.register_arrow("arrow_tbl_path", arrow_path) + result = ctx.sql("SELECT * FROM arrow_tbl_path").collect() + assert result[0].column(0) == pa.array([10, 20, 30]) + + +def test_register_batch(ctx): + batch = pa.RecordBatch.from_pydict({"a": [1, 2, 3], "b": [4, 5, 6]}) + ctx.register_batch("batch_tbl", batch) + result = ctx.sql("SELECT * FROM batch_tbl").collect() + assert result[0].column(0) == pa.array([1, 2, 3]) + assert result[0].column(1) == pa.array([4, 5, 6]) + + +def test_register_batch_empty(ctx): + batch = pa.RecordBatch.from_pydict({"a": pa.array([], type=pa.int64())}) + ctx.register_batch("empty_batch_tbl", batch) + result = ctx.sql("SELECT * FROM empty_batch_tbl").collect() + assert result[0].num_rows == 0 + + def test_create_sql_options(): SQLOptions() From ecd14c10aff67169f2bfe1b7f86ff07621088dd0 Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Wed, 8 Apr 2026 11:11:48 -0400 Subject: [PATCH 49/56] Add missing SessionContext utility methods (#1475) * Add missing SessionContext utility methods Expose upstream DataFusion v53 utility methods: session_start_time, enable_ident_normalization, parse_sql_expr, execute_logical_plan, refresh_catalogs, remove_optimizer_rule, and table_provider. The add_optimizer_rule and add_analyzer_rule methods are omitted as the OptimizerRule and AnalyzerRule traits are not yet exposed to Python. Closes #1459. Co-Authored-By: Claude Opus 4.6 (1M context) * Raise KeyError from table_provider for consistency with table() Co-Authored-By: Claude Opus 4.6 (1M context) * Add docstring examples for new SessionContext utility methods Co-Authored-By: Claude Opus 4.6 (1M context) * update docstring * Address PR review feedback for SessionContext utility methods - Improve docstring examples to show actual output instead of asserts - Use doctest +SKIP for non-deterministic session_start_time output - Fix table_provider error mapping: outer async error is now RuntimeError - Strengthen tests: validate RFC 3339 with fromisoformat, test both optimizer rule removal paths, exact string match for parse_sql_expr, verify enable_ident_normalization with dynamic state change Co-Authored-By: Claude Opus 4.6 (1M context) * Fix test_session_start_time failure on Python 3.10 datetime.fromisoformat() only supports up to 6 fractional-second digits (microseconds) on Python 3.10. Truncate nanosecond precision before parsing. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- crates/core/src/context.rs | 49 ++++++++++++++- python/datafusion/context.py | 118 ++++++++++++++++++++++++++++++++++- python/tests/test_context.py | 55 ++++++++++++++++ 3 files changed, 219 insertions(+), 3 deletions(-) diff --git a/crates/core/src/context.rs b/crates/core/src/context.rs index ce11ef04e..b4fe524df 100644 --- a/crates/core/src/context.rs +++ b/crates/core/src/context.rs @@ -28,7 +28,7 @@ use datafusion::arrow::datatypes::{DataType, Schema, SchemaRef}; use datafusion::arrow::pyarrow::PyArrowType; use datafusion::arrow::record_batch::RecordBatch; use datafusion::catalog::{CatalogProvider, CatalogProviderList, TableProviderFactory}; -use datafusion::common::{ScalarValue, TableReference, exec_err}; +use datafusion::common::{DFSchema, ScalarValue, TableReference, exec_err}; use datafusion::datasource::file_format::file_compression_type::FileCompressionType; use datafusion::datasource::file_format::parquet::ParquetFormat; use datafusion::datasource::listing::{ @@ -60,7 +60,7 @@ use datafusion_python_util::{ }; use object_store::ObjectStore; use pyo3::IntoPyObjectExt; -use pyo3::exceptions::{PyKeyError, PyValueError}; +use pyo3::exceptions::{PyKeyError, PyRuntimeError, PyValueError}; use pyo3::prelude::*; use pyo3::types::{PyCapsule, PyDict, PyList, PyTuple}; use url::Url; @@ -70,11 +70,13 @@ use crate::catalog::{ PyCatalog, PyCatalogList, RustWrappedPyCatalogProvider, RustWrappedPyCatalogProviderList, }; use crate::common::data_type::PyScalarValue; +use crate::common::df_schema::PyDFSchema; use crate::dataframe::PyDataFrame; use crate::dataset::Dataset; use crate::errors::{ PyDataFusionError, PyDataFusionResult, from_datafusion_error, py_datafusion_err, }; +use crate::expr::PyExpr; use crate::expr::sort_expr::PySortExpr; use crate::options::PyCsvReadOptions; use crate::physical_plan::PyExecutionPlan; @@ -1113,6 +1115,49 @@ impl PySessionContext { self.ctx.session_id() } + pub fn session_start_time(&self) -> String { + self.ctx.session_start_time().to_rfc3339() + } + + pub fn enable_ident_normalization(&self) -> bool { + self.ctx.enable_ident_normalization() + } + + pub fn parse_sql_expr(&self, sql: &str, schema: PyDFSchema) -> PyDataFusionResult { + let df_schema: DFSchema = schema.into(); + Ok(self.ctx.parse_sql_expr(sql, &df_schema)?.into()) + } + + pub fn execute_logical_plan( + &self, + plan: PyLogicalPlan, + py: Python, + ) -> PyDataFusionResult { + let df = wait_for_future( + py, + self.ctx.execute_logical_plan(plan.plan.as_ref().clone()), + )??; + Ok(PyDataFrame::new(df)) + } + + pub fn refresh_catalogs(&self, py: Python) -> PyDataFusionResult<()> { + wait_for_future(py, self.ctx.refresh_catalogs())??; + Ok(()) + } + + pub fn remove_optimizer_rule(&self, name: &str) -> bool { + self.ctx.remove_optimizer_rule(name) + } + + pub fn table_provider(&self, name: &str, py: Python) -> PyResult { + let provider = wait_for_future(py, self.ctx.table_provider(name)) + // Outer error: runtime/async failure + .map_err(|e| PyRuntimeError::new_err(e.to_string()))? + // Inner error: table not found + .map_err(|e| PyKeyError::new_err(e.to_string()))?; + Ok(PyTable { table: provider }) + } + #[allow(clippy::too_many_arguments)] #[pyo3(signature = (path, schema=None, schema_infer_max_records=1000, file_extension=".json", table_partition_cols=vec![], file_compression_type=None))] pub fn read_json( diff --git a/python/datafusion/context.py b/python/datafusion/context.py index 7a306f04c..e3949de83 100644 --- a/python/datafusion/context.py +++ b/python/datafusion/context.py @@ -63,7 +63,8 @@ import polars as pl # type: ignore[import] from datafusion.catalog import CatalogProvider, Table - from datafusion.expr import SortKey + from datafusion.common import DFSchema + from datafusion.expr import Expr, SortKey from datafusion.plan import ExecutionPlan, LogicalPlan from datafusion.user_defined import ( AggregateUDF, @@ -1283,6 +1284,121 @@ def session_id(self) -> str: """Return an id that uniquely identifies this :py:class:`SessionContext`.""" return self.ctx.session_id() + def session_start_time(self) -> str: + """Return the session start time as an RFC 3339 formatted string. + + Examples: + >>> ctx = SessionContext() + >>> ctx.session_start_time() # doctest: +SKIP + '2026-01-01T12:34:56.123456789+00:00' + """ + return self.ctx.session_start_time() + + def enable_ident_normalization(self) -> bool: + """Return whether identifier normalization (lowercasing) is enabled. + + Examples: + >>> ctx = SessionContext() + >>> ctx.enable_ident_normalization() + True + """ + return self.ctx.enable_ident_normalization() + + def parse_sql_expr(self, sql: str, schema: DFSchema) -> Expr: + """Parse a SQL expression string into a logical expression. + + Args: + sql: SQL expression string. + schema: Schema to use for resolving column references. + + Returns: + Parsed expression. + + Examples: + >>> from datafusion.common import DFSchema + >>> ctx = SessionContext() + >>> schema = DFSchema.empty() + >>> ctx.parse_sql_expr("1 + 2", schema=schema) + Expr(Int64(1) + Int64(2)) + """ + from datafusion.expr import Expr # noqa: PLC0415 + + return Expr(self.ctx.parse_sql_expr(sql, schema)) + + def execute_logical_plan(self, plan: LogicalPlan) -> DataFrame: + """Execute a :py:class:`~datafusion.plan.LogicalPlan` and return a DataFrame. + + Args: + plan: Logical plan to execute. + + Returns: + DataFrame resulting from the execution. + + Examples: + >>> ctx = SessionContext() + >>> df = ctx.from_pydict({"a": [1, 2, 3]}) + >>> plan = df.logical_plan() + >>> df2 = ctx.execute_logical_plan(plan) + >>> df2.collect()[0].column(0) + + [ + 1, + 2, + 3 + ] + """ + return DataFrame(self.ctx.execute_logical_plan(plan._raw_plan)) + + def refresh_catalogs(self) -> None: + """Refresh catalog metadata. + + Examples: + >>> ctx = SessionContext() + >>> ctx.refresh_catalogs() + """ + self.ctx.refresh_catalogs() + + def remove_optimizer_rule(self, name: str) -> bool: + """Remove an optimizer rule by name. + + Args: + name: Name of the optimizer rule to remove. + + Returns: + True if a rule with the given name was found and removed. + + Examples: + >>> ctx = SessionContext() + >>> ctx.remove_optimizer_rule("nonexistent_rule") + False + """ + return self.ctx.remove_optimizer_rule(name) + + def table_provider(self, name: str) -> Table: + """Return the :py:class:`~datafusion.catalog.Table` for the given table name. + + Args: + name: Name of the table. + + Returns: + The table provider. + + Raises: + KeyError: If the table is not found. + + Examples: + >>> import pyarrow as pa + >>> ctx = SessionContext() + >>> batch = pa.RecordBatch.from_pydict({"x": [1, 2]}) + >>> ctx.register_record_batches("my_table", [[batch]]) + >>> tbl = ctx.table_provider("my_table") + >>> tbl.schema + x: int64 + """ + from datafusion.catalog import Table # noqa: PLC0415 + + return Table(self.ctx.table_provider(name)) + def read_json( self, path: str | pathlib.Path, diff --git a/python/tests/test_context.py b/python/tests/test_context.py index 25f66a647..13c05a9e6 100644 --- a/python/tests/test_context.py +++ b/python/tests/test_context.py @@ -671,6 +671,61 @@ def test_table_not_found(ctx): ctx.table(f"not-found-{uuid4()}") +def test_session_start_time(ctx): + import datetime + import re + + st = ctx.session_start_time() + assert isinstance(st, str) + # Truncate nanoseconds to microseconds for Python 3.10 compat + st = re.sub(r"(\.\d{6})\d+", r"\1", st) + dt = datetime.datetime.fromisoformat(st) + assert dt.isoformat() + + +def test_enable_ident_normalization(ctx): + assert ctx.enable_ident_normalization() is True + ctx.sql("SET datafusion.sql_parser.enable_ident_normalization = false") + assert ctx.enable_ident_normalization() is False + + +def test_parse_sql_expr(ctx): + from datafusion.common import DFSchema + + schema = DFSchema.empty() + expr = ctx.parse_sql_expr("1 + 2", schema) + assert str(expr) == "Expr(Int64(1) + Int64(2))" + + +def test_execute_logical_plan(ctx): + df = ctx.from_pydict({"a": [1, 2, 3]}) + plan = df.logical_plan() + df2 = ctx.execute_logical_plan(plan) + result = df2.collect() + assert result[0].column(0) == pa.array([1, 2, 3]) + + +def test_refresh_catalogs(ctx): + ctx.refresh_catalogs() + + +def test_remove_optimizer_rule(ctx): + assert ctx.remove_optimizer_rule("push_down_filter") is True + assert ctx.remove_optimizer_rule("nonexistent_rule") is False + + +def test_table_provider(ctx): + batch = pa.RecordBatch.from_pydict({"x": [10, 20, 30]}) + ctx.register_record_batches("provider_test", [[batch]]) + tbl = ctx.table_provider("provider_test") + assert tbl.schema == pa.schema([("x", pa.int64())]) + + +def test_table_provider_not_found(ctx): + with pytest.raises(KeyError): + ctx.table_provider("nonexistent_table") + + def test_read_json(ctx): path = pathlib.Path(__file__).parent.resolve() From 3585c11eed778810e3317c56c2c25a8cdc29be5b Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Thu, 9 Apr 2026 07:38:59 -0400 Subject: [PATCH 50/56] minor: remove deprecated interfaces (#1481) * udf module has been deprecated since DF47. html_formatter module has been deprecated since DF48. * database has been deprecated since DF48 * select_columns has been deprecated since DF43 * unnest_column has been deprecated since DF42 * display_name has been deprecated since DF42 * window() has been deprecated since DF50 * serde functions have been deprecated since DF42 * from_arrow_table and tables have been deprecated since DF42 * RuntimeConfig has been deprecated since DF44 * Update user documentation to remove deprecated function * update tpch examples for latest function uses * Remove unnecessary options in example * update rendering for the most recent dataframe_formatter instead of the deprecated html_formatter --- crates/core/src/context.rs | 15 -- crates/core/src/dataframe.rs | 29 +-- crates/core/src/functions.rs | 133 +---------- .../user-guide/common-operations/windows.rst | 15 +- .../source/user-guide/dataframe/rendering.rst | 225 ++++++++++-------- examples/tpch/q02_minimum_cost_supplier.py | 8 +- .../q11_important_stock_identification.py | 3 +- examples/tpch/q15_top_supplier.py | 4 +- examples/tpch/q17_small_quantity_order.py | 8 +- examples/tpch/q22_global_sales_opportunity.py | 4 +- python/datafusion/__init__.py | 3 +- python/datafusion/catalog.py | 10 - python/datafusion/context.py | 21 -- python/datafusion/dataframe.py | 20 -- python/datafusion/dataframe_formatter.py | 8 +- python/datafusion/expr.py | 15 -- python/datafusion/functions.py | 55 +---- python/datafusion/html_formatter.py | 29 --- python/datafusion/substrait.py | 25 -- python/datafusion/udf.py | 29 --- python/tests/test_expr.py | 21 -- 21 files changed, 154 insertions(+), 526 deletions(-) delete mode 100644 python/datafusion/html_formatter.py delete mode 100644 python/datafusion/udf.py diff --git a/crates/core/src/context.rs b/crates/core/src/context.rs index b4fe524df..e46d359d6 100644 --- a/crates/core/src/context.rs +++ b/crates/core/src/context.rs @@ -1072,21 +1072,6 @@ impl PySessionContext { self.ctx.catalog_names().into_iter().collect() } - pub fn tables(&self) -> HashSet { - self.ctx - .catalog_names() - .into_iter() - .filter_map(|name| self.ctx.catalog(&name)) - .flat_map(move |catalog| { - catalog - .schema_names() - .into_iter() - .filter_map(move |name| catalog.schema(&name)) - }) - .flat_map(|schema| schema.table_names()) - .collect() - } - pub fn table(&self, name: &str, py: Python) -> PyResult { let res = wait_for_future(py, self.ctx.table(name)) .map_err(|e| PyKeyError::new_err(e.to_string()))?; diff --git a/crates/core/src/dataframe.rs b/crates/core/src/dataframe.rs index fff5118d5..c067eac30 100644 --- a/crates/core/src/dataframe.rs +++ b/crates/core/src/dataframe.rs @@ -468,17 +468,17 @@ impl PyDataFrame { fn __getitem__(&self, key: Bound<'_, PyAny>) -> PyDataFusionResult { if let Ok(key) = key.extract::() { // df[col] - self.select_columns(vec![key]) + self.select_exprs(vec![key]) } else if let Ok(tuple) = key.cast::() { // df[col1, col2, col3] let keys = tuple .iter() .map(|item| item.extract::()) .collect::>>()?; - self.select_columns(keys) + self.select_exprs(keys) } else if let Ok(keys) = key.extract::>() { // df[[col1, col2, col3]] - self.select_columns(keys) + self.select_exprs(keys) } else { let message = "DataFrame can only be indexed by string index or indices".to_string(); Err(PyDataFusionError::Common(message)) @@ -554,13 +554,6 @@ impl PyDataFrame { Ok(PyTable::from(table_provider)) } - #[pyo3(signature = (*args))] - fn select_columns(&self, args: Vec) -> PyDataFusionResult { - let args = args.iter().map(|s| s.as_ref()).collect::>(); - let df = self.df.as_ref().clone().select_columns(&args)?; - Ok(Self::new(df)) - } - #[pyo3(signature = (*args))] fn select_exprs(&self, args: Vec) -> PyDataFusionResult { let args = args.iter().map(|s| s.as_ref()).collect::>(); @@ -890,22 +883,6 @@ impl PyDataFrame { Ok(Self::new(new_df)) } - #[pyo3(signature = (column, preserve_nulls=true, recursions=None))] - fn unnest_column( - &self, - column: &str, - preserve_nulls: bool, - recursions: Option>, - ) -> PyDataFusionResult { - let unnest_options = build_unnest_options(preserve_nulls, recursions); - let df = self - .df - .as_ref() - .clone() - .unnest_columns_with_options(&[column], unnest_options)?; - Ok(Self::new(df)) - } - #[pyo3(signature = (columns, preserve_nulls=true, recursions=None))] fn unnest_columns( &self, diff --git a/crates/core/src/functions.rs b/crates/core/src/functions.rs index f173aaa51..7feb62d79 100644 --- a/crates/core/src/functions.rs +++ b/crates/core/src/functions.rs @@ -18,20 +18,14 @@ use std::collections::HashMap; use datafusion::common::{Column, ScalarValue, TableReference}; -use datafusion::execution::FunctionRegistry; -use datafusion::functions_aggregate::all_default_aggregate_functions; -use datafusion::functions_window::all_default_window_functions; -use datafusion::logical_expr::expr::{ - Alias, FieldMetadata, NullTreatment as DFNullTreatment, WindowFunction, WindowFunctionParams, -}; -use datafusion::logical_expr::{Expr, ExprFunctionExt, WindowFrame, WindowFunctionDefinition, lit}; +use datafusion::logical_expr::expr::{Alias, FieldMetadata, NullTreatment as DFNullTreatment}; +use datafusion::logical_expr::{Expr, ExprFunctionExt, lit}; use datafusion::{functions, functions_aggregate, functions_window}; use pyo3::prelude::*; use pyo3::wrap_pyfunction; use crate::common::data_type::{NullTreatment, PyScalarValue}; -use crate::context::PySessionContext; -use crate::errors::{PyDataFusionError, PyDataFusionResult}; +use crate::errors::PyDataFusionResult; use crate::expr::PyExpr; use crate::expr::conditional_expr::PyCaseBuilder; use crate::expr::sort_expr::{PySortExpr, to_sort_expressions}; @@ -306,126 +300,6 @@ fn when(when: PyExpr, then: PyExpr) -> PyResult { Ok(PyCaseBuilder::new(None).when(when, then)) } -/// Helper function to find the appropriate window function. -/// -/// Search procedure: -/// 1) Search built in window functions, which are being deprecated. -/// 1) If a session context is provided: -/// 1) search User Defined Aggregate Functions (UDAFs) -/// 1) search registered window functions -/// 1) search registered aggregate functions -/// 1) If no function has been found, search default aggregate functions. -/// -/// NOTE: we search the built-ins first because the `UDAF` versions currently do not have the same behavior. -fn find_window_fn( - name: &str, - ctx: Option, -) -> PyDataFusionResult { - if let Some(ctx) = ctx { - // search UDAFs - let udaf = ctx - .ctx - .udaf(name) - .map(WindowFunctionDefinition::AggregateUDF) - .ok(); - - if let Some(udaf) = udaf { - return Ok(udaf); - } - - let session_state = ctx.ctx.state(); - - // search registered window functions - let window_fn = session_state - .window_functions() - .get(name) - .map(|f| WindowFunctionDefinition::WindowUDF(f.clone())); - - if let Some(window_fn) = window_fn { - return Ok(window_fn); - } - - // search registered aggregate functions - let agg_fn = session_state - .aggregate_functions() - .get(name) - .map(|f| WindowFunctionDefinition::AggregateUDF(f.clone())); - - if let Some(agg_fn) = agg_fn { - return Ok(agg_fn); - } - } - - // search default aggregate functions - let agg_fn = all_default_aggregate_functions() - .iter() - .find(|v| v.name() == name || v.aliases().contains(&name.to_string())) - .map(|f| WindowFunctionDefinition::AggregateUDF(f.clone())); - - if let Some(agg_fn) = agg_fn { - return Ok(agg_fn); - } - - // search default window functions - let window_fn = all_default_window_functions() - .iter() - .find(|v| v.name() == name || v.aliases().contains(&name.to_string())) - .map(|f| WindowFunctionDefinition::WindowUDF(f.clone())); - - if let Some(window_fn) = window_fn { - return Ok(window_fn); - } - - Err(PyDataFusionError::Common(format!( - "window function `{name}` not found" - ))) -} - -/// Creates a new Window function expression -#[allow(clippy::too_many_arguments)] -#[pyfunction] -#[pyo3(signature = (name, args, partition_by=None, order_by=None, window_frame=None, filter=None, distinct=false, ctx=None))] -fn window( - name: &str, - args: Vec, - partition_by: Option>, - order_by: Option>, - window_frame: Option, - filter: Option, - distinct: bool, - ctx: Option, -) -> PyResult { - let fun = find_window_fn(name, ctx)?; - - let window_frame = window_frame - .map(|w| w.into()) - .unwrap_or(WindowFrame::new(order_by.as_ref().map(|v| !v.is_empty()))); - let filter = filter.map(|f| f.expr.into()); - - Ok(PyExpr { - expr: datafusion::logical_expr::Expr::WindowFunction(Box::new(WindowFunction { - fun, - params: WindowFunctionParams { - args: args.into_iter().map(|x| x.expr).collect::>(), - partition_by: partition_by - .unwrap_or_default() - .into_iter() - .map(|x| x.expr) - .collect::>(), - order_by: order_by - .unwrap_or_default() - .into_iter() - .map(|x| x.into()) - .collect::>(), - window_frame, - filter, - distinct, - null_treatment: None, - }, - })), - }) -} - // Generates a [pyo3] wrapper for associated aggregate functions. // All of the builder options are exposed to the python internal // function and we rely on the wrappers to only use those that @@ -1186,7 +1060,6 @@ pub(crate) fn init_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(self::uuid))?; // Use self to avoid name collision m.add_wrapped(wrap_pyfunction!(var_pop))?; m.add_wrapped(wrap_pyfunction!(var_sample))?; - m.add_wrapped(wrap_pyfunction!(window))?; m.add_wrapped(wrap_pyfunction!(regr_avgx))?; m.add_wrapped(wrap_pyfunction!(regr_avgy))?; m.add_wrapped(wrap_pyfunction!(regr_count))?; diff --git a/docs/source/user-guide/common-operations/windows.rst b/docs/source/user-guide/common-operations/windows.rst index c8fdea8f4..d77881bcf 100644 --- a/docs/source/user-guide/common-operations/windows.rst +++ b/docs/source/user-guide/common-operations/windows.rst @@ -175,10 +175,7 @@ it's ``Type 2`` column that are null. Aggregate Functions ------------------- -You can use any :ref:`Aggregation Function` as a window function. Currently -aggregate functions must use the deprecated -:py:func:`datafusion.functions.window` API but this should be resolved in -DataFusion 42.0 (`Issue Link `_). Here +You can use any :ref:`Aggregation Function` as a window function. Here is an example that shows how to compare each pokemons’s attack power with the average attack power in its ``"Type 1"`` using the :py:func:`datafusion.functions.avg` function. @@ -189,10 +186,12 @@ power in its ``"Type 1"`` using the :py:func:`datafusion.functions.avg` function col('"Name"'), col('"Attack"'), col('"Type 1"'), - f.window("avg", [col('"Attack"')]) - .partition_by(col('"Type 1"')) - .build() - .alias("Average Attack"), + f.avg(col('"Attack"')).over( + Window( + window_frame=WindowFrame("rows", None, None), + partition_by=[col('"Type 1"')], + ) + ).alias("Average Attack"), ) Available Functions diff --git a/docs/source/user-guide/dataframe/rendering.rst b/docs/source/user-guide/dataframe/rendering.rst index 9dea948bb..dc61a422f 100644 --- a/docs/source/user-guide/dataframe/rendering.rst +++ b/docs/source/user-guide/dataframe/rendering.rst @@ -15,18 +15,18 @@ .. specific language governing permissions and limitations .. under the License. -HTML Rendering in Jupyter -========================= +DataFrame Rendering +=================== -When working in Jupyter notebooks or other environments that support rich HTML display, -DataFusion DataFrames automatically render as nicely formatted HTML tables. This functionality -is provided by the ``_repr_html_`` method, which is automatically called by Jupyter to provide -a richer visualization than plain text output. +DataFusion provides configurable rendering for DataFrames in both plain text and HTML +formats. The ``datafusion.dataframe_formatter`` module controls how DataFrames are +displayed in Jupyter notebooks (via ``_repr_html_``), in the terminal (via ``__repr__``), +and anywhere else a string or HTML representation is needed. -Basic HTML Rendering --------------------- +Basic Rendering +--------------- -In a Jupyter environment, simply displaying a DataFrame object will trigger HTML rendering: +In a Jupyter environment, displaying a DataFrame triggers HTML rendering: .. code-block:: python @@ -36,74 +36,117 @@ In a Jupyter environment, simply displaying a DataFrame object will trigger HTML # Explicit display also uses HTML rendering display(df) -Customizing HTML Rendering ---------------------------- +In a terminal or when converting to string, plain text rendering is used: + +.. code-block:: python -DataFusion provides extensive customization options for HTML table rendering through the -``datafusion.html_formatter`` module. + # Plain text table output + print(df) -Configuring the HTML Formatter -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Configuring the Formatter +------------------------- -You can customize how DataFrames are rendered by configuring the formatter: +You can customize how DataFrames are rendered by configuring the global formatter: .. code-block:: python - from datafusion.html_formatter import configure_formatter - - # Change the default styling + from datafusion.dataframe_formatter import configure_formatter + configure_formatter( - max_cell_length=25, # Maximum characters in a cell before truncation - max_width=1000, # Maximum width in pixels - max_height=300, # Maximum height in pixels - max_memory_bytes=2097152, # Maximum memory for rendering (2MB) - min_rows=10, # Minimum number of rows to display - max_rows=10, # Maximum rows to display in __repr__ - enable_cell_expansion=True,# Allow expanding truncated cells - custom_css=None, # Additional custom CSS + max_cell_length=25, # Maximum characters in a cell before truncation + max_width=1000, # Maximum width in pixels (HTML only) + max_height=300, # Maximum height in pixels (HTML only) + max_memory_bytes=2097152, # Maximum memory for rendering (2MB) + min_rows=10, # Minimum number of rows to display + max_rows=10, # Maximum rows to display + enable_cell_expansion=True, # Allow expanding truncated cells (HTML only) + custom_css=None, # Additional custom CSS (HTML only) show_truncation_message=True, # Show message when data is truncated - style_provider=None, # Custom styling provider - use_shared_styles=True # Share styles across tables + style_provider=None, # Custom styling provider (HTML only) + use_shared_styles=True, # Share styles across tables (HTML only) ) The formatter settings affect all DataFrames displayed after configuration. Custom Style Providers ------------------------ +---------------------- -For advanced styling needs, you can create a custom style provider: +For HTML styling, you can create a custom style provider that implements the +``StyleProvider`` protocol: .. code-block:: python - from datafusion.html_formatter import StyleProvider, configure_formatter - - class MyStyleProvider(StyleProvider): - def get_table_styles(self): - return { - "table": "border-collapse: collapse; width: 100%;", - "th": "background-color: #007bff; color: white; padding: 8px; text-align: left;", - "td": "border: 1px solid #ddd; padding: 8px;", - "tr:nth-child(even)": "background-color: #f2f2f2;", - } - - def get_value_styles(self, dtype, value): - """Return custom styles for specific values""" - if dtype == "float" and value < 0: - return "color: red;" - return None - + from datafusion.dataframe_formatter import configure_formatter + + class MyStyleProvider: + def get_cell_style(self): + """Return CSS style string for table data cells.""" + return "border: 1px solid #ddd; padding: 8px; text-align: left;" + + def get_header_style(self): + """Return CSS style string for table header cells.""" + return ( + "background-color: #007bff; color: white; " + "padding: 8px; text-align: left;" + ) + # Apply the custom style provider configure_formatter(style_provider=MyStyleProvider()) +Custom Cell Formatters +---------------------- + +You can register custom formatters for specific Python types. A cell formatter is any +callable that takes a value and returns a string: + +.. code-block:: python + + from datafusion.dataframe_formatter import get_formatter + + formatter = get_formatter() + + # Format floats to 2 decimal places + formatter.register_formatter(float, lambda v: f"{v:.2f}") + + # Format dates in a custom way + from datetime import date + formatter.register_formatter(date, lambda v: v.strftime("%B %d, %Y")) + +Custom Cell and Header Builders +------------------------------- + +For full control over the HTML of individual cells or headers, you can set custom +builder functions: + +.. code-block:: python + + from datafusion.dataframe_formatter import get_formatter + + formatter = get_formatter() + + # Custom cell builder receives (value, row, col, table_id) and returns HTML + def my_cell_builder(value, row, col, table_id): + color = "red" if isinstance(value, (int, float)) and value < 0 else "black" + return f"{value}" + + formatter.set_custom_cell_builder(my_cell_builder) + + # Custom header builder receives a schema field and returns HTML + def my_header_builder(field): + return f"{field.name}" + + formatter.set_custom_header_builder(my_header_builder) + Performance Optimization with Shared Styles -------------------------------------------- -The ``use_shared_styles`` parameter (enabled by default) optimizes performance when displaying -multiple DataFrames in notebook environments: +The ``use_shared_styles`` parameter (enabled by default) optimizes performance when +displaying multiple DataFrames in notebook environments: .. code-block:: python - from datafusion.html_formatter import StyleProvider, configure_formatter + from datafusion.dataframe_formatter import configure_formatter + # Default: Use shared styles (recommended for notebooks) configure_formatter(use_shared_styles=True) @@ -111,76 +154,48 @@ multiple DataFrames in notebook environments: configure_formatter(use_shared_styles=False) When ``use_shared_styles=True``: + - CSS styles and JavaScript are included only once per notebook session - This reduces HTML output size and prevents style duplication - Improves rendering performance with many DataFrames - Applies consistent styling across all DataFrames -Creating a Custom Formatter ----------------------------- +Working with the Formatter Directly +------------------------------------ -For complete control over rendering, you can implement a custom formatter: +You can use ``get_formatter()`` and ``set_formatter()`` for direct access to the global +formatter instance: .. code-block:: python - from datafusion.html_formatter import Formatter, get_formatter - - class MyFormatter(Formatter): - def format_html(self, batches, schema, has_more=False, table_uuid=None): - # Create your custom HTML here - html = "
" - # ... formatting logic ... - html += "
" - return html - - # Set as the global formatter - configure_formatter(formatter_class=MyFormatter) - - # Or use the formatter just for specific operations + from datafusion.dataframe_formatter import ( + DataFrameHtmlFormatter, + get_formatter, + set_formatter, + ) + + # Get and modify the current formatter formatter = get_formatter() - custom_html = formatter.format_html(batches, schema) + print(formatter.max_rows) + print(formatter.max_cell_length) -Managing Formatters -------------------- + # Create and set a fully custom formatter + custom_formatter = DataFrameHtmlFormatter( + max_cell_length=50, + max_rows=20, + enable_cell_expansion=False, + ) + set_formatter(custom_formatter) Reset to default formatting: .. code-block:: python - from datafusion.html_formatter import reset_formatter - + from datafusion.dataframe_formatter import reset_formatter + # Reset to default settings reset_formatter() -Get the current formatter settings: - -.. code-block:: python - - from datafusion.html_formatter import get_formatter - - formatter = get_formatter() - print(formatter.max_rows) - print(formatter.theme) - -Contextual Formatting ----------------------- - -You can also use a context manager to temporarily change formatting settings: - -.. code-block:: python - - from datafusion.html_formatter import formatting_context - - # Default formatting - df.show() - - # Temporarily use different formatting - with formatting_context(max_rows=100, theme="dark"): - df.show() # Will use the temporary settings - - # Back to default formatting - df.show() - Memory and Display Controls --------------------------- @@ -188,10 +203,12 @@ You can control how much data is displayed and how much memory is used for rende .. code-block:: python + from datafusion.dataframe_formatter import configure_formatter + configure_formatter( max_memory_bytes=4 * 1024 * 1024, # 4MB maximum memory for display min_rows=20, # Always show at least 20 rows - max_rows=50 # Show up to 50 rows in output + max_rows=50, # Show up to 50 rows in output ) These parameters help balance comprehensive data display against performance considerations. @@ -216,7 +233,7 @@ Additional Resources * :doc:`../io/index` - I/O Guide for reading data from various sources * :doc:`../data-sources` - Comprehensive data sources guide * :ref:`io_csv` - CSV file reading -* :ref:`io_parquet` - Parquet file reading +* :ref:`io_parquet` - Parquet file reading * :ref:`io_json` - JSON file reading * :ref:`io_avro` - Avro file reading * :ref:`io_custom_table_provider` - Custom table providers diff --git a/examples/tpch/q02_minimum_cost_supplier.py b/examples/tpch/q02_minimum_cost_supplier.py index 7390d0892..47961d2ef 100644 --- a/examples/tpch/q02_minimum_cost_supplier.py +++ b/examples/tpch/q02_minimum_cost_supplier.py @@ -32,6 +32,7 @@ import datafusion from datafusion import SessionContext, col, lit from datafusion import functions as F +from datafusion.expr import Window from util import get_data_path # This is the part we're looking for. Values selected here differ from the spec in order to run @@ -106,11 +107,8 @@ window_frame = datafusion.WindowFrame("rows", None, None) df = df.with_column( "min_cost", - F.window( - "min", - [col("ps_supplycost")], - partition_by=[col("ps_partkey")], - window_frame=window_frame, + F.min(col("ps_supplycost")).over( + Window(partition_by=[col("ps_partkey")], window_frame=window_frame) ), ) diff --git a/examples/tpch/q11_important_stock_identification.py b/examples/tpch/q11_important_stock_identification.py index 22829ab7c..de309fa64 100644 --- a/examples/tpch/q11_important_stock_identification.py +++ b/examples/tpch/q11_important_stock_identification.py @@ -29,6 +29,7 @@ from datafusion import SessionContext, WindowFrame, col, lit from datafusion import functions as F +from datafusion.expr import Window from util import get_data_path NATION = "GERMANY" @@ -71,7 +72,7 @@ window_frame = WindowFrame("rows", None, None) df = df.with_column( - "total_value", F.window("sum", [col("value")], window_frame=window_frame) + "total_value", F.sum(col("value")).over(Window(window_frame=window_frame)) ) # Limit to the parts for which there is a significant value based on the fraction of the total diff --git a/examples/tpch/q15_top_supplier.py b/examples/tpch/q15_top_supplier.py index c321048f2..5128937a7 100644 --- a/examples/tpch/q15_top_supplier.py +++ b/examples/tpch/q15_top_supplier.py @@ -31,6 +31,7 @@ import pyarrow as pa from datafusion import SessionContext, WindowFrame, col, lit from datafusion import functions as F +from datafusion.expr import Window from util import get_data_path DATE = "1996-01-01" @@ -70,7 +71,8 @@ # Use a window function to find the maximum revenue across the entire dataframe window_frame = WindowFrame("rows", None, None) df = df.with_column( - "max_revenue", F.window("max", [col("total_revenue")], window_frame=window_frame) + "max_revenue", + F.max(col("total_revenue")).over(Window(window_frame=window_frame)), ) # Find all suppliers whose total revenue is the same as the maximum diff --git a/examples/tpch/q17_small_quantity_order.py b/examples/tpch/q17_small_quantity_order.py index 6d76fe506..5ccb38422 100644 --- a/examples/tpch/q17_small_quantity_order.py +++ b/examples/tpch/q17_small_quantity_order.py @@ -30,6 +30,7 @@ from datafusion import SessionContext, WindowFrame, col, lit from datafusion import functions as F +from datafusion.expr import Window from util import get_data_path BRAND = "Brand#23" @@ -58,11 +59,8 @@ window_frame = WindowFrame("rows", None, None) df = df.with_column( "avg_quantity", - F.window( - "avg", - [col("l_quantity")], - window_frame=window_frame, - partition_by=[col("l_partkey")], + F.avg(col("l_quantity")).over( + Window(partition_by=[col("l_partkey")], window_frame=window_frame) ), ) diff --git a/examples/tpch/q22_global_sales_opportunity.py b/examples/tpch/q22_global_sales_opportunity.py index c4d115b74..a2d41b215 100644 --- a/examples/tpch/q22_global_sales_opportunity.py +++ b/examples/tpch/q22_global_sales_opportunity.py @@ -28,6 +28,7 @@ from datafusion import SessionContext, WindowFrame, col, lit from datafusion import functions as F +from datafusion.expr import Window from util import get_data_path NATION_CODES = [13, 31, 23, 29, 30, 18, 17] @@ -55,7 +56,8 @@ # current row. We want our frame to cover the entire data frame. window_frame = WindowFrame("rows", None, None) df = df.with_column( - "avg_balance", F.window("avg", [col("c_acctbal")], window_frame=window_frame) + "avg_balance", + F.avg(col("c_acctbal")).over(Window(window_frame=window_frame)), ) df.show() diff --git a/python/datafusion/__init__.py b/python/datafusion/__init__.py index a736c3966..ee02c921d 100644 --- a/python/datafusion/__init__.py +++ b/python/datafusion/__init__.py @@ -35,7 +35,7 @@ # The following imports are okay to remain as opaque to the user. from ._internal import Config -from .catalog import Catalog, Database, Table +from .catalog import Catalog, Table from .col import col, column from .common import DFSchema from .context import ( @@ -81,7 +81,6 @@ "DFSchema", "DataFrame", "DataFrameWriteOptions", - "Database", "ExecutionPlan", "ExplainFormat", "Expr", diff --git a/python/datafusion/catalog.py b/python/datafusion/catalog.py index 03c0ddc68..20da5e671 100644 --- a/python/datafusion/catalog.py +++ b/python/datafusion/catalog.py @@ -129,11 +129,6 @@ def schema(self, name: str = "public") -> Schema: else schema ) - @deprecated("Use `schema` instead.") - def database(self, name: str = "public") -> Schema: - """Returns the database with the given ``name`` from this catalog.""" - return self.schema(name) - def register_schema( self, name: str, @@ -195,11 +190,6 @@ def table_exist(self, name: str) -> bool: return self._raw_schema.table_exist(name) -@deprecated("Use `Schema` instead.") -class Database(Schema): - """See `Schema`.""" - - class Table: """A DataFusion table. diff --git a/python/datafusion/context.py b/python/datafusion/context.py index e3949de83..c3f94cc16 100644 --- a/python/datafusion/context.py +++ b/python/datafusion/context.py @@ -426,11 +426,6 @@ def with_temp_file_path(self, path: str | pathlib.Path) -> RuntimeEnvBuilder: return self -@deprecated("Use `RuntimeEnvBuilder` instead.") -class RuntimeConfig(RuntimeEnvBuilder): - """See `RuntimeEnvBuilder`.""" - - class SQLOptions: """Options to be used when performing SQL queries.""" @@ -785,14 +780,6 @@ def from_arrow( """ return DataFrame(self.ctx.from_arrow(data, name)) - @deprecated("Use ``from_arrow`` instead.") - def from_arrow_table(self, data: pa.Table, name: str | None = None) -> DataFrame: - """Create a :py:class:`~datafusion.dataframe.DataFrame` from an Arrow table. - - This is an alias for :py:func:`from_arrow`. - """ - return self.from_arrow(data, name) - def from_pandas(self, data: pd.DataFrame, name: str | None = None) -> DataFrame: """Create a :py:class:`~datafusion.dataframe.DataFrame` from a Pandas DataFrame. @@ -1260,14 +1247,6 @@ def catalog(self, name: str = "datafusion") -> Catalog: """Retrieve a catalog by name.""" return Catalog(self.ctx.catalog(name)) - @deprecated( - "Use the catalog provider interface ``SessionContext.Catalog`` to " - "examine available catalogs, schemas and tables" - ) - def tables(self) -> set[str]: - """Deprecated.""" - return self.ctx.tables() - def table(self, name: str) -> DataFrame: """Retrieve a previously registered table by name.""" return DataFrame(self.ctx.table(name)) diff --git a/python/datafusion/dataframe.py b/python/datafusion/dataframe.py index 9dc5f0e7d..c00c85fdb 100644 --- a/python/datafusion/dataframe.py +++ b/python/datafusion/dataframe.py @@ -489,17 +489,6 @@ def find_qualified_columns(self, *names: str) -> list[Expr]: raw_exprs = self.df.find_qualified_columns(list(names)) return [Expr(e) for e in raw_exprs] - @deprecated( - "select_columns() is deprecated. Use :py:meth:`~DataFrame.select` instead" - ) - def select_columns(self, *args: str) -> DataFrame: - """Filter the DataFrame by columns. - - Returns: - DataFrame only containing the specified columns. - """ - return self.select(*args) - def select_exprs(self, *args: str) -> DataFrame: """Project arbitrary list of expression strings into a new DataFrame. @@ -1603,15 +1592,6 @@ def count(self) -> int: """ return self.df.count() - @deprecated("Use :py:func:`unnest_columns` instead.") - def unnest_column( - self, - column: str, - preserve_nulls: bool = True, - ) -> DataFrame: - """See :py:func:`unnest_columns`.""" - return DataFrame(self.df.unnest_column(column, preserve_nulls=preserve_nulls)) - def unnest_columns( self, *columns: str, diff --git a/python/datafusion/dataframe_formatter.py b/python/datafusion/dataframe_formatter.py index b8af45a1b..fd2da99f0 100644 --- a/python/datafusion/dataframe_formatter.py +++ b/python/datafusion/dataframe_formatter.py @@ -748,7 +748,7 @@ def get_formatter() -> DataFrameHtmlFormatter: The global HTML formatter instance Example: - >>> from datafusion.html_formatter import get_formatter + >>> from datafusion.dataframe_formatter import get_formatter >>> formatter = get_formatter() >>> formatter.max_cell_length = 50 # Increase cell length """ @@ -762,7 +762,7 @@ def set_formatter(formatter: DataFrameHtmlFormatter) -> None: formatter: The formatter instance to use globally Example: - >>> from datafusion.html_formatter import get_formatter, set_formatter + >>> from datafusion.dataframe_formatter import get_formatter, set_formatter >>> custom_formatter = DataFrameHtmlFormatter(max_cell_length=100) >>> set_formatter(custom_formatter) """ @@ -783,7 +783,7 @@ def configure_formatter(**kwargs: Any) -> None: ValueError: If any invalid parameters are provided Example: - >>> from datafusion.html_formatter import configure_formatter + >>> from datafusion.dataframe_formatter import configure_formatter >>> configure_formatter( ... max_cell_length=50, ... max_height=500, @@ -827,7 +827,7 @@ def reset_formatter() -> None: and sets it as the global formatter for all DataFrames. Example: - >>> from datafusion.html_formatter import reset_formatter + >>> from datafusion.dataframe_formatter import reset_formatter >>> reset_formatter() # Reset formatter to default settings """ formatter = DataFrameHtmlFormatter() diff --git a/python/datafusion/expr.py b/python/datafusion/expr.py index 35388468c..7cd74ecd5 100644 --- a/python/datafusion/expr.py +++ b/python/datafusion/expr.py @@ -27,11 +27,6 @@ from collections.abc import Iterable, Sequence from typing import TYPE_CHECKING, Any, ClassVar -try: - from warnings import deprecated # Python 3.13+ -except ImportError: - from typing_extensions import deprecated # Python 3.12 - import pyarrow as pa from ._internal import expr as expr_internal @@ -356,16 +351,6 @@ def to_variant(self) -> Any: """Convert this expression into a python object if possible.""" return self.expr.to_variant() - @deprecated( - "display_name() is deprecated. Use :py:meth:`~Expr.schema_name` instead" - ) - def display_name(self) -> str: - """Returns the name of this expression as it should appear in a schema. - - This name will not include any CAST expressions. - """ - return self.schema_name() - def schema_name(self) -> str: """Returns the name of this expression as it should appear in a schema. diff --git a/python/datafusion/functions.py b/python/datafusion/functions.py index 9dfabb62d..841cd9c0b 100644 --- a/python/datafusion/functions.py +++ b/python/datafusion/functions.py @@ -18,7 +18,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import Any import pyarrow as pa @@ -29,19 +29,11 @@ Expr, SortExpr, SortKey, - WindowFrame, expr_list_to_raw_expr_list, sort_list_to_raw_sort_list, sort_or_default, ) -try: - from warnings import deprecated # Python 3.13+ -except ImportError: - from typing_extensions import deprecated # Python 3.12 - -if TYPE_CHECKING: - from datafusion.context import SessionContext __all__ = [ "abs", "acos", @@ -339,8 +331,6 @@ "var_sample", "version", "when", - # Window Functions - "window", ] @@ -664,49 +654,6 @@ def when(when: Expr, then: Expr) -> CaseBuilder: return CaseBuilder(f.when(when.expr, then.expr)) -@deprecated("Prefer to call Expr.over() instead") -def window( - name: str, - args: list[Expr], - partition_by: list[Expr] | Expr | None = None, - order_by: list[SortKey] | SortKey | None = None, - window_frame: WindowFrame | None = None, - filter: Expr | None = None, - distinct: bool = False, - ctx: SessionContext | None = None, -) -> Expr: - """Creates a new Window function expression. - - This interface will soon be deprecated. Instead of using this interface, - users should call the window functions directly. For example, to perform a - lag use:: - - df.select(functions.lag(col("a")).partition_by(col("b")).build()) - - The ``order_by`` parameter accepts column names or expressions, e.g.:: - - window("lag", [col("a")], order_by="ts") - """ - args = [a.expr for a in args] - partition_by_raw = expr_list_to_raw_expr_list(partition_by) - order_by_raw = sort_list_to_raw_sort_list(order_by) - window_frame = window_frame.window_frame if window_frame is not None else None - ctx = ctx.ctx if ctx is not None else None - filter_raw = filter.expr if filter is not None else None - return Expr( - f.window( - name, - args, - partition_by=partition_by_raw, - order_by=order_by_raw, - window_frame=window_frame, - ctx=ctx, - filter=filter_raw, - distinct=distinct, - ) - ) - - # scalar functions def abs(arg: Expr) -> Expr: """Return the absolute value of a given number. diff --git a/python/datafusion/html_formatter.py b/python/datafusion/html_formatter.py deleted file mode 100644 index 65eb1f042..000000000 --- a/python/datafusion/html_formatter.py +++ /dev/null @@ -1,29 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -"""Deprecated module for dataframe formatting.""" - -import warnings - -from datafusion.dataframe_formatter import * # noqa: F403 - -warnings.warn( - "The module 'html_formatter' is deprecated and will be removed in the next release." - "Please use 'dataframe_formatter' instead.", - DeprecationWarning, - stacklevel=3, -) diff --git a/python/datafusion/substrait.py b/python/datafusion/substrait.py index 3115238fa..6353ef8cc 100644 --- a/python/datafusion/substrait.py +++ b/python/datafusion/substrait.py @@ -25,11 +25,6 @@ from typing import TYPE_CHECKING -try: - from warnings import deprecated # Python 3.13+ -except ImportError: - from typing_extensions import deprecated # Python 3.12 - from datafusion.plan import LogicalPlan from ._internal import substrait as substrait_internal @@ -88,11 +83,6 @@ def from_json(json: str) -> Plan: return Plan(substrait_internal.Plan.from_json(json)) -@deprecated("Use `Plan` instead.") -class plan(Plan): # noqa: N801 - """See `Plan`.""" - - class Serde: """Provides the ``Substrait`` serialization and deserialization.""" @@ -158,11 +148,6 @@ def deserialize_bytes(proto_bytes: bytes) -> Plan: return Plan(substrait_internal.Serde.deserialize_bytes(proto_bytes)) -@deprecated("Use `Serde` instead.") -class serde(Serde): # noqa: N801 - """See `Serde` instead.""" - - class Producer: """Generates substrait plans from a logical plan.""" @@ -184,11 +169,6 @@ def to_substrait_plan(logical_plan: LogicalPlan, ctx: SessionContext) -> Plan: ) -@deprecated("Use `Producer` instead.") -class producer(Producer): # noqa: N801 - """Use `Producer` instead.""" - - class Consumer: """Generates a logical plan from a substrait plan.""" @@ -206,8 +186,3 @@ def from_substrait_plan(ctx: SessionContext, plan: Plan) -> LogicalPlan: return LogicalPlan( substrait_internal.Consumer.from_substrait_plan(ctx.ctx, plan.plan_internal) ) - - -@deprecated("Use `Consumer` instead.") -class consumer(Consumer): # noqa: N801 - """Use `Consumer` instead.""" diff --git a/python/datafusion/udf.py b/python/datafusion/udf.py deleted file mode 100644 index c7265fa09..000000000 --- a/python/datafusion/udf.py +++ /dev/null @@ -1,29 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -"""Deprecated module for user defined functions.""" - -import warnings - -from datafusion.user_defined import * # noqa: F403 - -warnings.warn( - "The module 'udf' is deprecated and will be removed in the next release. " - "Please use 'user_defined' instead.", - DeprecationWarning, - stacklevel=2, -) diff --git a/python/tests/test_expr.py b/python/tests/test_expr.py index 9a287c1f7..1cf824a15 100644 --- a/python/tests/test_expr.py +++ b/python/tests/test_expr.py @@ -319,27 +319,6 @@ def test_expr_getitem() -> None: assert array_values == [2, 5, None, None] -def test_display_name_deprecation(): - import warnings - - expr = col("foo") - with warnings.catch_warnings(record=True) as w: - # Cause all warnings to always be triggered - warnings.simplefilter("always") - - # should trigger warning - name = expr.display_name() - - # Verify some things - assert len(w) == 1 - assert issubclass(w[-1].category, DeprecationWarning) - assert "deprecated" in str(w[-1].message) - - # returns appropriate result - assert name == expr.schema_name() - assert name == "foo" - - @pytest.fixture def df(): ctx = SessionContext() From 1be838bb47f04bcf4d1a0f65e3e6958aa9366f3f Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Sun, 12 Apr 2026 21:24:39 -0400 Subject: [PATCH 51/56] Release 53.0.0 (#1491) * Update version number and changelog * minor: set version number on dependency to publish to crates.io * taplo fmt --- Cargo.lock | 6 +-- Cargo.toml | 6 +-- dev/changelog/53.0.0.md | 107 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 dev/changelog/53.0.0.md diff --git a/Cargo.lock b/Cargo.lock index ee89c8bda..1cbb0acb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1312,7 +1312,7 @@ dependencies = [ [[package]] name = "datafusion-ffi-example" -version = "52.0.0" +version = "53.0.0" dependencies = [ "arrow", "arrow-array", @@ -1662,7 +1662,7 @@ dependencies = [ [[package]] name = "datafusion-python" -version = "52.0.0" +version = "53.0.0" dependencies = [ "arrow", "arrow-select", @@ -1692,7 +1692,7 @@ dependencies = [ [[package]] name = "datafusion-python-util" -version = "52.0.0" +version = "53.0.0" dependencies = [ "arrow", "datafusion", diff --git a/Cargo.toml b/Cargo.toml index 3a34e204c..14408d2bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ # under the License. [workspace.package] -version = "52.0.0" +version = "53.0.0" homepage = "https://datafusion.apache.org/python" repository = "https://github.com/apache/datafusion-python" authors = ["Apache DataFusion "] @@ -59,9 +59,9 @@ object_store = { version = "0.13.1" } url = "2" log = "0.4.29" parking_lot = "0.12" -prost-types = "0.14.3" # keep in line with `datafusion-substrait` +prost-types = "0.14.3" # keep in line with `datafusion-substrait` pyo3-build-config = "0.28" -datafusion-python-util = { path = "crates/util" } +datafusion-python-util = { path = "crates/util", version = "53.0.0" } [profile.release] lto = "thin" diff --git a/dev/changelog/53.0.0.md b/dev/changelog/53.0.0.md new file mode 100644 index 000000000..3e27a852d --- /dev/null +++ b/dev/changelog/53.0.0.md @@ -0,0 +1,107 @@ + + +# Apache DataFusion Python 53.0.0 Changelog + +This release consists of 52 commits from 9 contributors. See credits at the end of this changelog for more information. + +**Breaking changes:** + +- minor: remove deprecated interfaces [#1481](https://github.com/apache/datafusion-python/pull/1481) (timsaucer) + +**Implemented enhancements:** + +- feat: feat: add to_time, to_local_time, to_date functions [#1387](https://github.com/apache/datafusion-python/pull/1387) (mesejo) +- feat: Add FFI_TableProviderFactory support [#1396](https://github.com/apache/datafusion-python/pull/1396) (davisp) + +**Fixed bugs:** + +- fix: satisfy rustfmt check in lib.rs re-exports [#1406](https://github.com/apache/datafusion-python/pull/1406) (kevinjqliu) + +**Documentation updates:** + +- docs: clarify DataFusion 52 FFI session-parameter requirement for provider hooks [#1439](https://github.com/apache/datafusion-python/pull/1439) (kevinjqliu) + +**Other:** + +- Merge release 52.0.0 into main [#1389](https://github.com/apache/datafusion-python/pull/1389) (timsaucer) +- Add workflow to verify release candidate on multiple systems [#1388](https://github.com/apache/datafusion-python/pull/1388) (timsaucer) +- Allow running "verify release candidate" github workflow on Windows [#1392](https://github.com/apache/datafusion-python/pull/1392) (kevinjqliu) +- ci: update pre-commit hooks, fix linting, and refresh dependencies [#1385](https://github.com/apache/datafusion-python/pull/1385) (dariocurr) +- Add CI check for crates.io patches [#1407](https://github.com/apache/datafusion-python/pull/1407) (timsaucer) +- Enable doc tests in local and CI testing [#1409](https://github.com/apache/datafusion-python/pull/1409) (ntjohnson1) +- Upgrade to DataFusion 53 [#1402](https://github.com/apache/datafusion-python/pull/1402) (nuno-faria) +- Catch warnings in FFI unit tests [#1410](https://github.com/apache/datafusion-python/pull/1410) (timsaucer) +- Add docstring examples for Scalar trigonometric functions [#1411](https://github.com/apache/datafusion-python/pull/1411) (ntjohnson1) +- Create workspace with core and util crates [#1414](https://github.com/apache/datafusion-python/pull/1414) (timsaucer) +- Add docstring examples for Scalar regex, crypto, struct and other [#1422](https://github.com/apache/datafusion-python/pull/1422) (ntjohnson1) +- Add docstring examples for Scalar math functions [#1421](https://github.com/apache/datafusion-python/pull/1421) (ntjohnson1) +- Add docstring examples for Common utility functions [#1419](https://github.com/apache/datafusion-python/pull/1419) (ntjohnson1) +- Add docstring examples for Aggregate basic and bitwise/boolean functions [#1416](https://github.com/apache/datafusion-python/pull/1416) (ntjohnson1) +- Fix CI errors on main [#1432](https://github.com/apache/datafusion-python/pull/1432) (timsaucer) +- Add docstring examples for Scalar temporal functions [#1424](https://github.com/apache/datafusion-python/pull/1424) (ntjohnson1) +- Add docstring examples for Aggregate statistical and regression functions [#1417](https://github.com/apache/datafusion-python/pull/1417) (ntjohnson1) +- Add docstring examples for Scalar array/list functions [#1420](https://github.com/apache/datafusion-python/pull/1420) (ntjohnson1) +- Add docstring examples for Scalar string functions [#1423](https://github.com/apache/datafusion-python/pull/1423) (ntjohnson1) +- Add docstring examples for Aggregate window functions [#1418](https://github.com/apache/datafusion-python/pull/1418) (ntjohnson1) +- ci: pin third-party actions to Apache-approved SHAs [#1438](https://github.com/apache/datafusion-python/pull/1438) (kevinjqliu) +- minor: bump datafusion to release version [#1441](https://github.com/apache/datafusion-python/pull/1441) (timsaucer) +- ci: add swap during build, use tpchgen-cli [#1443](https://github.com/apache/datafusion-python/pull/1443) (timsaucer) +- Update remaining existing examples to make testable/standalone executable [#1437](https://github.com/apache/datafusion-python/pull/1437) (ntjohnson1) +- Do not run validate_pycapsule if pointer_checked is used [#1426](https://github.com/apache/datafusion-python/pull/1426) (Tpt) +- Implement configuration extension support [#1391](https://github.com/apache/datafusion-python/pull/1391) (timsaucer) +- Add a working, more complete example of using a catalog (docs) [#1427](https://github.com/apache/datafusion-python/pull/1427) (toppyy) +- chore: update dependencies [#1447](https://github.com/apache/datafusion-python/pull/1447) (timsaucer) +- Complete doc string examples for functions.py [#1435](https://github.com/apache/datafusion-python/pull/1435) (ntjohnson1) +- chore: enforce uv lockfile consistency in CI and pre-commit [#1398](https://github.com/apache/datafusion-python/pull/1398) (mesejo) +- CI: Add CodeQL workflow for GitHub Actions security scanning [#1408](https://github.com/apache/datafusion-python/pull/1408) (kevinjqliu) +- ci: update codespell paths [#1469](https://github.com/apache/datafusion-python/pull/1469) (timsaucer) +- Add missing datetime functions [#1467](https://github.com/apache/datafusion-python/pull/1467) (timsaucer) +- Add AI skill to check current repository against upstream APIs [#1460](https://github.com/apache/datafusion-python/pull/1460) (timsaucer) +- Add missing string function `contains` [#1465](https://github.com/apache/datafusion-python/pull/1465) (timsaucer) +- Add missing conditional functions [#1464](https://github.com/apache/datafusion-python/pull/1464) (timsaucer) +- Reduce peak memory usage during release builds to fix OOM on manylinux runners [#1445](https://github.com/apache/datafusion-python/pull/1445) (kevinjqliu) +- Add missing map functions [#1461](https://github.com/apache/datafusion-python/pull/1461) (timsaucer) +- minor: Fix pytest instructions in the README [#1477](https://github.com/apache/datafusion-python/pull/1477) (nuno-faria) +- Add missing array functions [#1468](https://github.com/apache/datafusion-python/pull/1468) (timsaucer) +- Add missing scalar functions [#1470](https://github.com/apache/datafusion-python/pull/1470) (timsaucer) +- Add missing aggregate functions [#1471](https://github.com/apache/datafusion-python/pull/1471) (timsaucer) +- Add missing Dataframe functions [#1472](https://github.com/apache/datafusion-python/pull/1472) (timsaucer) +- Add missing deregister methods to SessionContext [#1473](https://github.com/apache/datafusion-python/pull/1473) (timsaucer) +- Add missing registration methods [#1474](https://github.com/apache/datafusion-python/pull/1474) (timsaucer) +- Add missing SessionContext utility methods [#1475](https://github.com/apache/datafusion-python/pull/1475) (timsaucer) + +## Credits + +Thank you to everyone who contributed to this release. Here is a breakdown of commits (PRs merged) per contributor. + +``` + 25 Tim Saucer + 13 Nick + 6 Kevin Liu + 2 Daniel Mesejo + 2 Nuno Faria + 1 Paul J. Davis + 1 Thomas Tanon + 1 Topias Pyykkönen + 1 dario curreri +``` + +Thank you also to everyone who contributed in other ways such as filing issues, reviewing PRs, and providing feedback on this release. + From 00b24572c98a257f06ff026a90c07634a86204d4 Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Mon, 13 Apr 2026 06:33:29 -0400 Subject: [PATCH 52/56] ci: disable symbol export on Windows verification (#1486) * Set rust flags on windows release verification * Forward flag to linker * Switch to msvc rust toolchain * Revert "Switch to msvc rust toolchain" This reverts commit 9879fc7dbe066098445b9600087e665435b58f8a. --- .github/workflows/verify-release-candidate.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/verify-release-candidate.yml b/.github/workflows/verify-release-candidate.yml index a10a4faa9..7a5deff5b 100644 --- a/.github/workflows/verify-release-candidate.yml +++ b/.github/workflows/verify-release-candidate.yml @@ -73,6 +73,11 @@ jobs: version: "27.4" repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Set RUSTFLAGS for Windows GNU linker + if: matrix.os == 'windows' + shell: bash + run: echo "RUSTFLAGS=-C link-arg=-Wl,--exclude-libs=ALL" >> "$GITHUB_ENV" + - name: Run release candidate verification shell: bash run: ./dev/release/verify-release-candidate.sh "${{ inputs.version }}" "${{ inputs.rc_number }}" From 8a7efead43cff8dc7515e27e53da7545100e25a7 Mon Sep 17 00:00:00 2001 From: Shreyesh Date: Mon, 13 Apr 2026 03:34:35 -0700 Subject: [PATCH 53/56] Add Python bindings for accessing ExecutionMetrics (#1381) * feat: add Python bindings for accessing ExecutionMetrics * test: imporve tests * first round of reviews * plan caching * address some concerns * merge and address comments * fix Ci issues * attempt to fix lint * fix build * fix docstring * address some more comments --------- Co-authored-by: ShreyeshArangath --- Cargo.lock | 1 + Cargo.toml | 1 + crates/core/Cargo.toml | 1 + crates/core/src/dataframe.rs | 49 +++- crates/core/src/lib.rs | 3 + crates/core/src/metrics.rs | 169 ++++++++++++++ crates/core/src/physical_plan.rs | 5 + .../dataframe/execution-metrics.rst | 215 ++++++++++++++++++ docs/source/user-guide/dataframe/index.rst | 9 + python/datafusion/__init__.py | 4 +- python/datafusion/plan.py | 177 ++++++++++++++ python/tests/test_plans.py | 192 +++++++++++++++- 12 files changed, 817 insertions(+), 9 deletions(-) create mode 100644 crates/core/src/metrics.rs create mode 100644 docs/source/user-guide/dataframe/execution-metrics.rst diff --git a/Cargo.lock b/Cargo.lock index 1cbb0acb8..4efca3eb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1667,6 +1667,7 @@ dependencies = [ "arrow", "arrow-select", "async-trait", + "chrono", "cstr", "datafusion", "datafusion-ffi", diff --git a/Cargo.toml b/Cargo.toml index 14408d2bc..d0e87a9a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ tokio = { version = "1.50" } pyo3 = { version = "0.28" } pyo3-async-runtimes = { version = "0.28" } pyo3-log = "0.13.3" +chrono = { version = "0.4", default-features = false } arrow = { version = "58" } arrow-array = { version = "58" } arrow-schema = { version = "58" } diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 3e2b01c8e..d714dc978 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -47,6 +47,7 @@ pyo3 = { workspace = true, features = [ ] } pyo3-async-runtimes = { workspace = true, features = ["tokio-runtime"] } pyo3-log = { workspace = true } +chrono = { workspace = true } arrow = { workspace = true, features = ["pyarrow"] } arrow-select = { workspace = true } datafusion = { workspace = true, features = ["avro", "unicode_expressions"] } diff --git a/crates/core/src/dataframe.rs b/crates/core/src/dataframe.rs index c067eac30..2d815ec76 100644 --- a/crates/core/src/dataframe.rs +++ b/crates/core/src/dataframe.rs @@ -37,9 +37,15 @@ use datafusion::config::{CsvOptions, ParquetColumnOptions, ParquetOptions, Table use datafusion::dataframe::{DataFrame, DataFrameWriteOptions}; use datafusion::error::DataFusionError; use datafusion::execution::SendableRecordBatchStream; +use datafusion::execution::context::TaskContext; use datafusion::logical_expr::SortExpr; use datafusion::logical_expr::dml::InsertOp; use datafusion::parquet::basic::{BrotliLevel, Compression, GzipLevel, ZstdLevel}; +use datafusion::physical_plan::{ + ExecutionPlan as DFExecutionPlan, collect as df_collect, + collect_partitioned as df_collect_partitioned, execute_stream as df_execute_stream, + execute_stream_partitioned as df_execute_stream_partitioned, +}; use datafusion::prelude::*; use datafusion_python_util::{is_ipython_env, spawn_future, wait_for_future}; use futures::{StreamExt, TryStreamExt}; @@ -308,6 +314,9 @@ pub struct PyDataFrame { // In IPython environment cache batches between __repr__ and _repr_html_ calls. batches: SharedCachedBatches, + + // Cache the last physical plan so that metrics are available after execution. + last_plan: Arc>>>, } impl PyDataFrame { @@ -316,6 +325,7 @@ impl PyDataFrame { Self { df: Arc::new(df), batches: Arc::new(Mutex::new(None)), + last_plan: Arc::new(Mutex::new(None)), } } @@ -387,6 +397,20 @@ impl PyDataFrame { Ok(html_str) } + /// Create the physical plan, cache it in `last_plan`, and return the plan together + /// with a task context. Centralises the repeated three-line pattern that appears in + /// `collect`, `collect_partitioned`, `execute_stream`, and `execute_stream_partitioned`. + fn create_and_cache_plan( + &self, + py: Python, + ) -> PyDataFusionResult<(Arc, Arc)> { + let df = self.df.as_ref().clone(); + let new_plan = wait_for_future(py, df.create_physical_plan())??; + *self.last_plan.lock() = Some(Arc::clone(&new_plan)); + let task_ctx = Arc::new(self.df.as_ref().task_ctx()); + Ok((new_plan, task_ctx)) + } + async fn collect_column_inner(&self, column: &str) -> Result { let batches = self .df @@ -646,8 +670,9 @@ impl PyDataFrame { /// Unless some order is specified in the plan, there is no /// guarantee of the order of the result. fn collect<'py>(&self, py: Python<'py>) -> PyResult>> { - let batches = wait_for_future(py, self.df.as_ref().clone().collect())? - .map_err(PyDataFusionError::from)?; + let (plan, task_ctx) = self.create_and_cache_plan(py)?; + let batches = + wait_for_future(py, df_collect(plan, task_ctx))?.map_err(PyDataFusionError::from)?; // cannot use PyResult> return type due to // https://github.com/PyO3/pyo3/issues/1813 batches.into_iter().map(|rb| rb.to_pyarrow(py)).collect() @@ -662,7 +687,8 @@ impl PyDataFrame { /// Executes this DataFrame and collects all results into a vector of vector of RecordBatch /// maintaining the input partitioning. fn collect_partitioned<'py>(&self, py: Python<'py>) -> PyResult>>> { - let batches = wait_for_future(py, self.df.as_ref().clone().collect_partitioned())? + let (plan, task_ctx) = self.create_and_cache_plan(py)?; + let batches = wait_for_future(py, df_collect_partitioned(plan, task_ctx))? .map_err(PyDataFusionError::from)?; batches @@ -840,7 +866,13 @@ impl PyDataFrame { } /// Get the execution plan for this `DataFrame` + /// + /// If the DataFrame has already been executed (e.g. via `collect()`), + /// returns the cached plan which includes populated metrics. fn execution_plan(&self, py: Python) -> PyDataFusionResult { + if let Some(plan) = self.last_plan.lock().as_ref() { + return Ok(PyExecutionPlan::new(Arc::clone(plan))); + } let plan = wait_for_future(py, self.df.as_ref().clone().create_physical_plan())??; Ok(plan.into()) } @@ -1198,14 +1230,17 @@ impl PyDataFrame { } fn execute_stream(&self, py: Python) -> PyDataFusionResult { - let df = self.df.as_ref().clone(); - let stream = spawn_future(py, async move { df.execute_stream().await })?; + let (plan, task_ctx) = self.create_and_cache_plan(py)?; + let stream = spawn_future(py, async move { df_execute_stream(plan, task_ctx) })?; Ok(PyRecordBatchStream::new(stream)) } fn execute_stream_partitioned(&self, py: Python) -> PyResult> { - let df = self.df.as_ref().clone(); - let streams = spawn_future(py, async move { df.execute_stream_partitioned().await })?; + let (plan, task_ctx) = self.create_and_cache_plan(py)?; + let streams = spawn_future( + py, + async move { df_execute_stream_partitioned(plan, task_ctx) }, + )?; Ok(streams.into_iter().map(PyRecordBatchStream::new).collect()) } diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index fc2d006d3..77d69911a 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -43,6 +43,7 @@ pub mod errors; pub mod expr; #[allow(clippy::borrow_deref_ref)] mod functions; +pub mod metrics; mod options; pub mod physical_plan; mod pyarrow_filter_expression; @@ -92,6 +93,8 @@ fn _internal(py: Python, m: Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/crates/core/src/metrics.rs b/crates/core/src/metrics.rs new file mode 100644 index 000000000..ee0937e25 --- /dev/null +++ b/crates/core/src/metrics.rs @@ -0,0 +1,169 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::collections::HashMap; +use std::sync::Arc; + +use chrono::{Datelike, Timelike}; +use datafusion::physical_plan::metrics::{Metric, MetricValue, MetricsSet, Timestamp}; +use pyo3::prelude::*; + +#[pyclass(from_py_object, frozen, name = "MetricsSet", module = "datafusion")] +#[derive(Debug, Clone)] +pub struct PyMetricsSet { + metrics: MetricsSet, +} + +impl PyMetricsSet { + pub fn new(metrics: MetricsSet) -> Self { + Self { metrics } + } +} + +#[pymethods] +impl PyMetricsSet { + fn metrics(&self) -> Vec { + self.metrics + .iter() + .map(|m| PyMetric::new(Arc::clone(m))) + .collect() + } + + fn output_rows(&self) -> Option { + self.metrics.output_rows() + } + + fn elapsed_compute(&self) -> Option { + self.metrics.elapsed_compute() + } + + fn spill_count(&self) -> Option { + self.metrics.spill_count() + } + + fn spilled_bytes(&self) -> Option { + self.metrics.spilled_bytes() + } + + fn spilled_rows(&self) -> Option { + self.metrics.spilled_rows() + } + + fn sum_by_name(&self, name: &str) -> Option { + self.metrics.sum_by_name(name).map(|v| v.as_usize()) + } + + fn __repr__(&self) -> String { + format!("{}", self.metrics) + } +} + +#[pyclass(from_py_object, frozen, name = "Metric", module = "datafusion")] +#[derive(Debug, Clone)] +pub struct PyMetric { + metric: Arc, +} + +impl PyMetric { + pub fn new(metric: Arc) -> Self { + Self { metric } + } + + fn timestamp_to_pyobject<'py>( + py: Python<'py>, + ts: &Timestamp, + ) -> PyResult>> { + match ts.value() { + Some(dt) => { + let datetime_mod = py.import("datetime")?; + let datetime_cls = datetime_mod.getattr("datetime")?; + let tz_utc = datetime_mod.getattr("timezone")?.getattr("utc")?; + let result = datetime_cls.call1(( + dt.year(), + dt.month(), + dt.day(), + dt.hour(), + dt.minute(), + dt.second(), + dt.timestamp_subsec_micros(), + tz_utc, + ))?; + Ok(Some(result)) + } + None => Ok(None), + } + } +} + +#[pymethods] +impl PyMetric { + #[getter] + fn name(&self) -> String { + self.metric.value().name().to_string() + } + + #[getter] + fn value<'py>(&self, py: Python<'py>) -> PyResult>> { + match self.metric.value() { + MetricValue::OutputRows(c) => Ok(Some(c.value().into_pyobject(py)?.into_any())), + MetricValue::OutputBytes(c) => Ok(Some(c.value().into_pyobject(py)?.into_any())), + MetricValue::ElapsedCompute(t) => Ok(Some(t.value().into_pyobject(py)?.into_any())), + MetricValue::SpillCount(c) => Ok(Some(c.value().into_pyobject(py)?.into_any())), + MetricValue::SpilledBytes(c) => Ok(Some(c.value().into_pyobject(py)?.into_any())), + MetricValue::SpilledRows(c) => Ok(Some(c.value().into_pyobject(py)?.into_any())), + MetricValue::CurrentMemoryUsage(g) => Ok(Some(g.value().into_pyobject(py)?.into_any())), + MetricValue::Count { count, .. } => { + Ok(Some(count.value().into_pyobject(py)?.into_any())) + } + MetricValue::Gauge { gauge, .. } => { + Ok(Some(gauge.value().into_pyobject(py)?.into_any())) + } + MetricValue::Time { time, .. } => Ok(Some(time.value().into_pyobject(py)?.into_any())), + MetricValue::StartTimestamp(ts) | MetricValue::EndTimestamp(ts) => { + Self::timestamp_to_pyobject(py, ts) + } + _ => Ok(None), + } + } + + #[getter] + fn value_as_datetime<'py>(&self, py: Python<'py>) -> PyResult>> { + match self.metric.value() { + MetricValue::StartTimestamp(ts) | MetricValue::EndTimestamp(ts) => { + Self::timestamp_to_pyobject(py, ts) + } + _ => Ok(None), + } + } + + #[getter] + fn partition(&self) -> Option { + self.metric.partition() + } + + fn labels(&self) -> HashMap { + self.metric + .labels() + .iter() + .map(|l| (l.name().to_string(), l.value().to_string())) + .collect() + } + + fn __repr__(&self) -> String { + format!("{}", self.metric.value()) + } +} diff --git a/crates/core/src/physical_plan.rs b/crates/core/src/physical_plan.rs index 8674a8b55..fac973884 100644 --- a/crates/core/src/physical_plan.rs +++ b/crates/core/src/physical_plan.rs @@ -26,6 +26,7 @@ use pyo3::types::PyBytes; use crate::context::PySessionContext; use crate::errors::PyDataFusionResult; +use crate::metrics::PyMetricsSet; #[pyclass( from_py_object, @@ -96,6 +97,10 @@ impl PyExecutionPlan { Ok(Self::new(plan)) } + pub fn metrics(&self) -> Option { + self.plan.metrics().map(PyMetricsSet::new) + } + fn __repr__(&self) -> String { self.display_indent() } diff --git a/docs/source/user-guide/dataframe/execution-metrics.rst b/docs/source/user-guide/dataframe/execution-metrics.rst new file mode 100644 index 000000000..764fa76ef --- /dev/null +++ b/docs/source/user-guide/dataframe/execution-metrics.rst @@ -0,0 +1,215 @@ +.. Licensed to the Apache Software Foundation (ASF) under one +.. or more contributor license agreements. See the NOTICE file +.. distributed with this work for additional information +.. regarding copyright ownership. The ASF licenses this file +.. to you under the Apache License, Version 2.0 (the +.. "License"); you may not use this file except in compliance +.. with the License. You may obtain a copy of the License at + +.. http://www.apache.org/licenses/LICENSE-2.0 + +.. Unless required by applicable law or agreed to in writing, +.. software distributed under the License is distributed on an +.. "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +.. KIND, either express or implied. See the License for the +.. specific language governing permissions and limitations +.. under the License. + +.. _execution_metrics: + +Execution Metrics +================= + +Overview +-------- + +When DataFusion executes a query it compiles the logical plan into a tree of +*physical plan operators* (e.g. ``FilterExec``, ``ProjectionExec``, +``HashAggregateExec``). Each operator can record runtime statistics while it +runs. These statistics are called **execution metrics**. + +Typical metrics include: + +- **output_rows** – number of rows produced by the operator +- **elapsed_compute** – total CPU time (nanoseconds) spent inside the operator +- **spill_count** – number of times the operator spilled data to disk +- **spilled_bytes** – total bytes written to disk during spills +- **spilled_rows** – total rows written to disk during spills + +Metrics are collected *per-partition*: DataFusion may execute each operator +in parallel across several partitions. The convenience properties on +:py:class:`~datafusion.MetricsSet` (e.g. ``output_rows``, ``elapsed_compute``) +automatically sum the named metric across **all** partitions, giving a single +aggregate value for the operator as a whole. You can also access the raw +per-partition :py:class:`~datafusion.Metric` objects via +:py:meth:`~datafusion.MetricsSet.metrics`. + +When Are Metrics Available? +--------------------------- + +Some operators (for example ``DataSourceExec``) eagerly create a +:py:class:`~datafusion.MetricsSet` when the physical plan is built, so +:py:meth:`~datafusion.ExecutionPlan.metrics` may return a set even before any +rows have been processed. However, metric **values** such as ``output_rows`` +are only meaningful **after** the DataFrame has been executed via one of the +terminal operations: + +- :py:meth:`~datafusion.DataFrame.collect` +- :py:meth:`~datafusion.DataFrame.collect_partitioned` +- :py:meth:`~datafusion.DataFrame.execute_stream` + (metrics are available once the stream has been fully consumed) +- :py:meth:`~datafusion.DataFrame.execute_stream_partitioned` + (metrics are available once all partition streams have been fully consumed) + +Before execution, metric values will be ``0`` or ``None``. + +.. note:: + + **display() does not populate metrics.** + When a DataFrame is displayed in a notebook (e.g. via ``display(df)`` or + automatic ``repr`` output), DataFusion runs a *limited* internal execution + to fetch preview rows. This internal execution does **not** cache the + physical plan used, so :py:meth:`~datafusion.ExecutionPlan.collect_metrics` + will not reflect the display execution. To access metrics you must call + one of the terminal operations listed above. + +If you call :py:meth:`~datafusion.DataFrame.collect` (or another terminal +operation) multiple times on the same DataFrame, each call creates a fresh +physical plan. Metrics from :py:meth:`~datafusion.DataFrame.execution_plan` +always reflect the **most recent** execution. + +Reading the Physical Plan Tree +-------------------------------- + +:py:meth:`~datafusion.DataFrame.execution_plan` returns the root +:py:class:`~datafusion.ExecutionPlan` node of the physical plan tree. The tree +mirrors the operator pipeline: the root is typically a projection or +coalescing node; its children are filters, aggregates, scans, etc. + +The ``operator_name`` string returned by +:py:meth:`~datafusion.ExecutionPlan.collect_metrics` is the *display* name of +the node, for example ``"FilterExec: column1@0 > 1"``. This is the same string +you would see when calling ``plan.display()``. + +Aggregated vs Per-Partition Metrics +------------------------------------ + +DataFusion executes each operator across one or more **partitions** in +parallel. The :py:class:`~datafusion.MetricsSet` convenience properties +(``output_rows``, ``elapsed_compute``, etc.) automatically **sum** the named +metric across all partitions, giving a single aggregate value. + +To inspect individual partitions — for example to detect data skew where one +partition processes far more rows than others — iterate over the raw +:py:class:`~datafusion.Metric` objects: + +.. code-block:: python + + for metric in metrics_set.metrics(): + print(f" partition={metric.partition} {metric.name}={metric.value}") + +The ``partition`` property is a 0-based index (``0``, ``1``, …) identifying +which parallel slot processed this metric. It is ``None`` for metrics that +apply globally (not tied to a specific partition). + +Available Metrics +----------------- + +The following metrics are directly accessible as properties on +:py:class:`~datafusion.MetricsSet`: + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Property + - Description + * - ``output_rows`` + - Number of rows emitted by the operator (summed across partitions). + * - ``elapsed_compute`` + - Wall-clock CPU time **in nanoseconds** spent inside the operator's + compute loop, excluding I/O wait. Useful for identifying which + operators are most expensive (summed across partitions). + * - ``spill_count`` + - Number of spill-to-disk events triggered by memory pressure. This is + a unitless count of events, not a measure of data volume (summed across + partitions). + * - ``spilled_bytes`` + - Total bytes written to disk during spill events (summed across + partitions). + * - ``spilled_rows`` + - Total rows written to disk during spill events (summed across + partitions). + +Any metric not listed above can be accessed via +:py:meth:`~datafusion.MetricsSet.sum_by_name`, or by iterating over the raw +:py:class:`~datafusion.Metric` objects returned by +:py:meth:`~datafusion.MetricsSet.metrics`. + +Labels +------ + +A :py:class:`~datafusion.Metric` may carry *labels*: key/value pairs that +provide additional context. Labels are operator-specific; most metrics have +an empty label dict. + +Some operators tag their metrics with labels to distinguish variants. For +example, a ``HashAggregateExec`` may record separate ``output_rows`` metrics +for intermediate and final output: + +.. code-block:: python + + for metric in metrics_set.metrics(): + print(metric.name, metric.labels()) + # output_rows {'output_type': 'final'} + # output_rows {'output_type': 'intermediate'} + +When summing by name (via :py:attr:`~datafusion.MetricsSet.output_rows` or +:py:meth:`~datafusion.MetricsSet.sum_by_name`), **all** metrics with that +name are summed regardless of labels. To filter by label, iterate over the +raw :py:class:`~datafusion.Metric` objects directly. + +End-to-End Example +------------------ + +.. code-block:: python + + from datafusion import SessionContext + + ctx = SessionContext() + ctx.sql("CREATE TABLE sales AS VALUES (1, 100), (2, 200), (3, 50)") + + df = ctx.sql("SELECT * FROM sales WHERE column1 > 1") + + # Execute the query — this populates the metrics + results = df.collect() + + # Retrieve the physical plan with metrics + plan = df.execution_plan() + + # Walk every operator and print its metrics + for operator_name, ms in plan.collect_metrics(): + if ms.output_rows is not None: + print(f"{operator_name}") + print(f" output_rows = {ms.output_rows}") + print(f" elapsed_compute = {ms.elapsed_compute} ns") + + # Access raw per-partition metrics + for operator_name, ms in plan.collect_metrics(): + for metric in ms.metrics(): + print( + f" partition={metric.partition} " + f"{metric.name}={metric.value} " + f"labels={metric.labels()}" + ) + +API Reference +------------- + +- :py:class:`datafusion.ExecutionPlan` — physical plan node +- :py:meth:`datafusion.ExecutionPlan.collect_metrics` — walk the tree and + return ``(operator_name, MetricsSet)`` pairs +- :py:meth:`datafusion.ExecutionPlan.metrics` — return the + :py:class:`~datafusion.MetricsSet` for a single node +- :py:class:`datafusion.MetricsSet` — aggregated metrics for one operator +- :py:class:`datafusion.Metric` — a single per-partition metric value diff --git a/docs/source/user-guide/dataframe/index.rst b/docs/source/user-guide/dataframe/index.rst index 510bcbc68..8475a7bd7 100644 --- a/docs/source/user-guide/dataframe/index.rst +++ b/docs/source/user-guide/dataframe/index.rst @@ -365,7 +365,16 @@ DataFusion provides many built-in functions for data manipulation: For a complete list of available functions, see the :py:mod:`datafusion.functions` module documentation. +Execution Metrics +----------------- + +After executing a DataFrame (via ``collect()``, ``execute_stream()``, etc.), +DataFusion populates per-operator runtime statistics such as row counts and +compute time. See :doc:`execution-metrics` for a full explanation and +worked example. + .. toctree:: :maxdepth: 1 rendering + execution-metrics diff --git a/python/datafusion/__init__.py b/python/datafusion/__init__.py index ee02c921d..80dfa2fab 100644 --- a/python/datafusion/__init__.py +++ b/python/datafusion/__init__.py @@ -56,7 +56,7 @@ from .expr import Expr, WindowFrame from .io import read_avro, read_csv, read_json, read_parquet from .options import CsvReadOptions -from .plan import ExecutionPlan, LogicalPlan +from .plan import ExecutionPlan, LogicalPlan, Metric, MetricsSet from .record_batch import RecordBatch, RecordBatchStream from .user_defined import ( Accumulator, @@ -86,6 +86,8 @@ "Expr", "InsertOp", "LogicalPlan", + "Metric", + "MetricsSet", "ParquetColumnOptions", "ParquetWriterOptions", "RecordBatch", diff --git a/python/datafusion/plan.py b/python/datafusion/plan.py index 9c96a18fc..c0cfd523f 100644 --- a/python/datafusion/plan.py +++ b/python/datafusion/plan.py @@ -24,11 +24,15 @@ import datafusion._internal as df_internal if TYPE_CHECKING: + import datetime + from datafusion.context import SessionContext __all__ = [ "ExecutionPlan", "LogicalPlan", + "Metric", + "MetricsSet", ] @@ -151,3 +155,176 @@ def to_proto(self) -> bytes: Tables created in memory from record batches are currently not supported. """ return self._raw_plan.to_proto() + + def metrics(self) -> MetricsSet | None: + """Return metrics for this plan node, or None if this plan has no MetricsSet. + + Some operators (e.g. DataSourceExec) eagerly initialize a MetricsSet + when the plan is created, so this may return a set even before + execution. Metric *values* (such as ``output_rows``) are only + meaningful after the DataFrame has been executed. + """ + raw = self._raw_plan.metrics() + if raw is None: + return None + return MetricsSet(raw) + + def collect_metrics(self) -> list[tuple[str, MetricsSet]]: + """Return runtime statistics for each step of the query execution. + + DataFusion executes a query as a pipeline of operators — for example a + data source scan, followed by a filter, followed by a projection. After + the DataFrame has been executed (via + :py:meth:`~datafusion.DataFrame.collect`, + :py:meth:`~datafusion.DataFrame.execute_stream`, etc.), each operator + records statistics such as how many rows it produced and how much CPU + time it consumed. + + Each entry in the returned list corresponds to one operator that + recorded metrics. The first element of the tuple is the operator's + description string — the same text shown by + :py:meth:`display_indent` — which identifies both the operator type + and its key parameters, for example ``"FilterExec: column1@0 > 1"`` + or ``"DataSourceExec: partitions=1"``. + + Returns: + A list of ``(description, MetricsSet)`` tuples ordered from the + outermost operator (top of the execution tree) down to the + data-source leaves. Only operators that recorded at least one + metric are included. Returns an empty list if called before the + DataFrame has been executed. + """ + result: list[tuple[str, MetricsSet]] = [] + + def _walk(node: ExecutionPlan) -> None: + ms = node.metrics() + if ms is not None: + result.append((node.display(), ms)) + for child in node.children(): + _walk(child) + + _walk(self) + return result + + +class MetricsSet: + """A set of metrics for a single execution plan operator. + + A physical plan operator runs independently across one or more partitions. + :py:meth:`metrics` returns the raw per-partition :py:class:`Metric` objects. + The convenience properties (:py:attr:`output_rows`, :py:attr:`elapsed_compute`, + etc.) automatically sum the named metric across *all* partitions, giving a + single aggregate value for the operator as a whole. + """ + + def __init__(self, raw: df_internal.MetricsSet) -> None: + """This constructor should not be called by the end user.""" + self._raw = raw + + def metrics(self) -> list[Metric]: + """Return all individual metrics in this set.""" + return [Metric(m) for m in self._raw.metrics()] + + @property + def output_rows(self) -> int | None: + """Sum of output_rows across all partitions.""" + return self._raw.output_rows() + + @property + def elapsed_compute(self) -> int | None: + """Total CPU time (in nanoseconds) spent inside this operator's execute loop. + + Summed across all partitions. Returns ``None`` if no ``elapsed_compute`` + metric was recorded. + """ + return self._raw.elapsed_compute() + + @property + def spill_count(self) -> int | None: + """Number of times this operator spilled data to disk due to memory pressure. + + This is a count of spill events, not a byte count. Summed across all + partitions. Returns ``None`` if no ``spill_count`` metric was recorded. + """ + return self._raw.spill_count() + + @property + def spilled_bytes(self) -> int | None: + """Sum of spilled_bytes across all partitions.""" + return self._raw.spilled_bytes() + + @property + def spilled_rows(self) -> int | None: + """Sum of spilled_rows across all partitions.""" + return self._raw.spilled_rows() + + def sum_by_name(self, name: str) -> int | None: + """Sum the named metric across all partitions. + + Useful for accessing any metric not exposed as a first-class property. + Returns ``None`` if no metric with the given name was recorded. + + Args: + name: The metric name, e.g. ``"output_rows"`` or ``"elapsed_compute"``. + """ + return self._raw.sum_by_name(name) + + def __repr__(self) -> str: + """Return a string representation of the metrics set.""" + return repr(self._raw) + + +class Metric: + """A single execution metric with name, value, partition, and labels.""" + + def __init__(self, raw: df_internal.Metric) -> None: + """This constructor should not be called by the end user.""" + self._raw = raw + + @property + def name(self) -> str: + """The name of this metric (e.g. ``output_rows``).""" + return self._raw.name + + @property + def value(self) -> int | datetime.datetime | None: + """The value of this metric. + + Returns an ``int`` for counters, gauges, and time-based metrics + (nanoseconds), a :py:class:`~datetime.datetime` (UTC) for + ``start_timestamp`` / ``end_timestamp`` metrics, or ``None`` + when the value has not been set or is not representable. + """ + return self._raw.value + + @property + def value_as_datetime(self) -> datetime.datetime | None: + """The value as a UTC :py:class:`~datetime.datetime` for timestamp metrics. + + Returns ``None`` for all non-timestamp metrics and for timestamp + metrics whose value has not been set (e.g. before execution). + """ + return self._raw.value_as_datetime + + @property + def partition(self) -> int | None: + """The 0-based partition index this metric applies to. + + Returns ``None`` for metrics that are not partition-specific (i.e. they + apply globally across all partitions of the operator). + """ + return self._raw.partition + + def labels(self) -> dict[str, str]: + """Return the labels associated with this metric. + + Labels provide additional context for a metric. For example:: + + metric.labels() + # {'output_type': 'final'} + """ + return self._raw.labels() + + def __repr__(self) -> str: + """Return a string representation of the metric.""" + return repr(self._raw) diff --git a/python/tests/test_plans.py b/python/tests/test_plans.py index 396acbe97..3705fc7ef 100644 --- a/python/tests/test_plans.py +++ b/python/tests/test_plans.py @@ -15,8 +15,16 @@ # specific language governing permissions and limitations # under the License. +import datetime + import pytest -from datafusion import ExecutionPlan, LogicalPlan, SessionContext +from datafusion import ( + ExecutionPlan, + LogicalPlan, + Metric, + MetricsSet, + SessionContext, +) # Note: We must use CSV because memory tables are currently not supported for @@ -40,3 +48,185 @@ def test_logical_plan_to_proto(ctx, df) -> None: execution_plan = ExecutionPlan.from_proto(ctx, execution_plan_bytes) assert str(original_execution_plan) == str(execution_plan) + + +def test_metrics_tree_walk() -> None: + ctx = SessionContext() + ctx.sql("CREATE TABLE t AS VALUES (1, 'a'), (2, 'b'), (3, 'c')") + df = ctx.sql("SELECT * FROM t WHERE column1 > 1") + df.collect() + plan = df.execution_plan() + + results = plan.collect_metrics() + assert len(results) >= 1 + output_rows_by_op: dict[str, int] = {} + for name, ms in results: + assert isinstance(name, str) + assert isinstance(ms, MetricsSet) + if ms.output_rows is not None: + output_rows_by_op[name] = ms.output_rows + + # The filter passes rows where column1 > 1, so exactly + # 2 rows from (1,'a'),(2,'b'),(3,'c'). + # At least one operator must report exactly 2 output rows (the filter). + assert 2 in output_rows_by_op.values(), ( + f"Expected an operator with output_rows=2, got {output_rows_by_op}" + ) + + +def test_metric_properties() -> None: + ctx = SessionContext() + ctx.sql("CREATE TABLE t AS VALUES (1, 'a'), (2, 'b'), (3, 'c')") + df = ctx.sql("SELECT * FROM t WHERE column1 > 1") + df.collect() + plan = df.execution_plan() + + found_any_metric = False + for _, ms in plan.collect_metrics(): + r = repr(ms) + assert isinstance(r, str) + for metric in ms.metrics(): + found_any_metric = True + assert isinstance(metric, Metric) + assert isinstance(metric.name, str) + assert len(metric.name) > 0 + assert metric.partition is None or isinstance(metric.partition, int) + assert metric.value is None or isinstance( + metric.value, int | datetime.datetime + ) + assert isinstance(metric.labels(), dict) + mr = repr(metric) + assert isinstance(mr, str) + assert len(mr) > 0 + assert found_any_metric, "Expected at least one metric after execution" + + +def test_no_meaningful_metrics_before_execution() -> None: + ctx = SessionContext() + ctx.sql("CREATE TABLE t AS VALUES (1, 'a'), (2, 'b'), (3, 'c')") + df = ctx.sql("SELECT * FROM t WHERE column1 > 1") + plan_before = df.execution_plan() + + # Some plan nodes (e.g. DataSourceExec) eagerly initialize a MetricsSet, + # so metrics() may return a set even before execution. However, no rows + # should have been processed yet — output_rows must be absent or zero. + for _, ms in plan_before.collect_metrics(): + rows = ms.output_rows + assert rows is None or rows == 0, ( + f"Expected 0 output_rows before execution, got {rows}" + ) + + # After execution, at least one operator must report rows processed. + df.collect() + plan_after = df.execution_plan() + output_rows_after = [ + ms.output_rows + for _, ms in plan_after.collect_metrics() + if ms.output_rows is not None and ms.output_rows > 0 + ] + assert len(output_rows_after) > 0, "Expected output_rows > 0 after execution" + + +def test_collect_partitioned_metrics() -> None: + ctx = SessionContext() + ctx.sql("CREATE TABLE t AS VALUES (1, 'a'), (2, 'b'), (3, 'c')") + df = ctx.sql("SELECT * FROM t WHERE column1 > 1") + + df.collect_partitioned() + plan = df.execution_plan() + + output_rows_values = [ + ms.output_rows for _, ms in plan.collect_metrics() if ms.output_rows is not None + ] + assert 2 in output_rows_values, f"Expected 2 in {output_rows_values}" + + +def test_execute_stream_metrics() -> None: + ctx = SessionContext() + ctx.sql("CREATE TABLE t AS VALUES (1, 'a'), (2, 'b'), (3, 'c')") + df = ctx.sql("SELECT * FROM t WHERE column1 > 1") + + for _ in df.execute_stream(): + pass + + plan = df.execution_plan() + output_rows_values = [ + ms.output_rows for _, ms in plan.collect_metrics() if ms.output_rows is not None + ] + assert 2 in output_rows_values, f"Expected 2 in {output_rows_values}" + + +def test_execute_stream_partitioned_metrics() -> None: + ctx = SessionContext() + ctx.sql("CREATE TABLE t AS VALUES (1, 'a'), (2, 'b'), (3, 'c')") + df = ctx.sql("SELECT * FROM t WHERE column1 > 1") + + for stream in df.execute_stream_partitioned(): + for _ in stream: + pass + + plan = df.execution_plan() + output_rows_values = [ + ms.output_rows for _, ms in plan.collect_metrics() if ms.output_rows is not None + ] + assert 2 in output_rows_values, f"Expected 2 in {output_rows_values}" + + +def test_value_as_datetime() -> None: + ctx = SessionContext() + ctx.sql("CREATE TABLE t AS VALUES (1, 'a'), (2, 'b'), (3, 'c')") + df = ctx.sql("SELECT * FROM t WHERE column1 > 1") + df.collect() + plan = df.execution_plan() + + for _, ms in plan.collect_metrics(): + for metric in ms.metrics(): + if metric.name in ("start_timestamp", "end_timestamp"): + dt = metric.value_as_datetime + assert dt is None or isinstance(dt, datetime.datetime) + if dt is not None: + assert dt.tzinfo is not None + else: + assert metric.value_as_datetime is None + + +def test_metric_names_and_labels() -> None: + """Verify that known metric names appear and labels are well-formed.""" + ctx = SessionContext() + ctx.sql("CREATE TABLE t AS VALUES (1, 'a'), (2, 'b'), (3, 'c')") + df = ctx.sql("SELECT * FROM t WHERE column1 > 1") + df.collect() + plan = df.execution_plan() + + all_metric_names: set[str] = set() + for _, ms in plan.collect_metrics(): + for metric in ms.metrics(): + all_metric_names.add(metric.name) + # Labels must be a dict of str->str + labels = metric.labels() + for k, v in labels.items(): + assert isinstance(k, str) + assert isinstance(v, str) + + # After a filter query, we expect at minimum these standard metric names. + assert "output_rows" in all_metric_names, ( + f"Expected 'output_rows' in {all_metric_names}" + ) + assert "elapsed_compute" in all_metric_names, ( + f"Expected 'elapsed_compute' in {all_metric_names}" + ) + + +def test_collect_twice_has_metrics() -> None: + ctx = SessionContext() + ctx.sql("CREATE TABLE t AS VALUES (1, 'a'), (2, 'b'), (3, 'c')") + df = ctx.sql("SELECT * FROM t WHERE column1 > 1") + + df.collect() + df.collect() + + plan = df.execution_plan() + output_rows_values = [ + ms.output_rows for _, ms in plan.collect_metrics() if ms.output_rows is not None + ] + assert len(output_rows_values) > 0 From 398980d1edbb8ad6d9744236f2dfe0c6ab4b4665 Mon Sep 17 00:00:00 2001 From: Zeel Desai <72783325+zeel2104@users.noreply.github.com> Date: Mon, 13 Apr 2026 09:24:56 -0400 Subject: [PATCH 54/56] Support None comparisons for null expressions (#1489) * Support None comparisons for null expressions * Fold None comparison coverage into relational expr test --- python/datafusion/expr.py | 4 ++++ python/tests/test_expr.py | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/python/datafusion/expr.py b/python/datafusion/expr.py index 7cd74ecd5..32004656f 100644 --- a/python/datafusion/expr.py +++ b/python/datafusion/expr.py @@ -483,6 +483,8 @@ def __eq__(self, rhs: object) -> Expr: Accepts either an expression or any valid PyArrow scalar literal value. """ + if rhs is None: + return self.is_null() if not isinstance(rhs, Expr): rhs = Expr.literal(rhs) return Expr(self.expr.__eq__(rhs.expr)) @@ -492,6 +494,8 @@ def __ne__(self, rhs: object) -> Expr: Accepts either an expression or any valid PyArrow scalar literal value. """ + if rhs is None: + return self.is_not_null() if not isinstance(rhs, Expr): rhs = Expr.literal(rhs) return Expr(self.expr.__ne__(rhs.expr)) diff --git a/python/tests/test_expr.py b/python/tests/test_expr.py index 1cf824a15..d046eb48c 100644 --- a/python/tests/test_expr.py +++ b/python/tests/test_expr.py @@ -153,8 +153,8 @@ def test_relational_expr(test_ctx): batch = pa.RecordBatch.from_arrays( [ - pa.array([1, 2, 3]), - pa.array(["alpha", "beta", "gamma"], type=pa.string_view()), + pa.array([1, 2, 3, None]), + pa.array(["alpha", "beta", "gamma", None], type=pa.string_view()), ], names=["a", "b"], ) @@ -171,6 +171,10 @@ def test_relational_expr(test_ctx): assert df.filter(col("b") != "beta").count() == 2 assert df.filter(col("a") == "beta").count() == 0 + assert df.filter(col("a") == None).count() == 1 # noqa: E711 + assert df.filter(col("a") != None).count() == 3 # noqa: E711 + assert df.filter(col("b") == None).count() == 1 # noqa: E711 + assert df.filter(col("b") != None).count() == 3 # noqa: E711 def test_expr_to_variant(): From 2715a32e939d17222c18e8adacf85ee45da464b9 Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Tue, 14 Apr 2026 03:27:00 -0400 Subject: [PATCH 55/56] chore: update release documentation (#1494) * Update release documentation * Minor change to workflow because release start at 1 --- .../workflows/verify-release-candidate.yml | 2 +- dev/release/README.md | 60 ++++++++++--------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/.github/workflows/verify-release-candidate.yml b/.github/workflows/verify-release-candidate.yml index 7a5deff5b..6ecb547b5 100644 --- a/.github/workflows/verify-release-candidate.yml +++ b/.github/workflows/verify-release-candidate.yml @@ -27,7 +27,7 @@ on: required: true type: string rc_number: - description: Release candidate number (e.g., 0) + description: Release candidate number (e.g., 1) required: true type: string diff --git a/dev/release/README.md b/dev/release/README.md index ed28f4aa6..4833be55a 100644 --- a/dev/release/README.md +++ b/dev/release/README.md @@ -26,11 +26,11 @@ required due to changes in DataFusion rather than having a large amount of work is available. When there is a new official release of DataFusion, we update the `main` branch to point to that, update the version -number, and create a new release branch, such as `branch-0.8`. Once this branch is created, we switch the `main` branch +number, and create a new release branch, such as `branch-53`. Once this branch is created, we switch the `main` branch back to using GitHub dependencies. The release activity (such as generating the changelog) can then happen on the release branch without blocking ongoing development in the `main` branch. -We can cherry-pick commits from the `main` branch into `branch-0.8` as needed and then create new patch releases +We can cherry-pick commits from the `main` branch into `branch-53` as needed and then create new patch releases from that branch. ## Detailed Guide @@ -54,7 +54,8 @@ Before creating a new release: - We need to ensure that the main branch does not have any GitHub dependencies - a PR should be created and merged to update the major version number of the project -- A new release branch should be created, such as `branch-0.8` +- A new release branch should be created, such as `branch-53` +- It is best to push this branch to the apache repository rather than a personal fork in case patch releases are required. ## Preparing a Release Candidate @@ -65,14 +66,14 @@ We maintain a `CHANGELOG.md` so our users know what has been changed between rel The changelog is generated using a Python script: ```bash -$ GITHUB_TOKEN= ./dev/release/generate-changelog.py 24.0.0 HEAD 25.0.0 > dev/changelog/25.0.0.md +$ GITHUB_TOKEN= ./dev/release/generate-changelog.py 52.0.0 HEAD 53.0.0 > dev/changelog/53.0.0.md ``` This script creates a changelog from GitHub PRs based on the labels associated with them as well as looking for titles starting with `feat:`, `fix:`, or `docs:` . The script will produce output similar to: ``` -Fetching list of commits between 24.0.0 and HEAD +Fetching list of commits between 52.0.0 and HEAD Fetching pull requests Categorizing pull requests Generating changelog content @@ -81,6 +82,7 @@ Generating changelog content ### Update the version number The only place you should need to update the version is in the root `Cargo.toml`. +You will need to update this both in the workspace section and also in the dependencies. After updating the toml file, run `cargo update` to update the cargo lock file. If you do not want to update all the dependencies, you can instead run `cargo build` which should only update the version number for `datafusion-python`. @@ -94,14 +96,14 @@ you need to push a tag to start the CI process for release candidates. The follo the upstream repository is called `apache`. ```bash -git tag 0.8.0-rc1 -git push apache 0.8.0-rc1 +git tag 53.0.0-rc1 +git push apache 53.0.0-rc1 ``` ### Create a source release ```bash -./dev/release/create-tarball.sh 0.8.0 1 +./dev/release/create-tarball.sh 53.0.0 1 ``` This will also create the email template to send to the mailing list. @@ -124,10 +126,10 @@ Click on the action and scroll down to the bottom of the page titled "Artifacts" contain files such as: ```text -datafusion-22.0.0-cp37-abi3-macosx_10_7_x86_64.whl -datafusion-22.0.0-cp37-abi3-macosx_11_0_arm64.whl -datafusion-22.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -datafusion-22.0.0-cp37-abi3-win_amd64.whl +datafusion-53.0.0-cp37-abi3-macosx_10_7_x86_64.whl +datafusion-53.0.0-cp37-abi3-macosx_11_0_arm64.whl +datafusion-53.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +datafusion-53.0.0-cp37-abi3-win_amd64.whl ``` Upload the wheels to testpypi. @@ -135,23 +137,23 @@ Upload the wheels to testpypi. ```bash unzip dist.zip python3 -m pip install --upgrade setuptools twine build -python3 -m twine upload --repository testpypi datafusion-22.0.0-cp37-abi3-*.whl +python3 -m twine upload --repository testpypi datafusion-53.0.0-cp37-abi3-*.whl ``` When prompted for username, enter `__token__`. When prompted for a password, enter a valid GitHub Personal Access Token #### Publish Python Source Distribution to testpypi -Download the source tarball created in the previous step, untar it, and run: +Download the source tarball from the Apache server created in the previous step, untar it, and run: ```bash maturin sdist ``` -This will create a file named `dist/datafusion-0.7.0.tar.gz`. Upload this to testpypi: +This will create a file named `dist/datafusion-53.0.0.tar.gz`. Upload this to testpypi: ```bash -python3 -m twine upload --repository testpypi dist/datafusion-0.7.0.tar.gz +python3 -m twine upload --repository testpypi dist/datafusion-53.0.0.tar.gz ``` ### Run Verify Release Candidate Workflow @@ -162,8 +164,8 @@ Before sending the vote email, run the manually triggered GitHub Actions workflo 1. Go to https://github.com/apache/datafusion-python/actions/workflows/verify-release-candidate.yml 2. Click "Run workflow" -3. Set `version` to the release version (for example, `52.0.0`) -4. Set `rc_number` to the RC number (for example, `0`) +3. Set `version` to the release version (for example, `53.0.0`) +4. Set `rc_number` to the RC number (for example, `1`) 5. Wait for all jobs to complete successfully Include a short note in the vote email template that this workflow was run across all OS/architecture @@ -183,7 +185,7 @@ Releases may be verified using `verify-release-candidate.sh`: ```bash git clone https://github.com/apache/datafusion-python.git -dev/release/verify-release-candidate.sh 48.0.0 1 +dev/release/verify-release-candidate.sh 53.0.0 1 ``` Alternatively, one can run unit tests against a testpypi release candidate: @@ -195,7 +197,7 @@ cd datafusion-python # checkout the release commit git fetch --tags -git checkout 40.0.0-rc1 +git checkout 53.0.0-rc1 git submodule update --init --recursive # create the env @@ -203,7 +205,7 @@ python3 -m venv .venv source .venv/bin/activate # install release candidate -pip install --extra-index-url https://test.pypi.org/simple/ datafusion==40.0.0 +pip install --extra-index-url https://test.pypi.org/simple/ datafusion==53.0.0 # install test dependencies pip install pytest numpy pytest-asyncio @@ -224,7 +226,7 @@ Once the vote passes, we can publish the release. Create the source release tarball: ```bash -./dev/release/release-tarball.sh 0.8.0 1 +./dev/release/release-tarball.sh 53.0.0 1 ``` ### Publishing Rust Crate to crates.io @@ -232,7 +234,7 @@ Create the source release tarball: Some projects depend on the Rust crate directly, so we publish this to crates.io ```shell -cargo publish +cargo publish --workspace ``` ### Publishing Python Artifacts to PyPi @@ -252,15 +254,15 @@ Pypi packages auto upload to conda-forge via [datafusion feedstock](https://gith ### Push the Release Tag ```bash -git checkout 0.8.0-rc1 -git tag 0.8.0 -git push apache 0.8.0 +git checkout 53.0.0-rc1 +git tag 53.0.0 +git push apache 53.0.0 ``` ### Add the release to Apache Reporter Add the release to https://reporter.apache.org/addrelease.html?datafusion with a version name prefixed with `DATAFUSION-PYTHON`, -for example `DATAFUSION-PYTHON-31.0.0`. +for example `DATAFUSION-PYTHON-53.0.0`. The release information is used to generate a template for a board report (see example from Apache Arrow [here](https://github.com/apache/arrow/pull/14357)). @@ -283,7 +285,7 @@ svn ls https://dist.apache.org/repos/dist/dev/datafusion | grep datafusion-pytho Delete a release candidate: ```bash -svn delete -m "delete old DataFusion RC" https://dist.apache.org/repos/dist/dev/datafusion/apache-datafusion-python-7.1.0-rc1/ +svn delete -m "delete old DataFusion RC" https://dist.apache.org/repos/dist/dev/datafusion/apache-datafusion-python-53.0.0-rc1/ ``` #### Deleting old releases from `release` svn @@ -299,5 +301,5 @@ svn ls https://dist.apache.org/repos/dist/release/datafusion | grep datafusion-p Delete a release: ```bash -svn delete -m "delete old DataFusion release" https://dist.apache.org/repos/dist/release/datafusion/datafusion-python-7.0.0 +svn delete -m "delete old DataFusion release" https://dist.apache.org/repos/dist/release/datafusion/datafusion-python-52.0.0 ``` From 60d8b5dbb5e409cd9ce7692972420e955b8a802e Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Tue, 14 Apr 2026 03:31:01 -0400 Subject: [PATCH 56/56] Fix error on show() with an explain plan (#1492) --- crates/core/src/dataframe.rs | 12 ++++++++++-- python/tests/test_dataframe.py | 10 ++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/crates/core/src/dataframe.rs b/crates/core/src/dataframe.rs index 2d815ec76..2e74991b8 100644 --- a/crates/core/src/dataframe.rs +++ b/crates/core/src/dataframe.rs @@ -38,8 +38,8 @@ use datafusion::dataframe::{DataFrame, DataFrameWriteOptions}; use datafusion::error::DataFusionError; use datafusion::execution::SendableRecordBatchStream; use datafusion::execution::context::TaskContext; -use datafusion::logical_expr::SortExpr; use datafusion::logical_expr::dml::InsertOp; +use datafusion::logical_expr::{LogicalPlan, SortExpr}; use datafusion::parquet::basic::{BrotliLevel, Compression, GzipLevel, ZstdLevel}; use datafusion::physical_plan::{ ExecutionPlan as DFExecutionPlan, collect as df_collect, @@ -707,7 +707,15 @@ impl PyDataFrame { /// Print the result, 20 lines by default #[pyo3(signature = (num=20))] fn show(&self, py: Python, num: usize) -> PyDataFusionResult<()> { - let df = self.df.as_ref().clone().limit(0, Some(num))?; + let mut df = self.df.as_ref().clone(); + df = match self.df.logical_plan() { + LogicalPlan::Explain(_) | LogicalPlan::Analyze(_) => { + // Explain and Analyzer require they are at the top + // of the plan, so do not add a limit. + df + } + _ => df.limit(0, Some(num))?, + }; print_dataframe(py, df) } diff --git a/python/tests/test_dataframe.py b/python/tests/test_dataframe.py index bb8e9685c..091fa9b56 100644 --- a/python/tests/test_dataframe.py +++ b/python/tests/test_dataframe.py @@ -412,6 +412,16 @@ def test_show_empty(df, capsys): assert "DataFrame has no rows" in captured.out +def test_show_on_explain(ctx, capsys): + ctx.sql("explain select 1").show() + captured = capsys.readouterr() + assert "1 as Int64(1)" in captured.out + + ctx.sql("explain analyze select 1").show() + captured = capsys.readouterr() + assert "1 as Int64(1)" in captured.out + + def test_sort(df): df = df.sort(column("b").sort(ascending=False))