sql-athame 0.4.0a10__tar.gz → 0.4.0a12__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.
- sql_athame-0.4.0a12/.bumpversion.cfg +19 -0
- sql_athame-0.4.0a12/.editorconfig +7 -0
- sql_athame-0.4.0a12/.github/workflows/publish.yml +27 -0
- sql_athame-0.4.0a12/.github/workflows/test.yml +49 -0
- sql_athame-0.4.0a12/.gitignore +9 -0
- {sql_athame-0.4.0a10 → sql_athame-0.4.0a12}/LICENSE +1 -1
- {sql_athame-0.4.0a10 → sql_athame-0.4.0a12}/PKG-INFO +10 -17
- {sql_athame-0.4.0a10 → sql_athame-0.4.0a12}/README.md +1 -1
- sql_athame-0.4.0a12/docker-compose.yml +9 -0
- {sql_athame-0.4.0a10 → sql_athame-0.4.0a12}/pyproject.toml +30 -28
- sql_athame-0.4.0a12/run +40 -0
- {sql_athame-0.4.0a10 → sql_athame-0.4.0a12}/sql_athame/dataclasses.py +109 -20
- sql_athame-0.4.0a12/tests/__init__.py +0 -0
- sql_athame-0.4.0a12/tests/test_asyncpg.py +323 -0
- sql_athame-0.4.0a12/tests/test_basic.py +287 -0
- sql_athame-0.4.0a12/tests/test_dataclasses.py +216 -0
- sql_athame-0.4.0a12/tests/test_sqlalchemy.py +60 -0
- sql_athame-0.4.0a12/uv.lock +757 -0
- {sql_athame-0.4.0a10 → sql_athame-0.4.0a12}/sql_athame/__init__.py +0 -0
- {sql_athame-0.4.0a10 → sql_athame-0.4.0a12}/sql_athame/base.py +0 -0
- {sql_athame-0.4.0a10 → sql_athame-0.4.0a12}/sql_athame/escape.py +0 -0
- {sql_athame-0.4.0a10 → sql_athame-0.4.0a12}/sql_athame/py.typed +0 -0
- {sql_athame-0.4.0a10 → sql_athame-0.4.0a12}/sql_athame/sqlalchemy.py +0 -0
- {sql_athame-0.4.0a10 → sql_athame-0.4.0a12}/sql_athame/types.py +0 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
[bumpversion]
|
2
|
+
current_version = 0.4.0-alpha-12
|
3
|
+
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-(?P<release>.*)-(?P<build>\d+))?
|
4
|
+
serialize =
|
5
|
+
{major}.{minor}.{patch}-{release}-{build}
|
6
|
+
{major}.{minor}.{patch}
|
7
|
+
commit = True
|
8
|
+
|
9
|
+
[bumpversion:file:pyproject.toml]
|
10
|
+
|
11
|
+
[bumpversion:part:release]
|
12
|
+
first_value = regular
|
13
|
+
optional_value = regular
|
14
|
+
values =
|
15
|
+
alpha
|
16
|
+
beta
|
17
|
+
rc
|
18
|
+
test
|
19
|
+
regular
|
@@ -0,0 +1,27 @@
|
|
1
|
+
name: publish
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
tags:
|
6
|
+
- 'v**'
|
7
|
+
|
8
|
+
jobs:
|
9
|
+
publish:
|
10
|
+
runs-on: ubuntu-latest
|
11
|
+
env:
|
12
|
+
UV_PUBLISH_TOKEN: ${{ secrets.POETRY_PYPI_TOKEN_PYPI }}
|
13
|
+
steps:
|
14
|
+
- uses: actions/checkout@v4
|
15
|
+
|
16
|
+
- uses: astral-sh/setup-uv@v5
|
17
|
+
with:
|
18
|
+
version: "0.6.6"
|
19
|
+
enable-cache: true
|
20
|
+
python-version: '3.12'
|
21
|
+
|
22
|
+
- uses: actions/setup-python@v5
|
23
|
+
with:
|
24
|
+
python-version: '3.12'
|
25
|
+
|
26
|
+
- run: uv build
|
27
|
+
- run: uv publish
|
@@ -0,0 +1,49 @@
|
|
1
|
+
name: test
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches:
|
6
|
+
- '**'
|
7
|
+
|
8
|
+
jobs:
|
9
|
+
test:
|
10
|
+
strategy:
|
11
|
+
matrix:
|
12
|
+
python-version:
|
13
|
+
- '3.9'
|
14
|
+
- '3.10'
|
15
|
+
- '3.11'
|
16
|
+
- '3.12'
|
17
|
+
- '3.13'
|
18
|
+
runs-on: ubuntu-latest
|
19
|
+
services:
|
20
|
+
postgres:
|
21
|
+
image: postgres:16
|
22
|
+
ports:
|
23
|
+
- 5432:5432
|
24
|
+
env:
|
25
|
+
POSTGRES_PASSWORD: password
|
26
|
+
env:
|
27
|
+
PGPORT: 5432
|
28
|
+
UV_LOCKED: 1
|
29
|
+
steps:
|
30
|
+
- uses: actions/checkout@v4
|
31
|
+
|
32
|
+
- uses: astral-sh/setup-uv@v5
|
33
|
+
with:
|
34
|
+
version: "0.6.6"
|
35
|
+
enable-cache: true
|
36
|
+
python-version: ${{ matrix.python-version }}
|
37
|
+
|
38
|
+
- uses: actions/setup-python@v5
|
39
|
+
with:
|
40
|
+
python-version: ${{ matrix.python-version }}
|
41
|
+
|
42
|
+
- run: uv sync
|
43
|
+
- run: uv run ruff check
|
44
|
+
- run: uv run ruff format --diff
|
45
|
+
if: success() || failure()
|
46
|
+
- run: uv run mypy sql_athame/**.py tests/**.py
|
47
|
+
if: success() || failure()
|
48
|
+
- run: uv run pytest
|
49
|
+
if: success() || failure()
|
@@ -1,22 +1,16 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: sql-athame
|
3
|
-
Version: 0.4.
|
3
|
+
Version: 0.4.0a12
|
4
4
|
Summary: Python tool for slicing and dicing SQL
|
5
|
-
|
5
|
+
Project-URL: homepage, https://github.com/bdowning/sql-athame
|
6
|
+
Project-URL: repository, https://github.com/bdowning/sql-athame
|
7
|
+
Author-email: Brian Downing <bdowning@lavos.net>
|
6
8
|
License: MIT
|
7
|
-
|
8
|
-
|
9
|
-
Requires-Python: >=3.9,<4.0
|
10
|
-
Classifier: License :: OSI Approved :: MIT License
|
11
|
-
Classifier: Programming Language :: Python :: 3
|
12
|
-
Classifier: Programming Language :: Python :: 3.9
|
13
|
-
Classifier: Programming Language :: Python :: 3.10
|
14
|
-
Classifier: Programming Language :: Python :: 3.11
|
15
|
-
Classifier: Programming Language :: Python :: 3.12
|
16
|
-
Provides-Extra: asyncpg
|
17
|
-
Requires-Dist: asyncpg ; extra == "asyncpg"
|
9
|
+
License-File: LICENSE
|
10
|
+
Requires-Python: <4.0,>=3.9
|
18
11
|
Requires-Dist: typing-extensions
|
19
|
-
|
12
|
+
Provides-Extra: asyncpg
|
13
|
+
Requires-Dist: asyncpg; extra == 'asyncpg'
|
20
14
|
Description-Content-Type: text/markdown
|
21
15
|
|
22
16
|
# sql-athame
|
@@ -404,5 +398,4 @@ TODO, for now read the [source](sql_athame/dataclasses.py).
|
|
404
398
|
MIT.
|
405
399
|
|
406
400
|
---
|
407
|
-
Copyright (c) 2019
|
408
|
-
|
401
|
+
Copyright (c) 2019–2025 Brian Downing
|
@@ -1,37 +1,42 @@
|
|
1
|
-
[
|
1
|
+
[project]
|
2
|
+
authors = [
|
3
|
+
{name = "Brian Downing", email = "bdowning@lavos.net"},
|
4
|
+
]
|
5
|
+
license = {text = "MIT"}
|
6
|
+
requires-python = "<4.0,>=3.9"
|
7
|
+
dependencies = [
|
8
|
+
"typing-extensions",
|
9
|
+
]
|
2
10
|
name = "sql-athame"
|
3
|
-
version = "0.4.0-alpha-
|
11
|
+
version = "0.4.0-alpha-12"
|
4
12
|
description = "Python tool for slicing and dicing SQL"
|
5
|
-
authors = ["Brian Downing <bdowning@lavos.net>"]
|
6
|
-
license = "MIT"
|
7
13
|
readme = "README.md"
|
14
|
+
|
15
|
+
[project.urls]
|
8
16
|
homepage = "https://github.com/bdowning/sql-athame"
|
9
17
|
repository = "https://github.com/bdowning/sql-athame"
|
10
18
|
|
11
|
-
[
|
12
|
-
asyncpg = [
|
13
|
-
|
14
|
-
|
15
|
-
python = "^3.9"
|
16
|
-
asyncpg = { version = "*", optional = true }
|
17
|
-
typing-extensions = "*"
|
19
|
+
[project.optional-dependencies]
|
20
|
+
asyncpg = [
|
21
|
+
"asyncpg",
|
22
|
+
]
|
18
23
|
|
19
|
-
[
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
pytest-asyncio
|
28
|
-
|
29
|
-
|
30
|
-
|
24
|
+
[dependency-groups]
|
25
|
+
dev = [
|
26
|
+
"SQLAlchemy",
|
27
|
+
"asyncpg",
|
28
|
+
"bump2version",
|
29
|
+
"ipython",
|
30
|
+
"mypy",
|
31
|
+
"pytest",
|
32
|
+
"pytest-asyncio",
|
33
|
+
"pytest-cov",
|
34
|
+
"ruff",
|
35
|
+
]
|
31
36
|
|
32
37
|
[build-system]
|
33
|
-
requires = ["
|
34
|
-
build-backend = "
|
38
|
+
requires = ["hatchling"]
|
39
|
+
build-backend = "hatchling.build"
|
35
40
|
|
36
41
|
[tool.ruff]
|
37
42
|
target-version = "py39"
|
@@ -60,9 +65,6 @@ select = [
|
|
60
65
|
flake8-comprehensions.allow-dict-calls-with-keyword-arguments = true
|
61
66
|
ignore = [
|
62
67
|
"E501", # line too long
|
63
|
-
"E721", # type checks, currently broken
|
64
|
-
"ISC001", # conflicts with ruff format
|
65
|
-
"PT004", # Fixture `...` does not return anything, add leading underscore
|
66
68
|
"RET505", # Unnecessary `else` after `return` statement
|
67
69
|
"RET506", # Unnecessary `else` after `raise` statement
|
68
70
|
]
|
sql_athame-0.4.0a12/run
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
usage=()
|
4
|
+
|
5
|
+
usage+=(" $0 tests - run tests")
|
6
|
+
tests() {
|
7
|
+
uv run pytest "$@"
|
8
|
+
lint
|
9
|
+
}
|
10
|
+
|
11
|
+
usage+=(" $0 refmt - reformat code")
|
12
|
+
refmt() {
|
13
|
+
uv run ruff check --select I --fix
|
14
|
+
uv run ruff format
|
15
|
+
}
|
16
|
+
|
17
|
+
usage+=(" $0 lint - run linting")
|
18
|
+
lint() {
|
19
|
+
uv run ruff check
|
20
|
+
uv run ruff format --diff
|
21
|
+
uv run mypy sql_athame/**.py tests/**.py
|
22
|
+
}
|
23
|
+
|
24
|
+
usage+=(" $0 bump2version {major|minor|patch} - bump version number")
|
25
|
+
bump2version() {
|
26
|
+
uv run bump2version "$@"
|
27
|
+
}
|
28
|
+
|
29
|
+
cmd=$1
|
30
|
+
shift
|
31
|
+
|
32
|
+
if ! declare -f "$cmd" >/dev/null; then
|
33
|
+
echo "Usage:"
|
34
|
+
for line in "${usage[@]}"; do echo "$line"; done
|
35
|
+
exit 1
|
36
|
+
fi
|
37
|
+
|
38
|
+
set -o xtrace
|
39
|
+
|
40
|
+
"$cmd" "$@"
|
@@ -157,7 +157,7 @@ class ModelBase:
|
|
157
157
|
_cache: dict[tuple, Any]
|
158
158
|
table_name: str
|
159
159
|
primary_key_names: tuple[str, ...]
|
160
|
-
|
160
|
+
insert_multiple_mode: str
|
161
161
|
|
162
162
|
def __init_subclass__(
|
163
163
|
cls,
|
@@ -169,12 +169,9 @@ class ModelBase:
|
|
169
169
|
):
|
170
170
|
cls._cache = {}
|
171
171
|
cls.table_name = table_name
|
172
|
-
if insert_multiple_mode
|
173
|
-
cls.array_safe_insert = True
|
174
|
-
elif insert_multiple_mode == "unnest":
|
175
|
-
cls.array_safe_insert = False
|
176
|
-
else:
|
172
|
+
if insert_multiple_mode not in ("array_safe", "unnest", "executemany"):
|
177
173
|
raise ValueError("Unknown `insert_multiple_mode`")
|
174
|
+
cls.insert_multiple_mode = insert_multiple_mode
|
178
175
|
if isinstance(primary_key, str):
|
179
176
|
cls.primary_key_names = (primary_key,)
|
180
177
|
else:
|
@@ -233,8 +230,21 @@ class ModelBase:
|
|
233
230
|
|
234
231
|
@classmethod
|
235
232
|
def field_names_sql(
|
236
|
-
cls,
|
233
|
+
cls,
|
234
|
+
*,
|
235
|
+
prefix: Optional[str] = None,
|
236
|
+
exclude: FieldNamesSet = (),
|
237
|
+
as_prepended: Optional[str] = None,
|
237
238
|
) -> list[Fragment]:
|
239
|
+
if as_prepended:
|
240
|
+
return [
|
241
|
+
sql(
|
242
|
+
"{} AS {}",
|
243
|
+
sql.identifier(f, prefix=prefix),
|
244
|
+
sql.identifier(f"{as_prepended}{f}"),
|
245
|
+
)
|
246
|
+
for f in cls.field_names(exclude=exclude)
|
247
|
+
]
|
238
248
|
return [
|
239
249
|
sql.identifier(f, prefix=prefix) for f in cls.field_names(exclude=exclude)
|
240
250
|
]
|
@@ -303,6 +313,16 @@ class ModelBase:
|
|
303
313
|
cls.from_mapping = from_mapping_fn # type: ignore
|
304
314
|
return from_mapping_fn(mapping)
|
305
315
|
|
316
|
+
@classmethod
|
317
|
+
def from_prepended_mapping(
|
318
|
+
cls: type[T], mapping: Mapping[str, Any], prepend: str
|
319
|
+
) -> T:
|
320
|
+
filtered_dict: dict[str, Any] = {}
|
321
|
+
for k, v in mapping.items():
|
322
|
+
if k.startswith(prepend):
|
323
|
+
filtered_dict[k[len(prepend) :]] = v
|
324
|
+
return cls.from_mapping(filtered_dict)
|
325
|
+
|
306
326
|
@classmethod
|
307
327
|
def ensure_model(cls: type[T], row: Union[T, Mapping[str, Any]]) -> T:
|
308
328
|
if isinstance(row, cls):
|
@@ -357,7 +377,17 @@ class ModelBase:
|
|
357
377
|
return query
|
358
378
|
|
359
379
|
@classmethod
|
360
|
-
async def
|
380
|
+
async def cursor_from(
|
381
|
+
cls: type[T],
|
382
|
+
connection: Connection,
|
383
|
+
query: Fragment,
|
384
|
+
prefetch: int = 1000,
|
385
|
+
) -> AsyncGenerator[T, None]:
|
386
|
+
async for row in connection.cursor(*query, prefetch=prefetch):
|
387
|
+
yield cls.from_mapping(row)
|
388
|
+
|
389
|
+
@classmethod
|
390
|
+
def select_cursor(
|
361
391
|
cls: type[T],
|
362
392
|
connection: Connection,
|
363
393
|
order_by: Union[FieldNames, str] = (),
|
@@ -365,11 +395,19 @@ class ModelBase:
|
|
365
395
|
where: Where = (),
|
366
396
|
prefetch: int = 1000,
|
367
397
|
) -> AsyncGenerator[T, None]:
|
368
|
-
|
369
|
-
|
398
|
+
return cls.cursor_from(
|
399
|
+
connection,
|
400
|
+
cls.select_sql(order_by=order_by, for_update=for_update, where=where),
|
370
401
|
prefetch=prefetch,
|
371
|
-
)
|
372
|
-
|
402
|
+
)
|
403
|
+
|
404
|
+
@classmethod
|
405
|
+
async def fetch_from(
|
406
|
+
cls: type[T],
|
407
|
+
connection_or_pool: Union[Connection, Pool],
|
408
|
+
query: Fragment,
|
409
|
+
) -> list[T]:
|
410
|
+
return [cls.from_mapping(row) for row in await connection_or_pool.fetch(*query)]
|
373
411
|
|
374
412
|
@classmethod
|
375
413
|
async def select(
|
@@ -379,12 +417,10 @@ class ModelBase:
|
|
379
417
|
for_update: bool = False,
|
380
418
|
where: Where = (),
|
381
419
|
) -> list[T]:
|
382
|
-
return
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
)
|
387
|
-
]
|
420
|
+
return await cls.fetch_from(
|
421
|
+
connection_or_pool,
|
422
|
+
cls.select_sql(order_by=order_by, for_update=for_update, where=where),
|
423
|
+
)
|
388
424
|
|
389
425
|
@classmethod
|
390
426
|
def create_sql(cls: type[T], **kwargs: Any) -> Fragment:
|
@@ -506,6 +542,37 @@ class ModelBase:
|
|
506
542
|
),
|
507
543
|
)
|
508
544
|
|
545
|
+
@classmethod
|
546
|
+
def insert_multiple_executemany_chunk_sql(
|
547
|
+
cls: type[T], chunk_size: int
|
548
|
+
) -> Fragment:
|
549
|
+
def generate() -> Fragment:
|
550
|
+
columns = len(cls.column_info())
|
551
|
+
values = ", ".join(
|
552
|
+
f"({', '.join(f'${i}' for i in chunk)})"
|
553
|
+
for chunk in chunked(range(1, columns * chunk_size + 1), columns)
|
554
|
+
)
|
555
|
+
return sql(
|
556
|
+
"INSERT INTO {table} ({fields}) VALUES {values}",
|
557
|
+
table=cls.table_name_sql(),
|
558
|
+
fields=sql.list(cls.field_names_sql()),
|
559
|
+
values=sql.literal(values),
|
560
|
+
).flatten()
|
561
|
+
|
562
|
+
return cls._cached(
|
563
|
+
("insert_multiple_executemany_chunk", chunk_size),
|
564
|
+
generate,
|
565
|
+
)
|
566
|
+
|
567
|
+
@classmethod
|
568
|
+
async def insert_multiple_executemany(
|
569
|
+
cls: type[T], connection_or_pool: Union[Connection, Pool], rows: Iterable[T]
|
570
|
+
) -> None:
|
571
|
+
args = [r.field_values() for r in rows]
|
572
|
+
query = cls.insert_multiple_executemany_chunk_sql(1).query()[0]
|
573
|
+
if args:
|
574
|
+
await connection_or_pool.executemany(query, args)
|
575
|
+
|
509
576
|
@classmethod
|
510
577
|
async def insert_multiple_unnest(
|
511
578
|
cls: type[T], connection_or_pool: Union[Connection, Pool], rows: Iterable[T]
|
@@ -527,11 +594,28 @@ class ModelBase:
|
|
527
594
|
async def insert_multiple(
|
528
595
|
cls: type[T], connection_or_pool: Union[Connection, Pool], rows: Iterable[T]
|
529
596
|
) -> str:
|
530
|
-
if cls.
|
597
|
+
if cls.insert_multiple_mode == "executemany":
|
598
|
+
await cls.insert_multiple_executemany(connection_or_pool, rows)
|
599
|
+
return "INSERT"
|
600
|
+
elif cls.insert_multiple_mode == "array_safe":
|
531
601
|
return await cls.insert_multiple_array_safe(connection_or_pool, rows)
|
532
602
|
else:
|
533
603
|
return await cls.insert_multiple_unnest(connection_or_pool, rows)
|
534
604
|
|
605
|
+
@classmethod
|
606
|
+
async def upsert_multiple_executemany(
|
607
|
+
cls: type[T],
|
608
|
+
connection_or_pool: Union[Connection, Pool],
|
609
|
+
rows: Iterable[T],
|
610
|
+
insert_only: FieldNamesSet = (),
|
611
|
+
) -> None:
|
612
|
+
args = [r.field_values() for r in rows]
|
613
|
+
query = cls.upsert_sql(
|
614
|
+
cls.insert_multiple_executemany_chunk_sql(1), exclude=insert_only
|
615
|
+
).query()[0]
|
616
|
+
if args:
|
617
|
+
await connection_or_pool.executemany(query, args)
|
618
|
+
|
535
619
|
@classmethod
|
536
620
|
async def upsert_multiple_unnest(
|
537
621
|
cls: type[T],
|
@@ -566,7 +650,12 @@ class ModelBase:
|
|
566
650
|
rows: Iterable[T],
|
567
651
|
insert_only: FieldNamesSet = (),
|
568
652
|
) -> str:
|
569
|
-
if cls.
|
653
|
+
if cls.insert_multiple_mode == "executemany":
|
654
|
+
await cls.upsert_multiple_executemany(
|
655
|
+
connection_or_pool, rows, insert_only=insert_only
|
656
|
+
)
|
657
|
+
return "INSERT"
|
658
|
+
elif cls.insert_multiple_mode == "array_safe":
|
570
659
|
return await cls.upsert_multiple_array_safe(
|
571
660
|
connection_or_pool, rows, insert_only=insert_only
|
572
661
|
)
|
File without changes
|