ahi-sql 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ahi_sql-0.2.0/.gitignore +25 -0
- ahi_sql-0.2.0/CHANGELOG.md +8 -0
- ahi_sql-0.2.0/PKG-INFO +30 -0
- ahi_sql-0.2.0/README.md +17 -0
- ahi_sql-0.2.0/pyproject.toml +22 -0
- ahi_sql-0.2.0/src/ahi/sql/__init__.py +5 -0
- ahi_sql-0.2.0/src/ahi/sql/identifiers.py +51 -0
- ahi_sql-0.2.0/tests/test_identifiers.py +42 -0
ahi_sql-0.2.0/.gitignore
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Python artifacts
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.so
|
|
5
|
+
*.egg-info/
|
|
6
|
+
.dist/
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
|
|
10
|
+
# Virtual environments
|
|
11
|
+
.venv/
|
|
12
|
+
|
|
13
|
+
# Tool caches
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
.ruff_cache/
|
|
17
|
+
.coverage
|
|
18
|
+
htmlcov/
|
|
19
|
+
|
|
20
|
+
# IDEs/OS
|
|
21
|
+
.DS_Store
|
|
22
|
+
.vscode/*.code-snippets
|
|
23
|
+
|
|
24
|
+
# Docs build
|
|
25
|
+
site/
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.0](https://github.com/adaptivehealthintelligence/ahi-python-packages/compare/ahi-sql-v0.1.0...ahi-sql-v0.2.0) (2026-06-27)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* scaffold uv workspace with core, sql, aws packages ([8c42f2f](https://github.com/adaptivehealthintelligence/ahi-python-packages/commit/8c42f2f371284180bf0569785837640e39deb80b))
|
ahi_sql-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ahi-sql
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Small, dependency-light SQL helpers for AHI (safe identifiers, placeholders).
|
|
5
|
+
Project-URL: Homepage, https://github.com/adaptivehealthintelligence/ahi-python-packages
|
|
6
|
+
Project-URL: Documentation, https://adaptivehealthintelligence.github.io/ahi-python-packages/
|
|
7
|
+
Project-URL: Repository, https://github.com/adaptivehealthintelligence/ahi-python-packages
|
|
8
|
+
Author: Adaptive Health Intelligence
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: ahi,sql
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# ahi-sql
|
|
15
|
+
|
|
16
|
+
Small, dependency-light SQL helpers — the bits people otherwise hand-roll and
|
|
17
|
+
get subtly wrong.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from ahi.sql import quote_identifier, placeholders
|
|
21
|
+
|
|
22
|
+
quote_identifier("user table") # '"user table"'
|
|
23
|
+
placeholders(3, style="qmark") # '?, ?, ?' (Athena)
|
|
24
|
+
placeholders(3, style="named") # ':1, :2, :3' (Redshift Data API)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
> Quote **identifiers** with `quote_identifier`. Quote **values** by passing them
|
|
28
|
+
> as bind parameters to your driver — never by string-formatting them in.
|
|
29
|
+
|
|
30
|
+
Part of [ahi-python-packages](https://github.com/adaptivehealthintelligence/ahi-python-packages).
|
ahi_sql-0.2.0/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# ahi-sql
|
|
2
|
+
|
|
3
|
+
Small, dependency-light SQL helpers — the bits people otherwise hand-roll and
|
|
4
|
+
get subtly wrong.
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
from ahi.sql import quote_identifier, placeholders
|
|
8
|
+
|
|
9
|
+
quote_identifier("user table") # '"user table"'
|
|
10
|
+
placeholders(3, style="qmark") # '?, ?, ?' (Athena)
|
|
11
|
+
placeholders(3, style="named") # ':1, :2, :3' (Redshift Data API)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
> Quote **identifiers** with `quote_identifier`. Quote **values** by passing them
|
|
15
|
+
> as bind parameters to your driver — never by string-formatting them in.
|
|
16
|
+
|
|
17
|
+
Part of [ahi-python-packages](https://github.com/adaptivehealthintelligence/ahi-python-packages).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ahi-sql"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Small, dependency-light SQL helpers for AHI (safe identifiers, placeholders)."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "Adaptive Health Intelligence" }]
|
|
9
|
+
keywords = ["ahi", "sql"]
|
|
10
|
+
dependencies = []
|
|
11
|
+
|
|
12
|
+
[project.urls]
|
|
13
|
+
Homepage = "https://github.com/adaptivehealthintelligence/ahi-python-packages"
|
|
14
|
+
Documentation = "https://adaptivehealthintelligence.github.io/ahi-python-packages/"
|
|
15
|
+
Repository = "https://github.com/adaptivehealthintelligence/ahi-python-packages"
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["hatchling>=1.24"]
|
|
19
|
+
build-backend = "hatchling.build"
|
|
20
|
+
|
|
21
|
+
[tool.hatch.build.targets.wheel]
|
|
22
|
+
packages = ["src/ahi"]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Helpers for building SQL fragments safely.
|
|
2
|
+
|
|
3
|
+
These cover the two things people most often hand-roll (and get wrong):
|
|
4
|
+
quoting an identifier, and generating the right number of bind placeholders.
|
|
5
|
+
Both default to ANSI/PostgreSQL-style quoting, which also matches Redshift.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def quote_identifier(name: str, *, quote: str = '"') -> str:
|
|
12
|
+
"""Safely quote a SQL identifier (table/column name).
|
|
13
|
+
|
|
14
|
+
Wraps ``name`` in ``quote`` characters and escapes any embedded quote by
|
|
15
|
+
doubling it, the ANSI rule. Use this for identifiers only — never for
|
|
16
|
+
values, which belong in bind parameters.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
name: The identifier to quote. Must be non-empty.
|
|
20
|
+
quote: The quote character (``"`` for ANSI/Redshift, `` ` `` for MySQL).
|
|
21
|
+
|
|
22
|
+
Raises:
|
|
23
|
+
ValueError: If ``name`` is empty.
|
|
24
|
+
"""
|
|
25
|
+
if not name:
|
|
26
|
+
raise ValueError("identifier must be non-empty")
|
|
27
|
+
escaped = name.replace(quote, quote * 2)
|
|
28
|
+
return f"{quote}{escaped}{quote}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def placeholders(count: int, *, style: str = "qmark") -> str:
|
|
32
|
+
"""Return a comma-separated list of bind placeholders for an ``IN`` clause.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
count: Number of placeholders (must be >= 1).
|
|
36
|
+
style: ``"qmark"`` -> ``?, ?, ?`` (Athena),
|
|
37
|
+
``"named"`` -> ``:1, :2, :3`` (Redshift Data API),
|
|
38
|
+
``"format"`` -> ``%s, %s, %s`` (psycopg).
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
ValueError: If ``count`` < 1 or ``style`` is unknown.
|
|
42
|
+
"""
|
|
43
|
+
if count < 1:
|
|
44
|
+
raise ValueError("count must be >= 1")
|
|
45
|
+
if style == "qmark":
|
|
46
|
+
return ", ".join(["?"] * count)
|
|
47
|
+
if style == "format":
|
|
48
|
+
return ", ".join(["%s"] * count)
|
|
49
|
+
if style == "named":
|
|
50
|
+
return ", ".join(f":{i}" for i in range(1, count + 1))
|
|
51
|
+
raise ValueError(f"unknown placeholder style: {style!r}")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from ahi.sql import placeholders, quote_identifier
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_quote_identifier_basic() -> None:
|
|
7
|
+
assert quote_identifier("users") == '"users"'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_quote_identifier_escapes_embedded_quote() -> None:
|
|
11
|
+
assert quote_identifier('we"ird') == '"we""ird"'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_quote_identifier_custom_quote() -> None:
|
|
15
|
+
assert quote_identifier("users", quote="`") == "`users`"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_quote_identifier_rejects_empty() -> None:
|
|
19
|
+
with pytest.raises(ValueError):
|
|
20
|
+
quote_identifier("")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.mark.parametrize(
|
|
24
|
+
("count", "style", "expected"),
|
|
25
|
+
[
|
|
26
|
+
(3, "qmark", "?, ?, ?"),
|
|
27
|
+
(2, "format", "%s, %s"),
|
|
28
|
+
(3, "named", ":1, :2, :3"),
|
|
29
|
+
],
|
|
30
|
+
)
|
|
31
|
+
def test_placeholders(count: int, style: str, expected: str) -> None:
|
|
32
|
+
assert placeholders(count, style=style) == expected
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_placeholders_rejects_zero() -> None:
|
|
36
|
+
with pytest.raises(ValueError):
|
|
37
|
+
placeholders(0)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_placeholders_rejects_unknown_style() -> None:
|
|
41
|
+
with pytest.raises(ValueError):
|
|
42
|
+
placeholders(2, style="bogus")
|