diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index f0838e4..0000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,13 +0,0 @@ -[bumpversion] -current_version = 0.1.1 -commit = True -tag = True -tag_name = {new_version} -serialize = - {major}.{minor}.{patch} - {major}.{minor} -parse = (?P\d+)\.(?P\d+)(\.(?P\d+))? - -[bumpversion:file:setup.py] - -[bumpversion:file:postgres_lock/__init__.py] diff --git a/.bumpversion.toml b/.bumpversion.toml new file mode 100644 index 0000000..ea84bb0 --- /dev/null +++ b/.bumpversion.toml @@ -0,0 +1,25 @@ +[tool.bumpversion] +current_version = "0.1.1" +parse = "(?P\\d+)\\.(?P\\d+)(\\.(?P\\d+))?" +serialize = [ + "{major}.{minor}.{patch}", + "{major}.{minor}", +] +search = "{current_version}" +replace = "{new_version}" +regex = false +ignore_missing_version = false +ignore_missing_files = false +tag = true +sign_tags = false +tag_name = "{new_version}" +tag_message = "Bump version: {current_version} → {new_version}" +allow_dirty = false +commit = true +message = "Bump version: {current_version} → {new_version}" +commit_args = "" + +[[tool.bumpversion.files]] +filename = "pyproject.toml" +search = "version = '{current_version}'" +replace = "version = '{new_version}'" diff --git a/.github/matrix.py b/.github/matrix.py index 17a63d3..34883f1 100644 --- a/.github/matrix.py +++ b/.github/matrix.py @@ -1,3 +1,4 @@ +# noqa:INP001 import fileinput import json import re @@ -9,21 +10,22 @@ def main(): actions_matrix = [] - for tox_env in fileinput.input(): - tox_env = tox_env.rstrip() - - if python_match := PY_VERSIONS_RE.match(tox_env): - version_tuple = python_match.groups() - else: - version_tuple = sys.version_info[0:2] - - python_version = "{}.{}".format(*version_tuple) - actions_matrix.append( - { - "python": python_version, - "tox_env": tox_env, - } - ) + with fileinput.input() as f: + for tox_env_line in f: + tox_env = tox_env_line.rstrip() + + if python_match := PY_VERSIONS_RE.match(tox_env): + version_tuple = python_match.groups() + else: + version_tuple = sys.version_info[0:2] + + python_version = "{}.{}".format(*version_tuple) + actions_matrix.append( + { + "python": python_version, + "tox_env": tox_env, + } + ) print(json.dumps(actions_matrix)) # noqa:T201 diff --git a/LICENSE b/LICENSE index 2c5097a..f82098c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019-2024, Developer Society Limited +Copyright (c) 2019-2025, Developer Society Limited All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/MANIFEST.in b/MANIFEST.in index 2021bcd..1eeef06 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1 @@ -include README.rst -include LICENSE -graft postgres_lock -global-exclude *.py[co] -global-exclude __pycache__ -global-exclude .DS_Store +prune tests diff --git a/Makefile b/Makefile index df0e566..e905ddf 100644 --- a/Makefile +++ b/Makefile @@ -46,11 +46,8 @@ test-report: coverage-clean test coverage-report test-lowest: ## Run tox with lowest (oldest) package dependencies. test-lowest: tox-test-lowest -dist: ## Builds source and wheel package -dist: clean build-dist - -release: ## Package and release this project to PyPI. -release: dist build-release +package: ## Builds source and wheel packages +package: clean build-package # --------------- @@ -67,23 +64,10 @@ build-clean: rm -rf .eggs find . -maxdepth 1 -name '*.egg-info' -exec rm -rf {} + -build-release: - @echo - @echo "This will package and release this project to PyPI." - @echo - @echo "A checklist before you continue:" - @echo - @echo " - have you ran 'bumpversion'?" - @echo " - have you pushed the commit and tag created by 'bumpversion'?" - @echo " - are you sure the project is in a state to be released?" - @echo - @read -p "Press to continue. Or -c to quit and address the above points." - twine upload dist/* - -build-dist: - python setup.py sdist - python setup.py bdist_wheel - ls -l dist +build-package: + python -m build + twine check --strict dist/* + check-wheel-contents dist/*.whl # Virtual Environments diff --git a/README.md b/README.md new file mode 100644 index 0000000..8c9e3ca --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# Django Postgres Lock + +A [Django](https://www.djangoproject.com/) management command which will run a command inside a +Postgres lock, ensuring that only a single instance of the inner command will run. + +## Installation + +Using [pip](https://pip.pypa.io/): + +```console +$ pip install django-postgres-lock +``` + +Edit your Django project's settings module, and add the application to ``INSTALLED_APPS``: + +```python +INSTALLED_APPS = [ + # ... + "postgres_lock", + # ... +] +``` + +## Usage + +To run clearsessions with the default lock: + +```console +$ ./manage.py command_lock -- ./manage.py clearsessions +``` + +To use a unique lock for this task: + +```console +$ ./manage.py command_lock --name clearsessions -- ./manage.py clearsessions +``` + +To exit immediately if a lock can't be acquired: + +```console +$ ./manage.py command_lock --try --name clearsessions -- ./manage.py clearsessions +``` + +To ignore a lock failure and return a successful exit code: + +```console +$ ./manage.py command_lock --try --ignore-fail --name clearsessions -- ./manage.py clearsessions +``` diff --git a/README.rst b/README.rst deleted file mode 100644 index de5709a..0000000 --- a/README.rst +++ /dev/null @@ -1,55 +0,0 @@ -Django Postgres Lock -==================== - -A Django_ management command which will run a command inside a Postgres lock, ensuring that only a -single instance of the inner command will run. - -.. _Django: https://www.djangoproject.com/ - -Installation ------------- - -Using pip_: - -.. _pip: https://pip.pypa.io/ - -.. code-block:: console - - $ pip install django-postgres-lock - -Edit your Django project's settings module, and add the application to ``INSTALLED_APPS``: - -.. code-block:: python - - INSTALLED_APPS = [ - # ... - "postgres_lock", - # ... - ] - -Usage ------ - -To run clearsessions with the default lock: - -.. code-block:: console - - $ ./manage.py command_lock -- ./manage.py clearsessions - -To use a unique lock for this task: - -.. code-block:: console - - $ ./manage.py command_lock --name clearsessions -- ./manage.py clearsessions - -To exit immediately if a lock can't be acquired: - -.. code-block:: console - - $ ./manage.py command_lock --try --name clearsessions -- ./manage.py clearsessions - -To ignore a lock failure and return a successful exit code: - -.. code-block:: console - - $ ./manage.py command_lock --try --ignore-fail --name clearsessions -- ./manage.py clearsessions diff --git a/postgres_lock/lock.py b/postgres_lock/lock.py index 812f01e..c617c07 100644 --- a/postgres_lock/lock.py +++ b/postgres_lock/lock.py @@ -61,5 +61,5 @@ def release(self): def __enter__(self): return self.lock() - def __exit__(self, type, value, traceback): + def __exit__(self, exc_type, exc_value, traceback): self.release() diff --git a/postgres_lock/management/commands/command_lock.py b/postgres_lock/management/commands/command_lock.py index eea796c..893fe99 100644 --- a/postgres_lock/management/commands/command_lock.py +++ b/postgres_lock/management/commands/command_lock.py @@ -45,7 +45,7 @@ def execute(self, *args, **options): using=options["database"], ) as acquired: if acquired: - completed = subprocess.run(options["command"]) # noqa:S603 + completed = subprocess.run(options["command"], check=False) # noqa:S603 # Raise the return code of the child process if an error occurs if completed.returncode != 0: sys.exit(completed.returncode) diff --git a/pyproject.toml b/pyproject.toml index 9063b8f..2f7988c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,31 +1,125 @@ +[project] +name = 'django-postgres-lock' +version = '0.1.1' +description = 'Django Postgres Lock' +readme = 'README.md' +maintainers = [{ name = 'The Developer Society', email = 'studio@dev.ngo' }] +requires-python = '>= 3.9' +dependencies = [ + 'Django>=3.2', +] +license = 'BSD-3-Clause' +classifiers = [ + 'Environment :: Web Environment', + 'Framework :: Django', + 'Framework :: Django :: 3.2', + 'Framework :: Django :: 4.2', + 'Framework :: Django :: 5.2', + 'Intended Audience :: Developers', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', +] + +[project.urls] +Homepage = 'https://github.com/developersociety/django-postgres-lock' + +[build-system] +requires = ['setuptools >= 77.0.3'] +build-backend = 'setuptools.build_meta' + +[tool.setuptools] +include-package-data = false + +[tool.setuptools.packages.find] +include = ['postgres_lock*'] + [tool.ruff] line-length = 99 target-version = 'py39' [tool.ruff.lint] -select = [ - 'F', # pyflakes - 'E', # pycodestyle - 'W', # pycodestyle - 'I', # isort - 'N', # pep8-naming - 'UP', # pyupgrade - 'S', # flake8-bandit - 'BLE', # flake8-blind-except - 'C4', # flake8-comprehensions - 'EM', # flake8-errmsg - 'T20', # flake8-print - 'RET', # flake8-return - 'RUF', # ruff +extend-select = [ + 'ERA', # eradicate + 'YTT', # flake8-2020 + 'ASYNC', # flake8-async + 'S', # flake8-bandit + 'BLE', # flake8-blind-except + 'B', # flake8-bugbear + 'A', # flake8-builtins + 'COM', # flake8-commas + 'C4', # flake8-comprehensions + 'DTZ', # flake8-datetimez + 'T10', # flake8-debugger + 'DJ', # flake8-django + 'EM', # flake8-errmsg + 'EXE', # flake8-executable + 'FA', # flake8-future-annotations + 'INT', # flake8-gettext + 'ISC', # flake8-implicit-str-concat + 'ICN', # flake8-import-conventions + 'LOG', # flake8-logging + 'G', # flake8-logging-format + 'INP', # flake8-no-pep420 + 'PIE', # flake8-pie + 'T20', # flake8-print + 'PYI', # flake8-pyi + 'Q', # flake8-quotes + 'RSE', # flake8-raise + 'RET', # flake8-return + 'SLOT', # flake8-slots + 'SIM', # flake8-simplify + 'TID', # flake8-tidy-imports + 'TD', # flake8-todos + 'TCH', # flake8-type-checking + 'PTH', # flake8-use-pathlib + 'FLY', # flynt + 'I', # isort + 'NPY', # numpy-specific rules + 'PD', # pandas-vet + 'N', # pep8-naming + 'PERF', # perflint + 'E', # pycodestyle + 'W', # pycodestyle + 'F', # pyflakes + 'PGH', # pygrep-hooks + 'PLC', # pylint + 'PLE', # pylint + 'PLW', # pylint + 'UP', # pyupgrade + 'FURB', # refurb + 'RUF', # ruff-specific rules + 'TRY', # tryceratops ] ignore = [ - 'EM101', # flake8-errmsg: raw-string-in-exception + 'COM812', # flake8-commas: missing-trailing-comma + 'EM101', # flake8-errmsg: raw-string-in-exception + 'ISC001', # flake8-implicit-str-concat: single-line-implicit-string-concatenation + 'RUF012', # ruff-specific rules: mutable-class-default + 'SIM105', # flake8-simplify: suppressible-exception + 'SIM108', # flake8-simplify: if-else-block-instead-of-if-exp + 'TD002', # flake8-todos: missing-todo-author + 'TRY003', # tryceratops: raise-vanilla-args ] [tool.ruff.lint.isort] combine-as-imports = true +section-order = [ + 'future', + 'standard-library', + 'django', + 'third-party', + 'first-party', + 'local-folder', +] + +[tool.ruff.lint.isort.sections] +'django' = ['django'] [tool.ruff.lint.pep8-naming] -extend-ignore-names = [ - 'assert*', -] +extend-ignore-names = ['assert*'] diff --git a/requirements/local.txt b/requirements/local.txt index bc1c6a0..eaaae2c 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -1,8 +1,7 @@ -r testing.txt -bump2version==1.0.1 -Django>=4.2,<5.0 -psycopg==3.2.3 -tox==4.21.2 -tox-uv==1.13.0 -twine==5.1.1 +bump-my-version==1.2.0 +Django>=5.2,<6.0 +psycopg==3.2.9 +tox==4.26.0 +tox-uv==1.26.0 diff --git a/requirements/testing.txt b/requirements/testing.txt index 49ddee3..de608ef 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -1,3 +1,6 @@ -coverage==7.6.1 -pipdeptree==2.23.4 -ruff==0.6.9 +build==1.2.2.post1 +check-wheel-contents==0.6.1 +coverage==7.9.1 +pipdeptree==2.26.1 +ruff==0.11.13 +twine==6.1.0 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index dd38343..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -python-tag = py3 diff --git a/setup.py b/setup.py deleted file mode 100644 index b5020b4..0000000 --- a/setup.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python -import os - -from setuptools import find_packages, setup - - -def read(filename): - with open(os.path.join(os.path.dirname(__file__), filename)) as f: - return f.read() - - -setup( - name="django-postgres-lock", - version="0.1.1", - description="Django Postgres Lock", - long_description=read("README.rst"), - long_description_content_type="text/x-rst", - url="https://github.com/developersociety/django-postgres-lock", - maintainer="The Developer Society", - maintainer_email="studio@dev.ngo", - platforms=["any"], - packages=find_packages(exclude=["tests"]), - include_package_data=True, - python_requires=">=3.9", - install_requires=["Django>=2.2"], - classifiers=[ - "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Framework :: Django", - "Framework :: Django :: 2.2", - "Framework :: Django :: 3.2", - "Framework :: Django :: 4.2", - ], - license="BSD", -) diff --git a/tox.ini b/tox.ini index 1893cd8..76ecfc4 100644 --- a/tox.ini +++ b/tox.ini @@ -2,21 +2,21 @@ envlist = check lint - py39-django2.2 py{39,310}-django3.2 py{39,310,311,312}-django4.2 + py{310,311,312,313}-django5.2 coverage no_package = true [testenv] deps = -rrequirements/testing.txt - django2.2: Django>=2.2,<3.0 django3.2: Django>=3.2,<4.0 django4.2: Django>=4.2,<5.0 - django2.2: psycopg2>=2.8,<2.9 + django5.2: Django>=5.2,<6.0 django3.2: psycopg2>=2.9,<2.10 django4.2: psycopg<3.3 + django5.2: psycopg<3.3 allowlist_externals = make commands = make test package = editable