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/test-libmaxminddb.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
# We don't test on Windows currently due to issues
# build libmaxminddb there.
platform: [macos-latest, ubuntu-latest]
python-version: [3.8, 3.9, "3.10", 3.11, 3.12, 3.13]
python-version: [3.9, "3.10", 3.11, 3.12, 3.13]

name: Python ${{ matrix.python-version }} on ${{ matrix.platform }}
runs-on: ${{ matrix.platform }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
strategy:
matrix:
platform: [macos-latest, ubuntu-24.04-arm, ubuntu-latest, windows-latest]
python-version: [3.8, 3.9, "3.10", 3.11, 3.12, 3.13]
python-version: [3.9, "3.10", 3.11, 3.12, 3.13]

name: Python ${{ matrix.python-version }} on ${{ matrix.platform }}
runs-on: ${{ matrix.platform }}
Expand Down
4 changes: 3 additions & 1 deletion HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
History
-------

2.6.4
2.7.0
++++++++++++++++++

* IMPORTANT: Python 3.9 or greater is required. If you are using an older
version, please use an earlier release.
* The vendored ``libmaxminddb`` has been updated to 1.12.2.
* The C extension now checks that the database metadata lookup was
successful.
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ invalid IP address or an IPv6 address in an IPv4 database.
Requirements
------------

This code requires Python 3.8+. Older versions are not supported. The C
This code requires Python 3.9+. Older versions are not supported. The C
extension requires CPython.

Versioning
Expand Down
14 changes: 11 additions & 3 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,28 @@

.. include:: ../README.rst

=======
Modules
=======
======
Module
======

.. automodule:: maxminddb
:members:
:undoc-members:
:show-inheritance:

======
Errors
======

.. automodule:: maxminddb.errors
:members:
:undoc-members:
:show-inheritance:

===============
Database Reader
===============

.. automodule:: maxminddb.reader
:members:
:undoc-members:
Expand Down
2 changes: 2 additions & 0 deletions maxminddb/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Module for reading MaxMind DB files."""

# pylint:disable=C0111
import os
from typing import IO, AnyStr, Union, cast
Expand Down
40 changes: 17 additions & 23 deletions maxminddb/decoder.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
"""
maxminddb.decoder
~~~~~~~~~~~~~~~~~

This package contains code for decoding the MaxMind DB data section.

"""
"""Decoder for the MaxMind DB data section."""

import struct
from typing import Dict, List, Tuple, Union, cast
from typing import Union, cast

try:
# pylint: disable=unused-import
Expand Down Expand Up @@ -43,35 +37,35 @@ def __init__(
self._buffer = database_buffer
self._pointer_base = pointer_base

def _decode_array(self, size: int, offset: int) -> Tuple[List[Record], int]:
def _decode_array(self, size: int, offset: int) -> tuple[list[Record], int]:
array = []
for _ in range(size):
(value, offset) = self.decode(offset)
array.append(value)
return array, offset

def _decode_boolean(self, size: int, offset: int) -> Tuple[bool, int]:
def _decode_boolean(self, size: int, offset: int) -> tuple[bool, int]:
return size != 0, offset

def _decode_bytes(self, size: int, offset: int) -> Tuple[bytes, int]:
def _decode_bytes(self, size: int, offset: int) -> tuple[bytes, int]:
new_offset = offset + size
return self._buffer[offset:new_offset], new_offset

def _decode_double(self, size: int, offset: int) -> Tuple[float, int]:
def _decode_double(self, size: int, offset: int) -> tuple[float, int]:
self._verify_size(size, 8)
new_offset = offset + size
packed_bytes = self._buffer[offset:new_offset]
(value,) = struct.unpack(b"!d", packed_bytes)
return value, new_offset

def _decode_float(self, size: int, offset: int) -> Tuple[float, int]:
def _decode_float(self, size: int, offset: int) -> tuple[float, int]:
self._verify_size(size, 4)
new_offset = offset + size
packed_bytes = self._buffer[offset:new_offset]
(value,) = struct.unpack(b"!f", packed_bytes)
return value, new_offset

def _decode_int32(self, size: int, offset: int) -> Tuple[int, int]:
def _decode_int32(self, size: int, offset: int) -> tuple[int, int]:
if size == 0:
return 0, offset
new_offset = offset + size
Expand All @@ -82,15 +76,15 @@ def _decode_int32(self, size: int, offset: int) -> Tuple[int, int]:
(value,) = struct.unpack(b"!i", packed_bytes)
return value, new_offset

def _decode_map(self, size: int, offset: int) -> Tuple[Dict[str, Record], int]:
container: Dict[str, Record] = {}
def _decode_map(self, size: int, offset: int) -> tuple[dict[str, Record], int]:
container: dict[str, Record] = {}
for _ in range(size):
(key, offset) = self.decode(offset)
(value, offset) = self.decode(offset)
container[cast(str, key)] = value
container[cast("str", key)] = value
return container, offset

def _decode_pointer(self, size: int, offset: int) -> Tuple[Record, int]:
def _decode_pointer(self, size: int, offset: int) -> tuple[Record, int]:
pointer_size = (size >> 3) + 1

buf = self._buffer[offset : offset + pointer_size]
Expand All @@ -113,12 +107,12 @@ def _decode_pointer(self, size: int, offset: int) -> Tuple[Record, int]:
(value, _) = self.decode(pointer)
return value, new_offset

def _decode_uint(self, size: int, offset: int) -> Tuple[int, int]:
def _decode_uint(self, size: int, offset: int) -> tuple[int, int]:
new_offset = offset + size
uint_bytes = self._buffer[offset:new_offset]
return int.from_bytes(uint_bytes, "big"), new_offset

def _decode_utf8_string(self, size: int, offset: int) -> Tuple[str, int]:
def _decode_utf8_string(self, size: int, offset: int) -> tuple[str, int]:
new_offset = offset + size
return self._buffer[offset:new_offset].decode("utf-8"), new_offset

Expand All @@ -138,7 +132,7 @@ def _decode_utf8_string(self, size: int, offset: int) -> Tuple[str, int]:
15: _decode_float,
}

def decode(self, offset: int) -> Tuple[Record, int]:
def decode(self, offset: int) -> tuple[Record, int]:
"""Decode a section of the data section starting at offset.

Arguments:
Expand All @@ -162,7 +156,7 @@ def decode(self, offset: int) -> Tuple[Record, int]:
(size, new_offset) = self._size_from_ctrl_byte(ctrl_byte, new_offset, type_num)
return decoder(self, size, new_offset)

def _read_extended(self, offset: int) -> Tuple[int, int]:
def _read_extended(self, offset: int) -> tuple[int, int]:
next_byte = self._buffer[offset]
type_num = next_byte + 7
if type_num < 7:
Expand All @@ -185,7 +179,7 @@ def _size_from_ctrl_byte(
ctrl_byte: int,
offset: int,
type_num: int,
) -> Tuple[int, int]:
) -> tuple[int, int]:
size = ctrl_byte & 0x1F
if type_num == 1 or size < 29:
return size, offset
Expand Down
9 changes: 2 additions & 7 deletions maxminddb/errors.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
"""
maxminddb.errors
~~~~~~~~~~~~~~~~
This module contains custom errors for the MaxMind DB reader
"""
"""Typed errors thrown by this library."""


