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
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
fail-fast: false
matrix:
os: [ "ubuntu-latest", "macos-latest" ]
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ]
python-version: [ "3.11", "3.12", "3.13", "3.14" ]
runs-on: ${{ matrix.os }}
steps:
- name: Check out repository
Expand Down
14 changes: 11 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,24 @@ exclude: |
)$
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: check-yaml
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.11.8
rev: v0.16.2
hooks:
# Run the linter.
- id: ruff
- id: ruff-check
args: ["--fix"]
# Run the formatter.
- id: ruff-format
- repo: local
hooks:
- id: mypy
name: mypy
entry: uv run mypy src
language: system
types: [python]
pass_filenames: false
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
- `NetworkIXLan.speed` verbose name updated to "Capacity (mbit/sec)" (#1888)
### Fixed
- missing cascade delete relationships for carrier, campus and carrierfac during initial syncs (peeringdb-py/#91)
- django-peeringdb/#136 Dependency updates and modernization
### Removed
- python 3.10 support (end of life)
- django 4.2 support (end of life)


## 3.7.0
Expand Down
6 changes: 5 additions & 1 deletion CHANGELOG.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ Unreleased:
fixed:
- missing cascade delete relationships for carrier, campus and carrierfac during
initial syncs (peeringdb-py/#91)
- django-peeringdb/#136 Dependency updates and modernization
changed:
- '`NetworkIXLan.speed` verbose name updated to "Capacity (mbit/sec)" (#1888)'
- '`Network.irr_as_set` label and help text no longer mention route-sets (#1973)'
deprecated: []
removed: []
removed:
- python 3.10 support (end of life)
- django 4.2 support (end of life)
security: []
3.7.0:
added:
Expand Down
51 changes: 38 additions & 13 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,25 @@ license = { text = "BSD-2-Clause" }
authors = [{ name = "PeeringDB", email = "support@peeringdb.com" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Framework :: Django :: 3.1",
"Framework :: Django :: 3.2",
"Framework :: Django :: 4.0",
"Framework :: Django :: 5.0",
"Framework :: Django :: 5.1",
"Framework :: Django :: 5.2",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3.10",
"License :: OSI Approved :: BSD License",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Internet",
]
requires-python = ">=3.10"
requires-python = ">=3.11"
dependencies = [
"django_countries>1",
"django_handleref>=2",
"django_inet>=1",
"asgiref>=3",
"django>=4.2,<6",
"django>=5.0,<6",
]

[dependency-groups]
Expand All @@ -42,7 +41,7 @@ dev = [
"tox-uv>=1.13.0",

# linting
"mypy>=0.950",
"mypy>=2.0",
"django-stubs[compatible-mypy]",
"pre-commit>=2.13",
"ruff",
Expand All @@ -67,13 +66,34 @@ requires = ["hatchling>=1.0.0"]
build-backend = "hatchling.build"

[tool.ruff.lint]
# Pinned explicitly rather than relying on ruff's implicit defaults, which
# change between minor releases (0.16 added RUF/DTZ/PIE/PLR rules to the
# default set, surfacing 111 findings in this codebase). New rules are now an
# explicit opt-in instead of arriving with a version bump.
select = [
"E4", # pycodestyle: imports
"E7", # pycodestyle: statements
"E9", # pycodestyle: runtime errors
"F", # pyflakes
]
extend-select = [
"I", # isort
"UP", # pyupgrade
"I", # isort
"UP", # pyupgrade
"PIE", # flake8-pie
"RUF", # ruff-specific
"PLR0402", # manual-from-import
]
# Deliberately not enabled:
# DTZ - this library works with naive datetimes on purpose
# (USE_TZ=False + PEERINGDB_SYNC_STRIP_TZ); see test_timezone.py.
# PLR - beyond PLR0402, PLR2004 (magic-value-comparison) is noise in tests.

[tool.ruff.lint.per-file-ignores]
# Migrations are generated by Django; makemigrations would drop any annotation.
"src/django_peeringdb/migrations/*" = ["RUF012"]

[tool.mypy]
python_version = "3.10"
python_version = "3.11"
plugins = ["mypy_django_plugin.main"]
warn_return_any = false
warn_unused_configs = true
Expand Down Expand Up @@ -102,9 +122,14 @@ exclude = [
[[tool.mypy.overrides]]
module = [
"django_peeringdb.models.abstract",
"django_peeringdb.models.concrete"
"django_peeringdb.models.concrete",
]
disable_error_code = [
"attr-defined",
"import-not-found",
"misc",
"django-manager-missing",
]
ignore_errors = true

[tool.django-stubs]
django_settings_module = "tests.settings"
Expand Down
17 changes: 12 additions & 5 deletions src/django_peeringdb/admin/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
from typing import ClassVar

from django.contrib import admin
from django.contrib.admin.options import InlineModelAdmin
from django.db.models import Model
from django.http import HttpRequest

from django_peeringdb.models import (
Facility,
Expand All @@ -21,7 +26,9 @@ class HandleRefAdminMixIn:


class ModelAdminBase(HandleRefAdminMixIn, admin.ModelAdmin):
def has_change_permission(self, request, obj=None):
def has_change_permission(
self, request: HttpRequest, obj: Model | None = None
) -> bool:
"""Make everything read-only, as this is PeeringDB's data."""
return False

Expand All @@ -37,7 +44,7 @@ class NetworkInline(HandleRefAdminMixIn, admin.TabularInline):
class OrganizationAdmin(ModelAdminBase):
search_fields = ("name",)
list_display = ("name", "website")
inlines = [
inlines: ClassVar[list[type[InlineModelAdmin]]] = [
NetworkInline,
]

Expand Down Expand Up @@ -66,7 +73,7 @@ class NetworkIXLanInline(HandleRefAdminMixIn, admin.TabularInline):
class NetworkAdmin(ModelAdminBase):
search_fields = ("name", "aka", "asn")
list_display = ("name", "aka", "asn")
inlines = [
inlines: ClassVar[list[type[InlineModelAdmin]]] = [
NetworkFacilityInline,
NetworkContactInline,
NetworkIXLanInline,
Expand All @@ -90,7 +97,7 @@ class InternetExchangeAdmin(ModelAdminBase):
"name_long",
)
list_display = ("name", "name_long", "city")
inlines = [
inlines: ClassVar[list[type[InlineModelAdmin]]] = [
InternetExchangeFacilityInline,
IXLanInline,
]
Expand All @@ -105,6 +112,6 @@ class IXLanPrefixInline(HandleRefAdminMixIn, admin.StackedInline):
class IXLanAdmin(ModelAdminBase):
search_fields = ("name", "ix__name")
list_display = ("__str__", "ix")
inlines = [
inlines: ClassVar[list[type[InlineModelAdmin]]] = [
IXLanPrefixInline,
]
45 changes: 24 additions & 21 deletions src/django_peeringdb/client_adaptor/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from collections import defaultdict
from decimal import Decimal
from ipaddress import IPv4Address, IPv6Address
from typing import ClassVar

from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.management import call_command
Expand All @@ -29,7 +30,7 @@ class Backend(Interface):
)

# Resource (abstract) and model (concrete) definitions
RESOURCE_MAP = {
RESOURCE_MAP: ClassVar[dict[type, type]] = {
resource.Carrier: concrete.Carrier,
resource.CarrierFacility: concrete.CarrierFacility,
resource.Facility: concrete.Facility,
Expand All @@ -45,7 +46,7 @@ class Backend(Interface):
resource.Campus: concrete.Campus,
}

ERROR_PATTERNS = {
ERROR_PATTERNS: ClassVar[dict[str, dict[str, list[tuple[str, int]]]]] = {
"mysql": {
"unique": [(r"Duplicate entry '[^\']+' for key '(?P<field_name>\w+)'", 1)],
},
Expand All @@ -58,7 +59,7 @@ class Backend(Interface):
}

@classmethod
def setup(cls):
def setup(cls) -> None:
# in order to copy updated / created times from server
# we need to turn off auto updating of those fields
# during update and add
Expand All @@ -73,17 +74,17 @@ def atomic_transaction(cls):
return atomic_transaction()

@classmethod
def validation_error(cls, concrete=None):
def validation_error(cls, concrete=None) -> type[ValidationError]:
return ValidationError

@classmethod
def object_missing_error(cls, concrete=None):
def object_missing_error(cls, concrete=None) -> type[ObjectDoesNotExist]:
if concrete:
return concrete.DoesNotExist
return ObjectDoesNotExist

@reftag_to_cls
def last_change(self, concrete):
def last_change(self, concrete) -> int:
upd = concrete.handleref.last_change()
if upd:
return int(calendar.timegm(upd.timetuple()))
Expand All @@ -94,7 +95,7 @@ def get_object(self, concrete, id):
return concrete.objects.get(pk=id)

@reftag_to_cls
def get_object_by(self, concrete, field_name, value):
def get_object_by(self, concrete, field_name: str, value):
return concrete.objects.get(**{field_name: value})

@reftag_to_cls
Expand All @@ -104,7 +105,7 @@ def get_objects(self, concrete, ids=None):
return concrete.objects.all()

@reftag_to_cls
def get_objects_by(self, concrete, field_name, value):
def get_objects_by(self, concrete, field_name: str, value):
return concrete.objects.filter(**{field_name: value})

@reftag_to_cls
Expand All @@ -116,35 +117,37 @@ def get_fields(self, concrete):
return concrete._meta.get_fields()

@reftag_to_cls
def get_field(self, concrete, field_name):
def get_field(self, concrete, field_name: str):
return concrete._meta.get_field(field_name)

@reftag_to_cls
def get_field_concrete(self, concrete, field_name):
def get_field_concrete(self, concrete, field_name: str):
return concrete._meta.get_field(field_name).related_model

@reftag_to_cls
def is_field_related(self, concrete, field_name):
def is_field_related(self, concrete, field_name: str):
field = self.get_field(concrete, field_name)
related = getattr(field, "related_model", False)
multiple = getattr(field, "multiple", False)
return (related, multiple)

def set_relation_many_to_many(self, obj, field_name, objs):
def set_relation_many_to_many(
self, obj: models.Model, field_name: str, objs
) -> None:
"Set a many-to-many field on an object"
relation = getattr(obj, field_name)
if hasattr(relation, "set"):
relation.set(objs) # Django 2.x
else:
setattr(obj, field_name, objs) # Django 1.x

def clean(self, obj):
def clean(self, obj: models.Model) -> None:
obj.full_clean()

def save(self, obj):
def save(self, obj: models.Model) -> None:
obj.save()

def convert_field(self, concrete, field_name, value):
def convert_field(self, concrete, field_name: str, value):
field = concrete._meta.get_field(field_name)
if isinstance(field, models.DecimalField) and isinstance(value, float):
return Decimal("{:.{prec}f}".format(value, prec=field.decimal_places))
Expand Down Expand Up @@ -176,14 +179,14 @@ def detect_missing_relations(self, obj, exc):
missing[res].add(int(m_choice.group(1)))
return missing

def detect_uniqueness_error(self, exc):
def detect_uniqueness_error(self, exc: Exception) -> list[str] | None:
"""
Parse error, and if it describes any violations of a uniqueness constraint,
return the corresponding fields, else None
"""
pattern = r"(\w+) with this (\w+) already exists"

fields = []
fields: list[str] = []
if isinstance(exc, IntegrityError):
return self._detect_integrity_error(exc)
assert isinstance(exc, ValidationError), TypeError
Expand All @@ -193,7 +196,7 @@ def detect_uniqueness_error(self, exc):
fields.append(name)
return fields or None

def _detect_integrity_error(self, exc):
def _detect_integrity_error(self, exc: IntegrityError) -> list[str] | None:
engine = connection.vendor
patterns = self.ERROR_PATTERNS[engine]

Expand All @@ -206,17 +209,17 @@ def _detect_integrity_error(self, exc):
return None

# Database
def migrate_database(self, verbosity=0):
def migrate_database(self, verbosity: int = 0) -> None:
call_command("migrate", interactive=False, verbosity=verbosity)

# credit to https://stackoverflow.com/a/31847406/1325447
def is_database_migrated(self, database=DEFAULT_DB_ALIAS):
def is_database_migrated(self, database: str = DEFAULT_DB_ALIAS) -> bool:
connection = connections[database]
connection.prepare_database()
executor = MigrationExecutor(connection)
targets = executor.loader.graph.leaf_nodes()
# No plan <=> Yes sync'd
return not executor.migration_plan(targets)

def delete_all(self):
def delete_all(self) -> None:
call_command("flush", interactive=False, verbosity=1)
Loading