-
-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
230 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
import asyncio | ||
import contextlib | ||
import functools | ||
import logging | ||
from typing import Any, Callable, Dict, Optional | ||
|
||
import psycopg2 | ||
import psycopg2.errors | ||
import psycopg2.pool | ||
from psycopg2.extras import Json, RealDictCursor | ||
|
||
from procrastinate import connector, exceptions, utils | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def wrap_exceptions(func: Callable) -> Callable: | ||
""" | ||
Wrap psycopg2 errors as connector exceptions | ||
This decorator is expected to be used on coroutine functions only | ||
""" | ||
|
||
@functools.wraps(func) | ||
def wrapped(*args, **kwargs): | ||
try: | ||
return func(*args, **kwargs) | ||
except psycopg2.errors.UniqueViolation as exc: | ||
raise exceptions.UniqueViolation(constraint_name=exc.diag.constraint_name) | ||
except psycopg2.Error as exc: | ||
raise exceptions.ConnectorException from exc | ||
|
||
# Attaching a custom attribute to ease testability and make the | ||
# decorator more introspectable | ||
wrapped._exceptions_wrapped = True # type: ignore | ||
return wrapped | ||
|
||
|
||
@utils.add_sync_api | ||
class Psycopg2Connector(connector.BaseSyncConnector): | ||
def __init__( | ||
self, *, json_dumps: Optional[Callable] = None, **kwargs: Any, | ||
): | ||
""" | ||
``psycopg2.pool.ThreadedConnectionPool`` which it creates at instanciation time. | ||
This is used if you want your .defer() calls to be pureley synchronous, not | ||
asynchronous with a sync wrapper. You may need this if your program is | ||
multithreaded and doesn't handle async loops well | ||
(see `discussion-async-sync-defer`). | ||
All other arguments than ``json_dumps`` are passed to | ||
:py:func:`ThreadedConnectionPool` (see psycopg2 documentation__), with default | ||
values that may differ from those of ``psycopg2`` (see a partial list of | ||
parameters below). | ||
.. _psycopg2 doc: https://www.psycopg.org/docs/extras.html#json-adaptation | ||
.. __: https://www.psycopg.org/docs/pool.html | ||
#psycopg2.pool.ThreadedConnectionPool | ||
Parameters | ||
---------- | ||
minconn : int | ||
Mandatory, passed to psycopg2. | ||
maxconn : int | ||
Mandatory, passed to psycopg2. | ||
json_dumps : | ||
The JSON dumps function to use for serializing job arguments. Defaults to | ||
the function used by psycopg2. See the `psycopg2 doc`_. | ||
dsn : ``Optional[str]`` | ||
Passed to psycopg2. Default is "" instead of None, which means if no | ||
argument is passed, it will connect to localhost:5432 instead of a | ||
Unix-domain local socket file. | ||
cursor_factory : ``psycopg2.extensions.cursor`` | ||
Passed to psycopg2. Default is ``psycopg2.extras.RealDictCursor`` | ||
instead of standard cursor. There is no identified use case for changing | ||
this. | ||
""" | ||
self.json_dumps = json_dumps | ||
pool_args = self._adapt_pool_args(kwargs) | ||
self._pool = psycopg2.pool.ThreadedConnectionPool(**pool_args) | ||
self._lock = asyncio.Lock() | ||
|
||
@staticmethod | ||
def _adapt_pool_args(pool_args: Dict[str, Any]) -> Dict[str, Any]: | ||
""" | ||
Adapt the pool args for ``psycopg2``, using sensible defaults for Procrastinate. | ||
""" | ||
final_args = { | ||
"dsn": "", | ||
"cursor_factory": RealDictCursor, | ||
} | ||
final_args.update(pool_args) | ||
return final_args | ||
|
||
@wrap_exceptions | ||
def close(self) -> None: | ||
""" | ||
Close the pool and awaits all connections to be released. | ||
""" | ||
if self._pool: | ||
self._pool.closeall() | ||
|
||
def _wrap_json(self, arguments: Dict[str, Any]): | ||
return { | ||
key: Json(value, dumps=self.json_dumps) | ||
if isinstance(value, dict) | ||
else value | ||
for key, value in arguments.items() | ||
} | ||
|
||
@contextlib.contextmanager | ||
def _connection(self) -> psycopg2.extensions.connection: | ||
try: | ||
with self._pool.getconn() as connection: | ||
yield connection | ||
finally: | ||
self._pool.putconn(connection) | ||
|
||
@wrap_exceptions | ||
def execute_query_one(self, query: str, **arguments: Any) -> Dict[str, Any]: | ||
with self._connection() as connection: | ||
with connection.cursor() as cursor: | ||
cursor.execute(query, self._wrap_json(arguments)) | ||
return cursor.fetchone() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
import procrastinate | ||
|
||
app = procrastinate.App( | ||
connector=procrastinate.AiopgConnector(), import_paths=["procrastinate_demo.tasks"], | ||
connector=procrastinate.AiopgConnector(), | ||
sync_connector=procrastinate.Psycopg2Connector(minconn=0, maxconn=2), | ||
import_paths=["procrastinate_demo.tasks"], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters