pgdevkit 0.15.2__py3-none-any.whl → 0.16.0__py3-none-any.whl

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.
@@ -1,3 +1,4 @@
1
+ from .cancel_on_disconnect import CancelOnDisconnectRoute
1
2
  from .json_response import PostgresJsonResponse
2
3
 
3
- __all__ = ["PostgresJsonResponse"]
4
+ __all__ = ["CancelOnDisconnectRoute", "PostgresJsonResponse"]
@@ -0,0 +1,132 @@
1
+ """Cancel a FastAPI handler when its client goes away, so the database work it started stops too.
2
+
3
+ Requires the ``fastapi`` extra: ``pip install pgdevkit[fastapi]``.
4
+
5
+ ASGI servers such as Granian do not cancel a request handler when the client disconnects: the handler runs to the
6
+ end and its result is thrown away. ``PostgresJsonResponse`` watches for the disconnect itself, but ``fetch_all`` and
7
+ friends (or any other awaitable work, raw psycopg cursors included) only stop when the awaiting task is cancelled.
8
+ ``CancelOnDisconnectRoute`` closes that gap: it runs the route handler as a task and cancels it on
9
+ ``http.disconnect``; psycopg and pgdevkit then cancel the running statement on the server.
10
+ """
11
+
12
+ import asyncio
13
+ import logging
14
+ from collections.abc import Callable, Coroutine
15
+ from typing import Any
16
+
17
+ from fastapi import HTTPException, Request, Response
18
+ from fastapi.routing import APIRoute
19
+ from starlette.requests import ClientDisconnect
20
+ from starlette.types import Message, Receive
21
+
22
+ from .json_response import CLIENT_CLOSED_REQUEST
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # Body chunks read ahead of the handler: enough to never stall it, too few to buffer an upload in memory.
27
+ _MAX_PENDING_MESSAGES = 8
28
+
29
+
30
+ class CancelOnDisconnectRoute(APIRoute):
31
+ """An ``APIRoute`` that cancels its handler as soon as the client disconnects.
32
+
33
+ Opt in per router, never for the whole app, and only for routes that are safe to abort halfway: reads, searches,
34
+ exports. A cancelled write is rolled back at best and half-applied at worst::
35
+
36
+ cancellable = APIRouter(route_class=CancelOnDisconnectRoute)
37
+
38
+ @cancellable.post("/customers/overview") # a POST that only reads
39
+ async def overview(payload: Filter) -> Page:
40
+ return await fetch_all(...) # aborted in Postgres when the client leaves
41
+
42
+ app.include_router(cancellable)
43
+
44
+ A single route can opt in with ``router.add_api_route(..., route_class_override=CancelOnDisconnectRoute)``
45
+ (``APIRouter`` only; the ``@router.get`` decorators don't take it).
46
+
47
+ Things to know:
48
+
49
+ * **Everything inside the route is cancelled**, dependencies included (authentication, audit logging, counters)
50
+ and so is ``BackgroundTasks`` registration, since the client gets no response. Don't rely on side effects that
51
+ must always happen; shield them or do them in a middleware. A ``Depends`` with ``yield`` sees the 499
52
+ ``HTTPException`` after the cancellation, so its cleanup rolls back instead of committing.
53
+ * The request body is not buffered: the handler reads it as usual, from a ``Request`` whose ``receive()`` is fed
54
+ by a reader task that also spots the disconnect. While the handler isn't reading a large body, the reader
55
+ stops after a few chunks, so a disconnect is only noticed once the handler has consumed the body.
56
+ * Work shared with other requests must survive the cancellation of one of them: await shared tasks and futures
57
+ through ``asyncio.shield`` (cache single-flight loaders, background refreshes), otherwise one aborted request
58
+ cancels the load every other request is waiting on.
59
+ * A returned ``StreamingResponse`` is not covered (the handler has finished by the time it is sent): Starlette
60
+ watches for the disconnect there only on servers with ASGI spec < 2.4, ``PostgresJsonResponse`` always does.
61
+ * Threads (``asyncio.to_thread``) and external services (Databricks statements, HTTP calls) keep running after
62
+ the cancellation; only awaiting them stops.
63
+ """
64
+
65
+ def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]:
66
+ handler = super().get_route_handler()
67
+
68
+ async def cancel_on_disconnect(request: Request) -> Response:
69
+ # The handler runs in the request's own task, as without this route (context variables, dependencies with
70
+ # `yield`, an outer `asyncio.timeout` all behave as usual); an asyncio timeout is the cancel scope, as in
71
+ # PostgresJsonResponse. The disconnect pulls its deadline to "now".
72
+ loop = asyncio.get_running_loop()
73
+ receiver = _Receiver(request.receive)
74
+ request._receive = receiver.receive # the same Request object, so a cached body is seen by everyone
75
+ reader = asyncio.create_task(receiver.read())
76
+
77
+ async def cancel_when_gone(cancel_scope: asyncio.Timeout) -> None:
78
+ await receiver.disconnected.wait()
79
+ cancel_scope.reschedule(loop.time())
80
+
81
+ try:
82
+ async with asyncio.timeout(None) as cancel_scope:
83
+ watcher = asyncio.create_task(cancel_when_gone(cancel_scope))
84
+ try:
85
+ return await handler(request)
86
+ finally:
87
+ watcher.cancel()
88
+ except TimeoutError:
89
+ if not cancel_scope.expired(): # the handler's own timeout
90
+ raise
91
+ except ClientDisconnect: # it was reading the body when the client left
92
+ pass
93
+ finally:
94
+ reader.cancel()
95
+ logger.debug("client disconnected, cancelled %s %s", request.method, request.url.path)
96
+ # Raised, not returned: the dependencies' exit code (e.g. a transaction) must see a failure, not a success.
97
+ raise HTTPException(status_code=CLIENT_CLOSED_REQUEST, detail="Client closed the request")
98
+
99
+ return cancel_on_disconnect
100
+
101
+
102
+ class _Receiver:
103
+ """Gives the handler's ``Request`` its own ``receive()`` while one reader task watches for the disconnect."""
104
+
105
+ def __init__(self, receive: Receive) -> None:
106
+ self._receive = receive
107
+ self._messages: asyncio.Queue[Message | Exception] = asyncio.Queue(maxsize=_MAX_PENDING_MESSAGES)
108
+ self._gone: Message | None = None
109
+ self.disconnected = asyncio.Event()
110
+
111
+ async def read(self) -> None:
112
+ """Pump the server's messages into the queue until the client is gone (the queue's limit is the back-pressure)."""
113
+ try:
114
+ while True:
115
+ message = await self._receive()
116
+ if message["type"] == "http.disconnect":
117
+ self.disconnected.set()
118
+ await self._messages.put(message)
119
+ if message["type"] == "http.disconnect":
120
+ return
121
+ except Exception as exc: # noqa: BLE001 - a broken server: the handler sees it, the client is not treated as gone
122
+ await self._messages.put(exc)
123
+
124
+ async def receive(self) -> Message:
125
+ if self._gone is not None: # the disconnect is final, whoever asks again
126
+ return self._gone
127
+ message = await self._messages.get()
128
+ if isinstance(message, Exception):
129
+ raise message
130
+ if message["type"] == "http.disconnect":
131
+ self._gone = message
132
+ return message
@@ -4,10 +4,11 @@ Requires the ``fastapi`` extra: ``pip install pgdevkit[fastapi]``.
4
4
  """
5
5
 
6
6
  import asyncio
7
+ import contextlib
7
8
  import json
8
9
  import logging
9
- from contextlib import aclosing
10
10
  from collections.abc import AsyncGenerator, Mapping
11
+ from contextlib import aclosing
11
12
  from string.templatelib import Template
12
13
  from typing import Any, cast
13
14
 
@@ -34,6 +35,8 @@ from ..db.fetch import (
34
35
 
35
36
  logger = logging.getLogger(__name__)
36
37
 
38
+ CLIENT_CLOSED_REQUEST = 499 # nginx's code for a client that left; nobody reads it
39
+
37
40
  _CANCEL_TIMEOUT_SECONDS = 5.0
38
41
  # libpq < 17 (a system libpq; psycopg's binary wheels bundle a newer one) has no chunked rows mode.
39
42
  _CHUNKED_ROWS = pq.version() >= 170000
@@ -115,17 +118,29 @@ class PostgresJsonResponse(StreamingResponse):
115
118
  pass
116
119
  cancel_scope.reschedule(loop.time()) # i.e. cancel the stream now
117
120
 
121
+ started = False
122
+
123
+ async def tracking_send(message: Message) -> None:
124
+ nonlocal started
125
+ started = started or message["type"] == "http.response.start"
126
+ await send(message)
127
+
118
128
  try:
119
129
  async with asyncio.timeout(None) as cancel_scope:
120
130
  watcher = asyncio.create_task(watch_for_disconnect(cancel_scope))
121
131
  try:
122
- await self.stream_response(send)
132
+ await self.stream_response(tracking_send)
123
133
  finally:
124
134
  watcher.cancel()
125
135
  except TimeoutError:
126
136
  if not cancel_scope.expired():
127
137
  raise
128
- # the client is gone and the query has been cancelled: nobody is left to answer
138
+ # The client is gone and the query has been cancelled: nobody is left to answer. A middleware in front
139
+ # (BaseHTTPMiddleware) still expects a response, though, or it fails with "No response returned.".
140
+ if not started:
141
+ with contextlib.suppress(OSError, ClientDisconnect):
142
+ await send(self._start_message(CLIENT_CLOSED_REQUEST, []))
143
+ await send({"type": "http.response.body", "body": b"", "more_body": False})
129
144
  if self.background is not None:
130
145
  await self.background()
131
146
 
@@ -6,7 +6,8 @@ description: >
6
6
  Docker/Podman test database (`pgdb testdb`), importable ORM-free CRUD and
7
7
  query helpers (`pgdevkit.db`: `pg_*`, `fetch_all`/`fetch_one`/`fetch_scalar`/`execute`, `readonly_transaction`, `PgPool`, `SqlLoader`), streaming a query as JSON from a
8
8
  FastAPI endpoint with query cancellation on client disconnect (`pgdevkit.fastapi.PostgresJsonResponse`: large grid
9
- endpoints), and the `database/`-folder schema-as-code convention. Supersedes the old postgres-test-setup, postgres-best-practices,
9
+ endpoints; `pgdevkit.fastapi.CancelOnDisconnectRoute`: cancel plain `fetch_all`/cursor read handlers when the client
10
+ aborts, e.g. a frontend AbortController), and the `database/`-folder schema-as-code convention. Supersedes the old postgres-test-setup, postgres-best-practices,
10
11
  and database-in-source skills — pgdevkit is a real dependency now, not
11
12
  copy-pasted reference files. Use whenever the user wants to set up a local
12
13
  test Postgres, write or review psycopg code, add a table/view/function to
@@ -233,7 +234,7 @@ deleted = await execute("DELETE FROM sessions WHERE user_id = %(id)s", {"id": 1}
233
234
  - `model=` validates each row into a Pydantic model; `row_mapper=` maps each dict row to anything else (exclusive with `model`).
234
235
  - Passing `con=` runs on *your* connection: `fetch_all` never commits, closes or releases it. Without it, a connection is borrowed from `pool=` (or the `set_default_pool()` pool) and released afterwards.
235
236
  - `statement_timeout=<seconds>` (optional) lets Postgres abort the query after that long, raising `QueryCanceled`; it is applied with `SET LOCAL` semantics for this call only (a borrowed `con` gets its previous value back). A plain value *replaces* the connection's own setting in both directions, so a per-call 900 s overrides a database-level safety net of 600 s; `statement_timeout=at_most(120)` (`from pgdevkit.db import at_most`) never loosens it: the effective timeout is `min(current, 120)`.
236
- - `cancel=` takes an `asyncio.Event`: set it (e.g. when the HTTP client disconnects) and the running query is cancelled on the server, raising `psycopg.errors.QueryCanceled`. If the awaiting task is cancelled instead, the server-side query is cancelled too and `CancelledError` propagates as usual (not `QueryCanceled`), so no query keeps running unattended. Don't use `cancel=` on a `con` that concurrent tasks share (it aborts whatever statement is running on it); if the cancel request itself can't be delivered, the connection is closed. After `QueryCanceled` a borrowed `con` is usable again once you `await con.rollback()` (autocommit connections need nothing). To stream a whole result as JSON from FastAPI (it wires the disconnect itself) see `PostgresJsonResponse` below.
237
+ - `cancel=` takes an `asyncio.Event`: set it (e.g. when the HTTP client disconnects) and the running query is cancelled on the server, raising `psycopg.errors.QueryCanceled`. If the awaiting task is cancelled instead, the server-side query is cancelled too and `CancelledError` propagates as usual (not `QueryCanceled`), so no query keeps running unattended. Don't use `cancel=` on a `con` that concurrent tasks share (it aborts whatever statement is running on it); if the cancel request itself can't be delivered, the connection is closed. After `QueryCanceled` a borrowed `con` is usable again once you `await con.rollback()` (autocommit connections need nothing). To stream a whole result as JSON from FastAPI (it wires the disconnect itself) see `PostgresJsonResponse` below; to cancel a plain `fetch_all` endpoint when the client leaves, see `CancelOnDisconnectRoute` below.
237
238
  - `query` is typed `SqlQuery`: a literal string (`SqlLoader.load_sql()`), a sqlglot expression (any `exp.Expr`, so Select, Union, Subquery and other `exp.Query` values are fine), a `psycopg.sql` composable or a t-string. A plain `str` fails type checking on purpose, so don't build SQL with f-strings/concatenation. A sqlglot expression is only as safe as the strings it was built from (its builders parse plain strings as SQL): pass user values as `exp.Placeholder` + `params`, never as literals/raw text. A literal `%` in a sqlglot expression (`LIKE 'a%'`, modulo) just works, with or without `params`; in a plain string or `psycopg.sql` composable it is psycopg's rule: write `%%` when you pass `params`. A t-string carries its own values, so don't also pass `params`.
238
239
 
239
240
  ### Dynamic SQL
@@ -326,6 +327,29 @@ async def articles(lng: str) -> PostgresJsonResponse:
326
327
  - `statement_timeout=<seconds>` (optional, as in `fetch_all`): Postgres aborts a slow query; before the first byte the client gets a 504. It does not free a client that stopped reading (use a proxy write timeout), and needs a non-autocommit connection.
327
328
  - It does no authorization; scope the query yourself. Full reference: the `pgdevkit.fastapi` section of the README.
328
329
 
330
+ ### Cancelling `fetch_all` endpoints when the client leaves
331
+
332
+ Granian does not cancel a request handler on disconnect, so a frontend abort (debounced search, grid filter change) only stops `PostgresJsonResponse` endpoints. Put other read endpoints — including POSTs that only read, e.g. grid/search — on a router with `CancelOnDisconnectRoute`: the handler task is cancelled on `http.disconnect` and psycopg cancels the statement in Postgres.
333
+
334
+ ```python
335
+ from fastapi import APIRouter
336
+ from pgdevkit.fastapi import CancelOnDisconnectRoute
337
+
338
+ cancellable = APIRouter(route_class=CancelOnDisconnectRoute) # single route: router.add_api_route(..., route_class_override=CancelOnDisconnectRoute)
339
+
340
+ @cancellable.post("/customers/overview") # reads only
341
+ async def overview(payload: Filter) -> list[Row]:
342
+ return await fetch_all(sql.load_sql("customers", "overview"), payload.model_dump())
343
+
344
+ app.include_router(cancellable)
345
+ ```
346
+
347
+ - **Reads only, opt-in.** Never for writes; don't select by HTTP method (searches are POSTs).
348
+ - **Everything in the route is cancelled**, dependencies included (auth, audit logging), and `BackgroundTasks` never run; a `yield` dependency sees an `HTTPException` (499) and rolls back. Don't put must-happen side effects in such a route.
349
+ - The client that left gets a 499 nobody reads. The body is not buffered (the handler gets a `Request` fed by a reader task).
350
+ - Shared work (single-flight caches, "refresh once, many wait" loaders) must be awaited through `asyncio.shield`, or one aborted request cancels it for all waiters.
351
+ - `to_thread` work and external services (Databricks statements, HTTP calls) keep running; only awaiting them stops. Returned `StreamingResponse`s aren't covered (use `PostgresJsonResponse`).
352
+
329
353
  ### SQL formatting
330
354
 
331
355
  ```bash
@@ -405,6 +429,7 @@ Reports drift between the `database/` `.sql` files and the actual schema — tab
405
429
  - [ ] Untrusted SQL runs under a SELECT-only role (plus `readonly_transaction(con)` and an `at_most(...)` timeout as safety nets, not as a sandbox)
406
430
  - [ ] `set_default_pool()` at startup; `helper.clear_cache()` after DDL on a long-lived connection when using `complex_helper=`
407
431
  - [ ] Large/grid reads from FastAPI may use `pgdevkit.fastapi.PostgresJsonResponse` (streams, cancels on disconnect, no model validation)
432
+ - [ ] Other read endpoints built on `fetch_all` that should stop when the client leaves use `APIRouter(route_class=CancelOnDisconnectRoute)` (never writes)
408
433
  - [ ] No `LATERAL JOIN` — use a CTE that groups/aggregates first, then joins it
409
434
  - [ ] `.<env>.sql` files (e.g. `.prod.sql`) are skipped by `pgdb testdb` unless it's run with a matching `--env`
410
435
  - [ ] Every table (and non-obvious column) has a `COMMENT ON`, placed in the object's own `.sql` file
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pgdevkit
3
- Version: 0.15.2
3
+ Version: 0.16.0
4
4
  Summary: A helper for developing with Postgres
5
5
  Requires-Python: >=3.14
6
6
  Requires-Dist: docker>=7.1.0
@@ -442,7 +442,7 @@ Install with the `db` extra: `pip install pgdevkit[db]`.
442
442
  with `set_default_pool()` (a `ConnectionSource` such as `PgPool`, or a plain callable returning an
443
443
  async connection context manager). Set the `cancel` `asyncio.Event` to abort the query on the server
444
444
  (`QueryCanceled`; to stream a result as JSON from FastAPI see `PostgresJsonResponse`, which watches for the
445
- disconnect itself); cancelling the awaiting task cancels it there too, and
445
+ disconnect itself, and for buffered endpoints see `CancelOnDisconnectRoute`); cancelling the awaiting task cancels it there too, and
446
446
  `CancelledError` propagates as usual. `statement_timeout=<seconds>` lets Postgres abort the query after that
447
447
  long (`QueryCanceled`, "canceling statement due to statement timeout"); it is applied per call with `SET LOCAL`
448
448
  semantics, so it never outlives the call (a borrowed `con` gets its previous setting back); see
@@ -536,14 +536,16 @@ async with pool.connection() as con:
536
536
  await pg_upsert(con, Widget(id=1, name="thing"), Widget)
537
537
  ```
538
538
 
539
- ## `pgdevkit.fastapi` — streaming JSON responses
539
+ ## `pgdevkit.fastapi` — streaming JSON responses and cancel-on-disconnect
540
540
 
541
541
  Requires the `fastapi` extra (`pip install pgdevkit[fastapi]`, FastAPI >= 0.118).
542
542
 
543
543
  `PostgresJsonResponse` runs a query and streams its rows to the client as a JSON array. Postgres builds the JSON
544
544
  (`row_to_json`), so there is no row-by-row Python serialization and no pydantic round trip, and memory use stays flat
545
545
  for big results. **If the client goes away, the query is cancelled in Postgres** instead of running to completion.
546
- (`fetch_all(..., cancel=event)` cancels a buffered query too, but you wire the event to the disconnect yourself.)
546
+ (`fetch_all(..., cancel=event)` cancels a buffered query too; for `fetch_all`-based endpoints put the route on
547
+ `CancelOnDisconnectRoute` ([below](#cancelling-other-endpoints-when-the-client-leaves)) instead of wiring the event
548
+ yourself.)
547
549
 
548
550
  ```python
549
551
  from pgdevkit.db import PgPool, set_default_pool
@@ -596,6 +598,49 @@ big results, can use up the pool and make every other request wait for a connect
596
598
  reverse proxy and size the pool accordingly. Disconnect behaviour can only be tested over a real socket
597
599
  (`httpx.ASGITransport` never sends `http.disconnect`); see `tests/test_pg_json_granian.py`.
598
600
 
601
+ ### Cancelling other endpoints when the client leaves
602
+
603
+ `PostgresJsonResponse` is the only thing that notices a disconnect by itself. ASGI servers such as Granian do **not**
604
+ cancel a request handler when the client goes away, so a handler built on `fetch_all` (or a raw cursor, Meilisearch,
605
+ ...) runs to the end and its result is thrown away, which defeats a frontend `AbortController`. Put such read
606
+ endpoints on a router with `CancelOnDisconnectRoute`: it runs the handler as a task and cancels it on
607
+ `http.disconnect` (in the request's own task, with an `asyncio.timeout` as the cancel scope, as
608
+ `PostgresJsonResponse` does), and psycopg/pgdevkit then cancel the running statement on the server (see `fetch_all`'s
609
+ `cancel`).
610
+
611
+ ```python
612
+ from fastapi import APIRouter
613
+ from pgdevkit.db import fetch_all
614
+ from pgdevkit.fastapi import CancelOnDisconnectRoute
615
+
616
+ cancellable = APIRouter(route_class=CancelOnDisconnectRoute) # or router.add_api_route(..., route_class_override=...)
617
+
618
+ @cancellable.post("/customers/overview") # a POST that only reads
619
+ async def overview(payload: Filter) -> list[Row]:
620
+ return await fetch_all("select ...", {"q": payload.q}) # aborted in Postgres when the client leaves
621
+
622
+ app.include_router(cancellable)
623
+ ```
624
+
625
+ - **Opt-in, reads only.** A cancelled write is rolled back at best and half-applied at worst. Don't select routes by
626
+ HTTP method: search and grid endpoints are often POSTs.
627
+ - **Everything inside the route is cancelled**, dependencies included (authentication, audit logging, counters), and
628
+ the client gets no response, so `BackgroundTasks` registered by the handler never run. Don't rely on side effects
629
+ that must always happen in an opted-in route: shield them or do them in a middleware. A `Depends` with `yield` sees
630
+ an `HTTPException` (status 499) after the cancellation, so a transaction rolls back instead of committing.
631
+ - The client that left gets a 499 nobody reads; the middleware stack sees a normal response (no
632
+ `No response returned`).
633
+ - The request body is **not** buffered: the handler reads it from a `Request` whose `receive()` is fed by a reader
634
+ task that also spots the disconnect. That reader runs at most a few chunks ahead of the handler, so a route that
635
+ never reads a large body can't pin it in memory, but a disconnect is then only noticed once the body is consumed.
636
+ - **Shared work must survive one caller's cancellation.** Await shared tasks and futures (single-flight caches,
637
+ "refresh once, many wait" loaders) through `asyncio.shield`; otherwise one aborted request cancels the load every
638
+ other request is waiting on.
639
+ - A returned `StreamingResponse` is not covered (Starlette watches for the disconnect there only on servers with ASGI
640
+ spec < 2.4; `PostgresJsonResponse` always does). Threads (`asyncio.to_thread`) and
641
+ external services (Databricks statements, HTTP calls) keep running; only the awaiting stops.
642
+ - Like the above, disconnects only show up over a real socket: see `tests/test_pg_json_granian.py` for a Granian test.
643
+
599
644
  ### Statement timeout
600
645
 
601
646
  `statement_timeout=<seconds>` (optional, also on `fetch_all`) lets **Postgres** abort the query after that long. It
@@ -30,8 +30,9 @@ pgdevkit/db/model.py,sha256=ntuXonq-SCGkbNw-lvdMS5VuzLjxYRT8O6xaVqiw56I,980
30
30
  pgdevkit/db/mssql_crud.py,sha256=yN4izZ0fsFwAs14008JECHH4dqjNbdHUHGEG8kidyNs,11374
31
31
  pgdevkit/db/mssql_sql.py,sha256=DO3UMsTFN9pg0tNdaPGb0wf6lWlHGcqwqzkrGVhJF2g,2292
32
32
  pgdevkit/db/transaction.py,sha256=oW5p8t8pTgIVtnVSyaGxzJxhdBG1iyx4Jsf4PXftRDo,1912
33
- pgdevkit/fastapi/__init__.py,sha256=Oku0U7wNblWJj_wbyiw_ik299vAsdl1fX6FCth0KliI,84
34
- pgdevkit/fastapi/json_response.py,sha256=H7d3M73H3nlxcEu_z7L2S5lLedATXr7l2StKNr4xgYI,13640
33
+ pgdevkit/fastapi/__init__.py,sha256=u1_oVExVun6shhRbEVCbh-yAYtSvoVfLRxDV7No06ZE,169
34
+ pgdevkit/fastapi/cancel_on_disconnect.py,sha256=-ukR4qBemX65dIGJutzDVatq8Al99p9oHlwrRaDjs-M,6898
35
+ pgdevkit/fastapi/json_response.py,sha256=luL3Vjwz1EZxKYDh8CDFKxNtrLctrf1RwDIood0pwvE,14378
35
36
  pgdevkit/testdb/__init__.py,sha256=tphuORo4t0QHQp_gFi6ZHXXgd32w6qUoKlaQCab6izA,366
36
37
  pgdevkit/testdb/_docker.py,sha256=-W59YBbcSIl9H_YmySRtVbOV4ImZPHOq9hxmlO-p56s,2807
37
38
  pgdevkit/testdb/api.py,sha256=13YgTz6hPcYWfsUlQJhJ7k20-W_4hzepedVaT8gWhxI,9709
@@ -46,10 +47,10 @@ pgdevkit/testdb/mssql/api.py,sha256=Ah95QTsOLvtubyHxRWDywjX9NCvTVgurmANLP5RY2Ck,
46
47
  pgdevkit/testdb/mssql/constants.py,sha256=qyA1FCv-2LAQ_C8OydePEXhyG_ZBPPB7vg1686xVpVU,3453
47
48
  pgdevkit/testdb/mssql/container.py,sha256=sqJPAuhN1by3kZZ96gLet4pRt3uJmUMO6Kkmvjydg1w,3791
48
49
  pgdevkit/docs/database-layout.md,sha256=j9N8GuT63D2oVf9rIOEv8GTWs4yTtjZTGH-zM5Wrg5o,8163
49
- pgdevkit/skills/pgdevkit/SKILL.md,sha256=1hWdhzGwFm4UQemrIRnr2S5K9bEcWKN_TtwSg0vPEZ0,25419
50
+ pgdevkit/skills/pgdevkit/SKILL.md,sha256=Cf3g4NdOR0d3JkzYStJs1IWLLuiSmWR7NXhtfL133i8,27566
50
51
  pgdevkit/skills/pgdevkit/references/dynamic-sql.md,sha256=iP7UAyJr5Mgm4c_CdL7ab9OCgpCGQONjBhNV1p4zY64,3058
51
52
  pgdevkit/skills/pgdevkit/references/temporal-tables.md,sha256=LwZnItvQWI7bB0oRHxIfUc0ynnq7N0V5Mnv-QHvFN3Q,1364
52
- pgdevkit-0.15.2.dist-info/METADATA,sha256=ANvN2-JDyQ2xQ00HwMJiZPQKg5ME6NycBx5ItmHwGwM,38567
53
- pgdevkit-0.15.2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
54
- pgdevkit-0.15.2.dist-info/entry_points.txt,sha256=FK2P55lf0WaGlOZJIINOGgCt8WB7ep0OnPsgnrp80bg,42
55
- pgdevkit-0.15.2.dist-info/RECORD,,
53
+ pgdevkit-0.16.0.dist-info/METADATA,sha256=sNHIr0E4cY9JuzwPlkW9he8CruGyHhQg4huwVSHE7Xs,41791
54
+ pgdevkit-0.16.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
55
+ pgdevkit-0.16.0.dist-info/entry_points.txt,sha256=FK2P55lf0WaGlOZJIINOGgCt8WB7ep0OnPsgnrp80bg,42
56
+ pgdevkit-0.16.0.dist-info/RECORD,,