Skip to content

Commit c39ca38

Browse files
author
Robert Holt
committed
Initial commit
0 parents  commit c39ca38

File tree

13 files changed

+732
-0
lines changed

13 files changed

+732
-0
lines changed

.github/workflows/linting.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Linting
2+
on: [push, pull_request]
3+
jobs:
4+
test:
5+
name: linting
6+
runs-on: ubuntu-latest
7+
8+
steps:
9+
- uses: actions/checkout@v2
10+
- name: Set up Python 3.9
11+
uses: actions/setup-python@v2
12+
with:
13+
python-version: "3.9"
14+
- name: Install poetry
15+
uses: snok/install-poetry@v1.1.1
16+
with:
17+
virtualenvs-create: true
18+
virtualenvs-in-project: true
19+
- name: Load cached venv
20+
id: cached-poetry-dependencies
21+
uses: actions/cache@v2
22+
with:
23+
path: .venv
24+
key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}
25+
- name: Install dependencies
26+
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
27+
run: poetry install --no-interaction
28+
- name: Run lint checks
29+
run: |
30+
source .venv/bin/activate
31+
black --check --diff .
32+
mypy

.github/workflows/release.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Release
2+
on:
3+
release:
4+
types:
5+
- created
6+
jobs:
7+
release:
8+
name: release
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- uses: actions/checkout@v2
13+
- name: Set up Python 3.9
14+
uses: actions/setup-python@v2
15+
with:
16+
python-version: "3.9"
17+
- name: Install poetry
18+
uses: snok/install-poetry@v1.1.1
19+
with:
20+
virtualenvs-create: true
21+
virtualenvs-in-project: true
22+
- name: Publish release
23+
env:
24+
PYPI_USERNAME: ${{ secrets.PYPI_USERNAME }}
25+
PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
26+
run: |
27+
poetry publish --build --username "${PYPI_USERNAME}" --password "${PYPI_PASSWORD}"

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
dist/
2+
.idea/
3+
__pycache__
4+
*.egg-info
5+
.mypy_cache
6+
.python-version
7+
*.venv

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2021 Datto Inc.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# sqlc-python-runtime
2+
3+
A small runtime library for python code generated by [sqlc](https://github.com/kyleconroy/sqlc)
4+
5+
## Installation
6+
7+
Add the `sqlc-python-runtime` package to your project's dependencies.
8+
9+
## Usage
10+
11+
All query functions generated by sqlc take a database connection object as their first argument.
12+
This connection object must conform to the `Connection`/`AsyncConnection` interface defined in this package.
13+
This package also provides wrappers for `psycopg2` and `asyncpg` that conform to this interface.
14+
15+
`psycopg2` example:
16+
```python
17+
import psycopg2
18+
from sqlc_runtime.psycopg2 import build_psycopg2_connection
19+
20+
21+
conn = build_psycopg2_connection(psycopg2.connect("postgresql://localhost:5432/mydatabase"))
22+
```
23+
24+
Also note that the generated query functions can use either a `Connection` or an `AsyncConnection`.
25+
If an `AsyncConnection` is passed the result must be awaited. The function type hints communicate this so any type
26+
checkers (like mypy) will raise errors if any are missed or incorrectly used.

mypy.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[mypy]
2+
ignore_missing_imports = True
3+
disallow_untyped_defs = True
4+
files = sqlc_runtime

poetry.lock

Lines changed: 353 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[tool.poetry]
2+
name = "sqlc-python-runtime"
3+
version = "1.0.1"
4+
description = "Runtime components for code generated by sqlc"
5+
authors = ["Robert Holt <rholt@datto.com>"]
6+
license = "MIT"
7+
homepage = "https://github.com/robholt/sqlc-python-runtime"
8+
readme = "README.md"
9+
packages = [{include = "sqlc_runtime"}]
10+
include = ["README.md"]
11+
12+
[tool.poetry.dependencies]
13+
python = "^3.7"
14+
psycopg2-binary = {version = "^2.8.6", optional = true}
15+
asyncpg = {version = "^0.22.0", optional = true}
16+
pydantic = "^1.4"
17+
18+
[tool.poetry.dev-dependencies]
19+
mypy = "^0.812"
20+
black = "^20.8b1"
21+
22+
[tool.poetry.extras]
23+
psycopg2 = ["psycopg2-binary"]
24+
asyncpg = ["asyncpg"]
25+
26+
[build-system]
27+
requires = ["poetry-core>=1.0.0"]
28+
build-backend = "poetry.core.masonry.api"

sqlc_runtime/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from .interface import (
2+
GenericConnection,
3+
Connection,
4+
AsyncConnection,
5+
GenericCursor,
6+
Cursor,
7+
AsyncCursor,
8+
ReturnType,
9+
IteratorReturn,
10+
)

sqlc_runtime/asyncpg.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from typing import Any, Type, Optional, AsyncIterator, TypeVar
2+
3+
import asyncpg
4+
import pydantic
5+
6+
from sqlc_runtime import AsyncCursor, AsyncConnection
7+
8+
T = TypeVar("T", bound=pydantic.BaseModel)
9+
10+
11+
def build_asyncpg_connection(conn: asyncpg.Connection) -> AsyncConnection:
12+
return AsyncpgConnection(conn)
13+
14+
15+
class AsyncpgConnection:
16+
def __init__(self, conn: asyncpg.Connection):
17+
self._conn = conn
18+
19+
async def execute(self, query: str, *params: Any) -> AsyncCursor:
20+
return await self._conn.cursor(query, *params)
21+
22+
async def execute_none(self, query: str, *params: Any) -> None:
23+
await self._conn.execute(query, *params)
24+
25+
async def execute_rowcount(self, query: str, *params: Any) -> int:
26+
status = await self._conn.execute(query, *params)
27+
return int(status.split(" ")[-1])
28+
29+
async def execute_one(self, query: str, *params: Any) -> Any:
30+
row = await self._conn.fetchrow(query, *params)
31+
return row[0] if row is not None else None
32+
33+
async def execute_one_model(
34+
self, model: Type[T], query: str, *params: Any
35+
) -> Optional[T]:
36+
row = await self._conn.fetchrow(query, *params)
37+
if row is None:
38+
return None
39+
return model.parse_obj(row)
40+
41+
async def execute_many(self, query: str, *params: Any) -> AsyncIterator[Any]:
42+
async with self._conn.transaction():
43+
async for row in self._conn.cursor(query, *params):
44+
yield row[0]
45+
46+
async def execute_many_model(
47+
self, model: Type[T], query: str, *params: Any
48+
) -> AsyncIterator[T]:
49+
async with self._conn.transaction():
50+
async for row in self._conn.cursor(query, *params):
51+
yield model.parse_obj(row)

sqlc_runtime/interface.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from typing import (
2+
Protocol,
3+
Any,
4+
Union,
5+
Sequence,
6+
Tuple,
7+
Optional,
8+
Mapping,
9+
Iterator,
10+
Type,
11+
TypeVar,
12+
AsyncIterator,
13+
Awaitable,
14+
)
15+
16+
import pydantic
17+
18+
19+
class Cursor(Protocol):
20+
rowcount: int
21+
22+
def fetchone(self) -> Mapping[str, Any]:
23+
pass
24+
25+
def fetchmany(self, size: int = None) -> Sequence[Mapping[str, Any]]:
26+
pass
27+
28+
def fetchall(self) -> Sequence[Mapping[str, Any]]:
29+
pass
30+
31+
def __iter__(self) -> Iterator[Mapping[str, Any]]:
32+
pass
33+
34+
def __len__(self) -> int:
35+
pass
36+
37+
38+
T = TypeVar("T", bound=pydantic.BaseModel)
39+
40+
41+
class Connection(Protocol):
42+
def execute(self, query: str, *params: Any) -> Cursor:
43+
pass
44+
45+
def execute_none(self, query: str, *params: Any) -> None:
46+
pass
47+
48+
def execute_rowcount(self, query: str, *params: Any) -> int:
49+
pass
50+
51+
def execute_one(self, query: str, *params: Any) -> Any:
52+
pass
53+
54+
def execute_one_model(
55+
self, model: Type[T], query: str, *params: Any
56+
) -> Optional[T]:
57+
pass
58+
59+
def execute_many(self, query: str, *params: Any) -> Iterator[Any]:
60+
pass
61+
62+
def execute_many_model(
63+
self, model: Type[T], query: str, *params: Any
64+
) -> Iterator[T]:
65+
pass
66+
67+
68+
class AsyncCursor(Protocol):
69+
async def fetch(
70+
self, n: int, *, timeout: float = None
71+
) -> Sequence[Mapping[str, Any]]:
72+
pass
73+
74+
async def fetchrow(self, *, timeout: float = None) -> Mapping[str, Any]:
75+
pass
76+
77+
async def forward(self, n: int, *, timeout: float = None) -> int:
78+
pass
79+
80+
81+
class AsyncConnection(Protocol):
82+
async def execute(self, query: str, *params: Any) -> AsyncCursor:
83+
pass
84+
85+
async def execute_none(self, query: str, *params: Any) -> None:
86+
pass
87+
88+
async def execute_rowcount(self, query: str, *params: Any) -> int:
89+
pass
90+
91+
async def execute_one(self, query: str, *params: Any) -> Any:
92+
pass
93+
94+
async def execute_one_model(
95+
self, model: Type[T], query: str, *params: Any
96+
) -> Optional[T]:
97+
pass
98+
99+
def execute_many(self, query: str, *params: Any) -> AsyncIterator[Any]:
100+
pass
101+
102+
def execute_many_model(
103+
self, model: Type[T], query: str, *params: Any
104+
) -> AsyncIterator[T]:
105+
pass
106+
107+
108+
GenericConnection = Union[Connection, AsyncConnection]
109+
GenericCursor = Union[Cursor, AsyncCursor]
110+
111+
RT = TypeVar("RT")
112+
ReturnType = Union[RT, Awaitable[RT]]
113+
IteratorReturn = Union[Iterator[RT], AsyncIterator[RT]]

sqlc_runtime/psycopg2.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import re
2+
from typing import Any, Type, Iterator, TypeVar, Optional, TYPE_CHECKING
3+
4+
import pydantic
5+
from psycopg2.extensions import connection
6+
from psycopg2.extras import DictCursor
7+
8+
from sqlc_runtime import Connection
9+
10+
T = TypeVar("T", bound=pydantic.BaseModel)
11+
PSYCOPG2_PLACEHOLDER_REGEXP = re.compile(r"\B\$\d+\b")
12+
13+
14+
def build_psycopg2_connection(conn: connection) -> Connection:
15+
return Psycopg2Connection(conn)
16+
17+
18+
class Psycopg2Connection:
19+
def __init__(self, conn: connection):
20+
self._conn = conn
21+
22+
def execute(self, query: str, *params: Any) -> DictCursor:
23+
query = PSYCOPG2_PLACEHOLDER_REGEXP.sub("%s", query)
24+
cur = self._conn.cursor(cursor_factory=DictCursor)
25+
cur.execute(query, params)
26+
return cur
27+
28+
def execute_none(self, query: str, *params: Any) -> None:
29+
with self.execute(query, *params):
30+
return
31+
32+
def execute_rowcount(self, query: str, *params: Any) -> int:
33+
with self.execute(query, *params) as cur:
34+
return cur.rowcount
35+
36+
def execute_one(self, query: str, *params: Any) -> Any:
37+
with self.execute(query, *params) as cur:
38+
row = cur.fetchone()
39+
return row[0] if row is not None else None
40+
41+
def execute_one_model(
42+
self, model: Type[T], query: str, *params: Any
43+
) -> Optional[T]:
44+
with self.execute(query, *params) as cur:
45+
row = cur.fetchone()
46+
if row is None:
47+
return None
48+
return model.parse_obj(row)
49+
50+
def execute_many(self, query: str, *params: Any) -> Iterator[Any]:
51+
with self.execute(query, *params) as cur:
52+
for row in cur:
53+
yield row[0]
54+
55+
def execute_many_model(
56+
self, model: Type[T], query: str, *params: Any
57+
) -> Iterator[T]:
58+
with self.execute(query, *params) as cur:
59+
for row in cur:
60+
yield model.parse_obj(row)

sqlc_runtime/py.typed

Whitespace-only changes.

0 commit comments

Comments
 (0)