class InvalidDatabaseError(RuntimeError):
"""This error is thrown when unexpected data is found in the database."""
"""An error thrown when unexpected data is found in the database."""
14 changes: 4 additions & 10 deletions maxminddb/extension.pyi
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
"""
maxminddb.extension
~~~~~~~~~~~~~~~~

This module contains the C extension database reader and related classes.

"""
"""C extension database reader and related classes."""

# pylint: disable=E0601,E0602
from ipaddress import IPv4Address, IPv6Address
from os import PathLike
from typing import IO, Any, AnyStr, Optional, Tuple, Union
from typing import IO, Any, AnyStr, Optional, Union

from maxminddb import MODE_AUTO
from maxminddb.const import MODE_AUTO
from maxminddb.types import Record

class Reader:
Expand Down Expand Up @@ -50,7 +44,7 @@ class Reader:
def get_with_prefix_len(
self,
ip_address: Union[str, IPv6Address, IPv4Address],
) -> Tuple[Optional[Record], int]:
) -> tuple[Optional[Record], int]:
"""Return a tuple with the record and the associated prefix length

Arguments:
Expand Down
22 changes: 8 additions & 14 deletions maxminddb/reader.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
"""
maxminddb.reader
~~~~~~~~~~~~~~~~

This module contains the pure Python database reader and related classes.

