sql-athame 0.4.0a10__tar.gz → 0.4.0a11__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.
@@ -0,0 +1,19 @@
1
+ [bumpversion]
2
+ current_version = 0.4.0-alpha-11
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,7 @@
1
+ root = true
2
+
3
+ [*]
4
+ end_of_line = lf
5
+ insert_final_newline = true
6
+ indent_style = space
7
+ indent_size = 4
@@ -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,48 @@
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
+ steps:
29
+ - uses: actions/checkout@v4
30
+
31
+ - uses: astral-sh/setup-uv@v5
32
+ with:
33
+ version: "0.6.6"
34
+ enable-cache: true
35
+ python-version: ${{ matrix.python-version }}
36
+
37
+ - uses: actions/setup-python@v5
38
+ with:
39
+ python-version: ${{ matrix.python-version }}
40
+
41
+ - run: uv sync
42
+ - run: uv run ruff check
43
+ - run: uv run ruff format --diff
44
+ if: success() || failure()
45
+ - run: uv run mypy sql_athame/**.py tests/**.py
46
+ if: success() || failure()
47
+ - run: uv run pytest
48
+ if: success() || failure()
@@ -0,0 +1,9 @@
1
+ *.egg-info
2
+ .*_cache
3
+ .coverage
4
+ .venv
5
+ __pycache__
6
+ pip-wheel-metadata
7
+
8
+ /dist
9
+ /results
@@ -1,22 +1,16 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: sql-athame
3
- Version: 0.4.0a10
3
+ Version: 0.4.0a11
4
4
  Summary: Python tool for slicing and dicing SQL
5
- Home-page: https://github.com/bdowning/sql-athame
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
- Author: Brian Downing
8
- Author-email: bdowning@lavos.net
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
- Project-URL: Repository, https://github.com/bdowning/sql-athame
12
+ Provides-Extra: asyncpg
13
+ Requires-Dist: asyncpg; extra == 'asyncpg'
20
14
  Description-Content-Type: text/markdown
21
15
 
22
16
  # sql-athame
@@ -405,4 +399,3 @@ MIT.
405
399
 
406
400
  ---
407
401
  Copyright (c) 2019, 2020 Brian Downing
408
-
@@ -0,0 +1,9 @@
1
+ version: "2"
2
+
3
+ services:
4
+ postgres:
5
+ image: postgres:12
6
+ ports:
7
+ - 29329:5432
8
+ environment:
9
+ POSTGRES_PASSWORD: password
@@ -1,37 +1,44 @@
1
- [tool.poetry]
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-10"
11
+ version = "0.4.0-alpha-11"
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
- [tool.poetry.extras]
12
- asyncpg = ["asyncpg"]
13
-
14
- [tool.poetry.dependencies]
15
- python = "^3.9"
16
- asyncpg = { version = "*", optional = true }
17
- typing-extensions = "*"
19
+ [project.optional-dependencies]
20
+ asyncpg = [
21
+ "asyncpg",
22
+ ]
18
23
 
19
- [tool.poetry.group.dev.dependencies]
20
- pytest = "*"
21
- mypy = "*"
22
- flake8 = "*"
23
- ipython = "*"
24
- pytest-cov = "*"
25
- bump2version = "*"
26
- asyncpg = "*"
27
- pytest-asyncio = "*"
28
- grip = "*"
29
- SQLAlchemy = "*"
30
- ruff = "*"
24
+ [dependency-groups]
25
+ dev = [
26
+ "SQLAlchemy",
27
+ "asyncpg",
28
+ "bump2version",
29
+ "flake8",
30
+ "grip",
31
+ "ipython",
32
+ "mypy",
33
+ "pytest",
34
+ "pytest-asyncio",
35
+ "pytest-cov",
36
+ "ruff",
37
+ ]
31
38
 
32
39
  [build-system]
33
- requires = ["poetry>=0.12"]
34
- build-backend = "poetry.masonry.api"
40
+ requires = ["hatchling"]
41
+ build-backend = "hatchling.build"
35
42
 
36
43
  [tool.ruff]
37
44
  target-version = "py39"
@@ -62,7 +69,6 @@ ignore = [
62
69
  "E501", # line too long
63
70
  "E721", # type checks, currently broken
64
71
  "ISC001", # conflicts with ruff format
65
- "PT004", # Fixture `...` does not return anything, add leading underscore
66
72
  "RET505", # Unnecessary `else` after `return` statement
67
73
  "RET506", # Unnecessary `else` after `raise` statement
68
74
  ]
@@ -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
- array_safe_insert: bool
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 == "array_safe":
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:
@@ -357,7 +354,17 @@ class ModelBase:
357
354
  return query
358
355
 
359
356
  @classmethod
360
- async def select_cursor(
357
+ async def cursor_from(
358
+ cls: type[T],
359
+ connection: Connection,
360
+ query: Fragment,
361
+ prefetch: int = 1000,
362
+ ) -> AsyncGenerator[T, None]:
363
+ async for row in connection.cursor(*query, prefetch=prefetch):
364
+ yield cls.from_mapping(row)
365
+
366
+ @classmethod
367
+ def select_cursor(
361
368
  cls: type[T],
362
369
  connection: Connection,
363
370
  order_by: Union[FieldNames, str] = (),
@@ -365,11 +372,19 @@ class ModelBase:
365
372
  where: Where = (),
366
373
  prefetch: int = 1000,
367
374
  ) -> AsyncGenerator[T, None]:
368
- async for row in connection.cursor(
369
- *cls.select_sql(order_by=order_by, for_update=for_update, where=where),
375
+ return cls.cursor_from(
376
+ connection,
377
+ cls.select_sql(order_by=order_by, for_update=for_update, where=where),
370
378
  prefetch=prefetch,
371
- ):
372
- yield cls.from_mapping(row)
379
+ )
380
+
381
+ @classmethod
382
+ async def fetch_from(
383
+ cls: type[T],
384
+ connection_or_pool: Union[Connection, Pool],
385
+ query: Fragment,
386
+ ) -> list[T]:
387
+ return [cls.from_mapping(row) for row in await connection_or_pool.fetch(*query)]
373
388
 
374
389
  @classmethod
375
390
  async def select(
@@ -379,12 +394,10 @@ class ModelBase:
379
394
  for_update: bool = False,
380
395
  where: Where = (),
381
396
  ) -> list[T]:
382
- return [
383
- cls.from_mapping(row)
384
- for row in await connection_or_pool.fetch(
385
- *cls.select_sql(order_by=order_by, for_update=for_update, where=where)
386
- )
387
- ]
397
+ return await cls.fetch_from(
398
+ connection_or_pool,
399
+ cls.select_sql(order_by=order_by, for_update=for_update, where=where),
400
+ )
388
401
 
389
402
  @classmethod
390
403
  def create_sql(cls: type[T], **kwargs: Any) -> Fragment:
@@ -506,6 +519,37 @@ class ModelBase:
506
519
  ),
507
520
  )
508
521
 
522
+ @classmethod
523
+ def insert_multiple_executemany_chunk_sql(
524
+ cls: type[T], chunk_size: int
525
+ ) -> Fragment:
526
+ def generate() -> Fragment:
527
+ columns = len(cls.column_info())
528
+ values = ", ".join(
529
+ f"({', '.join(f'${i}' for i in chunk)})"
530
+ for chunk in chunked(range(1, columns * chunk_size + 1), columns)
531
+ )
532
+ return sql(
533
+ "INSERT INTO {table} ({fields}) VALUES {values}",
534
+ table=cls.table_name_sql(),
535
+ fields=sql.list(cls.field_names_sql()),
536
+ values=sql.literal(values),
537
+ ).flatten()
538
+
539
+ return cls._cached(
540
+ ("insert_multiple_executemany_chunk", chunk_size),
541
+ generate,
542
+ )
543
+
544
+ @classmethod
545
+ async def insert_multiple_executemany(
546
+ cls: type[T], connection_or_pool: Union[Connection, Pool], rows: Iterable[T]
547
+ ) -> None:
548
+ args = [r.field_values() for r in rows]
549
+ query = cls.insert_multiple_executemany_chunk_sql(1).query()[0]
550
+ if args:
551
+ await connection_or_pool.executemany(query, args)
552
+
509
553
  @classmethod
510
554
  async def insert_multiple_unnest(
511
555
  cls: type[T], connection_or_pool: Union[Connection, Pool], rows: Iterable[T]
@@ -527,11 +571,28 @@ class ModelBase:
527
571
  async def insert_multiple(
528
572
  cls: type[T], connection_or_pool: Union[Connection, Pool], rows: Iterable[T]
529
573
  ) -> str:
530
- if cls.array_safe_insert:
574
+ if cls.insert_multiple_mode == "executemany":
575
+ await cls.insert_multiple_executemany(connection_or_pool, rows)
576
+ return "INSERT"
577
+ elif cls.insert_multiple_mode == "array_safe":
531
578
  return await cls.insert_multiple_array_safe(connection_or_pool, rows)
532
579
  else:
533
580
  return await cls.insert_multiple_unnest(connection_or_pool, rows)
534
581
 
582
+ @classmethod
583
+ async def upsert_multiple_executemany(
584
+ cls: type[T],
585
+ connection_or_pool: Union[Connection, Pool],
586
+ rows: Iterable[T],
587
+ insert_only: FieldNamesSet = (),
588
+ ) -> None:
589
+ args = [r.field_values() for r in rows]
590
+ query = cls.upsert_sql(
591
+ cls.insert_multiple_executemany_chunk_sql(1), exclude=insert_only
592
+ ).query()[0]
593
+ if args:
594
+ await connection_or_pool.executemany(query, args)
595
+
535
596
  @classmethod
536
597
  async def upsert_multiple_unnest(
537
598
  cls: type[T],
@@ -566,7 +627,12 @@ class ModelBase:
566
627
  rows: Iterable[T],
567
628
  insert_only: FieldNamesSet = (),
568
629
  ) -> str:
569
- if cls.array_safe_insert:
630
+ if cls.insert_multiple_mode == "executemany":
631
+ await cls.upsert_multiple_executemany(
632
+ connection_or_pool, rows, insert_only=insert_only
633
+ )
634
+ return "INSERT"
635
+ elif cls.insert_multiple_mode == "array_safe":
570
636
  return await cls.upsert_multiple_array_safe(
571
637
  connection_or_pool, rows, insert_only=insert_only
572
638
  )
File without changes