diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index da0b513..1a3fa60 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,7 +22,7 @@ jobs: - name: Run ruff via nox run: | - uv run nox -s format + uv run nox -s format_check pyright: runs-on: ubuntu-latest @@ -38,3 +38,25 @@ jobs: - name: Run pyright via nox run: | uv run nox -s pyright + sqlc: + runs-on: ubuntu-latest + name: "Run sqlc verify via nox" + steps: + - uses: actions/checkout@v4 + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: "0.6.9" + python-version: "3.13" + + - name: Install sqlc + uses: sqlc-dev/setup-sqlc@v4 + with: + sqlc-version: '1.28.0' + + - name: Run sqlc verify via nox + run: | + uv run nox -s sqlc_verify + + + diff --git a/db/query.sql b/db/query.sql new file mode 100644 index 0000000..212a226 --- /dev/null +++ b/db/query.sql @@ -0,0 +1,25 @@ +-- name: GetAuthor :one +SELECT * FROM authors +WHERE id = ? LIMIT 1; + +-- name: ListAuthors :many +SELECT * FROM authors +ORDER BY name; + +-- name: CreateAuthor :one +INSERT INTO authors ( + name, bio +) VALUES ( + ?, ? +) +RETURNING *; + +-- name: UpdateAuthor :exec +UPDATE authors +set name = ?, +bio = ? +WHERE id = ?; + +-- name: DeleteAuthor :exec +DELETE FROM authors +WHERE id = ?; diff --git a/db/schema.sql b/db/schema.sql new file mode 100644 index 0000000..9d2770b --- /dev/null +++ b/db/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE authors ( + id INTEGER PRIMARY KEY, + name text NOT NULL, + bio text +); diff --git a/noxfile.py b/noxfile.py index 7932517..122d4ce 100644 --- a/noxfile.py +++ b/noxfile.py @@ -6,9 +6,10 @@ PATH_TO_PROJECT = os.path.join(".", "src") SCRIPT_PATHS = [PATH_TO_PROJECT, "noxfile.py"] +SQLC_CONFIG = "sqlc.yaml" options.default_venv_backend = "uv" -options.sessions = ["format_fix", "pyright"] +options.sessions = ["format", "pyright"] # uv_sync taken from: https://github.com/hikari-py/hikari/blob/master/pipelines/nox.py#L48 @@ -45,14 +46,14 @@ def uv_sync( @nox.session() -def format_fix(session: nox.Session) -> None: +def format(session: nox.Session) -> None: uv_sync(session, groups=["dev"]) session.run("python", "-m", "ruff", "format", *SCRIPT_PATHS) session.run("python", "-m", "ruff", "check", *SCRIPT_PATHS, "--fix") @nox.session() -def format(session: nox.Session) -> None: +def format_check(session: nox.Session) -> None: uv_sync(session, groups=["dev"]) session.run("python", "-m", "ruff", "format", *SCRIPT_PATHS, "--check") session.run("python", "-m", "ruff", "check", *SCRIPT_PATHS) @@ -62,3 +63,17 @@ def format(session: nox.Session) -> None: def pyright(session: nox.Session) -> None: uv_sync(session, include_self=True, groups=["dev"]) session.run("pyright", *SCRIPT_PATHS) + + +@nox.session() +def sqlc(session: nox.Session) -> None: + # note: this required sqlc to be installed on the system. + # https://docs.sqlc.dev/en/latest/overview/install.html + session.run("sqlc", "generate", "-f", SQLC_CONFIG, external=True) + + +@nox.session() +def sqlc_verify(session: nox.Session) -> None: + # note: this required sqlc to be installed on the system. + # https://docs.sqlc.dev/en/latest/overview/install.html + session.run("sqlc", "diff", "-f", SQLC_CONFIG, external=True) diff --git a/pyproject.toml b/pyproject.toml index 237948a..1cc2bd1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ dependencies = [ "hikari-arc>=2.0.0", "hikari[speedups]>=2.2.0", "python-dotenv>=1.0.1", + "sqlc-asyncpg-compat", ] [dependency-groups] @@ -78,6 +79,9 @@ ignore_errors = true [tool.uv] required-version = "~=0.6" +[tool.uv.sources] +sqlc-asyncpg-compat = { git = "https://github.com/tandemdude/sqlc-asyncpg-compat" } + [tool.pyright] pythonVersion = "3.13" typeCheckingMode = "strict" diff --git a/sqlc.yaml b/sqlc.yaml new file mode 100644 index 0000000..7cd6c56 --- /dev/null +++ b/sqlc.yaml @@ -0,0 +1,18 @@ +version: "2" +plugins: +- name: py + wasm: + url: "https://downloads.sqlc.dev/plugin/sqlc-gen-python_1.3.0.wasm" + sha256: "fbedae96b5ecae2380a70fb5b925fd4bff58a6cfb1f3140375d098fbab7b3a3c" +sql: + - engine: "sqlite" + queries: "db/query.sql" + schema: "db/schema.sql" + codegen: + - out: "src/db" + plugin: py + options: + package: "src.db" + emit_sync_querier: false + emit_async_querier: true + emit_str_enum: true diff --git a/src/db/__init__.py b/src/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/db/models.py b/src/db/models.py new file mode 100644 index 0000000..bdf0ce5 --- /dev/null +++ b/src/db/models.py @@ -0,0 +1,12 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.28.0 +import dataclasses +from typing import Any, Optional + + +@dataclasses.dataclass() +class Author: + id: Any + name: Any + bio: Optional[Any] diff --git a/src/db/query.py b/src/db/query.py new file mode 100644 index 0000000..1da6c1f --- /dev/null +++ b/src/db/query.py @@ -0,0 +1,73 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.28.0 +# source: query.sql +from typing import Any, AsyncIterator, Optional + +import sqlalchemy +import sqlalchemy.ext.asyncio + +from src.db import models + +CREATE_AUTHOR = """-- name: create_author \\:one +INSERT INTO authors ( + name, bio +) VALUES ( + ?, ? +) +RETURNING id, name, bio +""" + + +DELETE_AUTHOR = """-- name: delete_author \\:exec +DELETE FROM authors +WHERE id = ? +""" + + +GET_AUTHOR = """-- name: get_author \\:one +SELECT id, name, bio FROM authors +WHERE id = ? LIMIT 1 +""" + + +LIST_AUTHORS = """-- name: list_authors \\:many +SELECT id, name, bio FROM authors +ORDER BY name +""" + + +UPDATE_AUTHOR = """-- name: update_author \\:exec +UPDATE authors +set name = ?, +bio = ? +WHERE id = ? +""" + + +class AsyncQuerier: + def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): + self._conn = conn + + async def create_author(self, *, name: Any, bio: Optional[Any]) -> Optional[models.Author]: + row = (await self._conn.execute(sqlalchemy.text(CREATE_AUTHOR), {"p1": name, "p2": bio})).first() + if row is None: + return None + return models.Author(id=row[0], name=row[1], bio=row[2]) + + async def delete_author(self, *, id: Any) -> None: + await self._conn.execute(sqlalchemy.text(DELETE_AUTHOR), {"p1": id}) + + async def get_author(self, *, id: Any) -> Optional[models.Author]: + row = (await self._conn.execute(sqlalchemy.text(GET_AUTHOR), {"p1": id})).first() + if row is None: + return None + return models.Author(id=row[0], name=row[1], bio=row[2]) + + async def list_authors(self) -> AsyncIterator[models.Author]: + result = await self._conn.stream(sqlalchemy.text(LIST_AUTHORS)) + async for row in result: + yield models.Author(id=row[0], name=row[1], bio=row[2]) + + async def update_author(self, *, name: Any, bio: Optional[Any], id: Any) -> None: + await self._conn.execute(sqlalchemy.text(UPDATE_AUTHOR), {"p1": name, "p2": bio, "p3": id}) diff --git a/uv.lock b/uv.lock index 105aa44..b931cc0 100644 --- a/uv.lock +++ b/uv.lock @@ -96,6 +96,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f5/c8/9fa0e6fa97c328d44e089278399b0a1a08268b06a4a71f7448c6b6effb9f/argcomplete-3.6.1-py3-none-any.whl", hash = "sha256:cef54d7f752560570291214f0f1c48c3b8ef09aca63d65de7747612666725dbc", size = 43984 }, ] +[[package]] +name = "asyncpg" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/22/e20602e1218dc07692acf70d5b902be820168d6282e69ef0d3cb920dc36f/asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", size = 670373 }, + { url = "https://files.pythonhosted.org/packages/3d/b3/0cf269a9d647852a95c06eb00b815d0b95a4eb4b55aa2d6ba680971733b9/asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", size = 634745 }, + { url = "https://files.pythonhosted.org/packages/8e/6d/a4f31bf358ce8491d2a31bfe0d7bcf25269e80481e49de4d8616c4295a34/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", size = 3512103 }, + { url = "https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4", size = 3592471 }, + { url = "https://files.pythonhosted.org/packages/67/e4/ab3ca38f628f53f0fd28d3ff20edff1c975dd1cb22482e0061916b4b9a74/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4", size = 3496253 }, + { url = "https://files.pythonhosted.org/packages/ef/5f/0bf65511d4eeac3a1f41c54034a492515a707c6edbc642174ae79034d3ba/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba", size = 3662720 }, + { url = "https://files.pythonhosted.org/packages/e7/31/1513d5a6412b98052c3ed9158d783b1e09d0910f51fbe0e05f56cc370bc4/asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590", size = 560404 }, + { url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623 }, +] + [[package]] name = "attrs" version = "25.3.0" @@ -289,6 +305,7 @@ dependencies = [ { name = "hikari", extra = ["speedups"] }, { name = "hikari-arc" }, { name = "python-dotenv" }, + { name = "sqlc-asyncpg-compat" }, ] [package.dev-dependencies] @@ -306,6 +323,7 @@ requires-dist = [ { name = "hikari", extras = ["speedups"], specifier = ">=2.2.0" }, { name = "hikari-arc", specifier = ">=2.0.0" }, { name = "python-dotenv", specifier = ">=1.0.1" }, + { name = "sqlc-asyncpg-compat", git = "https://github.com/tandemdude/sqlc-asyncpg-compat" }, ] [package.metadata.requires-dev] @@ -551,6 +569,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d6/d4/dd813703af8a1e2ac33bf3feb27e8a5ad514c9f219df80c64d69807e7f71/ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4", size = 10441990 }, ] +[[package]] +name = "sqlc-asyncpg-compat" +version = "0.0.1" +source = { git = "https://github.com/tandemdude/sqlc-asyncpg-compat#f2018a95a80e4e0e3e771355061da7bee4d34232" } +dependencies = [ + { name = "asyncpg" }, + { name = "typing-extensions" }, +] + [[package]] name = "typing-extensions" version = "4.12.2"