From 302c2aee5010d5cb2b1b5647d7b73108075a3557 Mon Sep 17 00:00:00 2001 From: qevolg <2227465945@qq.com> Date: Fri, 27 Mar 2026 14:22:39 +0800 Subject: [PATCH 01/24] feat: add AGENTS.md for project overview and setup instructions --- AGENTS.md | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..6c4c9d69 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,78 @@ +# AGENTS.md — taos-connector-python + +Think in English, but always provide your final response in Chinese. + +## Project Overview + +This is **taospy**, the official Python connector for TDengine (a time-series database). The repo ships two PyPI packages from a single codebase: + +- **taospy** (modules: `taos` + `taosrest`) — native C and REST connectors +- **taos-ws-py** (module: `taosws`) — WebSocket connector built in Rust via PyO3 + +## Architecture — Three Connection Modes + +| Package | Module | Transport | Dependency | +|---------|--------|-----------|------------| +| taospy | `taos` | Native C FFI (`ctypes` → `libtaos.so`) | TDengine client library installed on host | +| taospy | `taosrest` | HTTP REST API (`requests`) | Running taosAdapter (default `localhost:6041`) | +| taos-ws-py | `taosws` | WebSocket (Rust → PyO3) | Running taosAdapter | + +**Key relationships:** +- `taos/cinterface.py` loads the native C library and wraps all C functions via `ctypes`. All native-mode classes depend on it. +- `taosrest/restclient.py` wraps the TDengine REST API. `taosrest/connection.py` and `taosrest/cursor.py` implement PEP 249 on top. +- `taos-ws-py/` is a standalone Rust crate (PyO3) that compiles to a `taosws` Python module. It has its own `Cargo.toml`, tests, and release cycle. +- All three modes register SQLAlchemy dialects: `taos://`, `taosrest://`, `taosws://` (see `taos/sqlalchemy.py`, `taosrest/sqlalchemy.py`). + +## Build & Dev Setup + +```bash +pip install poetry==1.8.5 +poetry install --no-interaction --with=dev +``` + +For WebSocket support: `poetry run pip install taos-ws-py` + +### Formatting + +Black with **line-length 119**: +```bash +poetry run black --check . +``` + +Pre-commit hooks also run `typos` for spell-checking and yaml/json validation. + +## Testing + +Tests require a **running TDengine server**. Most tests connect to a live instance. + +```bash +# Set the REST API endpoint (required for REST tests) +export TDENGINE_URL=localhost:6041 + +# Run all tests +poetry run pytest tests/ + +# Run a single test file +poetry run pytest tests/test_connection.py + +# Run a single test function +poetry run pytest tests/test_connection.py::test_default_connect + +# WebSocket tests (separate directory) +poetry run pytest taos-ws-py/tests/ +``` + +`tests/decorators.py` provides a `@check_env` decorator that skips tests when `TDENGINE_URL` is not set. + +## Conventions + +- **PEP 249 (DB-API 2.0)**: Both `taos` and `taosrest` implement the Python DB-API 2.0 spec — `connect()`, `Connection`, `Cursor` pattern. +- **Error hierarchy**: `taos/error.py` and `taosrest/errors.py` each define their own PEP 249-aligned exception trees rooted at `Error`. Errors include a hex error code: `"[0x%04x]: %s"`. +- **Version management**: `taos/_version.py` holds the taospy version. `taos-ws-py/Cargo.toml` holds the taos-ws-py version. They are versioned independently. +- **IS_V3 guard**: Many tests and features check `taos.IS_V3` to branch between TDengine v2 and v3 behavior. +- **Default credentials**: `user="root"`, `password="taosdata"` throughout. +- **Release**: `ci/release.sh ` for taospy, `ci/release-ws.sh ` for taos-ws-py. These generate changelogs, bump versions, tag, and push. + +## Modifying taos-ws-py + +The `taos-ws-py/` directory is a Rust project using PyO3. It depends on the `taos` Rust crate from `taos-connector-rust`. Changes require a Rust toolchain and `maturin` or `cargo build`. This sub-project has its own tests under `taos-ws-py/tests/`. From 4c1f8f46a0462b05626d5dae77d377cfd27a66a1 Mon Sep 17 00:00:00 2001 From: qevolg <2227465945@qq.com> Date: Fri, 27 Mar 2026 16:59:00 +0800 Subject: [PATCH 02/24] Refactor taos-ws-py integration and update SQLAlchemy dialect handling - Removed TaosWsDialect from taos/sqlalchemy.py and moved it to taos-ws-py/taosws/sqlalchemy.py for better separation of concerns. - Updated taos-ws-py/Cargo.toml to change the library name to _taosws for internal consistency. - Adjusted import paths in examples and tests to reflect the new module structure. - Added comprehensive tests for the taosws SQLAlchemy dialect to ensure compatibility and functionality. - Updated AGENTS.md to clarify project structure and development setup for taos-ws-py. - Removed deprecated taosws tests from the main test suite and integrated them into the new taos-ws-py test structure. --- AGENTS.md | 118 +++- pyproject.toml | 1 - taos-ws-py/Cargo.toml | 2 +- taos-ws-py/examples/schemaless_example.py | 2 +- taos-ws-py/pyproject.toml | 27 + taos-ws-py/src/lib.rs | 36 +- taos-ws-py/taosws/__init__.py | 20 + taos-ws-py/taosws/sqlalchemy.py | 603 ++++++++++++++++++ taos-ws-py/tests/test_schemaless.py | 2 +- taos-ws-py/tests/test_sqlalchemy.py | 122 ++++ taos-ws-py/tests/test_sqlalchemy_migration.py | 61 ++ taos/sqlalchemy.py | 64 -- tests/test_sqlalchemy.py | 70 -- 13 files changed, 958 insertions(+), 170 deletions(-) create mode 100644 taos-ws-py/pyproject.toml create mode 100644 taos-ws-py/taosws/__init__.py create mode 100644 taos-ws-py/taosws/sqlalchemy.py create mode 100644 taos-ws-py/tests/test_sqlalchemy.py create mode 100644 taos-ws-py/tests/test_sqlalchemy_migration.py diff --git a/AGENTS.md b/AGENTS.md index 6c4c9d69..50026d48 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ Think in English, but always provide your final response in Chinese. ## Project Overview -This is **taospy**, the official Python connector for TDengine (a time-series database). The repo ships two PyPI packages from a single codebase: +This is the official Python connector for TDengine (a time-series database). The repo ships two PyPI packages from a single codebase: - **taospy** (modules: `taos` + `taosrest`) — native C and REST connectors - **taos-ws-py** (module: `taosws`) — WebSocket connector built in Rust via PyO3 @@ -21,35 +21,80 @@ This is **taospy**, the official Python connector for TDengine (a time-series da - `taos/cinterface.py` loads the native C library and wraps all C functions via `ctypes`. All native-mode classes depend on it. - `taosrest/restclient.py` wraps the TDengine REST API. `taosrest/connection.py` and `taosrest/cursor.py` implement PEP 249 on top. - `taos-ws-py/` is a standalone Rust crate (PyO3) that compiles to a `taosws` Python module. It has its own `Cargo.toml`, tests, and release cycle. -- All three modes register SQLAlchemy dialects: `taos://`, `taosrest://`, `taosws://` (see `taos/sqlalchemy.py`, `taosrest/sqlalchemy.py`). +- SQLAlchemy dialect ownership (current state): `TaosDialect` and `TaosWsDialect` are both implemented in `taos/sqlalchemy.py`, while `TaosRestDialect` is in `taosrest/sqlalchemy.py`. +- SQLAlchemy entry points are currently registered in root `pyproject.toml` (`taos`, `taosrest`, `taosws`). + +## Directory Structure Guidance + +Current baseline (no mandatory restructure): keep the current top-level layout for regular fixes/features. + +- `taos/` (native + shared SQLAlchemy dialect code) +- `taosrest/` (REST DB-API and REST SQLAlchemy dialect) +- `taos-ws-py/` (Rust/PyO3 WebSocket connector) +- `tests/` and `taos-ws-py/tests/` (split test suites) ## Build & Dev Setup +Run all build/format/test/release commands in a conda virtual environment first: + ```bash +conda activate base +``` + +### taospy (taos + taosrest) + +```bash +conda activate base pip install poetry==1.8.5 poetry install --no-interaction --with=dev ``` -For WebSocket support: `poetry run pip install taos-ws-py` +### taos-ws-py (taosws) + +Install WebSocket package into the same Poetry environment when you need taosws runtime/tests: + +```bash +conda activate base +pip install taos-ws-py +``` + +When you modify Rust code under `taos-ws-py/`, build and install the local wheel for validation: + +```bash +conda activate base +cd taos-ws-py +python3 -m maturin build --strip +pip3 install ./target/wheels/.whl --force-reinstall +``` ### Formatting -Black with **line-length 119**: +For Python code in this repo (taospy and shared Python files), use Black with **line-length 119**: ```bash +conda activate base poetry run black --check . ``` +For Rust code in `taos-ws-py/`, use rustfmt: +```bash +conda activate base +cd taos-ws-py && cargo fmt --all --check +``` + Pre-commit hooks also run `typos` for spell-checking and yaml/json validation. ## Testing Tests require a **running TDengine server**. Most tests connect to a live instance. +### taospy tests (`tests/`) + ```bash +conda activate base # Set the REST API endpoint (required for REST tests) export TDENGINE_URL=localhost:6041 -# Run all tests +# Run all taospy/taosrest tests poetry run pytest tests/ # Run a single test file @@ -57,22 +102,67 @@ poetry run pytest tests/test_connection.py # Run a single test function poetry run pytest tests/test_connection.py::test_default_connect +``` + +`tests/test_sqlalchemy.py` includes `taos://`, `taosrest://`, and `taosws://` coverage in current main branch. + +### taos-ws-py tests (`taos-ws-py/tests/`) -# WebSocket tests (separate directory) +```bash +conda activate base +# WebSocket package tests poetry run pytest taos-ws-py/tests/ ``` +Quick scripts: + +```bash +conda activate base +bash ./test_taospy.sh +bash ./test_taos-ws-py.sh +``` + `tests/decorators.py` provides a `@check_env` decorator that skips tests when `TDENGINE_URL` is not set. ## Conventions -- **PEP 249 (DB-API 2.0)**: Both `taos` and `taosrest` implement the Python DB-API 2.0 spec — `connect()`, `Connection`, `Cursor` pattern. -- **Error hierarchy**: `taos/error.py` and `taosrest/errors.py` each define their own PEP 249-aligned exception trees rooted at `Error`. Errors include a hex error code: `"[0x%04x]: %s"`. -- **Version management**: `taos/_version.py` holds the taospy version. `taos-ws-py/Cargo.toml` holds the taos-ws-py version. They are versioned independently. -- **IS_V3 guard**: Many tests and features check `taos.IS_V3` to branch between TDengine v2 and v3 behavior. -- **Default credentials**: `user="root"`, `password="taosdata"` throughout. -- **Release**: `ci/release.sh ` for taospy, `ci/release-ws.sh ` for taos-ws-py. These generate changelogs, bump versions, tag, and push. +- **taospy conventions (`taos`, `taosrest`)**: + - PEP 249 (DB-API 2.0): `connect()`, `Connection`, `Cursor` + - Error trees: `taos/error.py` and `taosrest/errors.py` (hex code format: `"[0x%04x]: %s"`) + - Version source: `taos/_version.py` + - `IS_V3` guard is widely used for v2/v3 behavior branches +- **taos-ws-py conventions (`taosws`)**: + - Version source: `taos-ws-py/Cargo.toml` + - Rust + PyO3 project under `taos-ws-py/`, tested separately in `taos-ws-py/tests/` +- **Shared defaults**: + - Default credentials: `user="root"`, `password="taosdata"` + - Release scripts: `ci/release.sh ` (taospy), `ci/release-ws.sh ` (taos-ws-py) + +## Modifying Packages + +### taospy (`taos`, `taosrest`) + +Most taospy changes are in Python modules under `taos/`, `taosrest/`, and `tests/`. + +- Use Poetry-managed Python tooling for formatting and tests +- Keep PEP 249 behavior consistent across `taos` and `taosrest` +- Validate with taospy tests in `tests/` +- Note: `TaosWsDialect` currently also lives in `taos/sqlalchemy.py`, so taosws SQLAlchemy changes are made in taospy code today + +### taos-ws-py (`taosws`) + +The `taos-ws-py/` directory is a Rust project using PyO3. It depends on the `taos` Rust crate from `taos-connector-rust`. + +- Changes require a Rust toolchain +- Build with `maturin` or `cargo build` +- Validate with tests in `taos-ws-py/tests/` +- If the change impacts SQLAlchemy behavior for `taosws://`, also verify `tests/test_sqlalchemy.py` + +## Documentation Sync Checklist -## Modifying taos-ws-py +When changing packaging, SQLAlchemy ownership, or test boundaries, update all relevant docs in the same PR: -The `taos-ws-py/` directory is a Rust project using PyO3. It depends on the `taos` Rust crate from `taos-connector-rust`. Changes require a Rust toolchain and `maturin` or `cargo build`. This sub-project has its own tests under `taos-ws-py/tests/`. +- `AGENTS.md` (developer workflow + ownership) +- `README.md` and `README-CN.md` (user-facing install/use paths) +- `taos-ws-py/README.md` and `taos-ws-py/dev.md` (taosws contributor workflow) +- `docs/superpowers/specs/*.md` (design/spec status and rollout notes) diff --git a/pyproject.toml b/pyproject.toml index 4bf65941..544b4317 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,6 @@ packages = [{ include = "taos" }, { include = "taosrest" }] [tool.poetry.plugins."sqlalchemy.dialects"] "taos" = "taos.sqlalchemy:TaosDialect" "taosrest" = "taosrest.sqlalchemy:TaosRestDialect" -"taosws" = "taos.sqlalchemy:TaosWsDialect" [tool.poetry.dependencies] python = ">=3.7,<4.0" diff --git a/taos-ws-py/Cargo.toml b/taos-ws-py/Cargo.toml index 873458e1..d7d9888f 100644 --- a/taos-ws-py/Cargo.toml +++ b/taos-ws-py/Cargo.toml @@ -6,7 +6,7 @@ publish = false license = "MIT" [lib] -name = "taosws" +name = "_taosws" crate-type = ["cdylib"] [dependencies] diff --git a/taos-ws-py/examples/schemaless_example.py b/taos-ws-py/examples/schemaless_example.py index e0e8ff47..8244903b 100644 --- a/taos-ws-py/examples/schemaless_example.py +++ b/taos-ws-py/examples/schemaless_example.py @@ -1,6 +1,6 @@ #! import taosws -from taosws.taosws import PySchemalessProtocol, PySchemalessPrecision +from taosws import PySchemalessProtocol, PySchemalessPrecision import taos diff --git a/taos-ws-py/pyproject.toml b/taos-ws-py/pyproject.toml new file mode 100644 index 00000000..07cd7739 --- /dev/null +++ b/taos-ws-py/pyproject.toml @@ -0,0 +1,27 @@ +[build-system] +requires = ["maturin>=1.8,<2.0"] +build-backend = "maturin" + +[project] +name = "taos-ws-py" +dynamic = ["version"] +description = "The official TDengine Python websocket connector" +readme = "README.md" +requires-python = ">=3.7" +license = { text = "MIT" } +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] + +[project.optional-dependencies] +sqlalchemy = ["sqlalchemy>=2.0.0"] + +[project.entry-points."sqlalchemy.dialects"] +taosws = "taosws.sqlalchemy:TaosWsDialect" + +[tool.maturin] +python-source = "." +module-name = "taosws._taosws" diff --git a/taos-ws-py/src/lib.rs b/taos-ws-py/src/lib.rs index c4bbd328..ee77cbbf 100644 --- a/taos-ws-py/src/lib.rs +++ b/taos-ws-py/src/lib.rs @@ -39,37 +39,37 @@ shadow_rs::shadow!(build); // ``` create_exception!( - taosws, + _taosws, Warning, PyException, "Calling some methods will produce warning." ); -create_exception!(taosws, Error, PyException, "The root error exception"); +create_exception!(_taosws, Error, PyException, "The root error exception"); create_exception!( - taosws, + _taosws, InterfaceError, PyException, "The low-level api caused exception" ); -create_exception!(taosws, DatabaseError, Error); -create_exception!(taosws, DataError, DatabaseError); -create_exception!(taosws, OperationalError, DatabaseError); -create_exception!(taosws, IntegrityError, DatabaseError); -create_exception!(taosws, InternalError, DatabaseError); -create_exception!(taosws, ProgrammingError, DatabaseError); -create_exception!(taosws, NotSupportedError, DatabaseError); - -create_exception!(taosws, QueryError, DatabaseError); -create_exception!(taosws, FetchError, DatabaseError); - -create_exception!(taosws, ConnectionError, Error, "Connection error"); +create_exception!(_taosws, DatabaseError, Error); +create_exception!(_taosws, DataError, DatabaseError); +create_exception!(_taosws, OperationalError, DatabaseError); +create_exception!(_taosws, IntegrityError, DatabaseError); +create_exception!(_taosws, InternalError, DatabaseError); +create_exception!(_taosws, ProgrammingError, DatabaseError); +create_exception!(_taosws, NotSupportedError, DatabaseError); + +create_exception!(_taosws, QueryError, DatabaseError); +create_exception!(_taosws, FetchError, DatabaseError); + +create_exception!(_taosws, ConnectionError, Error, "Connection error"); create_exception!( - taosws, + _taosws, AlreadyClosedError, ConnectionError, "Connection error" ); -create_exception!(taosws, ConsumerException, Error); +create_exception!(_taosws, ConsumerException, Error); mod common; mod consumer; @@ -1020,7 +1020,7 @@ fn blob_to_column(values: Vec>>) -> PyColumnView { } #[pymodule] -fn taosws(py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn _taosws(py: Python<'_>, m: &PyModule) -> PyResult<()> { if std::env::var("RUST_LOG").is_ok() { let _ = pretty_env_logger::try_init(); } diff --git a/taos-ws-py/taosws/__init__.py b/taos-ws-py/taosws/__init__.py new file mode 100644 index 00000000..d54e0ff6 --- /dev/null +++ b/taos-ws-py/taosws/__init__.py @@ -0,0 +1,20 @@ +import sys +from pathlib import Path + + +_current_package_dir = Path(__file__).resolve().parent +for search_path in list(sys.path): + candidate = Path(search_path) / "taosws" + if candidate.is_dir() and candidate != _current_package_dir and str(candidate) not in __path__: + __path__.append(str(candidate)) + +try: + from . import _taosws as _native + from ._taosws import * +except ImportError: + from . import taosws as _native + from .taosws import * + +__doc__ = _native.__doc__ +if hasattr(_native, "__all__"): + __all__ = _native.__all__ diff --git a/taos-ws-py/taosws/sqlalchemy.py b/taos-ws-py/taosws/sqlalchemy.py new file mode 100644 index 00000000..3be87ad1 --- /dev/null +++ b/taos-ws-py/taosws/sqlalchemy.py @@ -0,0 +1,603 @@ +from sqlalchemy import sql +from sqlalchemy import text +from sqlalchemy import types as sqltypes +from sqlalchemy.engine import default, reflection +from sqlalchemy.sql import compiler + +TYPES_MAP = { + "BOOL": sqltypes.Boolean, + "TIMESTAMP": sqltypes.DATETIME, + "INT": sqltypes.Integer, + "INT UNSIGNED": sqltypes.Integer, + "BIGINT": sqltypes.BigInteger, + "BIGINT UNSIGNED": sqltypes.BigInteger, + "FLOAT": sqltypes.FLOAT, + "DOUBLE": sqltypes.FLOAT, + "TINYINT": sqltypes.SmallInteger, + "TINYINT UNSIGNED": sqltypes.SmallInteger, + "SMALLINT": sqltypes.SmallInteger, + "SMALLINT UNSIGNED": sqltypes.SmallInteger, + "BINARY": sqltypes.String, + "VARCHAR": sqltypes.String, + "VARBINARY": sqltypes.BINARY, + "NCHAR": sqltypes.Unicode, + "JSON": sqltypes.JSON, + "BLOB": sqltypes.BLOB, + "GEOMETRY": sqltypes.BINARY, +} + +# TDengine reserved words +RESERVED_WORDS_TDENGINE = { + "account", + "accounts", + "add", + "aggregate", + "all", + "alter", + "analyze", + "and", + "anti", + "anode", + "anodes", + "anomaly_window", + "apps", + "as", + "asc", + "asof", + "at_once", + "balance", + "batch_scan", + "between", + "bigint", + "binary", + "bnode", + "bnodes", + "bool", + "both", + "buffer", + "bufsize", + "by", + "cache", + "cachemodel", + "cachesize", + "case", + "cast", + "child", + "client_version", + "cluster", + "column", + "comment", + "comp", + "compact", + "compacts", + "connection", + "connections", + "conns", + "consumer", + "consumers", + "contains", + "count", + "count_window", + "create", + "createdb", + "current_user", + "database", + "databases", + "dbs", + "decimal", + "delete", + "delete_mark", + "desc", + "describe", + "distinct", + "distributed", + "dnode", + "dnodes", + "double", + "drop", + "duration", + "else", + "enable", + "encryptions", + "encrypt_algorithm", + "encrypt_key", + "end", + "exists", + "expired", + "explain", + "event_window", + "every", + "file", + "fill", + "fill_history", + "first", + "float", + "flush", + "from", + "for", + "force", + "full", + "function", + "functions", + "geometry", + "grant", + "grants", + "full", + "logs", + "machines", + "group", + "hash_join", + "having", + "host", + "if", + "ignore", + "import", + "in", + "index", + "indexes", + "inner", + "insert", + "int", + "integer", + "interval", + "into", + "is", + "jlimit", + "join", + "json", + "keep", + "key", + "kill", + "language", + "last", + "last_row", + "leader", + "leading", + "left", + "licences", + "like", + "limit", + "linear", + "local", + "match", + "maxrows", + "max_delay", + "bwlimit", + "merge", + "meta", + "only", + "minrows", + "minus", + "mnode", + "mnodes", + "modify", + "modules", + "normal", + "nchar", + "next", + "near", + "nmatch", + "none", + "not", + "now", + "no_batch_scan", + "null", + "null_f", + "nulls", + "offset", + "on", + "or", + "order", + "outer", + "outputtype", + "pages", + "pagesize", + "para_tables_sort", + "partition", + "partition_first", + "pass", + "port", + "position", + "pps", + "primary", + "precision", + "prev", + "privileges", + "qnode", + "qnodes", + "qtime", + "queries", + "query", + "pi", + "rand", + "range", + "ratio", + "pause", + "read", + "recursive", + "redistribute", + "rename", + "replace", + "replica", + "reset", + "resume", + "restore", + "retentions", + "revoke", + "right", + "rollup", + "schemaless", + "scores", + "select", + "semi", + "server_status", + "server_version", + "session", + "set", + "show", + "single_stable", + "skip_tsma", + "sliding", + "slimit", + "sma", + "smalldata_ts_sort", + "smallint", + "snode", + "snodes", + "sort_for_group", + "soffset", + "split", + "stable", + "stables", + "start", + "state", + "state_window", + "storage", + "stream", + "streams", + "strict", + "stt_trigger", + "subscribe", + "subscriptions", + "substr", + "substring", + "subtable", + "sysinfo", + "system", + "table", + "tables", + "table_prefix", + "table_suffix", + "tag", + "tags", + "tbname", + "then", + "timestamp", + "timezone", + "tinyint", + "to", + "today", + "topic", + "topics", + "trailing", + "transaction", + "transactions", + "trigger", + "trim", + "tsdb_pagesize", + "tseries", + "tsma", + "tsmas", + "ttl", + "union", + "unsafe", + "unsigned", + "untreated", + "update", + "use", + "user", + "users", + "using", + "value", + "value_f", + "values", + "varchar", + "variables", + "verbose", + "vgroup", + "vgroups", + "view", + "views", + "vnode", + "vnodes", + "wal_fsync_period", + "wal_level", + "wal_retention_period", + "wal_retention_size", + "wal_roll_period", + "wal_segment_size", + "watermark", + "when", + "where", + "window", + "window_close", + "window_offset", + "with", + "write", + "_c0", + "_irowts", + "_irowts_origin", + "_isfilled", + "_qduration", + "_qend", + "_qstart", + "_rowts", + "_tags", + "_wduration", + "_wend", + "_wstart", + "_flow", + "_fhigh", + "_frowts", + "alive", + "varbinary", + "s3_chunkpages", + "s3_keeplocal", + "s3_compact", + "s3migrate", + "keep_time_offset", + "arbgroups", + "is_import", + "force_window_close", +} + +# backup generator function +""" +generator from TDengine/source/libs/parse/src/parTokenizer.c -> keywordTable + +import sys +def readKeyWord(filename): + keys = "" + print(f"read file {filename}\n") + with open(filename) as file: + for line in file.readlines(): + pos1 = line.find('"') + if pos1 == -1 : + print(f"NO FOUND FIRST QUOTA: {line}\n") + continue + pos2 = line.find('"', pos1 + 1) + if pos2 == -1 : + print(f"NO FOUND SECOND QUOTA: {line}\n") + continue + word = line[pos1:pos2+1] + if keys == "": + keys = "RESERVED_WORDS_TDENGINE = {\n " + word.lower() + else: + keys += ",\n " + word.lower() + + # end + keys += "\n}" + print(f"\n\n{keys}\n") + + +if __name__ == "__main__": + readKeyWord("./keyword.txt") + +""" + + +# +# identifier for TDengine +# +class TDengineIdentifierPreparer(sql.compiler.IdentifierPreparer): + reserved_words = RESERVED_WORDS_TDENGINE + + def __init__(self, dialect, server_ansiquotes=False, **kw): + if not server_ansiquotes: + quote = "`" + else: + quote = '"' + + super(TDengineIdentifierPreparer, self).__init__(dialect, initial_quote=quote, escape_quote=quote) + + def _quote_free_identifiers(self, *ids): + """Unilaterally identifier-quote any number of strings.""" + return tuple([self.quote_identifier(i) for i in ids if i is not None]) + + +# +# base class for dialect +# +class BaseDialect(default.DefaultDialect): + supports_native_boolean = True + implicit_returning = True + # supports_statement_cache = True + + # Set back-quote identifier preparer for TDengine keywords + preparer = TDengineIdentifierPreparer + + def is_sys_db(self, dbname): + return dbname.lower() in ["information_schema", "performance_schema"] + + def do_rollback(self, connection): + pass + + def _get_server_version_info(self, connection): + cursor = connection.execute(text("select server_version()")) + return cursor.fetchone() + + def do_execute(self, cursor, statement, parameters, context=None): + if parameters is None or len(parameters) == 0: + cursor.execute(statement, parameters) + else: + cursor.execute(statement, [parameters]) + + return cursor + + def do_executemany(self, cursor, statement, parameters, context=None): + cursor.executemany(statement, parameters) + return cursor + + @reflection.cache + def has_schema(self, connection, schema, **kw): + return schema in self.get_schema_names(connection) + + # Check if table exists + @reflection.cache + def has_table(self, connection, table_name, schema=None, **kw): + return table_name in self.get_table_names(connection, schema) + + # Get column information + @reflection.cache + def get_columns(self, connection, table_name, schema=None, **kw): + sysdb = False + if schema is None: + sql = f"describe {table_name}" + else: + sql = f"describe {schema}.{table_name}" + # sysdb = self.is_sys_db(schema) + try: + cursor = connection.execute(text(sql)) + columns = [] + for row in cursor.fetchall(): + # print(row) + column = dict() + column["name"] = row[0] + column["type"] = self._resolve_type(row[1]) + columns.append(column) + return columns + except: + return [] + + @reflection.cache + def get_pk_constraint(self, connection, table_name, schema=None, **kw): + columns = self.get_columns(connection, table_name, schema) + return {"constrained_columns": [columns[0]["name"]], "name": None} + + @reflection.cache + def get_foreign_keys(self, connection, table_name, schema=None, **kw): + # No foreign key is supported by TDengine + return [] + + # Get indexes information + @reflection.cache + def get_indexes(self, connection, table_name, schema=None, **kw): + sql = ( + "SELECT * FROM information_schema.INS_INDEXES " + f"WHERE db_name = '{schema}'" + f"AND table_name = '{table_name}'" + ) + try: + cursor = connection.execute(text(sql)) + rows = cursor.fetchall() + indexes = [] + for row in rows: + index = {"name": row[0], "column_names": [row[5]], "type": "index", "unique": False} + indexes.append(index) + return indexes + except: + return [] + + # Get database names + @reflection.cache + def get_schema_names(self, connection, **kw): + sql = text("SHOW DATABASES") + try: + cursor = connection.execute(sql) + names = [] + for row in cursor.fetchall(): + if self.is_sys_db(row[0]) is False: + names.append(row[0]) + return names + except: + return [] + + # Get table names + @reflection.cache + def get_table_names(self, connection, schema=None, **kw): + if schema is None: + sqls = [f"show stables", f"show normal tables"] + else: + sqls = [f"show `{schema}`.stables", f"show normal `{schema}`.tables"] + # Execute queries + try: + names = [] + for sql in sqls: + cursor = connection.execute(text(sql)) + for row in cursor.fetchall(): + names.append(row[0]) + return names + except: + return [] + + @reflection.cache + def get_view_names(self, connection, schema=None, **kw): + if schema is None: + return [] + # SQL query for views + sql = f"show `{schema}`.views" + # Execute query + try: + + cursor = connection.execute(text(sql)) + return [row[0] for row in cursor.fetchall()] + except: + return [] + + def _resolve_type(self, type_): + # print(f"call function {sys._getframe().f_code.co_name} type: {type_} ...\n") + return TYPES_MAP.get(type_, sqltypes.UserDefinedType) + +# WebSocket dialect +class TaosWsDialect(BaseDialect): + name = "taosws" + driver = "taosws" + + @classmethod + def dbapi(cls): + import taosws + + return taosws + + @classmethod + def import_dbapi(cls): + import taosws + + return taosws + + @classmethod + def create_connect_args(cls, url): + if url.username and url.password: + userpass = f"{url.username}:{url.password}" + elif url.username: + userpass = f"{url.username}" + elif url.password: + userpass = f":{url.password}" + else: + userpass = "" + + at = "@" if userpass else "" + + hosts = url.query.get("hosts") + if hosts: + addr = hosts + else: + if url.host and url.port: + addr = f"{url.host}:{url.port}" + elif url.host: + addr = f"{url.host}" + elif url.port: + addr = f":{url.port}" + else: + addr = "" + + query_params = [(key, value) for key, value in url.query.items() if key != "hosts"] + params = "&".join(f"{key}={value}" for key, value in query_params) + + dsn = f"{url.drivername}://{userpass}{at}{addr}" + if url.database: + dsn += f"/{url.database}" + if params: + dsn += f"?{params}" + + return ([dsn], {}) diff --git a/taos-ws-py/tests/test_schemaless.py b/taos-ws-py/tests/test_schemaless.py index 0c5453fe..8adfebad 100644 --- a/taos-ws-py/tests/test_schemaless.py +++ b/taos-ws-py/tests/test_schemaless.py @@ -1,5 +1,5 @@ import taosws -from taosws.taosws import PySchemalessProtocol, PySchemalessPrecision +from taosws import PySchemalessProtocol, PySchemalessPrecision line_data = [ "measurement,host=host1 field1=2i,field2=2.0 1577837300000", diff --git a/taos-ws-py/tests/test_sqlalchemy.py b/taos-ws-py/tests/test_sqlalchemy.py new file mode 100644 index 00000000..9e925fe2 --- /dev/null +++ b/taos-ws-py/tests/test_sqlalchemy.py @@ -0,0 +1,122 @@ +import os + +import pytest +import taosws +import utils +from sqlalchemy import create_engine +from sqlalchemy import inspect +from sqlalchemy.dialects import registry + + +pytest.importorskip("sqlalchemy") +pytestmark = pytest.mark.skipif("TDENGINE_URL" not in os.environ, reason="Please set environment variable TDENGINE_URL") +registry.register("taosws", "taosws.sqlalchemy", "TaosWsDialect") + +HOST = "localhost" +PORT = 6041 + + +def insert_data(conn=None): + close_on_exit = conn is None + c = conn or taosws.connect(f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}") + c.execute("drop database if exists test") + c.execute("create database if not exists test") + c.execute("create table test.meters (ts timestamp, c1 int, c2 double) tags(t1 int)") + c.execute("insert into test.d0 using test.meters tags(0) values (1733189403001, 1, 1.11) (1733189403002, 2, 2.22)") + c.execute("insert into test.d1 using test.meters tags(1) values (1733189403003, 3, 3.33) (1733189403004, 4, 4.44)") + c.execute("create table test.ntb(ts timestamp, age int)") + c.execute("insert into test.ntb values(now, 23)") + if close_on_exit: + c.close() + + +def check_list_equal(list1, list2, tips): + if list1 != list2: + raise BaseException(f"{tips} failed. list1={list1} list2={list2}") + + +def check_result_equal(result1, result2, tips): + if result1 != result2: + raise BaseException(f"{tips} failed. result1={result1} result2={result2}") + + +def check_basic(conn, inspection, sub_tables=None): + tables = sub_tables or ["meters", "ntb"] + + databases = inspection.get_schema_names() + if "test" not in databases: + raise BaseException(f"test not in {databases}") + + check_list_equal(inspection.get_table_names("test"), tables, "check get_table_names()") + + expected_columns = [ + {"name": "ts", "type": inspection.dialect._resolve_type("TIMESTAMP")}, + {"name": "c1", "type": inspection.dialect._resolve_type("INT")}, + {"name": "c2", "type": inspection.dialect._resolve_type("DOUBLE")}, + {"name": "t1", "type": inspection.dialect._resolve_type("INT")}, + ] + columns = inspection.get_columns("meters", "test") + for index, column in enumerate(columns): + expected = expected_columns[index] + if column["name"] != expected["name"]: + raise BaseException(f"column name mismatch: {column['name']} != {expected['name']}") + if type(column["type"]) != expected["type"]: + raise BaseException(f"column type mismatch: {type(column['type'])} != {expected['type']}") + + check_result_equal(inspection.has_table("meters", "test"), True, "check has_table()") + check_result_equal(inspection.dialect.has_schema(conn, "test"), True, "check has_schema()") + + conn.close() + + +def test_read_from_sqlalchemy_taosws(): + engine = create_engine( + f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}?timezone=Asia/Shanghai" + ) + conn = engine.connect() + insert_data() + inspection = inspect(engine) + check_basic(conn, inspection) + + +def test_read_from_sqlalchemy_taosws_failover(): + db_name = "test_1755496227" + conn = taosws.connect(f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}") + conn.execute(f"drop database if exists {db_name}") + conn.execute(f"create database {db_name}") + + try: + urls = [ + "taosws://", + "taosws://localhost", + f"taosws://localhost:{PORT}", + f"taosws://localhost:{PORT}/{db_name}", + f"taosws://root@localhost:{PORT}/{db_name}", + f"taosws://root:@localhost:{PORT}/{db_name}", + f"taosws://{utils.test_username()}:{utils.test_password()}@localhost:{PORT}/{db_name}", + f"taosws://{utils.test_username()}:{utils.test_password()}@localhost:{PORT}/{db_name}?hosts=", + f"taosws://{utils.test_username()}:{utils.test_password()}@/{db_name}?hosts=localhost:{PORT}", + f"taosws://{utils.test_username()}:{utils.test_password()}@localhost:{PORT}/{db_name}?hosts=localhost:{PORT}", + f"taosws://{utils.test_username()}:{utils.test_password()}@localhost:{PORT}/{db_name}?hosts=localhost:{PORT},127.0.0.1:{PORT}", + f"taosws://{utils.test_username()}:{utils.test_password()}@localhost:{PORT}/{db_name}?hosts=localhost:{PORT},127.0.0.1:{PORT}&timezone=Asia/Shanghai", + ] + + for url in urls: + engine = create_engine(url) + econn = engine.connect() + econn.close() + + invalid_urls = [ + f"taosws://:{PORT}", + f"taosws://:taosdata@=localhost:{PORT}/{db_name}", + ] + + for url in invalid_urls: + with pytest.raises(Exception): + engine = create_engine(url) + econn = engine.connect() + econn.close() + + finally: + conn.execute(f"drop database if exists {db_name}") + conn.close() diff --git a/taos-ws-py/tests/test_sqlalchemy_migration.py b/taos-ws-py/tests/test_sqlalchemy_migration.py new file mode 100644 index 00000000..76cd5a07 --- /dev/null +++ b/taos-ws-py/tests/test_sqlalchemy_migration.py @@ -0,0 +1,61 @@ +import importlib +import ast +from pathlib import Path + +from sqlalchemy.engine.url import make_url +from sqlalchemy import types as sqltypes + + +def test_taosws_sqlalchemy_module_is_available(): + module = importlib.import_module("taosws.sqlalchemy") + assert hasattr(module, "TaosWsDialect") + + +def test_resolve_type_covers_all_declared_tdengine_types(): + module = importlib.import_module("taosws.sqlalchemy") + dialect = module.TaosWsDialect() + + for tdengine_type, sqlalchemy_type in module.TYPES_MAP.items(): + assert dialect._resolve_type(tdengine_type) is sqlalchemy_type + + assert dialect._resolve_type("TYPE_NOT_EXISTS") is sqltypes.UserDefinedType + + +def test_create_connect_args_prefers_hosts_and_keeps_other_query_params(): + module = importlib.import_module("taosws.sqlalchemy") + dialect = module.TaosWsDialect() + url = make_url( + "taosws://root:taosdata@localhost:6041/test_1755496227?" + "hosts=localhost:6041,127.0.0.1:6041&timezone=Asia/Shanghai" + ) + + args, kwargs = dialect.create_connect_args(url) + + assert args == ["taosws://root:taosdata@localhost:6041,127.0.0.1:6041/test_1755496227?timezone=Asia/Shanghai"] + assert kwargs == {} + + +def test_create_connect_args_no_trailing_ampersand_when_hosts_is_last_param(): + module = importlib.import_module("taosws.sqlalchemy") + dialect = module.TaosWsDialect() + url = make_url( + "taosws://root:taosdata@localhost:6041/test_1755496227?" + "timezone=Asia/Shanghai&hosts=localhost:6041" + ) + + args, kwargs = dialect.create_connect_args(url) + + assert args == ["taosws://root:taosdata@localhost:6041/test_1755496227?timezone=Asia/Shanghai"] + assert kwargs == {} + + +def test_sqlalchemy_module_does_not_depend_on_taospy_imports(): + module = importlib.import_module("taosws.sqlalchemy") + source = Path(module.__file__).read_text(encoding="utf-8") + tree = ast.parse(source) + + for node in ast.walk(tree): + if isinstance(node, (ast.Import, ast.ImportFrom)): + for alias in node.names: + top_level_name = alias.name.split(".")[0] + assert top_level_name not in {"taos", "taospy"} diff --git a/taos/sqlalchemy.py b/taos/sqlalchemy.py index 2649165d..3b6edfce 100644 --- a/taos/sqlalchemy.py +++ b/taos/sqlalchemy.py @@ -607,67 +607,3 @@ def dbapi(cls): def import_dbapi(cls): return taos - -# -# ---------------- TDengine WebSocket connector implementation ------------- -# - - -# WebSocket dialect -class TaosWsDialect(BaseDialect): - name = "taosws" - driver = "taosws" - - @classmethod - def dbapi(cls): - import taosws - - return taosws - - @classmethod - def import_dbapi(cls): - import taosws - - return taosws - - @classmethod - def create_connect_args(cls, url): - if url.username and url.password: - userpass = f"{url.username}:{url.password}" - elif url.username: - userpass = f"{url.username}" - elif url.password: - userpass = f":{url.password}" - else: - userpass = "" - - at = "@" if userpass else "" - - hosts = url.query.get("hosts") - if hosts: - addr = hosts - else: - if url.host and url.port: - addr = f"{url.host}:{url.port}" - elif url.host: - addr = f"{url.host}" - elif url.port: - addr = f":{url.port}" - else: - addr = "" - - params = "" - for i, (key, value) in enumerate(url.query.items()): - if key == "hosts": - continue - params += f"{key}={value}" - if i != len(url.query.items()) - 1: - params += "&" - - dsn = f"{url.drivername}://{userpass}{at}{addr}" - if url.database: - dsn += f"/{url.database}" - if params: - dsn += f"?{params}" - - return ([dsn], {}) diff --git a/tests/test_sqlalchemy.py b/tests/test_sqlalchemy.py index ab8220cf..44a31bd3 100644 --- a/tests/test_sqlalchemy.py +++ b/tests/test_sqlalchemy.py @@ -227,72 +227,6 @@ def test_sqlalchemy_format_stmt_taos(): check_basic(conn, inspection, subTables=["meters"]) -# taosws -def test_read_from_sqlalchemy_taosws(): - try: - import taosws - except ImportError: - return - - engine = create_engine( - f"taosws://{utils.test_username()}:{utils.test_password()}@{host}:6041?timezone=Asia/Shanghai" - ) - conn = engine.connect() - insert_data(None) - inspection = inspect(engine) - check_basic(conn, inspection) - - -def test_read_from_sqlalchemy_taosws_failover(): - try: - import taosws - except ImportError: - print("taosws not installed, skip test_read_from_sqlalchemy_taosws_failover") - return - - conn = taos.connect() - conn.execute("drop database if exists test_1755496227") - conn.execute("create database test_1755496227") - - try: - urls = [ - "taosws://", - "taosws://localhost", - "taosws://localhost:6041", - "taosws://localhost:6041/test_1755496227", - "taosws://root@localhost:6041/test_1755496227", - "taosws://root:@localhost:6041/test_1755496227", - f"taosws://{utils.test_username()}:{utils.test_password()}@localhost:6041/test_1755496227", - f"taosws://{utils.test_username()}:{utils.test_password()}@localhost:6041/test_1755496227?hosts=", - f"taosws://{utils.test_username()}:{utils.test_password()}@/test_1755496227?hosts=localhost:6041", - f"taosws://{utils.test_username()}:{utils.test_password()}@localhost:6041/test_1755496227?hosts=localhost:6041", - f"taosws://{utils.test_username()}:{utils.test_password()}@localhost:6041/test_1755496227?hosts=localhost:6041,127.0.0.1:6041", - f"taosws://{utils.test_username()}:{utils.test_password()}@localhost:6041/test_1755496227?hosts=localhost:6041,127.0.0.1:6041&timezone=Asia/Shanghai", - ] - - for url in urls: - engine = create_engine(url) - econn = engine.connect() - econn.close() - - invalid_urls = [ - "taosws://:6041", - "taosws://:taosdata@=localhost:6041/test_1755496227", - ] - - for url in invalid_urls: - try: - engine = create_engine(url) - econn = engine.connect() - econn.close() - except Exception as e: - print(f"expected error for {url}: {e}") - - finally: - conn.execute("drop database test_1755496227") - conn.close() - - # taosrest def test_read_from_sqlalchemy_taosrest(): if not taos.IS_V3: @@ -313,7 +247,3 @@ def test_read_from_sqlalchemy_taosrest(): print("Test taos api ..................................... [OK]\n") test_read_from_sqlalchemy_taosrest() print("Test taosrest api ................................. [OK]\n") - test_read_from_sqlalchemy_taosws() - print("Test taosws api ................................... [OK]\n") - test_read_from_sqlalchemy_taosws_failover() - print("Test taosws failover api .......................... [OK]\n") From 32ac190631c85d54c6b738d12473e36e5a5a081d Mon Sep 17 00:00:00 2001 From: qevolg <2227465945@qq.com> Date: Fri, 27 Mar 2026 17:05:31 +0800 Subject: [PATCH 03/24] chore: remove AGENTS.md as part of project restructuring --- AGENTS.md | 168 ------------------------------------------------------ 1 file changed, 168 deletions(-) delete mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 50026d48..00000000 --- a/AGENTS.md +++ /dev/null @@ -1,168 +0,0 @@ -# AGENTS.md — taos-connector-python - -Think in English, but always provide your final response in Chinese. - -## Project Overview - -This is the official Python connector for TDengine (a time-series database). The repo ships two PyPI packages from a single codebase: - -- **taospy** (modules: `taos` + `taosrest`) — native C and REST connectors -- **taos-ws-py** (module: `taosws`) — WebSocket connector built in Rust via PyO3 - -## Architecture — Three Connection Modes - -| Package | Module | Transport | Dependency | -|---------|--------|-----------|------------| -| taospy | `taos` | Native C FFI (`ctypes` → `libtaos.so`) | TDengine client library installed on host | -| taospy | `taosrest` | HTTP REST API (`requests`) | Running taosAdapter (default `localhost:6041`) | -| taos-ws-py | `taosws` | WebSocket (Rust → PyO3) | Running taosAdapter | - -**Key relationships:** -- `taos/cinterface.py` loads the native C library and wraps all C functions via `ctypes`. All native-mode classes depend on it. -- `taosrest/restclient.py` wraps the TDengine REST API. `taosrest/connection.py` and `taosrest/cursor.py` implement PEP 249 on top. -- `taos-ws-py/` is a standalone Rust crate (PyO3) that compiles to a `taosws` Python module. It has its own `Cargo.toml`, tests, and release cycle. -- SQLAlchemy dialect ownership (current state): `TaosDialect` and `TaosWsDialect` are both implemented in `taos/sqlalchemy.py`, while `TaosRestDialect` is in `taosrest/sqlalchemy.py`. -- SQLAlchemy entry points are currently registered in root `pyproject.toml` (`taos`, `taosrest`, `taosws`). - -## Directory Structure Guidance - -Current baseline (no mandatory restructure): keep the current top-level layout for regular fixes/features. - -- `taos/` (native + shared SQLAlchemy dialect code) -- `taosrest/` (REST DB-API and REST SQLAlchemy dialect) -- `taos-ws-py/` (Rust/PyO3 WebSocket connector) -- `tests/` and `taos-ws-py/tests/` (split test suites) - -## Build & Dev Setup - -Run all build/format/test/release commands in a conda virtual environment first: - -```bash -conda activate base -``` - -### taospy (taos + taosrest) - -```bash -conda activate base -pip install poetry==1.8.5 -poetry install --no-interaction --with=dev -``` - -### taos-ws-py (taosws) - -Install WebSocket package into the same Poetry environment when you need taosws runtime/tests: - -```bash -conda activate base -pip install taos-ws-py -``` - -When you modify Rust code under `taos-ws-py/`, build and install the local wheel for validation: - -```bash -conda activate base -cd taos-ws-py -python3 -m maturin build --strip -pip3 install ./target/wheels/.whl --force-reinstall -``` - -### Formatting - -For Python code in this repo (taospy and shared Python files), use Black with **line-length 119**: -```bash -conda activate base -poetry run black --check . -``` - -For Rust code in `taos-ws-py/`, use rustfmt: -```bash -conda activate base -cd taos-ws-py && cargo fmt --all --check -``` - -Pre-commit hooks also run `typos` for spell-checking and yaml/json validation. - -## Testing - -Tests require a **running TDengine server**. Most tests connect to a live instance. - -### taospy tests (`tests/`) - -```bash -conda activate base -# Set the REST API endpoint (required for REST tests) -export TDENGINE_URL=localhost:6041 - -# Run all taospy/taosrest tests -poetry run pytest tests/ - -# Run a single test file -poetry run pytest tests/test_connection.py - -# Run a single test function -poetry run pytest tests/test_connection.py::test_default_connect -``` - -`tests/test_sqlalchemy.py` includes `taos://`, `taosrest://`, and `taosws://` coverage in current main branch. - -### taos-ws-py tests (`taos-ws-py/tests/`) - -```bash -conda activate base -# WebSocket package tests -poetry run pytest taos-ws-py/tests/ -``` - -Quick scripts: - -```bash -conda activate base -bash ./test_taospy.sh -bash ./test_taos-ws-py.sh -``` - -`tests/decorators.py` provides a `@check_env` decorator that skips tests when `TDENGINE_URL` is not set. - -## Conventions - -- **taospy conventions (`taos`, `taosrest`)**: - - PEP 249 (DB-API 2.0): `connect()`, `Connection`, `Cursor` - - Error trees: `taos/error.py` and `taosrest/errors.py` (hex code format: `"[0x%04x]: %s"`) - - Version source: `taos/_version.py` - - `IS_V3` guard is widely used for v2/v3 behavior branches -- **taos-ws-py conventions (`taosws`)**: - - Version source: `taos-ws-py/Cargo.toml` - - Rust + PyO3 project under `taos-ws-py/`, tested separately in `taos-ws-py/tests/` -- **Shared defaults**: - - Default credentials: `user="root"`, `password="taosdata"` - - Release scripts: `ci/release.sh ` (taospy), `ci/release-ws.sh ` (taos-ws-py) - -## Modifying Packages - -### taospy (`taos`, `taosrest`) - -Most taospy changes are in Python modules under `taos/`, `taosrest/`, and `tests/`. - -- Use Poetry-managed Python tooling for formatting and tests -- Keep PEP 249 behavior consistent across `taos` and `taosrest` -- Validate with taospy tests in `tests/` -- Note: `TaosWsDialect` currently also lives in `taos/sqlalchemy.py`, so taosws SQLAlchemy changes are made in taospy code today - -### taos-ws-py (`taosws`) - -The `taos-ws-py/` directory is a Rust project using PyO3. It depends on the `taos` Rust crate from `taos-connector-rust`. - -- Changes require a Rust toolchain -- Build with `maturin` or `cargo build` -- Validate with tests in `taos-ws-py/tests/` -- If the change impacts SQLAlchemy behavior for `taosws://`, also verify `tests/test_sqlalchemy.py` - -## Documentation Sync Checklist - -When changing packaging, SQLAlchemy ownership, or test boundaries, update all relevant docs in the same PR: - -- `AGENTS.md` (developer workflow + ownership) -- `README.md` and `README-CN.md` (user-facing install/use paths) -- `taos-ws-py/README.md` and `taos-ws-py/dev.md` (taosws contributor workflow) -- `docs/superpowers/specs/*.md` (design/spec status and rollout notes) From 6bf539bde4e1840c8ad7d5882420f920bd7f966f Mon Sep 17 00:00:00 2001 From: qevolg <2227465945@qq.com> Date: Fri, 27 Mar 2026 17:15:39 +0800 Subject: [PATCH 04/24] refactor: clean up code formatting and remove unnecessary blank lines --- examples/pandas-read-sql.py | 2 +- taos-ws-py/taosws/sqlalchemy.py | 2 +- taos-ws-py/tests/test_sqlalchemy.py | 4 +- taos-ws-py/tests/test_sqlalchemy_migration.py | 3 +- taos-ws-py/tests/test_tmq.py | 132 ++++++++++-------- taos/_version.py | 2 +- taos/sqlalchemy.py | 2 - taosrest/sqlalchemy.py | 2 + tests/test_tmq.py | 20 +-- 9 files changed, 97 insertions(+), 72 deletions(-) diff --git a/examples/pandas-read-sql.py b/examples/pandas-read-sql.py index f885e1bb..e0f95788 100644 --- a/examples/pandas-read-sql.py +++ b/examples/pandas-read-sql.py @@ -3,7 +3,7 @@ protocol_and_port = {"taos": 6030, "taosrest": 6041, "taosws": 6041} -for (protocol, port) in protocol_and_port.items(): +for protocol, port in protocol_and_port.items(): engine = create_engine(f"{protocol}://root:taosdata@localhost") conn = engine.connect() res = pandas.read_sql(text("show databases"), conn) diff --git a/taos-ws-py/taosws/sqlalchemy.py b/taos-ws-py/taosws/sqlalchemy.py index 3be87ad1..b0f38b74 100644 --- a/taos-ws-py/taosws/sqlalchemy.py +++ b/taos-ws-py/taosws/sqlalchemy.py @@ -538,7 +538,6 @@ def get_view_names(self, connection, schema=None, **kw): sql = f"show `{schema}`.views" # Execute query try: - cursor = connection.execute(text(sql)) return [row[0] for row in cursor.fetchall()] except: @@ -548,6 +547,7 @@ def _resolve_type(self, type_): # print(f"call function {sys._getframe().f_code.co_name} type: {type_} ...\n") return TYPES_MAP.get(type_, sqltypes.UserDefinedType) + # WebSocket dialect class TaosWsDialect(BaseDialect): name = "taosws" diff --git a/taos-ws-py/tests/test_sqlalchemy.py b/taos-ws-py/tests/test_sqlalchemy.py index 9e925fe2..5a27399a 100644 --- a/taos-ws-py/tests/test_sqlalchemy.py +++ b/taos-ws-py/tests/test_sqlalchemy.py @@ -9,7 +9,9 @@ pytest.importorskip("sqlalchemy") -pytestmark = pytest.mark.skipif("TDENGINE_URL" not in os.environ, reason="Please set environment variable TDENGINE_URL") +pytestmark = pytest.mark.skipif( + "TDENGINE_URL" not in os.environ, reason="Please set environment variable TDENGINE_URL" +) registry.register("taosws", "taosws.sqlalchemy", "TaosWsDialect") HOST = "localhost" diff --git a/taos-ws-py/tests/test_sqlalchemy_migration.py b/taos-ws-py/tests/test_sqlalchemy_migration.py index 76cd5a07..4cecbeae 100644 --- a/taos-ws-py/tests/test_sqlalchemy_migration.py +++ b/taos-ws-py/tests/test_sqlalchemy_migration.py @@ -39,8 +39,7 @@ def test_create_connect_args_no_trailing_ampersand_when_hosts_is_last_param(): module = importlib.import_module("taosws.sqlalchemy") dialect = module.TaosWsDialect() url = make_url( - "taosws://root:taosdata@localhost:6041/test_1755496227?" - "timezone=Asia/Shanghai&hosts=localhost:6041" + "taosws://root:taosdata@localhost:6041/test_1755496227?" "timezone=Asia/Shanghai&hosts=localhost:6041" ) args, kwargs = dialect.create_connect_args(url) diff --git a/taos-ws-py/tests/test_tmq.py b/taos-ws-py/tests/test_tmq.py index a560c6bf..944f2901 100644 --- a/taos-ws-py/tests/test_tmq.py +++ b/taos-ws-py/tests/test_tmq.py @@ -109,55 +109,67 @@ def test_tmq_with_token(): rs = conn.query(f"create token token_1772607422 from user {utils.test_username()}") token = next(iter(rs))[0] - consumer = Consumer(conf={ - "td.connect.websocket.scheme": "ws", - "td.connect.ip": "localhost", - "td.connect.port": 6041, - "td.connect.user": "invalid_user", - "td.connect.pass": "invalid_pass", - "td.connect.bearer_token": token, - "group.id": "1001", - "client.id": "1001", - }) + consumer = Consumer( + conf={ + "td.connect.websocket.scheme": "ws", + "td.connect.ip": "localhost", + "td.connect.port": 6041, + "td.connect.user": "invalid_user", + "td.connect.pass": "invalid_pass", + "td.connect.bearer_token": token, + "group.id": "1001", + "client.id": "1001", + } + ) consumer.subscribe(["topic_1772607422"]) consumer.unsubscribe() - consumer2 = Consumer(conf={ - "td.connect.websocket.scheme": "ws", - "td.connect.ip": "localhost", - "td.connect.port": 6041, - "td.connect.user": "invalid_user", - "td.connect.pass": "invalid_pass", - "bearer_token": token, - "group.id": "1001", - "client.id": "1001", - }) + consumer2 = Consumer( + conf={ + "td.connect.websocket.scheme": "ws", + "td.connect.ip": "localhost", + "td.connect.port": 6041, + "td.connect.user": "invalid_user", + "td.connect.pass": "invalid_pass", + "bearer_token": token, + "group.id": "1001", + "client.id": "1001", + } + ) consumer2.subscribe(["topic_1772607422"]) consumer2.unsubscribe() - consumer3 = Consumer(conf={ - "td.connect.websocket.scheme": "ws", - "td.connect.ip": "localhost", - "td.connect.port": 6041, - "td.connect.user": "invalid_user", - "td.connect.pass": "invalid_pass", - "td.connect.bearer_token": token, - "bearer_token": "invalid_token", - "group.id": "1001", - "client.id": "1001", - }) + consumer3 = Consumer( + conf={ + "td.connect.websocket.scheme": "ws", + "td.connect.ip": "localhost", + "td.connect.port": 6041, + "td.connect.user": "invalid_user", + "td.connect.pass": "invalid_pass", + "td.connect.bearer_token": token, + "bearer_token": "invalid_token", + "group.id": "1001", + "client.id": "1001", + } + ) consumer3.subscribe(["topic_1772607422"]) consumer3.unsubscribe() - consumer4 = Consumer(dsn = f"ws://invalid_user:invalid_pass@localhost:6041?group.id=1001&client.id=1001&td.connect.bearer_token={token}") + consumer4 = Consumer( + dsn=f"ws://invalid_user:invalid_pass@localhost:6041?group.id=1001&client.id=1001&td.connect.bearer_token={token}" + ) consumer4.subscribe(["topic_1772607422"]) consumer4.unsubscribe() - consumer5 = Consumer(dsn = f"ws://invalid_user:invalid_pass@localhost:6041?group.id=1001&client.id=1001&bearer_token={token}") + consumer5 = Consumer( + dsn=f"ws://invalid_user:invalid_pass@localhost:6041?group.id=1001&client.id=1001&bearer_token={token}" + ) consumer5.subscribe(["topic_1772607422"]) consumer5.unsubscribe() - consumer6 = Consumer(dsn = f"ws://invalid_user:invalid_pass@localhost:6041?group.id=1001&client.id=1001&td.connect.bearer_token={token}&bearer_token=invalid_token") + consumer6 = Consumer( + dsn=f"ws://invalid_user:invalid_pass@localhost:6041?group.id=1001&client.id=1001&td.connect.bearer_token={token}&bearer_token=invalid_token" + ) consumer6.subscribe(["topic_1772607422"]) consumer6.unsubscribe() finally: @@ -177,37 +189,45 @@ def test_tmq_with_invalid_token(): conn.execute("create database test_1772611547") conn.execute("create topic topic_1772611547 as database test_1772611547") - consumer = Consumer(conf={ - "td.connect.websocket.scheme": "ws", - "td.connect.ip": "localhost", - "td.connect.port": 6041, - "td.connect.user": "invalid_user", - "td.connect.pass": "invalid_pass", - "td.connect.bearer_token": "invalid_token", - "group.id": "1001", - "client.id": "1001", - }) + consumer = Consumer( + conf={ + "td.connect.websocket.scheme": "ws", + "td.connect.ip": "localhost", + "td.connect.port": 6041, + "td.connect.user": "invalid_user", + "td.connect.pass": "invalid_pass", + "td.connect.bearer_token": "invalid_token", + "group.id": "1001", + "client.id": "1001", + } + ) with pytest.raises(Exception, match=r"init tscObj with token failed"): consumer.subscribe(["topic_1772611547"]) - consumer2 = Consumer(conf={ - "td.connect.websocket.scheme": "ws", - "td.connect.ip": "localhost", - "td.connect.port": 6041, - "td.connect.user": "invalid_user", - "td.connect.pass": "invalid_pass", - "bearer_token": "invalid_token", - "group.id": "1001", - "client.id": "1001", - }) + consumer2 = Consumer( + conf={ + "td.connect.websocket.scheme": "ws", + "td.connect.ip": "localhost", + "td.connect.port": 6041, + "td.connect.user": "invalid_user", + "td.connect.pass": "invalid_pass", + "bearer_token": "invalid_token", + "group.id": "1001", + "client.id": "1001", + } + ) with pytest.raises(Exception, match=r"init tscObj with token failed"): consumer2.subscribe(["topic_1772611547"]) - consumer3 = Consumer(dsn = f"ws://invalid_user:invalid_pass@localhost:6041?group.id=1001&client.id=1001&td.connect.bearer_token=invalid_token") + consumer3 = Consumer( + dsn=f"ws://invalid_user:invalid_pass@localhost:6041?group.id=1001&client.id=1001&td.connect.bearer_token=invalid_token" + ) with pytest.raises(Exception, match=r"init tscObj with token failed"): consumer3.subscribe(["topic_1772611547"]) - consumer4 = Consumer(dsn = f"ws://invalid_user:invalid_pass@localhost:6041?group.id=1001&client.id=1001&bearer_token=invalid_token") + consumer4 = Consumer( + dsn=f"ws://invalid_user:invalid_pass@localhost:6041?group.id=1001&client.id=1001&bearer_token=invalid_token" + ) with pytest.raises(Exception, match=r"init tscObj with token failed"): consumer4.subscribe(["topic_1772611547"]) finally: diff --git a/taos/_version.py b/taos/_version.py index bf560199..e71de367 100644 --- a/taos/_version.py +++ b/taos/_version.py @@ -1 +1 @@ -__version__ = '2.8.9' +__version__ = "2.8.9" diff --git a/taos/sqlalchemy.py b/taos/sqlalchemy.py index 3b6edfce..b4cf3d55 100644 --- a/taos/sqlalchemy.py +++ b/taos/sqlalchemy.py @@ -574,7 +574,6 @@ def get_view_names(self, connection, schema=None, **kw): sql = f"show `{schema}`.views" # Execute query try: - cursor = connection.execute(text(sql)) return [row[0] for row in cursor.fetchall()] except: @@ -606,4 +605,3 @@ def dbapi(cls): @classmethod def import_dbapi(cls): return taos - diff --git a/taosrest/sqlalchemy.py b/taosrest/sqlalchemy.py index b8add2fa..111167c4 100644 --- a/taosrest/sqlalchemy.py +++ b/taosrest/sqlalchemy.py @@ -12,11 +12,13 @@ import taos.sqlalchemy import taosrest + # rest connect class AlchemyRestConnection: threadsafety = 0 paramstyle = "pyformat" Error = taosrest.Error + # connect def connect(self, **kwargs): host = kwargs["host"] if "host" in kwargs else None diff --git a/tests/test_tmq.py b/tests/test_tmq.py index fe7502fd..c5344fd2 100644 --- a/tests/test_tmq.py +++ b/tests/test_tmq.py @@ -491,18 +491,22 @@ def test_tmq_with_token(): conn = taos.connect() try: conn.select_db("tmq_test") - conn.execute("insert into t1 using stb1 tags(true, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, '1', '1') values (now-4s, true, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, '1', '1', 'binary value_1', 'POINT (3.0 5.0)', '9876.123456', '123456789012.0987654321', 'axxxxxxxxxxxxxxxxxxxa')") + conn.execute( + "insert into t1 using stb1 tags(true, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, '1', '1') values (now-4s, true, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, '1', '1', 'binary value_1', 'POINT (3.0 5.0)', '9876.123456', '123456789012.0987654321', 'axxxxxxxxxxxxxxxxxxxa')" + ) conn.execute("drop token if exists token_1772680884") rs = conn.query(f"create token token_1772680884 from user {utils.test_username()}") token = next(iter(rs))[0] - consumer = Consumer({ - "group.id": "token_test_group", - "td.connect.user": "invalid_user", - "td.connect.pass": "invalid_pass", - "td.connect.bearer_token": token, - "auto.offset.reset": "earliest", - }) + consumer = Consumer( + { + "group.id": "token_test_group", + "td.connect.user": "invalid_user", + "td.connect.pass": "invalid_pass", + "td.connect.bearer_token": token, + "auto.offset.reset": "earliest", + } + ) consumer.subscribe(["topic1"]) data = consumer.poll(1) From ffc547669b7df218f7bde970b345b8900b57ba8c Mon Sep 17 00:00:00 2001 From: qevolg <2227465945@qq.com> Date: Fri, 27 Mar 2026 17:51:09 +0800 Subject: [PATCH 05/24] feat: update dependencies to include SQLAlchemy in workflow tests --- .github/workflows/taos-ws-py-compatibility.yml | 2 +- .github/workflows/taos-ws-py-enterprise.yml | 2 +- .github/workflows/taos-ws-py.yml | 2 +- taos-ws-py/tests/test_sqlalchemy.py | 1 - 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/taos-ws-py-compatibility.yml b/.github/workflows/taos-ws-py-compatibility.yml index 94084a28..ca1270a9 100644 --- a/.github/workflows/taos-ws-py-compatibility.yml +++ b/.github/workflows/taos-ws-py-compatibility.yml @@ -111,5 +111,5 @@ jobs: WS_CLOUD_TOKEN: ${{ secrets.WS_CLOUD_TOKEN }} TEST_TD_3360: "true" run: | - pip3 install pytest toml + pip3 install pytest toml sqlalchemy pytest ./taos-ws-py/tests/ diff --git a/.github/workflows/taos-ws-py-enterprise.yml b/.github/workflows/taos-ws-py-enterprise.yml index f9c9be9b..e94a9917 100644 --- a/.github/workflows/taos-ws-py-enterprise.yml +++ b/.github/workflows/taos-ws-py-enterprise.yml @@ -117,5 +117,5 @@ jobs: WS_CLOUD_TOKEN: ${{ secrets.WS_CLOUD_TOKEN }} TEST_TD_ENTERPRISE: "true" run: | - pip3 install pytest toml + pip3 install pytest toml sqlalchemy pytest ./taos-ws-py/tests/ diff --git a/.github/workflows/taos-ws-py.yml b/.github/workflows/taos-ws-py.yml index ce4dc800..8bf99743 100644 --- a/.github/workflows/taos-ws-py.yml +++ b/.github/workflows/taos-ws-py.yml @@ -225,7 +225,7 @@ jobs: WS_CLOUD_TOKEN: ${{ secrets.WS_CLOUD_TOKEN }} run: | curl -L -u "$TDENGINE_TEST_USERNAME:$TDENGINE_TEST_PASSWORD" -d "show databases" localhost:6041/rest/sql - pip3 install pytest toml + pip3 install pytest toml sqlalchemy pip3 install ./ pytest ./taos-ws-py/tests/ diff --git a/taos-ws-py/tests/test_sqlalchemy.py b/taos-ws-py/tests/test_sqlalchemy.py index 5a27399a..c25e3587 100644 --- a/taos-ws-py/tests/test_sqlalchemy.py +++ b/taos-ws-py/tests/test_sqlalchemy.py @@ -8,7 +8,6 @@ from sqlalchemy.dialects import registry -pytest.importorskip("sqlalchemy") pytestmark = pytest.mark.skipif( "TDENGINE_URL" not in os.environ, reason="Please set environment variable TDENGINE_URL" ) From e4646e39f9f3da446f1acfe0a92375296a6f0bc8 Mon Sep 17 00:00:00 2001 From: qevolg <2227465945@qq.com> Date: Fri, 27 Mar 2026 18:07:29 +0800 Subject: [PATCH 06/24] refactor: enhance native module loading and improve error handling in SQLAlchemy dialect --- taos-ws-py/taosws/__init__.py | 46 ++++++++----- taos-ws-py/taosws/sqlalchemy.py | 10 ++- taos-ws-py/tests/test_sqlalchemy.py | 68 +++++++++---------- taos-ws-py/tests/test_sqlalchemy_migration.py | 4 ++ taos/sqlalchemy.py | 3 + 5 files changed, 78 insertions(+), 53 deletions(-) diff --git a/taos-ws-py/taosws/__init__.py b/taos-ws-py/taosws/__init__.py index d54e0ff6..b50cdf01 100644 --- a/taos-ws-py/taosws/__init__.py +++ b/taos-ws-py/taosws/__init__.py @@ -1,19 +1,33 @@ -import sys -from pathlib import Path - - -_current_package_dir = Path(__file__).resolve().parent -for search_path in list(sys.path): - candidate = Path(search_path) / "taosws" - if candidate.is_dir() and candidate != _current_package_dir and str(candidate) not in __path__: - __path__.append(str(candidate)) - -try: - from . import _taosws as _native - from ._taosws import * -except ImportError: - from . import taosws as _native - from .taosws import * +import importlib +import importlib.util +import pkgutil + + +__path__ = pkgutil.extend_path(__path__, __name__) + + +def _load_native_module(): + for module_name in ("_taosws", "taosws"): + full_name = f"{__name__}.{module_name}" + if importlib.util.find_spec(full_name) is None: + continue + return importlib.import_module(full_name) + + raise ImportError( + "Failed to import native extension 'taosws._taosws' or 'taosws.taosws'. " + "Ensure taos-ws-py is built and installed correctly." + ) + + +_native = _load_native_module() + +if hasattr(_native, "__all__"): + for name in _native.__all__: + globals()[name] = getattr(_native, name) +else: + for name in dir(_native): + if not name.startswith("_"): + globals()[name] = getattr(_native, name) __doc__ = _native.__doc__ if hasattr(_native, "__all__"): diff --git a/taos-ws-py/taosws/sqlalchemy.py b/taos-ws-py/taosws/sqlalchemy.py index b0f38b74..3cf8a0a1 100644 --- a/taos-ws-py/taosws/sqlalchemy.py +++ b/taos-ws-py/taosws/sqlalchemy.py @@ -466,12 +466,14 @@ def get_columns(self, connection, table_name, schema=None, **kw): column["type"] = self._resolve_type(row[1]) columns.append(column) return columns - except: + except Exception: return [] @reflection.cache def get_pk_constraint(self, connection, table_name, schema=None, **kw): columns = self.get_columns(connection, table_name, schema) + if not columns: + return {"constrained_columns": [], "name": None} return {"constrained_columns": [columns[0]["name"]], "name": None} @reflection.cache @@ -482,9 +484,13 @@ def get_foreign_keys(self, connection, table_name, schema=None, **kw): # Get indexes information @reflection.cache def get_indexes(self, connection, table_name, schema=None, **kw): + if schema is None: + return [] + sql = ( "SELECT * FROM information_schema.INS_INDEXES " f"WHERE db_name = '{schema}'" + " " f"AND table_name = '{table_name}'" ) try: @@ -495,7 +501,7 @@ def get_indexes(self, connection, table_name, schema=None, **kw): index = {"name": row[0], "column_names": [row[5]], "type": "index", "unique": False} indexes.append(index) return indexes - except: + except Exception: return [] # Get database names diff --git a/taos-ws-py/tests/test_sqlalchemy.py b/taos-ws-py/tests/test_sqlalchemy.py index c25e3587..294c1172 100644 --- a/taos-ws-py/tests/test_sqlalchemy.py +++ b/taos-ws-py/tests/test_sqlalchemy.py @@ -1,20 +1,29 @@ import os +from urllib.parse import urlparse import pytest import taosws import utils + +pytest.importorskip("sqlalchemy") + from sqlalchemy import create_engine from sqlalchemy import inspect from sqlalchemy.dialects import registry -pytestmark = pytest.mark.skipif( - "TDENGINE_URL" not in os.environ, reason="Please set environment variable TDENGINE_URL" -) +TDENGINE_URL = os.getenv("TDENGINE_URL") +pytestmark = pytest.mark.skipif(TDENGINE_URL is None, reason="Please set environment variable TDENGINE_URL") registry.register("taosws", "taosws.sqlalchemy", "TaosWsDialect") -HOST = "localhost" -PORT = 6041 + +def resolve_tdengine_host_port(url): + normalized = url if "://" in url else f"ws://{url}" + parsed = urlparse(normalized) + return parsed.hostname or "localhost", parsed.port or 6041 + + +HOST, PORT = resolve_tdengine_host_port(TDENGINE_URL) if TDENGINE_URL else ("localhost", 6041) def insert_data(conn=None): @@ -31,24 +40,13 @@ def insert_data(conn=None): c.close() -def check_list_equal(list1, list2, tips): - if list1 != list2: - raise BaseException(f"{tips} failed. list1={list1} list2={list2}") - - -def check_result_equal(result1, result2, tips): - if result1 != result2: - raise BaseException(f"{tips} failed. result1={result1} result2={result2}") - - def check_basic(conn, inspection, sub_tables=None): tables = sub_tables or ["meters", "ntb"] databases = inspection.get_schema_names() - if "test" not in databases: - raise BaseException(f"test not in {databases}") + assert "test" in databases, f"test not in {databases}" - check_list_equal(inspection.get_table_names("test"), tables, "check get_table_names()") + assert inspection.get_table_names("test") == tables, "check get_table_names() failed" expected_columns = [ {"name": "ts", "type": inspection.dialect._resolve_type("TIMESTAMP")}, @@ -59,13 +57,13 @@ def check_basic(conn, inspection, sub_tables=None): columns = inspection.get_columns("meters", "test") for index, column in enumerate(columns): expected = expected_columns[index] - if column["name"] != expected["name"]: - raise BaseException(f"column name mismatch: {column['name']} != {expected['name']}") - if type(column["type"]) != expected["type"]: - raise BaseException(f"column type mismatch: {type(column['type'])} != {expected['type']}") + assert column["name"] == expected["name"], f"column name mismatch: {column['name']} != {expected['name']}" + assert ( + type(column["type"]) == expected["type"] + ), f"column type mismatch: {type(column['type'])} != {expected['type']}" - check_result_equal(inspection.has_table("meters", "test"), True, "check has_table()") - check_result_equal(inspection.dialect.has_schema(conn, "test"), True, "check has_schema()") + assert inspection.has_table("meters", "test") is True, "check has_table() failed" + assert inspection.dialect.has_schema(conn, "test") is True, "check has_schema() failed" conn.close() @@ -89,17 +87,17 @@ def test_read_from_sqlalchemy_taosws_failover(): try: urls = [ "taosws://", - "taosws://localhost", - f"taosws://localhost:{PORT}", - f"taosws://localhost:{PORT}/{db_name}", - f"taosws://root@localhost:{PORT}/{db_name}", - f"taosws://root:@localhost:{PORT}/{db_name}", - f"taosws://{utils.test_username()}:{utils.test_password()}@localhost:{PORT}/{db_name}", - f"taosws://{utils.test_username()}:{utils.test_password()}@localhost:{PORT}/{db_name}?hosts=", - f"taosws://{utils.test_username()}:{utils.test_password()}@/{db_name}?hosts=localhost:{PORT}", - f"taosws://{utils.test_username()}:{utils.test_password()}@localhost:{PORT}/{db_name}?hosts=localhost:{PORT}", - f"taosws://{utils.test_username()}:{utils.test_password()}@localhost:{PORT}/{db_name}?hosts=localhost:{PORT},127.0.0.1:{PORT}", - f"taosws://{utils.test_username()}:{utils.test_password()}@localhost:{PORT}/{db_name}?hosts=localhost:{PORT},127.0.0.1:{PORT}&timezone=Asia/Shanghai", + f"taosws://{HOST}", + f"taosws://{HOST}:{PORT}", + f"taosws://{HOST}:{PORT}/{db_name}", + f"taosws://root@{HOST}:{PORT}/{db_name}", + f"taosws://root:@{HOST}:{PORT}/{db_name}", + f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}/{db_name}", + f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}/{db_name}?hosts=", + f"taosws://{utils.test_username()}:{utils.test_password()}@/{db_name}?hosts={HOST}:{PORT}", + f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}/{db_name}?hosts={HOST}:{PORT}", + f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}/{db_name}?hosts={HOST}:{PORT},127.0.0.1:{PORT}", + f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}/{db_name}?hosts={HOST}:{PORT},127.0.0.1:{PORT}&timezone=Asia/Shanghai", ] for url in urls: diff --git a/taos-ws-py/tests/test_sqlalchemy_migration.py b/taos-ws-py/tests/test_sqlalchemy_migration.py index 4cecbeae..f07a6268 100644 --- a/taos-ws-py/tests/test_sqlalchemy_migration.py +++ b/taos-ws-py/tests/test_sqlalchemy_migration.py @@ -2,6 +2,10 @@ import ast from pathlib import Path +import pytest + +pytest.importorskip("sqlalchemy") + from sqlalchemy.engine.url import make_url from sqlalchemy import types as sqltypes diff --git a/taos/sqlalchemy.py b/taos/sqlalchemy.py index b4cf3d55..ce489980 100644 --- a/taos/sqlalchemy.py +++ b/taos/sqlalchemy.py @@ -508,6 +508,8 @@ def get_columns(self, connection, table_name, schema=None, **kw): @reflection.cache def get_pk_constraint(self, connection, table_name, schema=None, **kw): columns = self.get_columns(connection, table_name, schema) + if not columns: + return {"constrained_columns": [], "name": None} return {"constrained_columns": [columns[0]["name"]], "name": None} @reflection.cache @@ -521,6 +523,7 @@ def get_indexes(self, connection, table_name, schema=None, **kw): sql = ( "SELECT * FROM information_schema.INS_INDEXES " f"WHERE db_name = '{schema}'" + " " f"AND table_name = '{table_name}'" ) try: From 5ced0961bea42a66e5ba7409a107f9f10ec78b2c Mon Sep 17 00:00:00 2001 From: qevolg <2227465945@qq.com> Date: Fri, 27 Mar 2026 21:46:32 +0800 Subject: [PATCH 07/24] fix: correct connection string formatting and enhance query parameter encoding in SQLAlchemy dialect tests --- examples/pandas-read-sql.py | 2 +- taos-ws-py/Cargo.lock | 16 +++--- taos-ws-py/taosws/sqlalchemy.py | 55 +++++-------------- taos-ws-py/tests/test_sqlalchemy_migration.py | 26 ++++++++- 4 files changed, 46 insertions(+), 53 deletions(-) diff --git a/examples/pandas-read-sql.py b/examples/pandas-read-sql.py index e0f95788..5e8c63db 100644 --- a/examples/pandas-read-sql.py +++ b/examples/pandas-read-sql.py @@ -4,7 +4,7 @@ protocol_and_port = {"taos": 6030, "taosrest": 6041, "taosws": 6041} for protocol, port in protocol_and_port.items(): - engine = create_engine(f"{protocol}://root:taosdata@localhost") + engine = create_engine(f"{protocol}://root:taosdata@localhost:{port}") conn = engine.connect() res = pandas.read_sql(text("show databases"), conn) conn.close() diff --git a/taos-ws-py/Cargo.lock b/taos-ws-py/Cargo.lock index 7b4c592c..5708d58b 100644 --- a/taos-ws-py/Cargo.lock +++ b/taos-ws-py/Cargo.lock @@ -69,9 +69,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-lc-rs" -version = "1.16.1" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" dependencies = [ "aws-lc-sys", "zeroize", @@ -79,9 +79,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.38.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" dependencies = [ "cc", "cmake", @@ -1881,9 +1881,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "aws-lc-rs", "once_cell", @@ -1927,9 +1927,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "aws-lc-rs", "ring", diff --git a/taos-ws-py/taosws/sqlalchemy.py b/taos-ws-py/taosws/sqlalchemy.py index 3cf8a0a1..59d8dd8a 100644 --- a/taos-ws-py/taosws/sqlalchemy.py +++ b/taos-ws-py/taosws/sqlalchemy.py @@ -2,7 +2,7 @@ from sqlalchemy import text from sqlalchemy import types as sqltypes from sqlalchemy.engine import default, reflection -from sqlalchemy.sql import compiler +from urllib.parse import quote, urlencode TYPES_MAP = { "BOOL": sqltypes.Boolean, @@ -351,39 +351,7 @@ "force_window_close", } -# backup generator function -""" -generator from TDengine/source/libs/parse/src/parTokenizer.c -> keywordTable - -import sys -def readKeyWord(filename): - keys = "" - print(f"read file {filename}\n") - with open(filename) as file: - for line in file.readlines(): - pos1 = line.find('"') - if pos1 == -1 : - print(f"NO FOUND FIRST QUOTA: {line}\n") - continue - pos2 = line.find('"', pos1 + 1) - if pos2 == -1 : - print(f"NO FOUND SECOND QUOTA: {line}\n") - continue - word = line[pos1:pos2+1] - if keys == "": - keys = "RESERVED_WORDS_TDENGINE = {\n " + word.lower() - else: - keys += ",\n " + word.lower() - - # end - keys += "\n}" - print(f"\n\n{keys}\n") - - -if __name__ == "__main__": - readKeyWord("./keyword.txt") - -""" +# NOTE: reserved words are synced from TDengine parser keyword table when needed. # @@ -573,16 +541,19 @@ def import_dbapi(cls): @classmethod def create_connect_args(cls, url): - if url.username and url.password: - userpass = f"{url.username}:{url.password}" - elif url.username: - userpass = f"{url.username}" - elif url.password: - userpass = f":{url.password}" + username = url.username + password = url.password + + if username is not None and password is not None: + userpass = f"{username}:{password}" + elif username is not None: + userpass = f"{username}" + elif password is not None: + userpass = f":{password}" else: userpass = "" - at = "@" if userpass else "" + at = "@" if (username is not None or password is not None) else "" hosts = url.query.get("hosts") if hosts: @@ -598,7 +569,7 @@ def create_connect_args(cls, url): addr = "" query_params = [(key, value) for key, value in url.query.items() if key != "hosts"] - params = "&".join(f"{key}={value}" for key, value in query_params) + params = urlencode(query_params, doseq=True, quote_via=quote) dsn = f"{url.drivername}://{userpass}{at}{addr}" if url.database: diff --git a/taos-ws-py/tests/test_sqlalchemy_migration.py b/taos-ws-py/tests/test_sqlalchemy_migration.py index f07a6268..dd8b0ca0 100644 --- a/taos-ws-py/tests/test_sqlalchemy_migration.py +++ b/taos-ws-py/tests/test_sqlalchemy_migration.py @@ -35,7 +35,7 @@ def test_create_connect_args_prefers_hosts_and_keeps_other_query_params(): args, kwargs = dialect.create_connect_args(url) - assert args == ["taosws://root:taosdata@localhost:6041,127.0.0.1:6041/test_1755496227?timezone=Asia/Shanghai"] + assert args == ["taosws://root:taosdata@localhost:6041,127.0.0.1:6041/test_1755496227?timezone=Asia%2FShanghai"] assert kwargs == {} @@ -48,7 +48,29 @@ def test_create_connect_args_no_trailing_ampersand_when_hosts_is_last_param(): args, kwargs = dialect.create_connect_args(url) - assert args == ["taosws://root:taosdata@localhost:6041/test_1755496227?timezone=Asia/Shanghai"] + assert args == ["taosws://root:taosdata@localhost:6041/test_1755496227?timezone=Asia%2FShanghai"] + assert kwargs == {} + + +def test_create_connect_args_encodes_query_values_safely(): + module = importlib.import_module("taosws.sqlalchemy") + dialect = module.TaosWsDialect() + url = make_url("taosws://root:taosdata@localhost:6041/test_1755496227?note=cn north") + + args, kwargs = dialect.create_connect_args(url) + + assert args == ["taosws://root:taosdata@localhost:6041/test_1755496227?note=cn%20north"] + assert kwargs == {} + + +def test_create_connect_args_preserves_explicit_empty_password(): + module = importlib.import_module("taosws.sqlalchemy") + dialect = module.TaosWsDialect() + url = make_url("taosws://root:@localhost:6041/test_1755496227") + + args, kwargs = dialect.create_connect_args(url) + + assert args == ["taosws://root:@localhost:6041/test_1755496227"] assert kwargs == {} From 04edc29fefbf1c82fce8a61cacbb302fa90e5e11 Mon Sep 17 00:00:00 2001 From: qevolg <2227465945@qq.com> Date: Fri, 27 Mar 2026 22:15:01 +0800 Subject: [PATCH 08/24] Update dependency versions in pyproject.toml and clean up test script - Adjusted requests and black dependencies to support specific Python versions. - Updated pandas and sqlalchemy dependencies to ensure compatibility. - Removed unnecessary newline in test_taospy.sh for cleaner script execution. --- poetry.lock | 858 +++++++++++++++++++++++++++++++------------------ pyproject.toml | 15 +- test_taospy.sh | 1 - 3 files changed, 562 insertions(+), 312 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3c0ae6ca..7701c26b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -50,137 +50,205 @@ d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "black" +version = "26.3.1" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.10" +files = [ + {file = "black-26.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:86a8b5035fce64f5dcd1b794cf8ec4d31fe458cf6ce3986a30deb434df82a1d2"}, + {file = "black-26.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5602bdb96d52d2d0672f24f6ffe5218795736dd34807fd0fd55ccd6bf206168b"}, + {file = "black-26.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c54a4a82e291a1fee5137371ab488866b7c86a3305af4026bdd4dc78642e1ac"}, + {file = "black-26.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:6e131579c243c98f35bce64a7e08e87fb2d610544754675d4a0e73a070a5aa3a"}, + {file = "black-26.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:5ed0ca58586c8d9a487352a96b15272b7fa55d139fc8496b519e78023a8dab0a"}, + {file = "black-26.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff"}, + {file = "black-26.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c"}, + {file = "black-26.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5"}, + {file = "black-26.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e9d0d86df21f2e1677cc4bd090cd0e446278bcbbe49bf3659c308c3e402843e"}, + {file = "black-26.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9a5e9f45e5d5e1c5b5c29b3bd4265dcc90e8b92cf4534520896ed77f791f4da5"}, + {file = "black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1"}, + {file = "black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f"}, + {file = "black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7"}, + {file = "black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983"}, + {file = "black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb"}, + {file = "black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54"}, + {file = "black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f"}, + {file = "black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56"}, + {file = "black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839"}, + {file = "black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2"}, + {file = "black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78"}, + {file = "black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568"}, + {file = "black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f"}, + {file = "black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c"}, + {file = "black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1"}, + {file = "black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b"}, + {file = "black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=1.0.0" +platformdirs = ">=2" +pytokens = ">=0.4.0,<0.5.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)", "winloop (>=0.5.0)"] + [[package]] name = "certifi" -version = "2025.10.5" +version = "2026.2.25" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" files = [ - {file = "certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"}, - {file = "certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"}, + {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, + {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, ] [[package]] name = "charset-normalizer" -version = "3.4.4" +version = "3.4.6" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" files = [ - {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, - {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, - {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-win32.whl", hash = "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:659a1e1b500fac8f2779dd9e1570464e012f43e580371470b45277a27baa7532"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f61aa92e4aad0be58eb6eb4e0c21acf32cf8065f4b2cae5665da756c4ceef982"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f50498891691e0864dc3da965f340fada0771f6142a378083dc4608f4ea513e2"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf625105bb9eef28a56a943fec8c8a98aeb80e7d7db99bd3c388137e6eb2d237"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2bd9d128ef93637a5d7a6af25363cf5dec3fa21cf80e68055aad627f280e8afa"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:d08ec48f0a1c48d75d0356cea971921848fb620fdeba805b28f937e90691209f"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ed80ff870ca6de33f4d953fda4d55654b9a2b340ff39ab32fa3adbcd718f264"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f98059e4fcd3e3e4e2d632b7cf81c2faae96c43c60b569e9c621468082f1d104"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:ab30e5e3e706e3063bc6de96b118688cb10396b70bb9864a430f67df98c61ecc"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:d5f5d1e9def3405f60e3ca8232d56f35c98fb7bf581efcc60051ebf53cb8b611"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:461598cd852bfa5a61b09cae2b1c02e2efcd166ee5516e243d540ac24bfa68a7"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:71be7e0e01753a89cf024abf7ecb6bca2c81738ead80d43004d9b5e3f1244e64"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df01808ee470038c3f8dc4f48620df7225c49c2d6639e38f96e6d6ac6e6f7b0e"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-win32.whl", hash = "sha256:69dd852c2f0ad631b8b60cfbe25a28c0058a894de5abb566619c205ce0550eae"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-win_amd64.whl", hash = "sha256:517ad0e93394ac532745129ceabdf2696b609ec9f87863d337140317ebce1c14"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31215157227939b4fb3d740cd23fe27be0439afef67b785a1eb78a3ae69cba9e"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecbbd45615a6885fe3240eb9db73b9e62518b611850fdf8ab08bd56de7ad2b17"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c45a03a4c69820a399f1dda9e1d8fbf3562eda46e7720458180302021b08f778"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e8aeb10fcbe92767f0fa69ad5a72deca50d0dca07fbde97848997d778a50c9fe"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54fae94be3d75f3e573c9a1b5402dc593de19377013c9a0e4285e3d402dd3a2a"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:2f7fdd9b6e6c529d6a2501a2d36b240109e78a8ceaef5687cfcfa2bbe671d297"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1d02209e06550bdaef34af58e041ad71b88e624f5d825519da3a3308e22687"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8bc5f0687d796c05b1e28ab0d38a50e6309906ee09375dd3aff6a9c09dd6e8f4"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ee4ec14bc1680d6b0afab9aea2ef27e26d2024f18b24a2d7155a52b60da7e833"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d1a2ee9c1499fc8f86f4521f27a973c914b211ffa87322f4ee33bb35392da2c5"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:48696db7f18afb80a068821504296eb0787d9ce239b91ca15059d1d3eaacf13b"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4f41da960b196ea355357285ad1316a00099f22d0929fe168343b99b254729c9"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:802168e03fba8bbc5ce0d866d589e4b1ca751d06edee69f7f3a19c5a9fe6b597"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-win32.whl", hash = "sha256:8761ac29b6c81574724322a554605608a9960769ea83d2c73e396f3df896ad54"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-win_amd64.whl", hash = "sha256:1cf0a70018692f85172348fe06d3a4b63f94ecb055e13a00c644d368eb82e5b8"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-win_arm64.whl", hash = "sha256:3516bbb8d42169de9e61b8520cbeeeb716f12f4ecfe3fd30a9919aa16c806ca8"}, + {file = "charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69"}, + {file = "charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6"}, ] [[package]] @@ -198,6 +266,20 @@ files = [ colorama = {version = "*", markers = "platform_system == \"Windows\""} importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +[[package]] +name = "click" +version = "8.3.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +files = [ + {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, + {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" @@ -417,6 +499,20 @@ files = [ [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +[[package]] +name = "idna" +version = "3.11" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "importlib-metadata" version = "6.7.0" @@ -550,137 +646,137 @@ files = [ [[package]] name = "numpy" -version = "2.0.1" +version = "2.0.2" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" files = [ - {file = "numpy-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fbb536eac80e27a2793ffd787895242b7f18ef792563d742c2d673bfcb75134"}, - {file = "numpy-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:69ff563d43c69b1baba77af455dd0a839df8d25e8590e79c90fcbe1499ebde42"}, - {file = "numpy-2.0.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:1b902ce0e0a5bb7704556a217c4f63a7974f8f43e090aff03fcf262e0b135e02"}, - {file = "numpy-2.0.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:f1659887361a7151f89e79b276ed8dff3d75877df906328f14d8bb40bb4f5101"}, - {file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4658c398d65d1b25e1760de3157011a80375da861709abd7cef3bad65d6543f9"}, - {file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4127d4303b9ac9f94ca0441138acead39928938660ca58329fe156f84b9f3015"}, - {file = "numpy-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e5eeca8067ad04bc8a2a8731183d51d7cbaac66d86085d5f4766ee6bf19c7f87"}, - {file = "numpy-2.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9adbd9bb520c866e1bfd7e10e1880a1f7749f1f6e5017686a5fbb9b72cf69f82"}, - {file = "numpy-2.0.1-cp310-cp310-win32.whl", hash = "sha256:7b9853803278db3bdcc6cd5beca37815b133e9e77ff3d4733c247414e78eb8d1"}, - {file = "numpy-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:81b0893a39bc5b865b8bf89e9ad7807e16717f19868e9d234bdaf9b1f1393868"}, - {file = "numpy-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75b4e316c5902d8163ef9d423b1c3f2f6252226d1aa5cd8a0a03a7d01ffc6268"}, - {file = "numpy-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6e4eeb6eb2fced786e32e6d8df9e755ce5be920d17f7ce00bc38fcde8ccdbf9e"}, - {file = "numpy-2.0.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1e01dcaab205fbece13c1410253a9eea1b1c9b61d237b6fa59bcc46e8e89343"}, - {file = "numpy-2.0.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a8fc2de81ad835d999113ddf87d1ea2b0f4704cbd947c948d2f5513deafe5a7b"}, - {file = "numpy-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a3d94942c331dd4e0e1147f7a8699a4aa47dffc11bf8a1523c12af8b2e91bbe"}, - {file = "numpy-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15eb4eca47d36ec3f78cde0a3a2ee24cf05ca7396ef808dda2c0ddad7c2bde67"}, - {file = "numpy-2.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b83e16a5511d1b1f8a88cbabb1a6f6a499f82c062a4251892d9ad5d609863fb7"}, - {file = "numpy-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f87fec1f9bc1efd23f4227becff04bd0e979e23ca50cc92ec88b38489db3b55"}, - {file = "numpy-2.0.1-cp311-cp311-win32.whl", hash = "sha256:36d3a9405fd7c511804dc56fc32974fa5533bdeb3cd1604d6b8ff1d292b819c4"}, - {file = "numpy-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:08458fbf403bff5e2b45f08eda195d4b0c9b35682311da5a5a0a0925b11b9bd8"}, - {file = "numpy-2.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6bf4e6f4a2a2e26655717a1983ef6324f2664d7011f6ef7482e8c0b3d51e82ac"}, - {file = "numpy-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6fddc5fe258d3328cd8e3d7d3e02234c5d70e01ebe377a6ab92adb14039cb4"}, - {file = "numpy-2.0.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5daab361be6ddeb299a918a7c0864fa8618af66019138263247af405018b04e1"}, - {file = "numpy-2.0.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:ea2326a4dca88e4a274ba3a4405eb6c6467d3ffbd8c7d38632502eaae3820587"}, - {file = "numpy-2.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529af13c5f4b7a932fb0e1911d3a75da204eff023ee5e0e79c1751564221a5c8"}, - {file = "numpy-2.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6790654cb13eab303d8402354fabd47472b24635700f631f041bd0b65e37298a"}, - {file = "numpy-2.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbab9fc9c391700e3e1287666dfd82d8666d10e69a6c4a09ab97574c0b7ee0a7"}, - {file = "numpy-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99d0d92a5e3613c33a5f01db206a33f8fdf3d71f2912b0de1739894668b7a93b"}, - {file = "numpy-2.0.1-cp312-cp312-win32.whl", hash = "sha256:173a00b9995f73b79eb0191129f2455f1e34c203f559dd118636858cc452a1bf"}, - {file = "numpy-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:bb2124fdc6e62baae159ebcfa368708867eb56806804d005860b6007388df171"}, - {file = "numpy-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bfc085b28d62ff4009364e7ca34b80a9a080cbd97c2c0630bb5f7f770dae9414"}, - {file = "numpy-2.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8fae4ebbf95a179c1156fab0b142b74e4ba4204c87bde8d3d8b6f9c34c5825ef"}, - {file = "numpy-2.0.1-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:72dc22e9ec8f6eaa206deb1b1355eb2e253899d7347f5e2fae5f0af613741d06"}, - {file = "numpy-2.0.1-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:ec87f5f8aca726117a1c9b7083e7656a9d0d606eec7299cc067bb83d26f16e0c"}, - {file = "numpy-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f682ea61a88479d9498bf2091fdcd722b090724b08b31d63e022adc063bad59"}, - {file = "numpy-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8efc84f01c1cd7e34b3fb310183e72fcdf55293ee736d679b6d35b35d80bba26"}, - {file = "numpy-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3fdabe3e2a52bc4eff8dc7a5044342f8bd9f11ef0934fcd3289a788c0eb10018"}, - {file = "numpy-2.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:24a0e1befbfa14615b49ba9659d3d8818a0f4d8a1c5822af8696706fbda7310c"}, - {file = "numpy-2.0.1-cp39-cp39-win32.whl", hash = "sha256:f9cf5ea551aec449206954b075db819f52adc1638d46a6738253a712d553c7b4"}, - {file = "numpy-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:e9e81fa9017eaa416c056e5d9e71be93d05e2c3c2ab308d23307a8bc4443c368"}, - {file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:61728fba1e464f789b11deb78a57805c70b2ed02343560456190d0501ba37b0f"}, - {file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:12f5d865d60fb9734e60a60f1d5afa6d962d8d4467c120a1c0cda6eb2964437d"}, - {file = "numpy-2.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eacf3291e263d5a67d8c1a581a8ebbcfd6447204ef58828caf69a5e3e8c75990"}, - {file = "numpy-2.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2c3a346ae20cfd80b6cfd3e60dc179963ef2ea58da5ec074fd3d9e7a1e7ba97f"}, - {file = "numpy-2.0.1.tar.gz", hash = "sha256:485b87235796410c3519a699cfe1faab097e509e90ebb05dcd098db2ae87e7b3"}, + {file = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"}, + {file = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"}, + {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66"}, + {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b"}, + {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd"}, + {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318"}, + {file = "numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8"}, + {file = "numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326"}, + {file = "numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97"}, + {file = "numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a"}, + {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669"}, + {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951"}, + {file = "numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9"}, + {file = "numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15"}, + {file = "numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4"}, + {file = "numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c"}, + {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692"}, + {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a"}, + {file = "numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c"}, + {file = "numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded"}, + {file = "numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5"}, + {file = "numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729"}, + {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1"}, + {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd"}, + {file = "numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d"}, + {file = "numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d"}, + {file = "numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa"}, + {file = "numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385"}, + {file = "numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78"}, ] [[package]] name = "numpy" -version = "2.4.1" +version = "2.4.3" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.11" files = [ - {file = "numpy-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5"}, - {file = "numpy-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:899d2c18024984814ac7e83f8f49d8e8180e2fbe1b2e252f2e7f1d06bea92425"}, - {file = "numpy-2.4.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:09aa8a87e45b55a1c2c205d42e2808849ece5c484b2aab11fecabec3841cafba"}, - {file = "numpy-2.4.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:edee228f76ee2dab4579fad6f51f6a305de09d444280109e0f75df247ff21501"}, - {file = "numpy-2.4.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a92f227dbcdc9e4c3e193add1a189a9909947d4f8504c576f4a732fd0b54240a"}, - {file = "numpy-2.4.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:538bf4ec353709c765ff75ae616c34d3c3dca1a68312727e8f2676ea644f8509"}, - {file = "numpy-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ac08c63cb7779b85e9d5318e6c3518b424bc1f364ac4cb2c6136f12e5ff2dccc"}, - {file = "numpy-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f9c360ecef085e5841c539a9a12b883dff005fbd7ce46722f5e9cef52634d82"}, - {file = "numpy-2.4.1-cp311-cp311-win32.whl", hash = "sha256:0f118ce6b972080ba0758c6087c3617b5ba243d806268623dc34216d69099ba0"}, - {file = "numpy-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:18e14c4d09d55eef39a6ab5b08406e84bc6869c1e34eef45564804f90b7e0574"}, - {file = "numpy-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:6461de5113088b399d655d45c3897fa188766415d0f568f175ab071c8873bd73"}, - {file = "numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2"}, - {file = "numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8"}, - {file = "numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a"}, - {file = "numpy-2.4.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0"}, - {file = "numpy-2.4.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c"}, - {file = "numpy-2.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02"}, - {file = "numpy-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162"}, - {file = "numpy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9"}, - {file = "numpy-2.4.1-cp312-cp312-win32.whl", hash = "sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f"}, - {file = "numpy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87"}, - {file = "numpy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8"}, - {file = "numpy-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b"}, - {file = "numpy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f"}, - {file = "numpy-2.4.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9"}, - {file = "numpy-2.4.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e"}, - {file = "numpy-2.4.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5"}, - {file = "numpy-2.4.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8"}, - {file = "numpy-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c"}, - {file = "numpy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2"}, - {file = "numpy-2.4.1-cp313-cp313-win32.whl", hash = "sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d"}, - {file = "numpy-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb"}, - {file = "numpy-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5"}, - {file = "numpy-2.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7"}, - {file = "numpy-2.4.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d"}, - {file = "numpy-2.4.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15"}, - {file = "numpy-2.4.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9"}, - {file = "numpy-2.4.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2"}, - {file = "numpy-2.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505"}, - {file = "numpy-2.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2"}, - {file = "numpy-2.4.1-cp313-cp313t-win32.whl", hash = "sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4"}, - {file = "numpy-2.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510"}, - {file = "numpy-2.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261"}, - {file = "numpy-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc"}, - {file = "numpy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3"}, - {file = "numpy-2.4.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220"}, - {file = "numpy-2.4.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee"}, - {file = "numpy-2.4.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556"}, - {file = "numpy-2.4.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844"}, - {file = "numpy-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3"}, - {file = "numpy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205"}, - {file = "numpy-2.4.1-cp314-cp314-win32.whl", hash = "sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745"}, - {file = "numpy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d"}, - {file = "numpy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df"}, - {file = "numpy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f"}, - {file = "numpy-2.4.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0"}, - {file = "numpy-2.4.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c"}, - {file = "numpy-2.4.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93"}, - {file = "numpy-2.4.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42"}, - {file = "numpy-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01"}, - {file = "numpy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b"}, - {file = "numpy-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a"}, - {file = "numpy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2"}, - {file = "numpy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295"}, - {file = "numpy-2.4.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ad35f20be147a204e28b6a0575fbf3540c5e5f802634d4258d55b1ff5facce1"}, - {file = "numpy-2.4.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8097529164c0f3e32bb89412a0905d9100bf434d9692d9fc275e18dcf53c9344"}, - {file = "numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ea66d2b41ca4a1630aae5507ee0a71647d3124d1741980138aa8f28f44dac36e"}, - {file = "numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d3f8f0df9f4b8be57b3bf74a1d087fec68f927a2fab68231fdb442bf2c12e426"}, - {file = "numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2023ef86243690c2791fd6353e5b4848eedaa88ca8a2d129f462049f6d484696"}, - {file = "numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8361ea4220d763e54cff2fbe7d8c93526b744f7cd9ddab47afeff7e14e8503be"}, - {file = "numpy-2.4.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4f1b68ff47680c2925f8063402a693ede215f0257f02596b1318ecdfb1d79e33"}, - {file = "numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690"}, + {file = "numpy-2.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33b3bf58ee84b172c067f56aeadc7ee9ab6de69c5e800ab5b10295d54c581adb"}, + {file = "numpy-2.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ba7b51e71c05aa1f9bc3641463cd82308eab40ce0d5c7e1fd4038cbf9938147"}, + {file = "numpy-2.4.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1988292870c7cb9d0ebb4cc96b4d447513a9644801de54606dc7aabf2b7d920"}, + {file = "numpy-2.4.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:23b46bb6d8ecb68b58c09944483c135ae5f0e9b8d8858ece5e4ead783771d2a9"}, + {file = "numpy-2.4.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a016db5c5dba78fa8fe9f5d80d6708f9c42ab087a739803c0ac83a43d686a470"}, + {file = "numpy-2.4.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:715de7f82e192e8cae5a507a347d97ad17598f8e026152ca97233e3666daaa71"}, + {file = "numpy-2.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ddb7919366ee468342b91dea2352824c25b55814a987847b6c52003a7c97f15"}, + {file = "numpy-2.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a315e5234d88067f2d97e1f2ef670a7569df445d55400f1e33d117418d008d52"}, + {file = "numpy-2.4.3-cp311-cp311-win32.whl", hash = "sha256:2b3f8d2c4589b1a2028d2a770b0fc4d1f332fb5e01521f4de3199a896d158ddd"}, + {file = "numpy-2.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:77e76d932c49a75617c6d13464e41203cd410956614d0a0e999b25e9e8d27eec"}, + {file = "numpy-2.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:eb610595dd91560905c132c709412b512135a60f1851ccbd2c959e136431ff67"}, + {file = "numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef"}, + {file = "numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e"}, + {file = "numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4"}, + {file = "numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18"}, + {file = "numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5"}, + {file = "numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97"}, + {file = "numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c"}, + {file = "numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc"}, + {file = "numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9"}, + {file = "numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5"}, + {file = "numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e"}, + {file = "numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3"}, + {file = "numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9"}, + {file = "numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee"}, + {file = "numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f"}, + {file = "numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f"}, + {file = "numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc"}, + {file = "numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476"}, + {file = "numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92"}, + {file = "numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687"}, + {file = "numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd"}, + {file = "numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d"}, + {file = "numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875"}, + {file = "numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070"}, + {file = "numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73"}, + {file = "numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368"}, + {file = "numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22"}, + {file = "numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a"}, + {file = "numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349"}, + {file = "numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c"}, + {file = "numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26"}, + {file = "numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02"}, + {file = "numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4"}, + {file = "numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168"}, + {file = "numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b"}, + {file = "numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950"}, + {file = "numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd"}, + {file = "numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24"}, + {file = "numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0"}, + {file = "numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0"}, + {file = "numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a"}, + {file = "numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc"}, + {file = "numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7"}, + {file = "numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657"}, + {file = "numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7"}, + {file = "numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093"}, + {file = "numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a"}, + {file = "numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611"}, + {file = "numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720"}, + {file = "numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5"}, + {file = "numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0"}, + {file = "numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b"}, + {file = "numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e"}, + {file = "numpy-2.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c6b124bfcafb9e8d3ed09130dbee44848c20b3e758b6bbf006e641778927c028"}, + {file = "numpy-2.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:76dbb9d4e43c16cf9aa711fcd8de1e2eeb27539dcefb60a1d5e9f12fae1d1ed8"}, + {file = "numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:29363fbfa6f8ee855d7569c96ce524845e3d726d6c19b29eceec7dd555dab152"}, + {file = "numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:bc71942c789ef415a37f0d4eab90341425a00d538cd0642445d30b41023d3395"}, + {file = "numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e58765ad74dcebd3ef0208a5078fba32dc8ec3578fe84a604432950cd043d79"}, + {file = "numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e236dbda4e1d319d681afcbb136c0c4a8e0f1a5c58ceec2adebb547357fe857"}, + {file = "numpy-2.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b42639cdde6d24e732ff823a3fa5b701d8acad89c4142bc1d0bd6dc85200ba5"}, + {file = "numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd"}, ] [[package]] @@ -694,6 +790,17 @@ files = [ {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] +[[package]] +name = "packaging" +version = "26.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, + {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, +] + [[package]] name = "pandas" version = "2.3.3" @@ -804,6 +911,23 @@ files = [ {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, ] +[[package]] +name = "pathspec" +version = "1.0.4" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"}, + {file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"}, +] + +[package.extras] +hyperscan = ["hyperscan (>=0.7)"] +optional = ["typing-extensions (>=4)"] +re2 = ["google-re2 (>=1.1)"] +tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] + [[package]] name = "platformdirs" version = "4.0.0" @@ -822,6 +946,17 @@ typing-extensions = {version = ">=4.7.1", markers = "python_version < \"3.8\""} docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] +[[package]] +name = "platformdirs" +version = "4.9.4" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.10" +files = [ + {file = "platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868"}, + {file = "platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934"}, +] + [[package]] name = "pluggy" version = "1.2.0" @@ -909,15 +1044,69 @@ files = [ [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "pytokens" +version = "0.4.1" +description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5"}, + {file = "pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe"}, + {file = "pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c"}, + {file = "pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7"}, + {file = "pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2"}, + {file = "pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440"}, + {file = "pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc"}, + {file = "pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d"}, + {file = "pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16"}, + {file = "pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6"}, + {file = "pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083"}, + {file = "pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1"}, + {file = "pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1"}, + {file = "pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9"}, + {file = "pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68"}, + {file = "pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b"}, + {file = "pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f"}, + {file = "pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1"}, + {file = "pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4"}, + {file = "pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78"}, + {file = "pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321"}, + {file = "pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa"}, + {file = "pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d"}, + {file = "pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324"}, + {file = "pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9"}, + {file = "pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb"}, + {file = "pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3"}, + {file = "pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975"}, + {file = "pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a"}, + {file = "pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918"}, + {file = "pytokens-0.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:da5baeaf7116dced9c6bb76dc31ba04a2dc3695f3d9f74741d7910122b456edc"}, + {file = "pytokens-0.4.1-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11edda0942da80ff58c4408407616a310adecae1ddd22eef8c692fe266fa5009"}, + {file = "pytokens-0.4.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0fc71786e629cef478cbf29d7ea1923299181d0699dbe7c3c0f4a583811d9fc1"}, + {file = "pytokens-0.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dcafc12c30dbaf1e2af0490978352e0c4041a7cde31f4f81435c2a5e8b9cabb6"}, + {file = "pytokens-0.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:42f144f3aafa5d92bad964d471a581651e28b24434d184871bd02e3a0d956037"}, + {file = "pytokens-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:34bcc734bd2f2d5fe3b34e7b3c0116bfb2397f2d9666139988e7a3eb5f7400e3"}, + {file = "pytokens-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941d4343bf27b605e9213b26bfa1c4bf197c9c599a9627eb7305b0defcfe40c1"}, + {file = "pytokens-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ad72b851e781478366288743198101e5eb34a414f1d5627cdd585ca3b25f1db"}, + {file = "pytokens-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:682fa37ff4d8e95f7df6fe6fe6a431e8ed8e788023c6bcc0f0880a12eab80ad1"}, + {file = "pytokens-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:30f51edd9bb7f85c748979384165601d028b84f7bd13fe14d3e065304093916a"}, + {file = "pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de"}, + {file = "pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a"}, +] + +[package.extras] +dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"] + [[package]] name = "pytz" -version = "2025.2" +version = "2026.1.post1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ - {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, - {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, + {file = "pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a"}, + {file = "pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1"}, ] [[package]] @@ -941,6 +1130,28 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "requests" +version = "2.33.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.10" +files = [ + {file = "requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b"}, + {file = "requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652"}, +] + +[package.dependencies] +certifi = ">=2023.5.7" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.26,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +test = ["PySocks (>=1.5.6,!=1.5.7)", "pytest (>=3)", "pytest-cov", "pytest-httpbin (==2.1.0)", "pytest-mock", "pytest-xdist"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] + [[package]] name = "shapely" version = "2.0.7" @@ -1012,68 +1223,74 @@ files = [ [[package]] name = "sqlalchemy" -version = "2.0.44" +version = "2.0.48" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-2.0.44-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:471733aabb2e4848d609141a9e9d56a427c0a038f4abf65dd19d7a21fd563632"}, - {file = "SQLAlchemy-2.0.44-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48bf7d383a35e668b984c805470518b635d48b95a3c57cb03f37eaa3551b5f9f"}, - {file = "SQLAlchemy-2.0.44-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf4bb6b3d6228fcf3a71b50231199fb94d2dd2611b66d33be0578ea3e6c2726"}, - {file = "SQLAlchemy-2.0.44-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:e998cf7c29473bd077704cea3577d23123094311f59bdc4af551923b168332b1"}, - {file = "SQLAlchemy-2.0.44-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ebac3f0b5732014a126b43c2b7567f2f0e0afea7d9119a3378bde46d3dcad88e"}, - {file = "SQLAlchemy-2.0.44-cp37-cp37m-win32.whl", hash = "sha256:3255d821ee91bdf824795e936642bbf43a4c7cedf5d1aed8d24524e66843aa74"}, - {file = "SQLAlchemy-2.0.44-cp37-cp37m-win_amd64.whl", hash = "sha256:78e6c137ba35476adb5432103ae1534f2f5295605201d946a4198a0dea4b38e7"}, - {file = "sqlalchemy-2.0.44-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c77f3080674fc529b1bd99489378c7f63fcb4ba7f8322b79732e0258f0ea3ce"}, - {file = "sqlalchemy-2.0.44-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26ef74ba842d61635b0152763d057c8d48215d5be9bb8b7604116a059e9985"}, - {file = "sqlalchemy-2.0.44-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4a172b31785e2f00780eccab00bc240ccdbfdb8345f1e6063175b3ff12ad1b0"}, - {file = "sqlalchemy-2.0.44-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9480c0740aabd8cb29c329b422fb65358049840b34aba0adf63162371d2a96e"}, - {file = "sqlalchemy-2.0.44-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:17835885016b9e4d0135720160db3095dc78c583e7b902b6be799fb21035e749"}, - {file = "sqlalchemy-2.0.44-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cbe4f85f50c656d753890f39468fcd8190c5f08282caf19219f684225bfd5fd2"}, - {file = "sqlalchemy-2.0.44-cp310-cp310-win32.whl", hash = "sha256:2fcc4901a86ed81dc76703f3b93ff881e08761c63263c46991081fd7f034b165"}, - {file = "sqlalchemy-2.0.44-cp310-cp310-win_amd64.whl", hash = "sha256:9919e77403a483ab81e3423151e8ffc9dd992c20d2603bf17e4a8161111e55f5"}, - {file = "sqlalchemy-2.0.44-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd"}, - {file = "sqlalchemy-2.0.44-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa"}, - {file = "sqlalchemy-2.0.44-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e"}, - {file = "sqlalchemy-2.0.44-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e"}, - {file = "sqlalchemy-2.0.44-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399"}, - {file = "sqlalchemy-2.0.44-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b"}, - {file = "sqlalchemy-2.0.44-cp311-cp311-win32.whl", hash = "sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3"}, - {file = "sqlalchemy-2.0.44-cp311-cp311-win_amd64.whl", hash = "sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5"}, - {file = "sqlalchemy-2.0.44-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250"}, - {file = "sqlalchemy-2.0.44-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29"}, - {file = "sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44"}, - {file = "sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1"}, - {file = "sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7"}, - {file = "sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d"}, - {file = "sqlalchemy-2.0.44-cp312-cp312-win32.whl", hash = "sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4"}, - {file = "sqlalchemy-2.0.44-cp312-cp312-win_amd64.whl", hash = "sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e"}, - {file = "sqlalchemy-2.0.44-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1"}, - {file = "sqlalchemy-2.0.44-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45"}, - {file = "sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976"}, - {file = "sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c"}, - {file = "sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d"}, - {file = "sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40"}, - {file = "sqlalchemy-2.0.44-cp313-cp313-win32.whl", hash = "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73"}, - {file = "sqlalchemy-2.0.44-cp313-cp313-win_amd64.whl", hash = "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e"}, - {file = "sqlalchemy-2.0.44-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2fc44e5965ea46909a416fff0af48a219faefd5773ab79e5f8a5fcd5d62b2667"}, - {file = "sqlalchemy-2.0.44-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dc8b3850d2a601ca2320d081874033684e246d28e1c5e89db0864077cfc8f5a9"}, - {file = "sqlalchemy-2.0.44-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d733dec0614bb8f4bcb7c8af88172b974f685a31dc3a65cca0527e3120de5606"}, - {file = "sqlalchemy-2.0.44-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22be14009339b8bc16d6b9dc8780bacaba3402aa7581658e246114abbd2236e3"}, - {file = "sqlalchemy-2.0.44-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:357bade0e46064f88f2c3a99808233e67b0051cdddf82992379559322dfeb183"}, - {file = "sqlalchemy-2.0.44-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4848395d932e93c1595e59a8672aa7400e8922c39bb9b0668ed99ac6fa867822"}, - {file = "sqlalchemy-2.0.44-cp38-cp38-win32.whl", hash = "sha256:2f19644f27c76f07e10603580a47278abb2a70311136a7f8fd27dc2e096b9013"}, - {file = "sqlalchemy-2.0.44-cp38-cp38-win_amd64.whl", hash = "sha256:1df4763760d1de0dfc8192cc96d8aa293eb1a44f8f7a5fbe74caf1b551905c5e"}, - {file = "sqlalchemy-2.0.44-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f7027414f2b88992877573ab780c19ecb54d3a536bef3397933573d6b5068be4"}, - {file = "sqlalchemy-2.0.44-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fe166c7d00912e8c10d3a9a0ce105569a31a3d0db1a6e82c4e0f4bf16d5eca9"}, - {file = "sqlalchemy-2.0.44-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3caef1ff89b1caefc28f0368b3bde21a7e3e630c2eddac16abd9e47bd27cc36a"}, - {file = "sqlalchemy-2.0.44-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc2856d24afa44295735e72f3c75d6ee7fdd4336d8d3a8f3d44de7aa6b766df2"}, - {file = "sqlalchemy-2.0.44-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:11bac86b0deada30b6b5f93382712ff0e911fe8d31cb9bf46e6b149ae175eff0"}, - {file = "sqlalchemy-2.0.44-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4d18cd0e9a0f37c9f4088e50e3839fcb69a380a0ec957408e0b57cff08ee0a26"}, - {file = "sqlalchemy-2.0.44-cp39-cp39-win32.whl", hash = "sha256:9e9018544ab07614d591a26c1bd4293ddf40752cc435caf69196740516af7100"}, - {file = "sqlalchemy-2.0.44-cp39-cp39-win_amd64.whl", hash = "sha256:8e0e4e66fd80f277a8c3de016a81a554e76ccf6b8d881ee0b53200305a8433f6"}, - {file = "sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05"}, - {file = "sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22"}, + {file = "sqlalchemy-2.0.48-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7001dc9d5f6bb4deb756d5928eaefe1930f6f4179da3924cbd95ee0e9f4dce89"}, + {file = "sqlalchemy-2.0.48-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a89ce07ad2d4b8cfc30bd5889ec40613e028ed80ef47da7d9dd2ce969ad30e0"}, + {file = "sqlalchemy-2.0.48-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10853a53a4a00417a00913d270dddda75815fcb80675874285f41051c094d7dd"}, + {file = "sqlalchemy-2.0.48-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fac0fa4e4f55f118fd87177dacb1c6522fe39c28d498d259014020fec9164c29"}, + {file = "sqlalchemy-2.0.48-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3713e21ea67bca727eecd4a24bf68bcd414c403faae4989442be60994301ded0"}, + {file = "sqlalchemy-2.0.48-cp310-cp310-win32.whl", hash = "sha256:d404dc897ce10e565d647795861762aa2d06ca3f4a728c5e9a835096c7059018"}, + {file = "sqlalchemy-2.0.48-cp310-cp310-win_amd64.whl", hash = "sha256:841a94c66577661c1f088ac958cd767d7c9bf507698f45afffe7a4017049de76"}, + {file = "sqlalchemy-2.0.48-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b4c575df7368b3b13e0cebf01d4679f9a28ed2ae6c1cd0b1d5beffb6b2007dc"}, + {file = "sqlalchemy-2.0.48-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e83e3f959aaa1c9df95c22c528096d94848a1bc819f5d0ebf7ee3df0ca63db6c"}, + {file = "sqlalchemy-2.0.48-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f7b7243850edd0b8b97043f04748f31de50cf426e939def5c16bedb540698f7"}, + {file = "sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82745b03b4043e04600a6b665cb98697c4339b24e34d74b0a2ac0a2488b6f94d"}, + {file = "sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5e088bf43f6ee6fec7dbf1ef7ff7774a616c236b5c0cb3e00662dd71a56b571"}, + {file = "sqlalchemy-2.0.48-cp311-cp311-win32.whl", hash = "sha256:9c7d0a77e36b5f4b01ca398482230ab792061d243d715299b44a0b55c89fe617"}, + {file = "sqlalchemy-2.0.48-cp311-cp311-win_amd64.whl", hash = "sha256:583849c743e0e3c9bb7446f5b5addeacedc168d657a69b418063dfdb2d90081c"}, + {file = "sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b"}, + {file = "sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb"}, + {file = "sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894"}, + {file = "sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9"}, + {file = "sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e"}, + {file = "sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99"}, + {file = "sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a"}, + {file = "sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4"}, + {file = "sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f"}, + {file = "sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed"}, + {file = "sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658"}, + {file = "sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8"}, + {file = "sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131"}, + {file = "sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2"}, + {file = "sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae"}, + {file = "sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb"}, + {file = "sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b"}, + {file = "sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121"}, + {file = "sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485"}, + {file = "sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79"}, + {file = "sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd"}, + {file = "sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f"}, + {file = "sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b"}, + {file = "sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0"}, + {file = "sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2"}, + {file = "sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6"}, + {file = "sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0"}, + {file = "sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241"}, + {file = "sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0"}, + {file = "sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3"}, + {file = "sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b"}, + {file = "sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f"}, + {file = "sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933"}, + {file = "sqlalchemy-2.0.48-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8649a14caa5f8a243628b1d61cf530ad9ae4578814ba726816adb1121fc493e"}, + {file = "sqlalchemy-2.0.48-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6bb85c546591569558571aa1b06aba711b26ae62f111e15e56136d69920e1616"}, + {file = "sqlalchemy-2.0.48-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6b764fb312bd35e47797ad2e63f0d323792837a6ac785a4ca967019357d2bc7"}, + {file = "sqlalchemy-2.0.48-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:7c998f2ace8bf76b453b75dbcca500d4f4b9dd3908c13e89b86289b37784848b"}, + {file = "sqlalchemy-2.0.48-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d64177f443594c8697369c10e4bbcac70ef558e0f7921a1de7e4a3d1734bcf67"}, + {file = "sqlalchemy-2.0.48-cp38-cp38-win32.whl", hash = "sha256:01f6bbd4308b23240cf7d3ef117557c8fd097ec9549d5d8a52977544e35b40ad"}, + {file = "sqlalchemy-2.0.48-cp38-cp38-win_amd64.whl", hash = "sha256:858e433f12b0e5b3ed2f8da917433b634f4937d0e8793e5cb33c54a1a01df565"}, + {file = "sqlalchemy-2.0.48-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4599a95f9430ae0de82b52ff0d27304fe898c17cb5f4099f7438a51b9998ac77"}, + {file = "sqlalchemy-2.0.48-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f27f9da0a7d22b9f981108fd4b62f8b5743423388915a563e651c20d06c1f457"}, + {file = "sqlalchemy-2.0.48-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d8fcccbbc0c13c13702c471da398b8cd72ba740dca5859f148ae8e0e8e0d3e7e"}, + {file = "sqlalchemy-2.0.48-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a5b429eb84339f9f05e06083f119ad814e6d85e27ecbdf9c551dfdbb128eaf8a"}, + {file = "sqlalchemy-2.0.48-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:bcb8ebbf2e2c36cfe01a94f2438012c6a9d494cf80f129d9753bcdf33bfc35a6"}, + {file = "sqlalchemy-2.0.48-cp39-cp39-win32.whl", hash = "sha256:e214d546c8ecb5fc22d6e6011746082abf13a9cf46eefb45769c7b31407c97b5"}, + {file = "sqlalchemy-2.0.48-cp39-cp39-win_amd64.whl", hash = "sha256:b8fc3454b4f3bd0a368001d0e968852dad45a873f8b4babd41bc302ec851a099"}, + {file = "sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096"}, + {file = "sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7"}, ] [package.dependencies] @@ -1108,17 +1325,25 @@ sqlcipher = ["sqlcipher3_binary"] [[package]] name = "taos-ws-py" -version = "0.6.2" +version = "0.6.6" description = "" optional = true python-versions = "*" files = [ - {file = "taos_ws_py-0.6.2-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:70100fc77a312e9f447d1636212872f955bc57626bfe62fc2738cc951352e4f1"}, - {file = "taos_ws_py-0.6.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:823e78cb87952f09361c5c7d1f17b1d3f18b506c530178519182adc0eb4b379b"}, - {file = "taos_ws_py-0.6.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7d6a9540ec4d245ab2b9ccd6fa0a12130ef93b4c0ec6b95834a918b3eb74bc5"}, - {file = "taos_ws_py-0.6.2-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:23a02ff2ae36c62d5ff5349e71c56aaec61ac3b7b18ea89406db37f0092a5210"}, - {file = "taos_ws_py-0.6.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b17cd5a624ac7acbc305fb2b1293e4581096de79f711315c96d4b1b7eaa982c"}, - {file = "taos_ws_py-0.6.2-cp37-abi3-win_amd64.whl", hash = "sha256:4da10dff566912504dc25e6fd889a49fbc736a4702a2d6094dfa9f236c60f4a0"}, + {file = "taos_ws_py-0.6.6-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:62c549fc2e4d42062c9f125de660391afa1dd8cfbc38d72c5be4d2f906e25393"}, + {file = "taos_ws_py-0.6.6-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f009f2e4c0bbfc2ad9e86590b11ce53832d970c1ec7eb17238f149aaf4a2f144"}, + {file = "taos_ws_py-0.6.6-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:791b77688061c65bdd5f9b85eebaaed4015e065705dfb54625d34bda26ad3fd6"}, + {file = "taos_ws_py-0.6.6-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a54e1da8469b332c9f762ff52ee060bd97f7a9cbb0bf4be405cbc14f3735371e"}, + {file = "taos_ws_py-0.6.6-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64b8904fcc6b4f96a2100daedc5dc7b3e02b02a7c1b1fec64efa9f21447c8f95"}, + {file = "taos_ws_py-0.6.6-cp37-abi3-win_amd64.whl", hash = "sha256:984e5f42efa542746db2a8be8ebbbc984f3b79c5d8186b6173a54be3a8b325d6"}, + {file = "taos_ws_py-0.6.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab440e2225ed422665014c9739225c8ac1412c7f55ee7148b016c73a9c00b68e"}, + {file = "taos_ws_py-0.6.6-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:929e490d30742b6754ba0c1e4c8a0122aaabe7712b9fc94ce796f19005d84070"}, + {file = "taos_ws_py-0.6.6-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64d6b6f3979a1d294d24040e3b6715a3085ba6216dc2f4c45ae46f02f85ca5b5"}, + {file = "taos_ws_py-0.6.6-pp37-pypy37_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ecfadd53009e0944443b11fef638b25d2ac51b03ec6e895815b7683ee26e35bf"}, + {file = "taos_ws_py-0.6.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a665a8104ed756f22464e38669255159e68ce35eafda9886adab8127e1626c0"}, + {file = "taos_ws_py-0.6.6-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3e1919d6f2ecb89ed0184b7bcbe78d7d248635431892e364e667d9c879373d"}, + {file = "taos_ws_py-0.6.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc4c94e9842bb127d02e18e0ca1320f2f8b6e89c9e625b28b3cdae101a67f5e"}, + {file = "taos_ws_py-0.6.6-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e8f03581a876c44893241e949dc2500401fad278ea59ed458a4aa157ab939eb"}, ] [[package]] @@ -1206,13 +1431,13 @@ files = [ [[package]] name = "tzdata" -version = "2025.2" +version = "2025.3" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" files = [ - {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, - {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, + {file = "tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1"}, + {file = "tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7"}, ] [[package]] @@ -1232,6 +1457,23 @@ secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17. socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "urllib3" +version = "2.6.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +files = [ + {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, + {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, +] + +[package.extras] +brotli = ["brotli (>=1.2.0)", "brotlicffi (>=1.2.0.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["backports-zstd (>=1.0.0)"] + [[package]] name = "zipp" version = "3.15.0" @@ -1253,4 +1495,4 @@ ws = ["taos-ws-py"] [metadata] lock-version = "2.0" python-versions = ">=3.7,<4.0" -content-hash = "aad58384268f8cd94e588876572cb5a7cf515c34a915c86cf966c51a1a9c28a4" +content-hash = "fe6020c8b5593f7f6cfc67e42cc263f76049efe6aabb5e407c19e290ebfaccc5" diff --git a/pyproject.toml b/pyproject.toml index 544b4317..3c075ad4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,10 @@ packages = [{ include = "taos" }, { include = "taosrest" }] python = ">=3.7,<4.0" pytz = "*" iso8601 = "1.0.2" -requests = ">=2.27.1" +requests = [ + { version = ">=2.33.0", python = ">=3.10,<4.0" }, + { version = ">=2.27.1,<2.33.0", python = ">=3.7,<3.10" }, +] typing-extensions = ">=4.2.0,<4.15.0" [tool.poetry.dependencies.taos-ws-py] @@ -35,7 +38,10 @@ typing = "*" pytest = [{ version = "^7.2.0", python = ">=3.7,<4.0" }] pytest-cov = "^4.0.0" mypy = { version = "^0.910", python = "^3.6" } -black = [{ version = ">=21.0", python = ">=3.6.2,<4.0" }] +black = [ + { version = ">=26.3.1", python = ">=3.10,<4.0" }, + { version = ">=21.0,<26.3.1", python = ">=3.6.2,<3.10" }, +] sqlalchemy = { version = "^2.0.0", python = ">=3.7,<4.0" } pandas = { version = ">=2.1.0", python = ">=3.9,<4.0" } python-dotenv = { version = "0.20.0" } @@ -49,7 +55,10 @@ typing = "*" pytest = [{ version = "^7.2.0", python = ">=3.7,<4.0" }] pytest-cov = "^4.0.0" mypy = { version = "^0.910", python = "^3.6" } -black = [{ version = ">=21.0", python = ">=3.6.2,<4.0" }] +black = [ + { version = ">=26.3.1", python = ">=3.10,<4.0" }, + { version = ">=21.0,<26.3.1", python = ">=3.6.2,<3.10" }, +] sqlalchemy = { version = "^2.0.0", python = ">=3.7,<4.0" } pandas = { version = ">=2.1.0", python = ">=3.9,<4.0" } python-dotenv = { version = "0.20.0" } diff --git a/test_taospy.sh b/test_taospy.sh index 40c226d9..40f7a2bd 100644 --- a/test_taospy.sh +++ b/test_taospy.sh @@ -4,4 +4,3 @@ poetry run pip install "numpy<2.0.0" export TDENGINE_URL=localhost:6041 curl -L -H "Authorization: Basic cm9vdDp0YW9zZGF0YQ==" -d "show databases" localhost:6041/rest/sql poetry run pytest ./tests - From a5b7419e9c2113dc5df86c0faa20e62f79f568f2 Mon Sep 17 00:00:00 2001 From: qevolg <2227465945@qq.com> Date: Sat, 28 Mar 2026 19:52:21 +0800 Subject: [PATCH 09/24] fix: normalize handling of explicit empty passwords in connection args --- taos-ws-py/taosws/sqlalchemy.py | 11 +++++++---- taos-ws-py/tests/test_sqlalchemy_migration.py | 5 +++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/taos-ws-py/taosws/sqlalchemy.py b/taos-ws-py/taosws/sqlalchemy.py index 59d8dd8a..32f7c146 100644 --- a/taos-ws-py/taosws/sqlalchemy.py +++ b/taos-ws-py/taosws/sqlalchemy.py @@ -544,16 +544,19 @@ def create_connect_args(cls, url): username = url.username password = url.password - if username is not None and password is not None: + has_username = username is not None and username != "" + has_password = password is not None and password != "" + + if has_username and has_password: userpass = f"{username}:{password}" - elif username is not None: + elif has_username: userpass = f"{username}" - elif password is not None: + elif has_password: userpass = f":{password}" else: userpass = "" - at = "@" if (username is not None or password is not None) else "" + at = "@" if (has_username or has_password) else "" hosts = url.query.get("hosts") if hosts: diff --git a/taos-ws-py/tests/test_sqlalchemy_migration.py b/taos-ws-py/tests/test_sqlalchemy_migration.py index dd8b0ca0..7e66d4d6 100644 --- a/taos-ws-py/tests/test_sqlalchemy_migration.py +++ b/taos-ws-py/tests/test_sqlalchemy_migration.py @@ -63,14 +63,15 @@ def test_create_connect_args_encodes_query_values_safely(): assert kwargs == {} -def test_create_connect_args_preserves_explicit_empty_password(): +def test_create_connect_args_normalizes_explicit_empty_password(): module = importlib.import_module("taosws.sqlalchemy") dialect = module.TaosWsDialect() url = make_url("taosws://root:@localhost:6041/test_1755496227") args, kwargs = dialect.create_connect_args(url) - assert args == ["taosws://root:@localhost:6041/test_1755496227"] + # taosws rejects explicit empty password (root:@...), so we normalize it. + assert args == ["taosws://root@localhost:6041/test_1755496227"] assert kwargs == {} From d98643e703c48bb182e1bf9e662e86bbe5822df2 Mon Sep 17 00:00:00 2001 From: qevolg <2227465945@qq.com> Date: Sat, 28 Mar 2026 20:29:32 +0800 Subject: [PATCH 10/24] fix: correct SQL query formatting in get_indexes method --- taos/sqlalchemy.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/taos/sqlalchemy.py b/taos/sqlalchemy.py index ce489980..482d69b1 100644 --- a/taos/sqlalchemy.py +++ b/taos/sqlalchemy.py @@ -522,8 +522,7 @@ def get_foreign_keys(self, connection, table_name, schema=None, **kw): def get_indexes(self, connection, table_name, schema=None, **kw): sql = ( "SELECT * FROM information_schema.INS_INDEXES " - f"WHERE db_name = '{schema}'" - " " + f"WHERE db_name = '{schema}' " f"AND table_name = '{table_name}'" ) try: From 3fff78d92b1792f650cdc1055a10a737c725f3de Mon Sep 17 00:00:00 2001 From: qevolg <2227465945@qq.com> Date: Sat, 28 Mar 2026 21:17:45 +0800 Subject: [PATCH 11/24] feat: enhance SQLAlchemy dialect with improved SQL rendering and schema injection prevention --- taos-ws-py/pyproject.toml | 2 +- taos-ws-py/taosws/sqlalchemy.py | 60 ++++++++-- taos-ws-py/tests/test_sqlalchemy.py | 107 +++++++++++++++--- taos-ws-py/tests/test_sqlalchemy_migration.py | 16 +-- taos/sqlalchemy.py | 11 +- tests/test_sqlalchemy.py | 58 +++++++++- 6 files changed, 211 insertions(+), 43 deletions(-) diff --git a/taos-ws-py/pyproject.toml b/taos-ws-py/pyproject.toml index 07cd7739..6019a19d 100644 --- a/taos-ws-py/pyproject.toml +++ b/taos-ws-py/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "maturin" [project] name = "taos-ws-py" dynamic = ["version"] -description = "The official TDengine Python websocket connector" +description = "The official TDengine Python WebSocket connector" readme = "README.md" requires-python = ">=3.7" license = { text = "MIT" } diff --git a/taos-ws-py/taosws/sqlalchemy.py b/taos-ws-py/taosws/sqlalchemy.py index 32f7c146..5774fb3a 100644 --- a/taos-ws-py/taosws/sqlalchemy.py +++ b/taos-ws-py/taosws/sqlalchemy.py @@ -1,3 +1,6 @@ +import numbers +from datetime import date, datetime + from sqlalchemy import sql from sqlalchemy import text from sqlalchemy import types as sqltypes @@ -394,16 +397,54 @@ def _get_server_version_info(self, connection): cursor = connection.execute(text("select server_version()")) return cursor.fetchone() + def _to_sql_literal(self, value): + if value is None: + return "NULL" + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, numbers.Number): + return str(value) + if isinstance(value, datetime): + return f"'{value.strftime('%Y-%m-%d %H:%M:%S.%f')}'" + if isinstance(value, date): + return f"'{value.isoformat()}'" + + text_value = str(value) + return "'" + text_value.replace("'", "''") + "'" + + def _render_sql(self, statement, parameters): + if parameters is None: + return statement + + if isinstance(parameters, dict): + if len(parameters) == 0: + return statement + literalized = {key: self._to_sql_literal(value) for key, value in parameters.items()} + return statement % literalized + + if isinstance(parameters, (list, tuple)): + if len(parameters) == 0: + return statement + literalized = tuple(self._to_sql_literal(value) for value in parameters) + return statement % literalized + + return statement % self._to_sql_literal(parameters) + def do_execute(self, cursor, statement, parameters, context=None): - if parameters is None or len(parameters) == 0: - cursor.execute(statement, parameters) - else: - cursor.execute(statement, [parameters]) + rendered_sql = self._render_sql(statement, parameters) + cursor.execute(rendered_sql) return cursor def do_executemany(self, cursor, statement, parameters, context=None): - cursor.executemany(statement, parameters) + if parameters is None: + return cursor + if isinstance(parameters, (list, tuple)) and len(parameters) == 0: + return cursor + + for parameter in parameters: + rendered_sql = self._render_sql(statement, parameter) + cursor.execute(rendered_sql) return cursor @reflection.cache @@ -455,14 +496,11 @@ def get_indexes(self, connection, table_name, schema=None, **kw): if schema is None: return [] - sql = ( - "SELECT * FROM information_schema.INS_INDEXES " - f"WHERE db_name = '{schema}'" - " " - f"AND table_name = '{table_name}'" + sql = text( + "SELECT * FROM information_schema.INS_INDEXES " "WHERE db_name = :schema " "AND table_name = :table_name" ) try: - cursor = connection.execute(text(sql)) + cursor = connection.execute(sql, {"schema": schema, "table_name": table_name}) rows = cursor.fetchall() indexes = [] for row in rows: diff --git a/taos-ws-py/tests/test_sqlalchemy.py b/taos-ws-py/tests/test_sqlalchemy.py index 294c1172..0366a9f5 100644 --- a/taos-ws-py/tests/test_sqlalchemy.py +++ b/taos-ws-py/tests/test_sqlalchemy.py @@ -9,6 +9,7 @@ from sqlalchemy import create_engine from sqlalchemy import inspect +from sqlalchemy import text from sqlalchemy.dialects import registry @@ -25,28 +26,38 @@ def resolve_tdengine_host_port(url): HOST, PORT = resolve_tdengine_host_port(TDENGINE_URL) if TDENGINE_URL else ("localhost", 6041) +SQLALCHEMY_DB = "test_1774703601" +FAILOVER_DB = "test_1774703602" +EXECUTE_DB = "test_1774703603" +INDEX_INJECTION_DB_1 = "test_1774703604" +INDEX_INJECTION_DB_2 = "test_1774703605" -def insert_data(conn=None): + +def insert_data(conn=None, db_name=SQLALCHEMY_DB): close_on_exit = conn is None c = conn or taosws.connect(f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}") - c.execute("drop database if exists test") - c.execute("create database if not exists test") - c.execute("create table test.meters (ts timestamp, c1 int, c2 double) tags(t1 int)") - c.execute("insert into test.d0 using test.meters tags(0) values (1733189403001, 1, 1.11) (1733189403002, 2, 2.22)") - c.execute("insert into test.d1 using test.meters tags(1) values (1733189403003, 3, 3.33) (1733189403004, 4, 4.44)") - c.execute("create table test.ntb(ts timestamp, age int)") - c.execute("insert into test.ntb values(now, 23)") + c.execute(f"drop database if exists {db_name}") + c.execute(f"create database if not exists {db_name}") + c.execute(f"create table {db_name}.meters (ts timestamp, c1 int, c2 double) tags(t1 int)") + c.execute( + f"insert into {db_name}.d0 using {db_name}.meters tags(0) values (1733189403001, 1, 1.11) (1733189403002, 2, 2.22)" + ) + c.execute( + f"insert into {db_name}.d1 using {db_name}.meters tags(1) values (1733189403003, 3, 3.33) (1733189403004, 4, 4.44)" + ) + c.execute(f"create table {db_name}.ntb(ts timestamp, age int)") + c.execute(f"insert into {db_name}.ntb values(now, 23)") if close_on_exit: c.close() -def check_basic(conn, inspection, sub_tables=None): +def check_basic(conn, inspection, schema=SQLALCHEMY_DB, sub_tables=None): tables = sub_tables or ["meters", "ntb"] databases = inspection.get_schema_names() - assert "test" in databases, f"test not in {databases}" + assert schema in databases, f"{schema} not in {databases}" - assert inspection.get_table_names("test") == tables, "check get_table_names() failed" + assert inspection.get_table_names(schema) == tables, "check get_table_names() failed" expected_columns = [ {"name": "ts", "type": inspection.dialect._resolve_type("TIMESTAMP")}, @@ -54,7 +65,7 @@ def check_basic(conn, inspection, sub_tables=None): {"name": "c2", "type": inspection.dialect._resolve_type("DOUBLE")}, {"name": "t1", "type": inspection.dialect._resolve_type("INT")}, ] - columns = inspection.get_columns("meters", "test") + columns = inspection.get_columns("meters", schema) for index, column in enumerate(columns): expected = expected_columns[index] assert column["name"] == expected["name"], f"column name mismatch: {column['name']} != {expected['name']}" @@ -62,8 +73,8 @@ def check_basic(conn, inspection, sub_tables=None): type(column["type"]) == expected["type"] ), f"column type mismatch: {type(column['type'])} != {expected['type']}" - assert inspection.has_table("meters", "test") is True, "check has_table() failed" - assert inspection.dialect.has_schema(conn, "test") is True, "check has_schema() failed" + assert inspection.has_table("meters", schema) is True, "check has_table() failed" + assert inspection.dialect.has_schema(conn, schema) is True, "check has_schema() failed" conn.close() @@ -73,13 +84,13 @@ def test_read_from_sqlalchemy_taosws(): f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}?timezone=Asia/Shanghai" ) conn = engine.connect() - insert_data() + insert_data(db_name=SQLALCHEMY_DB) inspection = inspect(engine) - check_basic(conn, inspection) + check_basic(conn, inspection, schema=SQLALCHEMY_DB) def test_read_from_sqlalchemy_taosws_failover(): - db_name = "test_1755496227" + db_name = FAILOVER_DB conn = taosws.connect(f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}") conn.execute(f"drop database if exists {db_name}") conn.execute(f"create database {db_name}") @@ -119,3 +130,65 @@ def test_read_from_sqlalchemy_taosws_failover(): finally: conn.execute(f"drop database if exists {db_name}") conn.close() + + +def test_sqlalchemy_taosws_execute_and_executemany_params(): + db_name = EXECUTE_DB + engine = create_engine( + f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}?timezone=Asia/Shanghai" + ) + conn = engine.connect() + try: + conn.execute(text(f"drop database if exists {db_name}")) + conn.execute(text(f"create database if not exists {db_name}")) + conn.execute(text(f"create table {db_name}.ntb(ts timestamp, v int)")) + + conn.execute(text(f"insert into {db_name}.ntb values (:ts, :v)"), {"ts": 1733189403001, "v": 1}) + conn.execute( + text(f"insert into {db_name}.ntb values (:ts, :v)"), + [ + {"ts": 1733189403002, "v": 2}, + {"ts": 1733189403003, "v": 3}, + ], + ) + + rows = conn.execute(text(f"select count(*) from {db_name}.ntb")).fetchall() + assert rows[0][0] == 3 + finally: + conn.execute(text(f"drop database if exists {db_name}")) + conn.close() + + +def test_taosws_get_indexes_prevents_schema_injection(): + db_name_1, db_name_2 = INDEX_INJECTION_DB_1, INDEX_INJECTION_DB_2 + engine = create_engine( + f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}?timezone=Asia/Shanghai" + ) + conn = engine.connect() + try: + conn.execute(text(f"drop database if exists {db_name_1}")) + conn.execute(text(f"drop database if exists {db_name_2}")) + conn.execute(text(f"create database if not exists {db_name_1}")) + conn.execute(text(f"create database if not exists {db_name_2}")) + + conn.execute( + text(f"create table {db_name_1}.meters(ts timestamp, c1 int, c2 double) tags(t1 int, location nchar(16))") + ) + conn.execute( + text(f"create table {db_name_2}.meters(ts timestamp, c1 int, c2 double) tags(t1 int, location nchar(16))") + ) + conn.execute(text(f"create index idx_location_wa on {db_name_1}.meters(location)")) + conn.execute(text(f"create index idx_location_wb on {db_name_2}.meters(location)")) + + inspection = inspect(engine) + indexes = inspection.get_indexes("meters", db_name_1) + assert any(idx.get("name") == "idx_location_wa" for idx in indexes) + assert all(idx.get("name") != "idx_location_wb" for idx in indexes) + + injected_schema = f"{db_name_1}' OR '1'='1" + injected_indexes = inspection.get_indexes("meters", injected_schema) + assert injected_indexes == [] + finally: + conn.execute(text(f"drop database if exists {db_name_1}")) + conn.execute(text(f"drop database if exists {db_name_2}")) + conn.close() diff --git a/taos-ws-py/tests/test_sqlalchemy_migration.py b/taos-ws-py/tests/test_sqlalchemy_migration.py index 7e66d4d6..bd79b920 100644 --- a/taos-ws-py/tests/test_sqlalchemy_migration.py +++ b/taos-ws-py/tests/test_sqlalchemy_migration.py @@ -29,13 +29,13 @@ def test_create_connect_args_prefers_hosts_and_keeps_other_query_params(): module = importlib.import_module("taosws.sqlalchemy") dialect = module.TaosWsDialect() url = make_url( - "taosws://root:taosdata@localhost:6041/test_1755496227?" + "taosws://root:taosdata@localhost:6041/test_1774703606?" "hosts=localhost:6041,127.0.0.1:6041&timezone=Asia/Shanghai" ) args, kwargs = dialect.create_connect_args(url) - assert args == ["taosws://root:taosdata@localhost:6041,127.0.0.1:6041/test_1755496227?timezone=Asia%2FShanghai"] + assert args == ["taosws://root:taosdata@localhost:6041,127.0.0.1:6041/test_1774703606?timezone=Asia%2FShanghai"] assert kwargs == {} @@ -43,35 +43,35 @@ def test_create_connect_args_no_trailing_ampersand_when_hosts_is_last_param(): module = importlib.import_module("taosws.sqlalchemy") dialect = module.TaosWsDialect() url = make_url( - "taosws://root:taosdata@localhost:6041/test_1755496227?" "timezone=Asia/Shanghai&hosts=localhost:6041" + "taosws://root:taosdata@localhost:6041/test_1774703606?" "timezone=Asia/Shanghai&hosts=localhost:6041" ) args, kwargs = dialect.create_connect_args(url) - assert args == ["taosws://root:taosdata@localhost:6041/test_1755496227?timezone=Asia%2FShanghai"] + assert args == ["taosws://root:taosdata@localhost:6041/test_1774703606?timezone=Asia%2FShanghai"] assert kwargs == {} def test_create_connect_args_encodes_query_values_safely(): module = importlib.import_module("taosws.sqlalchemy") dialect = module.TaosWsDialect() - url = make_url("taosws://root:taosdata@localhost:6041/test_1755496227?note=cn north") + url = make_url("taosws://root:taosdata@localhost:6041/test_1774703606?note=cn north") args, kwargs = dialect.create_connect_args(url) - assert args == ["taosws://root:taosdata@localhost:6041/test_1755496227?note=cn%20north"] + assert args == ["taosws://root:taosdata@localhost:6041/test_1774703606?note=cn%20north"] assert kwargs == {} def test_create_connect_args_normalizes_explicit_empty_password(): module = importlib.import_module("taosws.sqlalchemy") dialect = module.TaosWsDialect() - url = make_url("taosws://root:@localhost:6041/test_1755496227") + url = make_url("taosws://root:@localhost:6041/test_1774703606") args, kwargs = dialect.create_connect_args(url) # taosws rejects explicit empty password (root:@...), so we normalize it. - assert args == ["taosws://root@localhost:6041/test_1755496227"] + assert args == ["taosws://root@localhost:6041/test_1774703606"] assert kwargs == {} diff --git a/taos/sqlalchemy.py b/taos/sqlalchemy.py index 482d69b1..009a115c 100644 --- a/taos/sqlalchemy.py +++ b/taos/sqlalchemy.py @@ -520,13 +520,14 @@ def get_foreign_keys(self, connection, table_name, schema=None, **kw): # Get indexes information @reflection.cache def get_indexes(self, connection, table_name, schema=None, **kw): - sql = ( - "SELECT * FROM information_schema.INS_INDEXES " - f"WHERE db_name = '{schema}' " - f"AND table_name = '{table_name}'" + if schema is None: + return [] + + sql = text( + "SELECT * FROM information_schema.INS_INDEXES " "WHERE db_name = :schema " "AND table_name = :table_name" ) try: - cursor = connection.execute(text(sql)) + cursor = connection.execute(sql, {"schema": schema, "table_name": table_name}) rows = cursor.fetchall() indexes = [] for row in rows: diff --git a/tests/test_sqlalchemy.py b/tests/test_sqlalchemy.py index 44a31bd3..3ad5be9c 100644 --- a/tests/test_sqlalchemy.py +++ b/tests/test_sqlalchemy.py @@ -13,6 +13,10 @@ host = "localhost" +PK_EMPTY_DB = "test_1774703701" +INDEX_INJECTION_DB_1 = "test_1774703702" +INDEX_INJECTION_DB_2 = "test_1774703703" + def prepare(conn, dbname, stbname, ntb1, ntb2): conn.execute(text("drop database if exists %s" % dbname)) @@ -227,7 +231,6 @@ def test_sqlalchemy_format_stmt_taos(): check_basic(conn, inspection, subTables=["meters"]) -# taosrest def test_read_from_sqlalchemy_taosrest(): if not taos.IS_V3: return @@ -240,6 +243,59 @@ def test_read_from_sqlalchemy_taosrest(): check_basic(conn, inspection) +def test_get_pk_constraint_returns_empty_when_no_columns(): + db_name = PK_EMPTY_DB + engine = create_engine( + f"taos://{utils.test_username()}:{utils.test_password()}@{host}:{PORT}?timezone=Asia/Shanghai" + ) + conn = engine.connect() + try: + conn.execute(text(f"drop database if exists {db_name}")) + conn.execute(text(f"create database if not exists {db_name}")) + + inspection = inspect(engine) + pk = inspection.get_pk_constraint("table_not_exists", db_name) + + assert pk == {"constrained_columns": [], "name": None} + finally: + conn.execute(text(f"drop database if exists {db_name}")) + conn.close() + + +def test_get_indexes_prevents_schema_injection(): + db_name_1, db_name_2 = INDEX_INJECTION_DB_1, INDEX_INJECTION_DB_2 + engine = create_engine( + f"taos://{utils.test_username()}:{utils.test_password()}@{host}:{PORT}?timezone=Asia/Shanghai" + ) + conn = engine.connect() + try: + conn.execute(text(f"drop database if exists {db_name_1}")) + conn.execute(text(f"drop database if exists {db_name_2}")) + conn.execute(text(f"create database if not exists {db_name_1}")) + conn.execute(text(f"create database if not exists {db_name_2}")) + conn.execute( + text(f"create table {db_name_1}.meters(ts timestamp, c1 int, c2 double) tags(t1 int, location nchar(16))") + ) + conn.execute( + text(f"create table {db_name_2}.meters(ts timestamp, c1 int, c2 double) tags(t1 int, location nchar(16))") + ) + conn.execute(text(f"create index idx_location_a on {db_name_1}.meters(location)")) + conn.execute(text(f"create index idx_location_b on {db_name_2}.meters(location)")) + + inspection = inspect(engine) + indexes = inspection.get_indexes("meters", db_name_1) + assert any(idx.get("name") == "idx_location_a" for idx in indexes) + assert all(idx.get("name") != "idx_location_b" for idx in indexes) + + injected_schema = f"{db_name_1}' OR '1'='1" + injected_indexes = inspection.get_indexes("meters", injected_schema) + assert injected_indexes == [] + finally: + conn.execute(text(f"drop database if exists {db_name_1}")) + conn.execute(text(f"drop database if exists {db_name_2}")) + conn.close() + + # main test if __name__ == "__main__": print("hello, test sqlalcemy db api. do nothing\n") From 6f252b0fd26ac070251451683fcebb1e885ff8c7 Mon Sep 17 00:00:00 2001 From: qevolg <2227465945@qq.com> Date: Mon, 30 Mar 2026 09:54:22 +0800 Subject: [PATCH 12/24] fix: streamline classifiers in pyproject.toml by removing redundant entries --- taos-ws-py/pyproject.toml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/taos-ws-py/pyproject.toml b/taos-ws-py/pyproject.toml index 6019a19d..1c7c4db7 100644 --- a/taos-ws-py/pyproject.toml +++ b/taos-ws-py/pyproject.toml @@ -9,12 +9,7 @@ description = "The official TDengine Python WebSocket connector" readme = "README.md" requires-python = ">=3.7" license = { text = "MIT" } -classifiers = [ - "Programming Language :: Python", - "Programming Language :: Rust", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", -] +classifiers = ["Programming Language :: Python", "Programming Language :: Rust"] [project.optional-dependencies] sqlalchemy = ["sqlalchemy>=2.0.0"] From 46eff51d02b07212975082e188e77c6d26b1d711 Mon Sep 17 00:00:00 2001 From: qevolg <2227465945@qq.com> Date: Mon, 30 Mar 2026 14:36:30 +0800 Subject: [PATCH 13/24] fix: simplify native module loading logic in __init__.py --- taos-ws-py/taosws/__init__.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/taos-ws-py/taosws/__init__.py b/taos-ws-py/taosws/__init__.py index b50cdf01..dbfe0e1e 100644 --- a/taos-ws-py/taosws/__init__.py +++ b/taos-ws-py/taosws/__init__.py @@ -7,15 +7,12 @@ def _load_native_module(): - for module_name in ("_taosws", "taosws"): - full_name = f"{__name__}.{module_name}" - if importlib.util.find_spec(full_name) is None: - continue + full_name = f"{__name__}._taosws" + if importlib.util.find_spec(full_name) is not None: return importlib.import_module(full_name) raise ImportError( - "Failed to import native extension 'taosws._taosws' or 'taosws.taosws'. " - "Ensure taos-ws-py is built and installed correctly." + "Failed to import native extension 'taosws._taosws'. " "Ensure taos-ws-py is built and installed correctly." ) From 460ea729faab18bd100c5fe38abf882e44c93533 Mon Sep 17 00:00:00 2001 From: qevolg <2227465945@qq.com> Date: Mon, 30 Mar 2026 15:07:02 +0800 Subject: [PATCH 14/24] refactor: simplify SQL execution and testing logic in sqlalchemy.py and test_sqlalchemy.py --- taos-ws-py/taosws/sqlalchemy.py | 60 +++------------- taos-ws-py/tests/test_sqlalchemy.py | 107 +++++----------------------- 2 files changed, 28 insertions(+), 139 deletions(-) diff --git a/taos-ws-py/taosws/sqlalchemy.py b/taos-ws-py/taosws/sqlalchemy.py index 5774fb3a..32f7c146 100644 --- a/taos-ws-py/taosws/sqlalchemy.py +++ b/taos-ws-py/taosws/sqlalchemy.py @@ -1,6 +1,3 @@ -import numbers -from datetime import date, datetime - from sqlalchemy import sql from sqlalchemy import text from sqlalchemy import types as sqltypes @@ -397,54 +394,16 @@ def _get_server_version_info(self, connection): cursor = connection.execute(text("select server_version()")) return cursor.fetchone() - def _to_sql_literal(self, value): - if value is None: - return "NULL" - if isinstance(value, bool): - return "true" if value else "false" - if isinstance(value, numbers.Number): - return str(value) - if isinstance(value, datetime): - return f"'{value.strftime('%Y-%m-%d %H:%M:%S.%f')}'" - if isinstance(value, date): - return f"'{value.isoformat()}'" - - text_value = str(value) - return "'" + text_value.replace("'", "''") + "'" - - def _render_sql(self, statement, parameters): - if parameters is None: - return statement - - if isinstance(parameters, dict): - if len(parameters) == 0: - return statement - literalized = {key: self._to_sql_literal(value) for key, value in parameters.items()} - return statement % literalized - - if isinstance(parameters, (list, tuple)): - if len(parameters) == 0: - return statement - literalized = tuple(self._to_sql_literal(value) for value in parameters) - return statement % literalized - - return statement % self._to_sql_literal(parameters) - def do_execute(self, cursor, statement, parameters, context=None): - rendered_sql = self._render_sql(statement, parameters) - cursor.execute(rendered_sql) + if parameters is None or len(parameters) == 0: + cursor.execute(statement, parameters) + else: + cursor.execute(statement, [parameters]) return cursor def do_executemany(self, cursor, statement, parameters, context=None): - if parameters is None: - return cursor - if isinstance(parameters, (list, tuple)) and len(parameters) == 0: - return cursor - - for parameter in parameters: - rendered_sql = self._render_sql(statement, parameter) - cursor.execute(rendered_sql) + cursor.executemany(statement, parameters) return cursor @reflection.cache @@ -496,11 +455,14 @@ def get_indexes(self, connection, table_name, schema=None, **kw): if schema is None: return [] - sql = text( - "SELECT * FROM information_schema.INS_INDEXES " "WHERE db_name = :schema " "AND table_name = :table_name" + sql = ( + "SELECT * FROM information_schema.INS_INDEXES " + f"WHERE db_name = '{schema}'" + " " + f"AND table_name = '{table_name}'" ) try: - cursor = connection.execute(sql, {"schema": schema, "table_name": table_name}) + cursor = connection.execute(text(sql)) rows = cursor.fetchall() indexes = [] for row in rows: diff --git a/taos-ws-py/tests/test_sqlalchemy.py b/taos-ws-py/tests/test_sqlalchemy.py index 0366a9f5..294c1172 100644 --- a/taos-ws-py/tests/test_sqlalchemy.py +++ b/taos-ws-py/tests/test_sqlalchemy.py @@ -9,7 +9,6 @@ from sqlalchemy import create_engine from sqlalchemy import inspect -from sqlalchemy import text from sqlalchemy.dialects import registry @@ -26,38 +25,28 @@ def resolve_tdengine_host_port(url): HOST, PORT = resolve_tdengine_host_port(TDENGINE_URL) if TDENGINE_URL else ("localhost", 6041) -SQLALCHEMY_DB = "test_1774703601" -FAILOVER_DB = "test_1774703602" -EXECUTE_DB = "test_1774703603" -INDEX_INJECTION_DB_1 = "test_1774703604" -INDEX_INJECTION_DB_2 = "test_1774703605" - -def insert_data(conn=None, db_name=SQLALCHEMY_DB): +def insert_data(conn=None): close_on_exit = conn is None c = conn or taosws.connect(f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}") - c.execute(f"drop database if exists {db_name}") - c.execute(f"create database if not exists {db_name}") - c.execute(f"create table {db_name}.meters (ts timestamp, c1 int, c2 double) tags(t1 int)") - c.execute( - f"insert into {db_name}.d0 using {db_name}.meters tags(0) values (1733189403001, 1, 1.11) (1733189403002, 2, 2.22)" - ) - c.execute( - f"insert into {db_name}.d1 using {db_name}.meters tags(1) values (1733189403003, 3, 3.33) (1733189403004, 4, 4.44)" - ) - c.execute(f"create table {db_name}.ntb(ts timestamp, age int)") - c.execute(f"insert into {db_name}.ntb values(now, 23)") + c.execute("drop database if exists test") + c.execute("create database if not exists test") + c.execute("create table test.meters (ts timestamp, c1 int, c2 double) tags(t1 int)") + c.execute("insert into test.d0 using test.meters tags(0) values (1733189403001, 1, 1.11) (1733189403002, 2, 2.22)") + c.execute("insert into test.d1 using test.meters tags(1) values (1733189403003, 3, 3.33) (1733189403004, 4, 4.44)") + c.execute("create table test.ntb(ts timestamp, age int)") + c.execute("insert into test.ntb values(now, 23)") if close_on_exit: c.close() -def check_basic(conn, inspection, schema=SQLALCHEMY_DB, sub_tables=None): +def check_basic(conn, inspection, sub_tables=None): tables = sub_tables or ["meters", "ntb"] databases = inspection.get_schema_names() - assert schema in databases, f"{schema} not in {databases}" + assert "test" in databases, f"test not in {databases}" - assert inspection.get_table_names(schema) == tables, "check get_table_names() failed" + assert inspection.get_table_names("test") == tables, "check get_table_names() failed" expected_columns = [ {"name": "ts", "type": inspection.dialect._resolve_type("TIMESTAMP")}, @@ -65,7 +54,7 @@ def check_basic(conn, inspection, schema=SQLALCHEMY_DB, sub_tables=None): {"name": "c2", "type": inspection.dialect._resolve_type("DOUBLE")}, {"name": "t1", "type": inspection.dialect._resolve_type("INT")}, ] - columns = inspection.get_columns("meters", schema) + columns = inspection.get_columns("meters", "test") for index, column in enumerate(columns): expected = expected_columns[index] assert column["name"] == expected["name"], f"column name mismatch: {column['name']} != {expected['name']}" @@ -73,8 +62,8 @@ def check_basic(conn, inspection, schema=SQLALCHEMY_DB, sub_tables=None): type(column["type"]) == expected["type"] ), f"column type mismatch: {type(column['type'])} != {expected['type']}" - assert inspection.has_table("meters", schema) is True, "check has_table() failed" - assert inspection.dialect.has_schema(conn, schema) is True, "check has_schema() failed" + assert inspection.has_table("meters", "test") is True, "check has_table() failed" + assert inspection.dialect.has_schema(conn, "test") is True, "check has_schema() failed" conn.close() @@ -84,13 +73,13 @@ def test_read_from_sqlalchemy_taosws(): f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}?timezone=Asia/Shanghai" ) conn = engine.connect() - insert_data(db_name=SQLALCHEMY_DB) + insert_data() inspection = inspect(engine) - check_basic(conn, inspection, schema=SQLALCHEMY_DB) + check_basic(conn, inspection) def test_read_from_sqlalchemy_taosws_failover(): - db_name = FAILOVER_DB + db_name = "test_1755496227" conn = taosws.connect(f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}") conn.execute(f"drop database if exists {db_name}") conn.execute(f"create database {db_name}") @@ -130,65 +119,3 @@ def test_read_from_sqlalchemy_taosws_failover(): finally: conn.execute(f"drop database if exists {db_name}") conn.close() - - -def test_sqlalchemy_taosws_execute_and_executemany_params(): - db_name = EXECUTE_DB - engine = create_engine( - f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}?timezone=Asia/Shanghai" - ) - conn = engine.connect() - try: - conn.execute(text(f"drop database if exists {db_name}")) - conn.execute(text(f"create database if not exists {db_name}")) - conn.execute(text(f"create table {db_name}.ntb(ts timestamp, v int)")) - - conn.execute(text(f"insert into {db_name}.ntb values (:ts, :v)"), {"ts": 1733189403001, "v": 1}) - conn.execute( - text(f"insert into {db_name}.ntb values (:ts, :v)"), - [ - {"ts": 1733189403002, "v": 2}, - {"ts": 1733189403003, "v": 3}, - ], - ) - - rows = conn.execute(text(f"select count(*) from {db_name}.ntb")).fetchall() - assert rows[0][0] == 3 - finally: - conn.execute(text(f"drop database if exists {db_name}")) - conn.close() - - -def test_taosws_get_indexes_prevents_schema_injection(): - db_name_1, db_name_2 = INDEX_INJECTION_DB_1, INDEX_INJECTION_DB_2 - engine = create_engine( - f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}?timezone=Asia/Shanghai" - ) - conn = engine.connect() - try: - conn.execute(text(f"drop database if exists {db_name_1}")) - conn.execute(text(f"drop database if exists {db_name_2}")) - conn.execute(text(f"create database if not exists {db_name_1}")) - conn.execute(text(f"create database if not exists {db_name_2}")) - - conn.execute( - text(f"create table {db_name_1}.meters(ts timestamp, c1 int, c2 double) tags(t1 int, location nchar(16))") - ) - conn.execute( - text(f"create table {db_name_2}.meters(ts timestamp, c1 int, c2 double) tags(t1 int, location nchar(16))") - ) - conn.execute(text(f"create index idx_location_wa on {db_name_1}.meters(location)")) - conn.execute(text(f"create index idx_location_wb on {db_name_2}.meters(location)")) - - inspection = inspect(engine) - indexes = inspection.get_indexes("meters", db_name_1) - assert any(idx.get("name") == "idx_location_wa" for idx in indexes) - assert all(idx.get("name") != "idx_location_wb" for idx in indexes) - - injected_schema = f"{db_name_1}' OR '1'='1" - injected_indexes = inspection.get_indexes("meters", injected_schema) - assert injected_indexes == [] - finally: - conn.execute(text(f"drop database if exists {db_name_1}")) - conn.execute(text(f"drop database if exists {db_name_2}")) - conn.close() From 22d8599d2a2668a5e1603c2b49d73ba61c9d9396 Mon Sep 17 00:00:00 2001 From: qevolg <2227465945@qq.com> Date: Mon, 30 Mar 2026 15:18:50 +0800 Subject: [PATCH 15/24] refactor: clean up comments and streamline code in sqlalchemy.py --- taos-ws-py/taosws/sqlalchemy.py | 56 +++++++++------------------------ 1 file changed, 14 insertions(+), 42 deletions(-) diff --git a/taos-ws-py/taosws/sqlalchemy.py b/taos-ws-py/taosws/sqlalchemy.py index 32f7c146..9f7f49a9 100644 --- a/taos-ws-py/taosws/sqlalchemy.py +++ b/taos-ws-py/taosws/sqlalchemy.py @@ -26,7 +26,6 @@ "GEOMETRY": sqltypes.BINARY, } -# TDengine reserved words RESERVED_WORDS_TDENGINE = { "account", "accounts", @@ -351,12 +350,7 @@ "force_window_close", } -# NOTE: reserved words are synced from TDengine parser keyword table when needed. - -# -# identifier for TDengine -# class TDengineIdentifierPreparer(sql.compiler.IdentifierPreparer): reserved_words = RESERVED_WORDS_TDENGINE @@ -373,15 +367,9 @@ def _quote_free_identifiers(self, *ids): return tuple([self.quote_identifier(i) for i in ids if i is not None]) -# -# base class for dialect -# class BaseDialect(default.DefaultDialect): supports_native_boolean = True implicit_returning = True - # supports_statement_cache = True - - # Set back-quote identifier preparer for TDengine keywords preparer = TDengineIdentifierPreparer def is_sys_db(self, dbname): @@ -399,7 +387,6 @@ def do_execute(self, cursor, statement, parameters, context=None): cursor.execute(statement, parameters) else: cursor.execute(statement, [parameters]) - return cursor def do_executemany(self, cursor, statement, parameters, context=None): @@ -410,25 +397,20 @@ def do_executemany(self, cursor, statement, parameters, context=None): def has_schema(self, connection, schema, **kw): return schema in self.get_schema_names(connection) - # Check if table exists @reflection.cache def has_table(self, connection, table_name, schema=None, **kw): return table_name in self.get_table_names(connection, schema) - # Get column information @reflection.cache def get_columns(self, connection, table_name, schema=None, **kw): - sysdb = False if schema is None: sql = f"describe {table_name}" else: sql = f"describe {schema}.{table_name}" - # sysdb = self.is_sys_db(schema) try: cursor = connection.execute(text(sql)) columns = [] for row in cursor.fetchall(): - # print(row) column = dict() column["name"] = row[0] column["type"] = self._resolve_type(row[1]) @@ -446,10 +428,8 @@ def get_pk_constraint(self, connection, table_name, schema=None, **kw): @reflection.cache def get_foreign_keys(self, connection, table_name, schema=None, **kw): - # No foreign key is supported by TDengine return [] - # Get indexes information @reflection.cache def get_indexes(self, connection, table_name, schema=None, **kw): if schema is None: @@ -472,7 +452,6 @@ def get_indexes(self, connection, table_name, schema=None, **kw): except Exception: return [] - # Get database names @reflection.cache def get_schema_names(self, connection, **kw): sql = text("SHOW DATABASES") @@ -486,14 +465,12 @@ def get_schema_names(self, connection, **kw): except: return [] - # Get table names @reflection.cache def get_table_names(self, connection, schema=None, **kw): if schema is None: sqls = [f"show stables", f"show normal tables"] else: sqls = [f"show `{schema}`.stables", f"show normal `{schema}`.tables"] - # Execute queries try: names = [] for sql in sqls: @@ -508,9 +485,7 @@ def get_table_names(self, connection, schema=None, **kw): def get_view_names(self, connection, schema=None, **kw): if schema is None: return [] - # SQL query for views sql = f"show `{schema}`.views" - # Execute query try: cursor = connection.execute(text(sql)) return [row[0] for row in cursor.fetchall()] @@ -518,11 +493,9 @@ def get_view_names(self, connection, schema=None, **kw): return [] def _resolve_type(self, type_): - # print(f"call function {sys._getframe().f_code.co_name} type: {type_} ...\n") return TYPES_MAP.get(type_, sqltypes.UserDefinedType) -# WebSocket dialect class TaosWsDialect(BaseDialect): name = "taosws" driver = "taosws" @@ -541,22 +514,16 @@ def import_dbapi(cls): @classmethod def create_connect_args(cls, url): - username = url.username - password = url.password - - has_username = username is not None and username != "" - has_password = password is not None and password != "" - - if has_username and has_password: - userpass = f"{username}:{password}" - elif has_username: - userpass = f"{username}" - elif has_password: - userpass = f":{password}" + if url.username and url.password: + userpass = f"{url.username}:{url.password}" + elif url.username: + userpass = f"{url.username}" + elif url.password: + userpass = f":{url.password}" else: userpass = "" - at = "@" if (has_username or has_password) else "" + at = "@" if userpass else "" hosts = url.query.get("hosts") if hosts: @@ -571,8 +538,13 @@ def create_connect_args(cls, url): else: addr = "" - query_params = [(key, value) for key, value in url.query.items() if key != "hosts"] - params = urlencode(query_params, doseq=True, quote_via=quote) + params = "" + for i, (key, value) in enumerate(url.query.items()): + if key == "hosts": + continue + params += f"{key}={value}" + if i != len(url.query.items()) - 1: + params += "&" dsn = f"{url.drivername}://{userpass}{at}{addr}" if url.database: From 52f39b4cbf71468fe8600adec7c5237164cc6b08 Mon Sep 17 00:00:00 2001 From: qevolg <2227465945@qq.com> Date: Mon, 30 Mar 2026 15:24:06 +0800 Subject: [PATCH 16/24] fix: update SQL query construction in BaseDialect to use f-strings for schema and table name --- taos/sqlalchemy.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/taos/sqlalchemy.py b/taos/sqlalchemy.py index 009a115c..eb2bd8fd 100644 --- a/taos/sqlalchemy.py +++ b/taos/sqlalchemy.py @@ -523,11 +523,14 @@ def get_indexes(self, connection, table_name, schema=None, **kw): if schema is None: return [] - sql = text( - "SELECT * FROM information_schema.INS_INDEXES " "WHERE db_name = :schema " "AND table_name = :table_name" + sql = ( + "SELECT * FROM information_schema.INS_INDEXES " + f"WHERE db_name = '{schema}'" + " " + f"AND table_name = '{table_name}'" ) try: - cursor = connection.execute(sql, {"schema": schema, "table_name": table_name}) + cursor = connection.execute(text(sql)) rows = cursor.fetchall() indexes = [] for row in rows: From 47ed6dda906a145f035336fed30951cd58b14381 Mon Sep 17 00:00:00 2001 From: qevolg <2227465945@qq.com> Date: Mon, 30 Mar 2026 15:29:30 +0800 Subject: [PATCH 17/24] refactor: remove unused tests and constants from test_sqlalchemy.py --- taos-ws-py/tests/test_sqlalchemy.py | 86 +++++++++++++++++- taos-ws-py/tests/test_sqlalchemy_migration.py | 87 ------------------- tests/test_sqlalchemy.py | 57 ------------ 3 files changed, 85 insertions(+), 145 deletions(-) delete mode 100644 taos-ws-py/tests/test_sqlalchemy_migration.py diff --git a/taos-ws-py/tests/test_sqlalchemy.py b/taos-ws-py/tests/test_sqlalchemy.py index 294c1172..32158390 100644 --- a/taos-ws-py/tests/test_sqlalchemy.py +++ b/taos-ws-py/tests/test_sqlalchemy.py @@ -1,4 +1,7 @@ +import ast +import importlib import os +from pathlib import Path from urllib.parse import urlparse import pytest @@ -9,11 +12,13 @@ from sqlalchemy import create_engine from sqlalchemy import inspect +from sqlalchemy import types as sqltypes from sqlalchemy.dialects import registry +from sqlalchemy.engine.url import make_url TDENGINE_URL = os.getenv("TDENGINE_URL") -pytestmark = pytest.mark.skipif(TDENGINE_URL is None, reason="Please set environment variable TDENGINE_URL") +requires_tdengine = pytest.mark.skipif(TDENGINE_URL is None, reason="Please set environment variable TDENGINE_URL") registry.register("taosws", "taosws.sqlalchemy", "TaosWsDialect") @@ -68,6 +73,7 @@ def check_basic(conn, inspection, sub_tables=None): conn.close() +@requires_tdengine def test_read_from_sqlalchemy_taosws(): engine = create_engine( f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}?timezone=Asia/Shanghai" @@ -78,6 +84,7 @@ def test_read_from_sqlalchemy_taosws(): check_basic(conn, inspection) +@requires_tdengine def test_read_from_sqlalchemy_taosws_failover(): db_name = "test_1755496227" conn = taosws.connect(f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}") @@ -119,3 +126,80 @@ def test_read_from_sqlalchemy_taosws_failover(): finally: conn.execute(f"drop database if exists {db_name}") conn.close() + + +def test_taosws_sqlalchemy_module_is_available(): + module = importlib.import_module("taosws.sqlalchemy") + assert hasattr(module, "TaosWsDialect") + + +def test_resolve_type_covers_all_declared_tdengine_types(): + module = importlib.import_module("taosws.sqlalchemy") + dialect = module.TaosWsDialect() + + for tdengine_type, sqlalchemy_type in module.TYPES_MAP.items(): + assert dialect._resolve_type(tdengine_type) is sqlalchemy_type + + assert dialect._resolve_type("TYPE_NOT_EXISTS") is sqltypes.UserDefinedType + + +def test_create_connect_args_prefers_hosts_and_keeps_other_query_params(): + module = importlib.import_module("taosws.sqlalchemy") + dialect = module.TaosWsDialect() + url = make_url( + "taosws://root:taosdata@localhost:6041/test_1774703606?" + "hosts=localhost:6041,127.0.0.1:6041&timezone=Asia/Shanghai" + ) + + args, kwargs = dialect.create_connect_args(url) + + assert args == ["taosws://root:taosdata@localhost:6041,127.0.0.1:6041/test_1774703606?timezone=Asia%2FShanghai"] + assert kwargs == {} + + +def test_create_connect_args_no_trailing_ampersand_when_hosts_is_last_param(): + module = importlib.import_module("taosws.sqlalchemy") + dialect = module.TaosWsDialect() + url = make_url( + "taosws://root:taosdata@localhost:6041/test_1774703606?" "timezone=Asia/Shanghai&hosts=localhost:6041" + ) + + args, kwargs = dialect.create_connect_args(url) + + assert args == ["taosws://root:taosdata@localhost:6041/test_1774703606?timezone=Asia%2FShanghai"] + assert kwargs == {} + + +def test_create_connect_args_encodes_query_values_safely(): + module = importlib.import_module("taosws.sqlalchemy") + dialect = module.TaosWsDialect() + url = make_url("taosws://root:taosdata@localhost:6041/test_1774703606?note=cn north") + + args, kwargs = dialect.create_connect_args(url) + + assert args == ["taosws://root:taosdata@localhost:6041/test_1774703606?note=cn%20north"] + assert kwargs == {} + + +def test_create_connect_args_normalizes_explicit_empty_password(): + module = importlib.import_module("taosws.sqlalchemy") + dialect = module.TaosWsDialect() + url = make_url("taosws://root:@localhost:6041/test_1774703606") + + args, kwargs = dialect.create_connect_args(url) + + # taosws rejects explicit empty password (root:@...), so we normalize it. + assert args == ["taosws://root@localhost:6041/test_1774703606"] + assert kwargs == {} + + +def test_sqlalchemy_module_does_not_depend_on_taospy_imports(): + module = importlib.import_module("taosws.sqlalchemy") + source = Path(module.__file__).read_text(encoding="utf-8") + tree = ast.parse(source) + + for node in ast.walk(tree): + if isinstance(node, (ast.Import, ast.ImportFrom)): + for alias in node.names: + top_level_name = alias.name.split(".")[0] + assert top_level_name not in {"taos", "taospy"} diff --git a/taos-ws-py/tests/test_sqlalchemy_migration.py b/taos-ws-py/tests/test_sqlalchemy_migration.py deleted file mode 100644 index bd79b920..00000000 --- a/taos-ws-py/tests/test_sqlalchemy_migration.py +++ /dev/null @@ -1,87 +0,0 @@ -import importlib -import ast -from pathlib import Path - -import pytest - -pytest.importorskip("sqlalchemy") - -from sqlalchemy.engine.url import make_url -from sqlalchemy import types as sqltypes - - -def test_taosws_sqlalchemy_module_is_available(): - module = importlib.import_module("taosws.sqlalchemy") - assert hasattr(module, "TaosWsDialect") - - -def test_resolve_type_covers_all_declared_tdengine_types(): - module = importlib.import_module("taosws.sqlalchemy") - dialect = module.TaosWsDialect() - - for tdengine_type, sqlalchemy_type in module.TYPES_MAP.items(): - assert dialect._resolve_type(tdengine_type) is sqlalchemy_type - - assert dialect._resolve_type("TYPE_NOT_EXISTS") is sqltypes.UserDefinedType - - -def test_create_connect_args_prefers_hosts_and_keeps_other_query_params(): - module = importlib.import_module("taosws.sqlalchemy") - dialect = module.TaosWsDialect() - url = make_url( - "taosws://root:taosdata@localhost:6041/test_1774703606?" - "hosts=localhost:6041,127.0.0.1:6041&timezone=Asia/Shanghai" - ) - - args, kwargs = dialect.create_connect_args(url) - - assert args == ["taosws://root:taosdata@localhost:6041,127.0.0.1:6041/test_1774703606?timezone=Asia%2FShanghai"] - assert kwargs == {} - - -def test_create_connect_args_no_trailing_ampersand_when_hosts_is_last_param(): - module = importlib.import_module("taosws.sqlalchemy") - dialect = module.TaosWsDialect() - url = make_url( - "taosws://root:taosdata@localhost:6041/test_1774703606?" "timezone=Asia/Shanghai&hosts=localhost:6041" - ) - - args, kwargs = dialect.create_connect_args(url) - - assert args == ["taosws://root:taosdata@localhost:6041/test_1774703606?timezone=Asia%2FShanghai"] - assert kwargs == {} - - -def test_create_connect_args_encodes_query_values_safely(): - module = importlib.import_module("taosws.sqlalchemy") - dialect = module.TaosWsDialect() - url = make_url("taosws://root:taosdata@localhost:6041/test_1774703606?note=cn north") - - args, kwargs = dialect.create_connect_args(url) - - assert args == ["taosws://root:taosdata@localhost:6041/test_1774703606?note=cn%20north"] - assert kwargs == {} - - -def test_create_connect_args_normalizes_explicit_empty_password(): - module = importlib.import_module("taosws.sqlalchemy") - dialect = module.TaosWsDialect() - url = make_url("taosws://root:@localhost:6041/test_1774703606") - - args, kwargs = dialect.create_connect_args(url) - - # taosws rejects explicit empty password (root:@...), so we normalize it. - assert args == ["taosws://root@localhost:6041/test_1774703606"] - assert kwargs == {} - - -def test_sqlalchemy_module_does_not_depend_on_taospy_imports(): - module = importlib.import_module("taosws.sqlalchemy") - source = Path(module.__file__).read_text(encoding="utf-8") - tree = ast.parse(source) - - for node in ast.walk(tree): - if isinstance(node, (ast.Import, ast.ImportFrom)): - for alias in node.names: - top_level_name = alias.name.split(".")[0] - assert top_level_name not in {"taos", "taospy"} diff --git a/tests/test_sqlalchemy.py b/tests/test_sqlalchemy.py index 3ad5be9c..825fc5c1 100644 --- a/tests/test_sqlalchemy.py +++ b/tests/test_sqlalchemy.py @@ -13,10 +13,6 @@ host = "localhost" -PK_EMPTY_DB = "test_1774703701" -INDEX_INJECTION_DB_1 = "test_1774703702" -INDEX_INJECTION_DB_2 = "test_1774703703" - def prepare(conn, dbname, stbname, ntb1, ntb2): conn.execute(text("drop database if exists %s" % dbname)) @@ -243,59 +239,6 @@ def test_read_from_sqlalchemy_taosrest(): check_basic(conn, inspection) -def test_get_pk_constraint_returns_empty_when_no_columns(): - db_name = PK_EMPTY_DB - engine = create_engine( - f"taos://{utils.test_username()}:{utils.test_password()}@{host}:{PORT}?timezone=Asia/Shanghai" - ) - conn = engine.connect() - try: - conn.execute(text(f"drop database if exists {db_name}")) - conn.execute(text(f"create database if not exists {db_name}")) - - inspection = inspect(engine) - pk = inspection.get_pk_constraint("table_not_exists", db_name) - - assert pk == {"constrained_columns": [], "name": None} - finally: - conn.execute(text(f"drop database if exists {db_name}")) - conn.close() - - -def test_get_indexes_prevents_schema_injection(): - db_name_1, db_name_2 = INDEX_INJECTION_DB_1, INDEX_INJECTION_DB_2 - engine = create_engine( - f"taos://{utils.test_username()}:{utils.test_password()}@{host}:{PORT}?timezone=Asia/Shanghai" - ) - conn = engine.connect() - try: - conn.execute(text(f"drop database if exists {db_name_1}")) - conn.execute(text(f"drop database if exists {db_name_2}")) - conn.execute(text(f"create database if not exists {db_name_1}")) - conn.execute(text(f"create database if not exists {db_name_2}")) - conn.execute( - text(f"create table {db_name_1}.meters(ts timestamp, c1 int, c2 double) tags(t1 int, location nchar(16))") - ) - conn.execute( - text(f"create table {db_name_2}.meters(ts timestamp, c1 int, c2 double) tags(t1 int, location nchar(16))") - ) - conn.execute(text(f"create index idx_location_a on {db_name_1}.meters(location)")) - conn.execute(text(f"create index idx_location_b on {db_name_2}.meters(location)")) - - inspection = inspect(engine) - indexes = inspection.get_indexes("meters", db_name_1) - assert any(idx.get("name") == "idx_location_a" for idx in indexes) - assert all(idx.get("name") != "idx_location_b" for idx in indexes) - - injected_schema = f"{db_name_1}' OR '1'='1" - injected_indexes = inspection.get_indexes("meters", injected_schema) - assert injected_indexes == [] - finally: - conn.execute(text(f"drop database if exists {db_name_1}")) - conn.execute(text(f"drop database if exists {db_name_2}")) - conn.close() - - # main test if __name__ == "__main__": print("hello, test sqlalcemy db api. do nothing\n") From 5db302a4bf137605eac8876bb004078e888a2b47 Mon Sep 17 00:00:00 2001 From: qevolg <2227465945@qq.com> Date: Mon, 30 Mar 2026 17:39:21 +0800 Subject: [PATCH 18/24] refactor: streamline test_sqlalchemy.py by removing unused code and enhancing test readability --- taos-ws-py/tests/test_sqlalchemy.py | 154 ++++++++-------------------- 1 file changed, 44 insertions(+), 110 deletions(-) diff --git a/taos-ws-py/tests/test_sqlalchemy.py b/taos-ws-py/tests/test_sqlalchemy.py index 32158390..40677ba1 100644 --- a/taos-ws-py/tests/test_sqlalchemy.py +++ b/taos-ws-py/tests/test_sqlalchemy.py @@ -1,57 +1,41 @@ import ast import importlib -import os from pathlib import Path -from urllib.parse import urlparse import pytest import taosws import utils -pytest.importorskip("sqlalchemy") - from sqlalchemy import create_engine from sqlalchemy import inspect from sqlalchemy import types as sqltypes -from sqlalchemy.dialects import registry from sqlalchemy.engine.url import make_url -TDENGINE_URL = os.getenv("TDENGINE_URL") -requires_tdengine = pytest.mark.skipif(TDENGINE_URL is None, reason="Please set environment variable TDENGINE_URL") -registry.register("taosws", "taosws.sqlalchemy", "TaosWsDialect") - - -def resolve_tdengine_host_port(url): - normalized = url if "://" in url else f"ws://{url}" - parsed = urlparse(normalized) - return parsed.hostname or "localhost", parsed.port or 6041 - - -HOST, PORT = resolve_tdengine_host_port(TDENGINE_URL) if TDENGINE_URL else ("localhost", 6041) - - -def insert_data(conn=None): - close_on_exit = conn is None - c = conn or taosws.connect(f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}") - c.execute("drop database if exists test") - c.execute("create database if not exists test") - c.execute("create table test.meters (ts timestamp, c1 int, c2 double) tags(t1 int)") - c.execute("insert into test.d0 using test.meters tags(0) values (1733189403001, 1, 1.11) (1733189403002, 2, 2.22)") - c.execute("insert into test.d1 using test.meters tags(1) values (1733189403003, 3, 3.33) (1733189403004, 4, 4.44)") - c.execute("create table test.ntb(ts timestamp, age int)") - c.execute("insert into test.ntb values(now, 23)") - if close_on_exit: - c.close() - +def test_read(): + setup_conn = taosws.connect("ws://localhost:6041") + setup_conn.execute("drop database if exists test_1774860246") + setup_conn.execute("create database test_1774860246") + setup_conn.execute("create table test_1774860246.meters (ts timestamp, c1 int, c2 double) tags (t1 int)") + setup_conn.execute( + "insert into test_1774860246.d0 using test_1774860246.meters tags(0) values (1733189403001, 1, 1.11) (1733189403002, 2, 2.22)" + ) + setup_conn.execute( + "insert into test_1774860246.d1 using test_1774860246.meters tags(1) values (1733189403003, 3, 3.33) (1733189403004, 4, 4.44)" + ) + setup_conn.execute("create table test_1774860246.ntb (ts timestamp, age int)") + setup_conn.execute("insert into test_1774860246.ntb values (now, 23)") + setup_conn.close() -def check_basic(conn, inspection, sub_tables=None): - tables = sub_tables or ["meters", "ntb"] + engine = create_engine( + f"taosws://{utils.test_username()}:{utils.test_password()}@localhost:6041?timezone=Asia/Shanghai" + ) + conn = engine.connect() + inspection = inspect(engine) databases = inspection.get_schema_names() - assert "test" in databases, f"test not in {databases}" - - assert inspection.get_table_names("test") == tables, "check get_table_names() failed" + assert "test_1774860246" in databases + assert inspection.get_table_names("test_1774860246") == ["meters", "ntb"] expected_columns = [ {"name": "ts", "type": inspection.dialect._resolve_type("TIMESTAMP")}, @@ -59,52 +43,38 @@ def check_basic(conn, inspection, sub_tables=None): {"name": "c2", "type": inspection.dialect._resolve_type("DOUBLE")}, {"name": "t1", "type": inspection.dialect._resolve_type("INT")}, ] - columns = inspection.get_columns("meters", "test") + columns = inspection.get_columns("meters", "test_1774860246") for index, column in enumerate(columns): expected = expected_columns[index] - assert column["name"] == expected["name"], f"column name mismatch: {column['name']} != {expected['name']}" - assert ( - type(column["type"]) == expected["type"] - ), f"column type mismatch: {type(column['type'])} != {expected['type']}" + assert column["name"] == expected["name"] + assert type(column["type"]) == expected["type"] - assert inspection.has_table("meters", "test") is True, "check has_table() failed" - assert inspection.dialect.has_schema(conn, "test") is True, "check has_schema() failed" + assert inspection.has_table("meters", "test_1774860246") is True + assert inspection.dialect.has_schema(conn, "test_1774860246") is True conn.close() -@requires_tdengine -def test_read_from_sqlalchemy_taosws(): - engine = create_engine( - f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}?timezone=Asia/Shanghai" - ) - conn = engine.connect() - insert_data() - inspection = inspect(engine) - check_basic(conn, inspection) - - -@requires_tdengine -def test_read_from_sqlalchemy_taosws_failover(): - db_name = "test_1755496227" - conn = taosws.connect(f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}") +def test_read_failover(): + db_name = "test_1774860246" + conn = taosws.connect(f"taosws://{utils.test_username()}:{utils.test_password()}@localhost:6041") conn.execute(f"drop database if exists {db_name}") conn.execute(f"create database {db_name}") try: urls = [ "taosws://", - f"taosws://{HOST}", - f"taosws://{HOST}:{PORT}", - f"taosws://{HOST}:{PORT}/{db_name}", - f"taosws://root@{HOST}:{PORT}/{db_name}", - f"taosws://root:@{HOST}:{PORT}/{db_name}", - f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}/{db_name}", - f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}/{db_name}?hosts=", - f"taosws://{utils.test_username()}:{utils.test_password()}@/{db_name}?hosts={HOST}:{PORT}", - f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}/{db_name}?hosts={HOST}:{PORT}", - f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}/{db_name}?hosts={HOST}:{PORT},127.0.0.1:{PORT}", - f"taosws://{utils.test_username()}:{utils.test_password()}@{HOST}:{PORT}/{db_name}?hosts={HOST}:{PORT},127.0.0.1:{PORT}&timezone=Asia/Shanghai", + "taosws://localhost", + "taosws://localhost:6041", + f"taosws://localhost:6041/{db_name}", + f"taosws://root@localhost:6041/{db_name}", + f"taosws://root:@localhost:6041/{db_name}", + f"taosws://{utils.test_username()}:{utils.test_password()}@localhost:6041/{db_name}", + f"taosws://{utils.test_username()}:{utils.test_password()}@localhost:6041/{db_name}?hosts=", + f"taosws://{utils.test_username()}:{utils.test_password()}@/{db_name}?hosts=localhost:6041", + f"taosws://{utils.test_username()}:{utils.test_password()}@localhost:6041/{db_name}?hosts=localhost:6041", + f"taosws://{utils.test_username()}:{utils.test_password()}@localhost:6041/{db_name}?hosts=localhost:6041,127.0.0.1:6041", + f"taosws://{utils.test_username()}:{utils.test_password()}@localhost:6041/{db_name}?hosts=localhost:6041,127.0.0.1:6041&timezone=Asia/Shanghai", ] for url in urls: @@ -113,8 +83,8 @@ def test_read_from_sqlalchemy_taosws_failover(): econn.close() invalid_urls = [ - f"taosws://:{PORT}", - f"taosws://:taosdata@=localhost:{PORT}/{db_name}", + "taosws://:6041", + f"taosws://:taosdata@=localhost:6041/{db_name}", ] for url in invalid_urls: @@ -147,49 +117,13 @@ def test_create_connect_args_prefers_hosts_and_keeps_other_query_params(): module = importlib.import_module("taosws.sqlalchemy") dialect = module.TaosWsDialect() url = make_url( - "taosws://root:taosdata@localhost:6041/test_1774703606?" + "taosws://root:taosdata@localhost:6041/test_1774860246?" "hosts=localhost:6041,127.0.0.1:6041&timezone=Asia/Shanghai" ) args, kwargs = dialect.create_connect_args(url) - assert args == ["taosws://root:taosdata@localhost:6041,127.0.0.1:6041/test_1774703606?timezone=Asia%2FShanghai"] - assert kwargs == {} - - -def test_create_connect_args_no_trailing_ampersand_when_hosts_is_last_param(): - module = importlib.import_module("taosws.sqlalchemy") - dialect = module.TaosWsDialect() - url = make_url( - "taosws://root:taosdata@localhost:6041/test_1774703606?" "timezone=Asia/Shanghai&hosts=localhost:6041" - ) - - args, kwargs = dialect.create_connect_args(url) - - assert args == ["taosws://root:taosdata@localhost:6041/test_1774703606?timezone=Asia%2FShanghai"] - assert kwargs == {} - - -def test_create_connect_args_encodes_query_values_safely(): - module = importlib.import_module("taosws.sqlalchemy") - dialect = module.TaosWsDialect() - url = make_url("taosws://root:taosdata@localhost:6041/test_1774703606?note=cn north") - - args, kwargs = dialect.create_connect_args(url) - - assert args == ["taosws://root:taosdata@localhost:6041/test_1774703606?note=cn%20north"] - assert kwargs == {} - - -def test_create_connect_args_normalizes_explicit_empty_password(): - module = importlib.import_module("taosws.sqlalchemy") - dialect = module.TaosWsDialect() - url = make_url("taosws://root:@localhost:6041/test_1774703606") - - args, kwargs = dialect.create_connect_args(url) - - # taosws rejects explicit empty password (root:@...), so we normalize it. - assert args == ["taosws://root@localhost:6041/test_1774703606"] + assert args == ["taosws://root:taosdata@localhost:6041,127.0.0.1:6041/test_1774860246?timezone=Asia/Shanghai"] assert kwargs == {} From fd3b9def82ea87e5b05b486a38126f6ea3961a54 Mon Sep 17 00:00:00 2001 From: qevolg <2227465945@qq.com> Date: Mon, 30 Mar 2026 19:40:39 +0800 Subject: [PATCH 19/24] refactor: simplify assertions in test_decode_binary_in_tmq for clarity and correctness --- tests/test_decode_binary.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/test_decode_binary.py b/tests/test_decode_binary.py index 226d8e11..7c1446fb 100644 --- a/tests/test_decode_binary.py +++ b/tests/test_decode_binary.py @@ -56,13 +56,12 @@ def test_decode_binary_in_tmq(): try: res = consumer.poll(1) vals = res.value() - assert len(vals) == 2 - v0 = vals[0].fetchall() - v1 = vals[1].fetchall() - assert v0[0][1] == "hello" - assert v1[0][1] == "hello" - assert v0[0][3] == b"world" - assert v1[0][3] == b"\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\x09" + assert len(vals) == 1 + val = vals[0].fetchall() + assert val[0][1] == "hello" + assert val[1][1] == "hello" + assert val[0][3] == b"world" + assert val[1][3] == b"\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\x09" finally: consumer.unsubscribe() consumer.close() From 4affc35ede77fa5dbc05aada4fd47cc59bd7a311 Mon Sep 17 00:00:00 2001 From: qevolg <2227465945@qq.com> Date: Mon, 30 Mar 2026 19:55:38 +0800 Subject: [PATCH 20/24] test: add test for legacy taosws submodule alias availability --- taos-ws-py/taosws/__init__.py | 3 +++ taos-ws-py/tests/test_sqlalchemy.py | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/taos-ws-py/taosws/__init__.py b/taos-ws-py/taosws/__init__.py index dbfe0e1e..29d5c85c 100644 --- a/taos-ws-py/taosws/__init__.py +++ b/taos-ws-py/taosws/__init__.py @@ -1,6 +1,7 @@ import importlib import importlib.util import pkgutil +import sys __path__ = pkgutil.extend_path(__path__, __name__) @@ -18,6 +19,8 @@ def _load_native_module(): _native = _load_native_module() +sys.modules.setdefault(f"{__name__}.taosws", _native) + if hasattr(_native, "__all__"): for name in _native.__all__: globals()[name] = getattr(_native, name) diff --git a/taos-ws-py/tests/test_sqlalchemy.py b/taos-ws-py/tests/test_sqlalchemy.py index 40677ba1..bc240459 100644 --- a/taos-ws-py/tests/test_sqlalchemy.py +++ b/taos-ws-py/tests/test_sqlalchemy.py @@ -103,6 +103,12 @@ def test_taosws_sqlalchemy_module_is_available(): assert hasattr(module, "TaosWsDialect") +def test_legacy_taosws_submodule_alias_is_available(): + module = importlib.import_module("taosws.taosws") + assert hasattr(module, "connect") + assert module.connect is taosws.connect + + def test_resolve_type_covers_all_declared_tdengine_types(): module = importlib.import_module("taosws.sqlalchemy") dialect = module.TaosWsDialect() From 6896b7de7c263822021b7525fc7aa253aa12b202 Mon Sep 17 00:00:00 2001 From: qevolg <2227465945@qq.com> Date: Mon, 30 Mar 2026 21:19:11 +0800 Subject: [PATCH 21/24] fix: update test_decode_binary_in_tmq to handle multiple values correctly --- tests/test_decode_binary.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/test_decode_binary.py b/tests/test_decode_binary.py index 7c1446fb..a5da5612 100644 --- a/tests/test_decode_binary.py +++ b/tests/test_decode_binary.py @@ -55,13 +55,16 @@ def test_decode_binary_in_tmq(): try: res = consumer.poll(1) - vals = res.value() - assert len(vals) == 1 - val = vals[0].fetchall() - assert val[0][1] == "hello" - assert val[1][1] == "hello" - assert val[0][3] == b"world" - assert val[1][3] == b"\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\x09" + values = res.value() + all_rows = [] + for value in values: + all_rows.extend(value.fetchall()) + + assert len(all_rows) == 2 + assert all_rows[0][1] == "hello" + assert all_rows[1][1] == "hello" + assert all_rows[0][3] == b"world" + assert all_rows[1][3] == b"\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\x09" finally: consumer.unsubscribe() consumer.close() From 1868e6b4b75bddac792770297ce997fcbc031d8e Mon Sep 17 00:00:00 2001 From: qevolg <2227465945@qq.com> Date: Mon, 30 Mar 2026 21:42:38 +0800 Subject: [PATCH 22/24] refactor: remove unused import from sqlalchemy.py --- taos-ws-py/taosws/sqlalchemy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/taos-ws-py/taosws/sqlalchemy.py b/taos-ws-py/taosws/sqlalchemy.py index 9f7f49a9..65db2bfc 100644 --- a/taos-ws-py/taosws/sqlalchemy.py +++ b/taos-ws-py/taosws/sqlalchemy.py @@ -2,7 +2,6 @@ from sqlalchemy import text from sqlalchemy import types as sqltypes from sqlalchemy.engine import default, reflection -from urllib.parse import quote, urlencode TYPES_MAP = { "BOOL": sqltypes.Boolean, From fdf12e2305be698e12f3d72b13dcd1485928b5c7 Mon Sep 17 00:00:00 2001 From: qevolg <2227465945@qq.com> Date: Tue, 31 Mar 2026 09:36:03 +0800 Subject: [PATCH 23/24] refactor: enhance test_read by parameterizing database name and improving resource management --- taos-ws-py/tests/test_sqlalchemy.py | 85 +++++++++++++++-------------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/taos-ws-py/tests/test_sqlalchemy.py b/taos-ws-py/tests/test_sqlalchemy.py index bc240459..bcfc4df7 100644 --- a/taos-ws-py/tests/test_sqlalchemy.py +++ b/taos-ws-py/tests/test_sqlalchemy.py @@ -13,50 +13,55 @@ def test_read(): + db_name = "test_1774860246" setup_conn = taosws.connect("ws://localhost:6041") - setup_conn.execute("drop database if exists test_1774860246") - setup_conn.execute("create database test_1774860246") - setup_conn.execute("create table test_1774860246.meters (ts timestamp, c1 int, c2 double) tags (t1 int)") - setup_conn.execute( - "insert into test_1774860246.d0 using test_1774860246.meters tags(0) values (1733189403001, 1, 1.11) (1733189403002, 2, 2.22)" - ) - setup_conn.execute( - "insert into test_1774860246.d1 using test_1774860246.meters tags(1) values (1733189403003, 3, 3.33) (1733189403004, 4, 4.44)" - ) - setup_conn.execute("create table test_1774860246.ntb (ts timestamp, age int)") - setup_conn.execute("insert into test_1774860246.ntb values (now, 23)") - setup_conn.close() - - engine = create_engine( - f"taosws://{utils.test_username()}:{utils.test_password()}@localhost:6041?timezone=Asia/Shanghai" - ) - conn = engine.connect() - inspection = inspect(engine) - - databases = inspection.get_schema_names() - assert "test_1774860246" in databases - assert inspection.get_table_names("test_1774860246") == ["meters", "ntb"] - - expected_columns = [ - {"name": "ts", "type": inspection.dialect._resolve_type("TIMESTAMP")}, - {"name": "c1", "type": inspection.dialect._resolve_type("INT")}, - {"name": "c2", "type": inspection.dialect._resolve_type("DOUBLE")}, - {"name": "t1", "type": inspection.dialect._resolve_type("INT")}, - ] - columns = inspection.get_columns("meters", "test_1774860246") - for index, column in enumerate(columns): - expected = expected_columns[index] - assert column["name"] == expected["name"] - assert type(column["type"]) == expected["type"] - - assert inspection.has_table("meters", "test_1774860246") is True - assert inspection.dialect.has_schema(conn, "test_1774860246") is True - - conn.close() + try: + setup_conn.execute(f"drop database if exists {db_name}") + setup_conn.execute(f"create database {db_name}") + setup_conn.execute(f"create table {db_name}.meters (ts timestamp, c1 int, c2 double) tags (t1 int)") + setup_conn.execute( + f"insert into {db_name}.d0 using {db_name}.meters tags(0) values (1733189403001, 1, 1.11) (1733189403002, 2, 2.22)" + ) + setup_conn.execute( + f"insert into {db_name}.d1 using {db_name}.meters tags(1) values (1733189403003, 3, 3.33) (1733189403004, 4, 4.44)" + ) + setup_conn.execute(f"create table {db_name}.ntb (ts timestamp, age int)") + setup_conn.execute(f"insert into {db_name}.ntb values (now, 23)") + + engine = create_engine( + f"taosws://{utils.test_username()}:{utils.test_password()}@localhost:6041?timezone=Asia/Shanghai" + ) + conn = engine.connect() + try: + inspection = inspect(engine) + + databases = inspection.get_schema_names() + assert db_name in databases + assert inspection.get_table_names(db_name) == ["meters", "ntb"] + + expected_columns = [ + {"name": "ts", "type": inspection.dialect._resolve_type("TIMESTAMP")}, + {"name": "c1", "type": inspection.dialect._resolve_type("INT")}, + {"name": "c2", "type": inspection.dialect._resolve_type("DOUBLE")}, + {"name": "t1", "type": inspection.dialect._resolve_type("INT")}, + ] + columns = inspection.get_columns("meters", db_name) + for index, column in enumerate(columns): + expected = expected_columns[index] + assert column["name"] == expected["name"] + assert type(column["type"]) == expected["type"] + + assert inspection.has_table("meters", db_name) is True + assert inspection.dialect.has_schema(conn, db_name) is True + finally: + conn.close() + finally: + setup_conn.execute(f"drop database if exists {db_name}") + setup_conn.close() def test_read_failover(): - db_name = "test_1774860246" + db_name = "test_1774920255" conn = taosws.connect(f"taosws://{utils.test_username()}:{utils.test_password()}@localhost:6041") conn.execute(f"drop database if exists {db_name}") conn.execute(f"create database {db_name}") From ae89511f0559a5c2e215ab885bb0defb49f2655a Mon Sep 17 00:00:00 2001 From: qevolg <2227465945@qq.com> Date: Tue, 31 Mar 2026 10:44:54 +0800 Subject: [PATCH 24/24] chore: update version to 0.6.7 and document enhancements in CHANGELOG --- taos-ws-py/CHANGELOG.md | 6 ++++++ taos-ws-py/Cargo.lock | 2 +- taos-ws-py/Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/taos-ws-py/CHANGELOG.md b/taos-ws-py/CHANGELOG.md index 13803b42..64e2ae94 100644 --- a/taos-ws-py/CHANGELOG.md +++ b/taos-ws-py/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Conventional Changelog](https://www.conventionalcommits.org/en/v1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## v0.6.7 - 2026-03-31 + +### Enhancements: + +- Move the `taosws` SQLAlchemy dialect into `taos-ws-py`, removing the need for a `taospy` dependency when using SQLAlchemy with `taosws`. + ## v0.6.6 - 2026-03-05 ### Features: diff --git a/taos-ws-py/Cargo.lock b/taos-ws-py/Cargo.lock index 5708d58b..4abbd716 100644 --- a/taos-ws-py/Cargo.lock +++ b/taos-ws-py/Cargo.lock @@ -2401,7 +2401,7 @@ dependencies = [ [[package]] name = "taos-ws-py" -version = "0.6.6" +version = "0.6.7" dependencies = [ "anyhow", "chrono", diff --git a/taos-ws-py/Cargo.toml b/taos-ws-py/Cargo.toml index d7d9888f..e82a5bd5 100644 --- a/taos-ws-py/Cargo.toml +++ b/taos-ws-py/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "taos-ws-py" -version = "0.6.6" +version = "0.6.7" edition = "2021" publish = false license = "MIT"