"""
"""Pure-Python reader for the MaxMind DB file format."""

try:
import mmap
Expand All @@ -14,9 +8,10 @@

import ipaddress
import struct
from collections.abc import Iterator
from ipaddress import IPv4Address, IPv6Address
from os import PathLike
from typing import IO, Any, AnyStr, Dict, Iterator, List, Optional, Tuple, Union
from typing import IO, Any, AnyStr, Optional, Union

from maxminddb.const import MODE_AUTO, MODE_FD, MODE_FILE, MODE_MEMORY, MODE_MMAP
from maxminddb.decoder import Decoder
Expand All @@ -28,8 +23,7 @@


class Reader:
"""
A pure Python implementation of a reader for the MaxMind DB format. IP
"""A pure Python implementation of a reader for the MaxMind DB format. IP
addresses can be looked up using the ``get`` method.
"""

Expand Down Expand Up @@ -148,7 +142,7 @@ def get(self, ip_address: Union[str, IPv6Address, IPv4Address]) -> Optional[Reco
def get_with_prefix_len(
self,
ip_address: Union[str, IPv6Address, IPv4Address],
) -> Tuple[Optional[Record], int]:
) -> tuple[Optional[Record], int]:
"""Return a tuple with the record and the associated prefix length.

Arguments:
Expand Down Expand Up @@ -202,7 +196,7 @@ def _generate_children(self, node, depth, ip_acc) -> Iterator:
right = self._read_node(node, 1)
yield from self._generate_children(right, depth, ip_acc | 1)

def _find_address_in_tree(self, packed: bytearray) -> Tuple[int, int]:
def _find_address_in_tree(self, packed: bytearray) -> tuple[int, int]:
bit_count = len(packed) * 8
node = self._start_node(bit_count)
node_count = self._metadata.node_count
Expand Down Expand Up @@ -303,7 +297,7 @@ class Metadata:
A string identifying the database type, e.g., "GeoIP2-City".
"""

description: Dict[str, str]
description: dict[str, str]
"""
A map from locales to text descriptions of the database.
"""
Expand All @@ -315,7 +309,7 @@ class Metadata:
both IPv4 and IPv6 lookups.
"""

languages: List[str]
languages: list[str]
"""
A list of locale codes supported by the databse.
"""
Expand Down
13 changes: 4 additions & 9 deletions maxminddb/types.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
"""
maxminddb.types
~~~~~~~~~~~~~~~
"""Types representing database records."""

This module provides a Record type that represents a database record.
"""

from typing import AnyStr, Dict, List, Union
from typing import AnyStr, Union

Primitive = Union[AnyStr, bool, float, int]
Record = Union[Primitive, "RecordList", "RecordDict"]


class RecordList(List[Record]): # pylint: disable=too-few-public-methods
class RecordList(list[Record]): # pylint: disable=too-few-public-methods
"""RecordList is a type for lists in a database record."""


class RecordDict(Dict[str, Record]): # pylint: disable=too-few-public-methods
class RecordDict(dict[str, Record]): # pylint: disable=too-few-public-methods
"""RecordDict is a type for dicts in a database record."""
14 changes: 12 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ description = "Reader for the MaxMind DB format"
authors = [
{name = "Gregory Oschwald", email = "[email protected]"},
]
requires-python = ">=3.8"
requires-python = ">=3.9"
readme = "README.rst"
license = {text = "Apache License, Version 2.0"}
classifiers = [
Expand All @@ -20,7 +20,6 @@ classifiers = [
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
Expand All @@ -36,6 +35,17 @@ Documentation = "https://maxminddb.readthedocs.org/"
"Source Code" = "https://github.com/maxmind/MaxMind-DB-Reader-python"
"Issue Tracker" = "https://github.com/maxmind/MaxMind-DB-Reader-python/issues"

[dependency-groups]
dev = [
"pytest>=8.3.5",
]
lint = [
"black>=25.1.0",
"mypy>=1.15.0",
"pylint>=3.3.6",
"ruff>=0.11.6",
]

[tool.setuptools.package-data]
maxminddb = ["py.typed"]

Expand Down
7 changes: 3 additions & 4 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,23 @@ package_dir =
packages = maxminddb
install_requires =
include_package_data = True
python_requires = >=3.8
python_requires = >=3.9

[options.package_data]
maxminddb = extension.pyi; py.typed

[tox:tox]
envlist = {py38,py39,py310,py311,py312,py313}-test,py313-{black,lint,flake8,mypy}
envlist = {py39,py310,py311,py312,py313}-test,py313-{black,lint,flake8,mypy}

[gh-actions]
python =
3.8: py38
3.9: py39
3.10: py310
3.11: py311
3.12: py312
3.13: py313,black,lint,flake8,mypy

[testenv:{py38,py39,py310,py311,py312,py313}-test]
[testenv:{py39,py310,py311,py312,py313}-test]
deps = pytest
commands = pytest tests
passenv = *
Expand Down
Loading
Loading