Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@ updates:
directory: "/"
schedule:
interval: monthly
time: "00:00"
open-pull-requests-limit: 10
groups:
packages:
pip-packages:
patterns:
- "*"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: monthly
open-pull-requests-limit: 10
40 changes: 16 additions & 24 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ jobs:

strategy:
matrix:
python-version: ['3.11', '3.12', '3.13']
python-version: ["3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Use python ${{ matrix.python-version }}
run: |
echo ${{ matrix.python-version }} > .python-version
- name: Install dependencies
run: |
make deps
Expand All @@ -44,23 +45,14 @@ jobs:
if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Python 3.11
uses: actions/setup-python@v3
with:
python-version: '3.11'
- name: Install dependencies
run:
python -m pip install -U pip wheel twine
- name: Make dists
run:
python setup.py sdist bdist_wheel
- name: Check dists
run:
twine check dist/*
- name: PyPI upload
uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Build
run: |
uv build
- name: Publish
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
run: |
twine upload dist/*
uv publish
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,6 @@ target/
profile_default/
ipython_config.py

# pyenv
.python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
Expand Down
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.11
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
## unreleased

* Do not wait for a connection closure
* Migrate to modern tooling (uv, ruff, hatchling) + python 3.14
* Version is derived from git tags


## v0.0.1 (2025-07-07)
Expand Down
31 changes: 13 additions & 18 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
all: deps lint test

deps:
@python3 -m pip install --upgrade pip && pip3 install -r requirements-dev.txt
uv:
@which uv >/dev/null 2>&1 || { \
echo "❌ uv is not installed"; \
exit 1;\
}

black:
@black --line-length 120 asyncpg_lock tests
deps: uv
@uv sync --all-extras

isort:
@isort --line-length 120 --use-parentheses --multi-line 3 --combine-as --trailing-comma asyncpg_lock tests
format:
@uv run ruff format asyncpg_lock tests example
@uv run ruff check --fix asyncpg_lock tests example

pyright:
pyright
@uv run pyright

flake8:
@flake8 --max-line-length 120 --ignore C901,C812,E203 --extend-ignore W503 asyncpg_lock tests

lint: black isort flake8 pyright
lint: format pyright

test:
@python3 -m pytest -vv --rootdir tests .

pyenv:
echo asyncpg_lock > .python-version && pyenv install -s 3.11 && pyenv virtualenv -f 3.11 asyncpg_lock

pyenv-delete:
pyenv virtualenv-delete -f asyncpg_lock
@uv run pytest -vv --rootdir tests .
29 changes: 3 additions & 26 deletions asyncpg_lock/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import collections
import re
import sys
from importlib.metadata import version as _get_version

from .guard import AdvisoryLockGuard, ConnectFunc, connect_func # noqa
from .guard import AdvisoryLockGuard, ConnectFunc, connect_func

__all__: tuple[str, ...] = (
# guard.py
Expand All @@ -11,28 +10,6 @@
"connect_func",
)

__version__ = "0.0.1"
__version__ = _get_version("asyncpg-lock")

version = f"{__version__}, Python {sys.version}"

VersionInfo = collections.namedtuple("VersionInfo", "major minor micro release_level serial")


def _parse_version(v: str) -> VersionInfo:
version_re = r"^(?P<major>\d+)\.(?P<minor>\d+)\.(?P<micro>\d+)" r"((?P<release_level>[a-z]+)(?P<serial>\d+)?)?$"
match = re.match(version_re, v)
if not match:
raise ImportError(f"Invalid package version {v}")
try:
major = int(match.group("major"))
minor = int(match.group("minor"))
micro = int(match.group("micro"))
levels = {"rc": "candidate", "a": "alpha", "b": "beta", None: "final"}
release_level = levels[match.group("release_level")]
serial = int(match.group("serial")) if match.group("serial") else 0
return VersionInfo(major, minor, micro, release_level, serial)
except Exception as e:
raise ImportError(f"Invalid package version {v}") from e


version_info = _parse_version(__version__)
9 changes: 5 additions & 4 deletions asyncpg_lock/guard.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import asyncio
import logging
import time
from typing import Any, Callable, Coroutine
from collections.abc import Callable, Coroutine
from typing import Any

import asyncpg

Expand All @@ -19,10 +20,10 @@ async def _connect() -> asyncpg.Connection:

class AdvisoryLockGuard:
__slots__ = (
"__connect",
"__reconnect_delay",
"__after_acquire_delay",
"__connect",
"__reacquire_delay",
"__reconnect_delay",
"__tasks",
)

Expand Down Expand Up @@ -121,7 +122,7 @@ async def __ensure_connection_alive(self, connection: asyncpg.Connection, key: i
raise Exception(f"Lock {key} might be lost: connection is closed")
try:
await connection.execute("SELECT 1", timeout=per_attempt_keep_alive_budget)
except asyncio.TimeoutError:
except TimeoutError:
raise Exception(f"Lock {key} might be lost: keep-alive query timed out")
except Exception:
raise Exception(f"Lock {key} might be lost: keep-alive query failed")
Expand Down
17 changes: 7 additions & 10 deletions example/main.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import asyncio
import random
import logging
import random

import asyncpg

import asyncpg_lock

logger = logging.getLogger(__name__)
Expand All @@ -15,25 +16,21 @@ async def process(id: int) -> None:
logger.info("Done processing %s", id)


async def main():
async def main() -> None:
guard = asyncpg_lock.AdvisoryLockGuard(connect=lambda: asyncpg.connect(database="postgres"))
lock_key = (42, 42)
process_one_id = random.randint(0, 100500)
process_one = asyncio.create_task(
guard.run(lock_key, lambda: process(process_one_id))
)
process_one = asyncio.create_task(guard.run(lock_key, lambda: process(process_one_id)))
process_two_id = random.randint(0, 100500)
process_two = asyncio.create_task(
guard.run(lock_key, lambda: process(process_two_id))
)
process_two = asyncio.create_task(guard.run(lock_key, lambda: process(process_two_id)))
await asyncio.gather(process_one, process_two)


if __name__ == "__main__":
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[logging.StreamHandler()]
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[logging.StreamHandler()],
)

asyncio.run(main())
84 changes: 84 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
[project]
name = "asyncpg-lock"
dynamic = ["version"]
description = "Run long-running work exclusively using PostgreSQL advisory locks"
readme = "README.md"
license = {text = "MIT"}
authors = [{name = "Iurii Pliner", email = "yury.pliner@gmail.com"}]
requires-python = ">=3.11"
dependencies = [
"asyncpg>=0.30.0",
]
classifiers = [
"License :: OSI Approved :: MIT License",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Operating System :: OS Independent",
"Environment :: Web Environment",
"Development Status :: 5 - Production/Stable",
"Framework :: AsyncIO",
"Typing :: Typed",
]

[project.urls]
Homepage = "https://github.com/Pliner/asyncpg-lock"

[project.optional-dependencies]
dev = [
"pytest==9.0.2",
"pytest-asyncio==1.4.0",
"pytest-pg==0.0.30",
"ruff==0.14.10",
"pyright==1.1.407",
"asyncpg==0.31.0",
]

[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"

[tool.hatch.version]
source = "vcs"

[tool.hatch.build.targets.wheel]
packages = ["asyncpg_lock"]

[tool.pytest.ini_options]
norecursedirs = [".git", ".venv"]
python_files = ["tests.py", "test_*.py", "*_tests.py"]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"

[tool.ruff]
line-length = 120
target-version = "py311"

[tool.ruff.lint]
select = ["SIM", "C4", "ANN", "N", "LOG", "G", "F", "E", "W", "Q", "I", "UP", "T20", "COM", "RSE", "RET", "ISC", "ASYNC", "SLOT", "RUF", "DTZ"]
ignore = ["COM812", "ISC001", "ASYNC109", "RUF018", "RUF022", "ANN401", "ANN204", "E501"]


[tool.pyright]
include = ["asyncpg_lock", "tests"]
pythonVersion = "3.11"
pythonPlatform = "All"
stubPath = ""
typeCheckingMode = "strict"
useLibraryCodeForTypes = true
reportPrivateImportUsage = false
reportMissingTypeStubs = false
reportUnknownVariableType = false
reportUnknownMemberType = false
reportUnknownLambdaType = false
reportUnknownArgumentType = false
reportUnknownParameterType = false
reportUntypedFunctionDecorator = false
reportUnusedImport = false
reportMissingTypeArgument = false
reportImportCycles = false
reportDeprecated = false
13 changes: 0 additions & 13 deletions requirements-dev.txt

This file was deleted.

5 changes: 0 additions & 5 deletions setup.cfg

This file was deleted.

Loading
Loading