diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml
new file mode 100644
index 0000000..52c09c9
--- /dev/null
+++ b/.github/workflows/packaging.yml
@@ -0,0 +1,141 @@
+name: Packaging
+
+on:
+ - push
+
+jobs:
+ format:
+ name: Check formatting
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+
+ - name: Install tox
+ run: python -m pip install tox
+
+ - name: Run black
+ run: tox -e format
+
+ lint:
+ name: Lint
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+
+ - name: Install tox
+ run: python -m pip install tox
+
+ - name: Run flake8
+ run: tox -e lint
+
+ typecheck:
+ name: Type check
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+
+ - name: Install tox
+ run: python -m pip install tox
+
+ - name: Run mypy
+ run: tox -e typecheck
+
+ test:
+ name: Test
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python:
+ - version: "3.13"
+ toxenv: "py313"
+ - version: "3.12"
+ toxenv: "py312"
+ - version: "3.11"
+ toxenv: "py311"
+ - version: "3.10"
+ toxenv: "py310"
+ - version: "3.9"
+ toxenv: "py39"
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python.version }}
+
+ - name: Install tox
+ run: python -m pip install tox
+
+ - name: Run pytest
+ run: tox -e ${{ matrix.python.toxenv }}
+
+ build_source_dist:
+ name: Build source distribution
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+
+ - name: Install build
+ run: python -m pip install build
+
+ - name: Run build
+ run: python -m build --sdist
+
+ - uses: actions/upload-artifact@v4
+ with:
+ path: dist/*.tar.gz
+
+ publish:
+ needs: [format, lint, typecheck, test]
+ if: startsWith(github.ref, 'refs/tags')
+ runs-on: ubuntu-latest
+ environment: release
+ permissions:
+ id-token: write
+ contents: write
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: 3.9
+
+ - name: Install pypa/build
+ run: python -m pip install build
+
+ - name: Build distribution
+ run: python -m build --outdir dist/
+
+ - name: Publish distribution to Test PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ repository_url: https://test.pypi.org/legacy/
+
+ - name: Publish distribution to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+
+ - name: Publish distribution to GitHub release
+ uses: softprops/action-gh-release@v2
+ with:
+ files: |
+ dist/django_webmention-*.whl
+ dist/django_webmention-*.tar.gz
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/pyproject.toml b/pyproject.toml
index 7276f76..debf12f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,235 @@
+[project]
+name = "django-webmention"
+version = "3.0.0"
+description = "A pluggable implementation of webmention for Django projects"
+authors = [
+ { name = "Dane Hillard", email = "github@danehillard.com" },
+]
+license = { file = "LICENSE" }
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "Framework :: Django",
+ "Framework :: Django :: 3.2",
+ "Framework :: Django :: 4.0",
+ "Framework :: Django :: 4.1",
+ "Topic :: Internet :: WWW/HTTP :: Indexing/Search",
+ "License :: OSI Approved :: MIT License",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3 :: Only",
+ "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",
+]
+dependencies = [
+ "Django>=4.2.0",
+ "requests>=2.32.3",
+]
+
+[project.urls]
+Repository = "https://github.com/easy-as-python/django-webmention"
+
+[tool.setuptools.packages.find]
+where = ["src"]
+exclude = ["test*"]
+
+######################
+# Tool configuration #
+######################
+
[tool.black]
line-length = 120
-target-version = ['py35', 'py36', 'py37', 'py38']
+target-version = ["py39", "py310", "py311", "py312", "py313"]
+
+[tool.mypy]
+python_version = "3.9"
+warn_unused_configs = true
+show_error_context = true
+pretty = true
+namespace_packages = true
+check_untyped_defs = true
+
+[[tool.mypy.overrides]]
+module = [
+ "django.core.urlresolvers",
+]
+ignore_missing_imports = true
+
+[tool.coverage.run]
+branch = true
+omit = [
+ "manage.py",
+ "webmention/checks.py",
+ "*test*",
+ "*/migrations/*",
+ "*/admin.py",
+ "*/__init__.py",
+]
+
+[tool.coverage.report]
+precision = 2
+show_missing = true
+skip_covered = true
+
+[tool.coverage.paths]
+source = [
+ "src/webmention",
+ "*/site-packages/webmention",
+]
+
+[tool.pytest.ini_options]
+DJANGO_SETTINGS_MODULE = "tests.settings"
+testpaths = ["tests"]
+addopts = ["-ra", "-q", "--cov=webmention"]
+xfail_strict = true
+
+[tool.tox]
+envlist = [
+ "py39-django4.2",
+ "py39-django5.0",
+ "py39-django5.1",
+ "py310-django4.2",
+ "py310-django5.0",
+ "py310-django5.1",
+ "py311-django4.2",
+ "py311-django5.0",
+ "py311-django5.1",
+ "py312-django4.2",
+ "py312-django5.0",
+ "py312-django5.1",
+ "py313-django4.2",
+ "py313-django5.0",
+ "py313-django5.1",
+]
+
+[tool.tox.env_run_base]
+deps = [
+ "pytest",
+ "pytest-cov",
+ "pytest-django",
+]
+commands = [
+ ["pytest", { replace = "posargs", default = [], extend = true }],
+]
+
+[tool.tox.env."py39-django4.2"]
+deps = [
+ { replace = "ref", of = ["tool", "tox", "env_run_base", "deps"], extend = true },
+ "Django>=4.2,<4.3",
+]
+
+[tool.tox.env."py39-django5.0"]
+deps = [
+ { replace = "ref", of = ["tool", "tox", "env_run_base", "deps"], extend = true },
+ "Django>=5.0,<5.1",
+]
+
+[tool.tox.env."py39-django5.1"]
+deps = [
+ { replace = "ref", of = ["tool", "tox", "env_run_base", "deps"], extend = true },
+ "Django>=5.1,<5.2",
+]
+
+[tool.tox.env."py310-django4.2"]
+deps = [
+ { replace = "ref", of = ["tool", "tox", "env_run_base", "deps"], extend = true },
+ "Django>=4.2,<4.3",
+]
+
+[tool.tox.env."py310-django5.0"]
+deps = [
+ { replace = "ref", of = ["tool", "tox", "env_run_base", "deps"], extend = true },
+ "Django>=5.0,<5.1",
+]
+
+[tool.tox.env."py310-django5.1"]
+deps = [
+ { replace = "ref", of = ["tool", "tox", "env_run_base", "deps"], extend = true },
+ "Django>=5.1,<5.2",
+]
+
+[tool.tox.env."py311-django4.2"]
+deps = [
+ { replace = "ref", of = ["tool", "tox", "env_run_base", "deps"], extend = true },
+ "Django>=4.2,<4.3",
+]
+
+[tool.tox.env."py311-django5.0"]
+deps = [
+ { replace = "ref", of = ["tool", "tox", "env_run_base", "deps"], extend = true },
+ "Django>=5.0,<5.1",
+]
+
+[tool.tox.env."py311-django5.1"]
+deps = [
+ { replace = "ref", of = ["tool", "tox", "env_run_base", "deps"], extend = true },
+ "Django>=5.1,<5.2",
+]
+
+[tool.tox.env."py312-django4.2"]
+deps = [
+ { replace = "ref", of = ["tool", "tox", "env_run_base", "deps"], extend = true },
+ "Django>=4.2,<4.3",
+]
+
+[tool.tox.env."py312-django5.0"]
+deps = [
+ { replace = "ref", of = ["tool", "tox", "env_run_base", "deps"], extend = true },
+ "Django>=5.0,<5.1",
+]
+
+[tool.tox.env."py312-django5.1"]
+deps = [
+ { replace = "ref", of = ["tool", "tox", "env_run_base", "deps"], extend = true },
+ "Django>=5.1,<5.2",
+]
+
+[tool.tox.env."py313-django4.2"]
+deps = [
+ { replace = "ref", of = ["tool", "tox", "env_run_base", "deps"], extend = true },
+ "Django>=4.2,<4.3",
+]
+
+[tool.tox.env."py313-django5.0"]
+deps = [
+ { replace = "ref", of = ["tool", "tox", "env_run_base", "deps"], extend = true },
+ "Django>=5.0,<5.1",
+]
+
+[tool.tox.env."py313-django5.1"]
+deps = [
+ { replace = "ref", of = ["tool", "tox", "env_run_base", "deps"], extend = true },
+ "Django>=5.1,<5.2",
+]
+
+[tool.tox.env.lint]
+skip_install = true
+deps = [
+ "ruff",
+]
+commands = [
+ ["ruff", "check", { replace = "posargs", default = ["--diff", "src/webmention", "tests"], extend = true }],
+]
+
+[tool.tox.env.format]
+skip_install = true
+deps = [
+ "black",
+]
+commands = [
+ ["black", { replace = "posargs", default = ["--check", "--diff", "src/webmention", "tests"], extend = true }],
+]
+
+[tool.tox.env.typecheck]
+deps = [
+ { replace = "ref", of = ["tool", "tox", "env_run_base", "deps"], extend = true },
+ "mypy",
+ "django-types",
+ "types-requests",
+]
+commands = [
+ ["mypy", { replace = "posargs", default = ["src/webmention", "tests"], extend = true }],
+]
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 8ffb45e..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,89 +0,0 @@
-[metadata]
-name = django-webmention
-version = 3.0.0
-description = A pluggable implementation of webmention for Django projects
-author = Dane Hillard
-author_email = github@danehillard.com
-long_description = file: README.md
-long_description_content_type = text/markdown
-url = https://github.com/easy-as-python/django-webmention
-license = MIT
-license_file = LICENSE
-classifiers =
- Development Status :: 5 - Production/Stable
- Intended Audience :: Developers
- Framework :: Django
- Framework :: Django :: 3.2
- Framework :: Django :: 4.0
- Framework :: Django :: 4.1
- Topic :: Internet :: WWW/HTTP :: Indexing/Search
- License :: OSI Approved :: MIT License
- Programming Language :: Python
- Programming Language :: Python :: 3 :: Only
- Programming Language :: Python :: 3
- Programming Language :: Python :: 3.7
- Programming Language :: Python :: 3.8
- Programming Language :: Python :: 3.9
- Programming Language :: Python :: 3.10
- Programming Language :: Python :: 3.11
-
-[options]
-package_dir = =src
-packages = find:
-install_requires =
- Django>=2.2.0
- requests>=2.7.0
-
-[options.packages.find]
-where = src
-
-[options.extras_require]
-test =
- coverage
- pytest
- pytest-cov
- pytest-django
-lint =
- pyflakes
- black
-
-[coverage:run]
-branch = True
-omit =
- manage.py
- setup.py
- webmention/checks.py
- *test*
- */migrations/*
- */admin.py
- */__init__.py
-
-[coverage:report]
-precision = 2
-show_missing = True
-skip_covered = True
-
-[tool:pytest]
-DJANGO_SETTINGS_MODULE = tests.test_settings
-python_files =
- tests.py
- test_*.py
-addopts = -ra -q --cov=webmention
-
-[tox:tox]
-envlist = {py37,py38,py39,py310,py311}-django{3.2,4.0,4.1}
-
-[testenv]
-extras = test
-commands =
- pytest {posargs}
-deps =
- django3.1: Django>=3.2,<3.3
- django4.0: Django>=4.0,<4.1
- django4.1: Django>=4.1,<4.2
-
-[testenv:lint]
-extras = lint
-commands =
- pyflakes src/webmention tests
- black --check src/webmention tests
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 056ba45..0000000
--- a/setup.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import setuptools
-
-
-setuptools.setup()
diff --git a/src/webmention/models.py b/src/webmention/models.py
index 55675e2..a0fe851 100644
--- a/src/webmention/models.py
+++ b/src/webmention/models.py
@@ -1,8 +1,10 @@
+from django.contrib.admin import display
from django.db import models
from django.utils.html import format_html
class WebMentionResponse(models.Model):
+ id: int
response_body = models.TextField()
response_to = models.URLField()
source = models.URLField()
@@ -18,15 +20,13 @@ class Meta:
def __str__(self):
return self.source
+ @display(description="source")
def source_for_admin(self):
- return format_html('{href}'.format(href=self.source))
-
- source_for_admin.short_description = "source"
+ return format_html('{}', self.source, self.source)
+ @display(description="response to")
def response_to_for_admin(self):
- return format_html('{href}'.format(href=self.response_to))
-
- response_to_for_admin.short_description = "response to"
+ return format_html('{}', self.response_to, self.response_to)
def invalidate(self):
if self.id:
diff --git a/src/webmention/views.py b/src/webmention/views.py
index 8b381b3..ff33ff3 100644
--- a/src/webmention/views.py
+++ b/src/webmention/views.py
@@ -27,7 +27,8 @@ def receive(request):
webmention.update(source, target, response_body)
return HttpResponse("The webmention was successfully received", status=202)
except (SourceFetchError, TargetNotFoundError) as e:
- webmention.invalidate()
+ if webmention:
+ webmention.invalidate()
return HttpResponseBadRequest(str(e))
except Exception as e:
return HttpResponseServerError(str(e))
diff --git a/tests/test_settings.py b/tests/settings.py
similarity index 100%
rename from tests/test_settings.py
rename to tests/settings.py