From 00b9f219aca5e6bb6bef287479a380d5609f191c Mon Sep 17 00:00:00 2001 From: Adrian Ehrsam Date: Fri, 9 Oct 2026 19:25:00 +0000 Subject: [PATCH 1/4] Widen sqlglot query annotation to exp.Expr (#56) Co-Authored-By: Claude Sonnet 5.5 --- pgdevkit/db/fetch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgdevkit/db/fetch.py b/pgdevkit/db/fetch.py index b3b94e8..3320462 100644 --- a/pgdevkit/db/fetch.py +++ b/pgdevkit/db/fetch.py @@ -28,7 +28,7 @@ # The only things a query may be: a literal string (e.g. from `SqlLoader.load_sql()`), a sqlglot # expression, or psycopg's own safe-composition types (`psycopg.sql` / a t-string). A plain `str` # is rejected by the type checker on purpose, so user-controlled text can't be concatenated in. -type SqlQueryNoTemplate = LiteralString | exp.Expression | Composable +type SqlQueryNoTemplate = LiteralString | exp.Expr | Composable type SqlQuery = SqlQueryNoTemplate | Template From 70739e147de64bc0a6ef4cf45f4c28a9b857eda6 Mon Sep 17 00:00:00 2001 From: Adrian Ehrsam Date: Fri, 9 Oct 2026 19:26:28 +0000 Subject: [PATCH 2/4] Test exp.Query-typed queries (Union/Subquery), bump to 0.15.2 (#56) Co-Authored-By: Claude Sonnet 5.5 --- pyproject.toml | 3 +- tests/db/test_fetch.py | 10 ++++ tests/db/test_query_expr_types.py | 89 +++++++++++++++++++++++++++++++ uv.lock | 30 ++++++++++- 4 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 tests/db/test_query_expr_types.py diff --git a/pyproject.toml b/pyproject.toml index 6c31d27..00a782c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ packages = ["pgdevkit"] [project] name = "pgdevkit" -version = "0.15.0" +version = "0.15.2" description = "A helper for developing with Postgres" readme = "README.md" requires-python = ">=3.14" @@ -44,6 +44,7 @@ name = "pgdevkit" [dependency-groups] dev = [ + "pyright>=1.1.400", "ty>=0.0.59", ] test = [ diff --git a/tests/db/test_fetch.py b/tests/db/test_fetch.py index 63e5428..80d0b71 100644 --- a/tests/db/test_fetch.py +++ b/tests/db/test_fetch.py @@ -120,6 +120,16 @@ async def test_accepts_sqlglot_expression_psycopg_sql_and_t_string(pool: PgPool) assert await fetch_all(t"SELECT name FROM widget WHERE id = {widget_id}", pool=pool) == [{"name": "sprocket"}] +@requires_podman +async def test_accepts_exp_query_typed_union_and_subquery(pool: PgPool): + union: exp.Query = select("name").from_("widget").where("id = 1").union(select("name").from_("widget").where("id = 2")) + assert sorted(r["name"] for r in await fetch_all(union, pool=pool)) == ["cog", "sprocket"] + sub: exp.Query = select("name").from_(select("name").from_("widget").subquery("w")).where("name = 'cog'") + assert await fetch_all(sub, pool=pool) == [{"name": "cog"}] + assert await fetch_one(union, pool=pool) is not None + assert await fetch_scalar(select("count(*)").from_(union.subquery("u")), pool=pool) == 2 + + async def test_already_set_cancel_raises_before_touching_a_connection(): cancel = asyncio.Event() cancel.set() diff --git a/tests/db/test_query_expr_types.py b/tests/db/test_query_expr_types.py new file mode 100644 index 0000000..c898dc8 --- /dev/null +++ b/tests/db/test_query_expr_types.py @@ -0,0 +1,89 @@ +"""sqlglot queries are typed as `exp.Expr`, so an `exp.Query` (base of Select/Union/Subquery) is accepted too. + +In sqlglot 30 `exp.Query` is an `exp.Expr` but not an `exp.Expression`; the annotations used to say +`exp.Expression`, which type checkers rejected for `exp.Query`-typed values (issue #56). +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +import pytest +from sqlglot import exp, select + +from pgdevkit.db import PgPool, execute, fetch_all, fetch_one, fetch_scalar +from pgdevkit.db.fetch import _render +from pgdevkit.fastapi import PostgresJsonResponse + + +def _union() -> exp.Query: + return select("1 AS a").union(select("2 AS a")) + + +def _subquery() -> exp.Query: + return select("a").from_(select("1 AS a").subquery("t")) + + +# --- type level: never executed, only checked by `ty` (see test_type_checker_accepts_exp_query_arguments) ------------------- + + +async def _typed_usage(pool: PgPool, query: exp.Query, expr: exp.Expr) -> None: + await fetch_all(query, pool=pool) + await fetch_one(query, pool=pool) + await fetch_scalar(query, pool=pool) + await execute(query, pool=pool) + await fetch_all(expr, pool=pool) + await fetch_all(_union(), pool=pool) + PostgresJsonResponse(query, pool=pool) + PostgresJsonResponse(expr, pool=pool) + + +@pytest.mark.parametrize("checker", ["pyright", "ty"]) +def test_type_checker_accepts_exp_query_arguments(checker: str): + # (ty currently does not resolve `exp.Query` and would accept this file even with the old annotation; pyright + # is what actually guards the regression, ty is kept as the checker CI runs on the package.) + args = ["check", str(Path(__file__))] if checker == "ty" else [str(Path(__file__))] + result = subprocess.run([sys.executable, "-m", checker, *args], capture_output=True, text=True, check=False) + if f"No module named {checker}" in result.stderr: + pytest.skip(f"{checker} is not installed") + assert result.returncode == 0, result.stdout + result.stderr + + +# --- runtime: no database needed for `_render` --------------------------------------------------------------------- + + +def test_render_union_and_subquery(): + assert _render(_union()) == "SELECT 1 AS a UNION SELECT 2 AS a" + assert _render(_subquery()) == "SELECT a FROM (SELECT 1 AS a) AS t" + assert _render(_union(), {}) == "SELECT 1 AS a UNION SELECT 2 AS a" + + +def test_render_keeps_placeholders_and_escapes_percent_inside_a_union(): + query = ( + select("name").from_("w").where(exp.column("id").eq(exp.Placeholder(this="id"))).where("name LIKE '50%'") + ).union(select("'x%'")) + rendered = _render(query, {"id": 1}) + assert "%(id)s" in rendered + assert "'50%%'" in rendered + assert "'x%%'" in rendered + + +def test_render_rejects_command_and_bad_placeholder_nested_in_a_union(): + command = exp.Command(this="SELECT", expression=" version()") + with pytest.raises(ValueError, match="Command"): + _render(select("1").union(select(command))) + with pytest.raises(ValueError, match="Command"): + _render(select("a").from_(select(command).subquery("t"))) + bad = exp.Placeholder(this="x); DROP TABLE t;--") + with pytest.raises(ValueError, match="placeholder name"): + _render(select("1").union(select("2").where(bad)), {"x": 1}) + with pytest.raises(ValueError, match="placeholder name"): + _render(select("a").from_(select("2 AS a").where(bad).subquery("t")), {"x": 1}) + + +def test_importing_pgdevkit_db_does_not_import_sqlglot(): + code = "import sys, pgdevkit.db; sys.exit('sqlglot' in sys.modules)" + assert subprocess.run([sys.executable, "-c", code], check=False).returncode == 0 + diff --git a/uv.lock b/uv.lock index 3dcbd74..859505d 100644 --- a/uv.lock +++ b/uv.lock @@ -428,6 +428,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/c0/bff06bc2ef7647fddc2f568239383a259d85b5b236d57522fb389780ffd5/mssql_python_odbc-18.6.2-py3-none-win_arm64.whl", hash = "sha256:2ab8057e1565a155ae740b5232c161cb0d9439ce48001a8998e4422f6a02c601", size = 6998918, upload-time = "2026-07-22T10:08:19.503Z" }, ] +[[package]] +name = "nodeenv" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/105de02c1322cfada6d9710d9146ef8026419d433c9d08359a2d35805811/nodeenv-1.11.0.tar.gz", hash = "sha256:3ce8fe5b71d16e8af7039ca65257354100bc772965d6bc549070649e53b1b146", size = 99301, upload-time = "2026-09-26T11:29:21.367Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/c8/12811c9b48fde162bb72b6f2e78fada9a247a09d7bb5be2050a5d099c77b/nodeenv-1.11.0-py2.py3-none-any.whl", hash = "sha256:edaa16e6c14d7cf395d75d4bbd5a26390f4dc06501a33b4e76282b02cc688a25", size = 34277, upload-time = "2026-09-26T11:29:19.933Z" }, +] + [[package]] name = "opentelemetry-api" version = "1.45.1" @@ -451,7 +460,7 @@ wheels = [ [[package]] name = "pgdevkit" -version = "0.15.0" +version = "0.15.2" source = { editable = "." } dependencies = [ { name = "docker" }, @@ -483,6 +492,7 @@ mssql = [ [package.dev-dependencies] dev = [ + { name = "pyright" }, { name = "ty" }, ] test = [ @@ -514,7 +524,10 @@ requires-dist = [ provides-extras = ["azure", "cli", "db", "fastapi", "mssql"] [package.metadata.requires-dev] -dev = [{ name = "ty", specifier = ">=0.0.59" }] +dev = [ + { name = "pyright", specifier = ">=1.1.400" }, + { name = "ty", specifier = ">=0.0.59" }, +] test = [ { name = "fastapi", specifier = ">=0.118.0" }, { name = "granian", specifier = ">=2.0" }, @@ -670,6 +683,19 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pyright" +version = "1.1.414" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/1b/244c7b710031ada80f27e579ec20d28a2285dfc318fed0339866b1047f12/pyright-1.1.414.tar.gz", hash = "sha256:523c0a97c60da6333234955c277730c9cf4f5bd6d5399e7b7d2b0fc5d3599524", size = 4154638, upload-time = "2026-09-10T12:26:53.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/ba/18b6e682ead424ad24bcc134339ae5d1b931cd9ae260540592a058a91279/pyright-1.1.414-py3-none-any.whl", hash = "sha256:2a6b4b3298c9eec174c5ed83bd338de6eee82df2992f3e1930e6199d381be36f", size = 6225049, upload-time = "2026-09-10T12:26:51.427Z" }, +] + [[package]] name = "pytest" version = "9.1.0" From a96f1c4dad56b7104dc1b6266d4375550246bee0 Mon Sep 17 00:00:00 2001 From: Adrian Ehrsam Date: Fri, 9 Oct 2026 19:33:03 +0000 Subject: [PATCH 3/4] Harden type-checker test, document exp.Expr choice (#56) Co-Authored-By: Claude Sonnet 5.5 --- README.md | 2 +- pgdevkit/db/fetch.py | 2 ++ skills/pgdevkit/SKILL.md | 2 +- tests/db/test_fetch.py | 1 + tests/db/test_query_expr_types.py | 25 ++++++++++++++++++------- 5 files changed, 23 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 099a959..05bfc84 100644 --- a/README.md +++ b/README.md @@ -531,7 +531,7 @@ for a `Response`, and the rows are *not* validated against the model. starts and released when it ends. A `con` must stay open until the response has been sent, i.e. come from a `Depends` with `yield` (FastAPI >= 0.118); `async with pool.connection() as conn: return PostgresJsonResponse(q, con=conn)` closes it too early. Prefer the pool. -- **Query** — a literal string, a `psycopg.sql` composable, a sqlglot expression or a t-string, with values bound +- **Query** — a literal string, a `psycopg.sql` composable, a sqlglot expression (any `exp.Expr`, so Select, Union, Subquery and other `exp.Query` values are fine) or a t-string, with values bound through `params` (`%(name)s`; a t-string carries its own values and takes no `params`). Passing only literals is enforced by the type checker, **not at runtime**: the query is wrapped as a subquery, never parsed, so never build it from user input. For a data-modifying CTE, or custom JSON, pass `query_produces_json=True` and return one diff --git a/pgdevkit/db/fetch.py b/pgdevkit/db/fetch.py index 3320462..3a234dd 100644 --- a/pgdevkit/db/fetch.py +++ b/pgdevkit/db/fetch.py @@ -28,6 +28,8 @@ # The only things a query may be: a literal string (e.g. from `SqlLoader.load_sql()`), a sqlglot # expression, or psycopg's own safe-composition types (`psycopg.sql` / a t-string). A plain `str` # is rejected by the type checker on purpose, so user-controlled text can't be concatenated in. +# The sqlglot type is `exp.Expr` (any query: Select, Union, Subquery...), not `exp.Expression`: in +# sqlglot 30 `exp.Query` is an `exp.Expr` but not an `exp.Expression` (issue #56), so don't narrow it back. type SqlQueryNoTemplate = LiteralString | exp.Expr | Composable type SqlQuery = SqlQueryNoTemplate | Template diff --git a/skills/pgdevkit/SKILL.md b/skills/pgdevkit/SKILL.md index 123efec..d05c59f 100644 --- a/skills/pgdevkit/SKILL.md +++ b/skills/pgdevkit/SKILL.md @@ -234,7 +234,7 @@ deleted = await execute("DELETE FROM sessions WHERE user_id = %(id)s", {"id": 1} - Passing `con=` runs on *your* connection: `fetch_all` never commits, closes or releases it. Without it, a connection is borrowed from `pool=` (or the `set_default_pool()` pool) and released afterwards. - `statement_timeout=` (optional) lets Postgres abort the query after that long, raising `QueryCanceled`; it is applied with `SET LOCAL` semantics for this call only (a borrowed `con` gets its previous value back). A plain value *replaces* the connection's own setting in both directions, so a per-call 900 s overrides a database-level safety net of 600 s; `statement_timeout=at_most(120)` (`from pgdevkit.db import at_most`) never loosens it: the effective timeout is `min(current, 120)`. - `cancel=` takes an `asyncio.Event`: set it (e.g. when the HTTP client disconnects) and the running query is cancelled on the server, raising `psycopg.errors.QueryCanceled`. If the awaiting task is cancelled instead, the server-side query is cancelled too and `CancelledError` propagates as usual (not `QueryCanceled`), so no query keeps running unattended. Don't use `cancel=` on a `con` that concurrent tasks share (it aborts whatever statement is running on it); if the cancel request itself can't be delivered, the connection is closed. After `QueryCanceled` a borrowed `con` is usable again once you `await con.rollback()` (autocommit connections need nothing). To stream a whole result as JSON from FastAPI (it wires the disconnect itself) see `PostgresJsonResponse` below. -- `query` is typed `SqlQuery`: a literal string (`SqlLoader.load_sql()`), a sqlglot expression, a `psycopg.sql` composable or a t-string. A plain `str` fails type checking on purpose, so don't build SQL with f-strings/concatenation. A sqlglot expression is only as safe as the strings it was built from (its builders parse plain strings as SQL): pass user values as `exp.Placeholder` + `params`, never as literals/raw text. A literal `%` in a sqlglot expression (`LIKE 'a%'`, modulo) just works, with or without `params`; in a plain string or `psycopg.sql` composable it is psycopg's rule: write `%%` when you pass `params`. A t-string carries its own values, so don't also pass `params`. +- `query` is typed `SqlQuery`: a literal string (`SqlLoader.load_sql()`), a sqlglot expression (any `exp.Expr`, so Select, Union, Subquery and other `exp.Query` values are fine), a `psycopg.sql` composable or a t-string. A plain `str` fails type checking on purpose, so don't build SQL with f-strings/concatenation. A sqlglot expression is only as safe as the strings it was built from (its builders parse plain strings as SQL): pass user values as `exp.Placeholder` + `params`, never as literals/raw text. A literal `%` in a sqlglot expression (`LIKE 'a%'`, modulo) just works, with or without `params`; in a plain string or `psycopg.sql` composable it is psycopg's rule: write `%%` when you pass `params`. A t-string carries its own values, so don't also pass `params`. ### Dynamic SQL diff --git a/tests/db/test_fetch.py b/tests/db/test_fetch.py index 80d0b71..b4972e8 100644 --- a/tests/db/test_fetch.py +++ b/tests/db/test_fetch.py @@ -122,6 +122,7 @@ async def test_accepts_sqlglot_expression_psycopg_sql_and_t_string(pool: PgPool) @requires_podman async def test_accepts_exp_query_typed_union_and_subquery(pool: PgPool): + # annotated exp.Query on purpose (issue #56) union: exp.Query = select("name").from_("widget").where("id = 1").union(select("name").from_("widget").where("id = 2")) assert sorted(r["name"] for r in await fetch_all(union, pool=pool)) == ["cog", "sprocket"] sub: exp.Query = select("name").from_(select("name").from_("widget").subquery("w")).where("name = 'cog'") diff --git a/tests/db/test_query_expr_types.py b/tests/db/test_query_expr_types.py index c898dc8..930c737 100644 --- a/tests/db/test_query_expr_types.py +++ b/tests/db/test_query_expr_types.py @@ -26,7 +26,7 @@ def _subquery() -> exp.Query: return select("a").from_(select("1 AS a").subquery("t")) -# --- type level: never executed, only checked by `ty` (see test_type_checker_accepts_exp_query_arguments) ------------------- +# --- type level: never executed, only checked by pyright/ty (see test_type_checker_accepts_exp_query_arguments) --- async def _typed_usage(pool: PgPool, query: exp.Query, expr: exp.Expr) -> None: @@ -42,13 +42,24 @@ async def _typed_usage(pool: PgPool, query: exp.Query, expr: exp.Expr) -> None: @pytest.mark.parametrize("checker", ["pyright", "ty"]) def test_type_checker_accepts_exp_query_arguments(checker: str): - # (ty currently does not resolve `exp.Query` and would accept this file even with the old annotation; pyright - # is what actually guards the regression, ty is kept as the checker CI runs on the package.) - args = ["check", str(Path(__file__))] if checker == "ty" else [str(Path(__file__))] - result = subprocess.run([sys.executable, "-m", checker, *args], capture_output=True, text=True, check=False) - if f"No module named {checker}" in result.stderr: + # ty does not resolve `exp.Query`, so it passes even with the old annotation; pyright is the real guard. + # (CI runs `ty check pgdevkit`, not the tests directory.) + file = str(Path(__file__)) + cmd = ( + [sys.executable, "-m", "ty", "check", "--python", sys.executable, file] + if checker == "ty" + else [sys.executable, "-m", "pyright", "--pythonpath", sys.executable, file] + ) + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=False, timeout=300) + except subprocess.TimeoutExpired: + pytest.skip(f"{checker} timed out") + output = result.stdout + result.stderr + if f"No module named {checker}" in output: pytest.skip(f"{checker} is not installed") - assert result.returncode == 0, result.stdout + result.stderr + if checker == "pyright" and result.returncode != 0 and " - error:" not in output: + pytest.skip(f"pyright could not start: {output[:200]}") # e.g. its node download (nodeenv) failed + assert result.returncode == 0, output # --- runtime: no database needed for `_render` --------------------------------------------------------------------- From 995af6f05ed1daeab07b26066074ddee1dcd8903 Mon Sep 17 00:00:00 2001 From: Adrian Ehrsam Date: Fri, 9 Oct 2026 19:35:25 +0000 Subject: [PATCH 4/4] Remove pyright type-check test and dev dependency (#56) Co-Authored-By: Claude Sonnet 5.5 --- pyproject.toml | 1 - tests/db/test_query_expr_types.py | 47 ++++++------------------------- uv.lock | 28 +----------------- 3 files changed, 9 insertions(+), 67 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 00a782c..a505512 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,6 @@ name = "pgdevkit" [dependency-groups] dev = [ - "pyright>=1.1.400", "ty>=0.0.59", ] test = [ diff --git a/tests/db/test_query_expr_types.py b/tests/db/test_query_expr_types.py index 930c737..166869f 100644 --- a/tests/db/test_query_expr_types.py +++ b/tests/db/test_query_expr_types.py @@ -1,19 +1,17 @@ """sqlglot queries are typed as `exp.Expr`, so an `exp.Query` (base of Select/Union/Subquery) is accepted too. In sqlglot 30 `exp.Query` is an `exp.Expr` but not an `exp.Expression`; the annotations used to say -`exp.Expression`, which type checkers rejected for `exp.Query`-typed values (issue #56). +`exp.Expression`, which a type checker such as pyright rejected for `exp.Query`-typed values (issue #56). """ from __future__ import annotations import subprocess import sys -from pathlib import Path import pytest from sqlglot import exp, select -from pgdevkit.db import PgPool, execute, fetch_all, fetch_one, fetch_scalar from pgdevkit.db.fetch import _render from pgdevkit.fastapi import PostgresJsonResponse @@ -26,42 +24,6 @@ def _subquery() -> exp.Query: return select("a").from_(select("1 AS a").subquery("t")) -# --- type level: never executed, only checked by pyright/ty (see test_type_checker_accepts_exp_query_arguments) --- - - -async def _typed_usage(pool: PgPool, query: exp.Query, expr: exp.Expr) -> None: - await fetch_all(query, pool=pool) - await fetch_one(query, pool=pool) - await fetch_scalar(query, pool=pool) - await execute(query, pool=pool) - await fetch_all(expr, pool=pool) - await fetch_all(_union(), pool=pool) - PostgresJsonResponse(query, pool=pool) - PostgresJsonResponse(expr, pool=pool) - - -@pytest.mark.parametrize("checker", ["pyright", "ty"]) -def test_type_checker_accepts_exp_query_arguments(checker: str): - # ty does not resolve `exp.Query`, so it passes even with the old annotation; pyright is the real guard. - # (CI runs `ty check pgdevkit`, not the tests directory.) - file = str(Path(__file__)) - cmd = ( - [sys.executable, "-m", "ty", "check", "--python", sys.executable, file] - if checker == "ty" - else [sys.executable, "-m", "pyright", "--pythonpath", sys.executable, file] - ) - try: - result = subprocess.run(cmd, capture_output=True, text=True, check=False, timeout=300) - except subprocess.TimeoutExpired: - pytest.skip(f"{checker} timed out") - output = result.stdout + result.stderr - if f"No module named {checker}" in output: - pytest.skip(f"{checker} is not installed") - if checker == "pyright" and result.returncode != 0 and " - error:" not in output: - pytest.skip(f"pyright could not start: {output[:200]}") # e.g. its node download (nodeenv) failed - assert result.returncode == 0, output - - # --- runtime: no database needed for `_render` --------------------------------------------------------------------- @@ -71,6 +33,13 @@ def test_render_union_and_subquery(): assert _render(_union(), {}) == "SELECT 1 AS a UNION SELECT 2 AS a" +def test_postgres_json_response_accepts_union_and_subquery(): + for query in (_union(), _subquery()): + assert _render(query) in PostgresJsonResponse(query).query.as_string() + with pytest.raises(ValueError, match="Command"): + PostgresJsonResponse(select("1").union(select(exp.Command(this="SELECT", expression=" version()")))) + + def test_render_keeps_placeholders_and_escapes_percent_inside_a_union(): query = ( select("name").from_("w").where(exp.column("id").eq(exp.Placeholder(this="id"))).where("name LIKE '50%'") diff --git a/uv.lock b/uv.lock index 859505d..9889250 100644 --- a/uv.lock +++ b/uv.lock @@ -428,15 +428,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/c0/bff06bc2ef7647fddc2f568239383a259d85b5b236d57522fb389780ffd5/mssql_python_odbc-18.6.2-py3-none-win_arm64.whl", hash = "sha256:2ab8057e1565a155ae740b5232c161cb0d9439ce48001a8998e4422f6a02c601", size = 6998918, upload-time = "2026-07-22T10:08:19.503Z" }, ] -[[package]] -name = "nodeenv" -version = "1.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/105de02c1322cfada6d9710d9146ef8026419d433c9d08359a2d35805811/nodeenv-1.11.0.tar.gz", hash = "sha256:3ce8fe5b71d16e8af7039ca65257354100bc772965d6bc549070649e53b1b146", size = 99301, upload-time = "2026-09-26T11:29:21.367Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/c8/12811c9b48fde162bb72b6f2e78fada9a247a09d7bb5be2050a5d099c77b/nodeenv-1.11.0-py2.py3-none-any.whl", hash = "sha256:edaa16e6c14d7cf395d75d4bbd5a26390f4dc06501a33b4e76282b02cc688a25", size = 34277, upload-time = "2026-09-26T11:29:19.933Z" }, -] - [[package]] name = "opentelemetry-api" version = "1.45.1" @@ -492,7 +483,6 @@ mssql = [ [package.dev-dependencies] dev = [ - { name = "pyright" }, { name = "ty" }, ] test = [ @@ -524,10 +514,7 @@ requires-dist = [ provides-extras = ["azure", "cli", "db", "fastapi", "mssql"] [package.metadata.requires-dev] -dev = [ - { name = "pyright", specifier = ">=1.1.400" }, - { name = "ty", specifier = ">=0.0.59" }, -] +dev = [{ name = "ty", specifier = ">=0.0.59" }] test = [ { name = "fastapi", specifier = ">=0.118.0" }, { name = "granian", specifier = ">=2.0" }, @@ -683,19 +670,6 @@ crypto = [ { name = "cryptography" }, ] -[[package]] -name = "pyright" -version = "1.1.414" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nodeenv" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e1/1b/244c7b710031ada80f27e579ec20d28a2285dfc318fed0339866b1047f12/pyright-1.1.414.tar.gz", hash = "sha256:523c0a97c60da6333234955c277730c9cf4f5bd6d5399e7b7d2b0fc5d3599524", size = 4154638, upload-time = "2026-09-10T12:26:53.181Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/ba/18b6e682ead424ad24bcc134339ae5d1b931cd9ae260540592a058a91279/pyright-1.1.414-py3-none-any.whl", hash = "sha256:2a6b4b3298c9eec174c5ed83bd338de6eee82df2992f3e1930e6199d381be36f", size = 6225049, upload-time = "2026-09-10T12:26:51.427Z" }, -] - [[package]] name = "pytest" version = "9.1.0"