maxminddb 2.8.2__cp313-cp313-android_21_arm64_v8a.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
maxminddb/__init__.py ADDED
@@ -0,0 +1,94 @@
1
+ """Module for reading MaxMind DB files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import IO, TYPE_CHECKING, AnyStr, cast
6
+
7
+ from .const import (
8
+ MODE_AUTO,
9
+ MODE_FD,
10
+ MODE_FILE,
11
+ MODE_MEMORY,
12
+ MODE_MMAP,
13
+ MODE_MMAP_EXT,
14
+ )
15
+ from .decoder import InvalidDatabaseError
16
+ from .reader import Reader
17
+
18
+ if TYPE_CHECKING:
19
+ import os
20
+
21
+ try:
22
+ from . import extension as _extension
23
+ except ImportError:
24
+ _extension = None # type: ignore[assignment]
25
+
26
+
27
+ __all__ = [
28
+ "MODE_AUTO",
29
+ "MODE_FD",
30
+ "MODE_FILE",
31
+ "MODE_MEMORY",
32
+ "MODE_MMAP",
33
+ "MODE_MMAP_EXT",
34
+ "InvalidDatabaseError",
35
+ "Reader",
36
+ "open_database",
37
+ ]
38
+
39
+
40
+ def open_database(
41
+ database: AnyStr | int | os.PathLike | IO,
42
+ mode: int = MODE_AUTO,
43
+ ) -> Reader:
44
+ """Open a MaxMind DB database.
45
+
46
+ Arguments:
47
+ database: A path to a valid MaxMind DB file such as a GeoIP2 database
48
+ file, or a file descriptor in the case of MODE_FD.
49
+ mode: mode to open the database with. Valid mode are:
50
+ * MODE_MMAP_EXT - use the C extension with memory map.
51
+ * MODE_MMAP - read from memory map. Pure Python.
52
+ * MODE_FILE - read database as standard file. Pure Python.
53
+ * MODE_MEMORY - load database into memory. Pure Python.
54
+ * MODE_FD - the param passed via database is a file descriptor, not
55
+ a path. This mode implies MODE_MEMORY.
56
+ * MODE_AUTO - tries MODE_MMAP_EXT, MODE_MMAP, MODE_FILE in that
57
+ order. Default mode.
58
+
59
+ """
60
+ if mode not in (
61
+ MODE_AUTO,
62
+ MODE_FD,
63
+ MODE_FILE,
64
+ MODE_MEMORY,
65
+ MODE_MMAP,
66
+ MODE_MMAP_EXT,
67
+ ):
68
+ msg = f"Unsupported open mode: {mode}"
69
+ raise ValueError(msg)
70
+
71
+ has_extension = _extension and hasattr(_extension, "Reader")
72
+ use_extension = has_extension if mode == MODE_AUTO else mode == MODE_MMAP_EXT
73
+
74
+ if not use_extension:
75
+ return Reader(database, mode)
76
+
77
+ if not has_extension:
78
+ msg = "MODE_MMAP_EXT requires the maxminddb.extension module to be available"
79
+ raise ValueError(
80
+ msg,
81
+ )
82
+
83
+ # The C type exposes the same API as the Python Reader, so for type
84
+ # checking purposes, pretend it is one. (Ideally this would be a subclass
85
+ # of, or share a common parent class with, the Python Reader
86
+ # implementation.)
87
+ return cast("Reader", _extension.Reader(database, mode))
88
+
89
+
90
+ __title__ = "maxminddb"
91
+ __version__ = "2.8.2"
92
+ __author__ = "Gregory Oschwald"
93
+ __license__ = "Apache License, Version 2.0"
94
+ __copyright__ = "Copyright 2013-2025 MaxMind, Inc."
maxminddb/const.py ADDED
@@ -0,0 +1,8 @@
1
+ """Constants used in the API."""
2
+
3
+ MODE_AUTO = 0
4
+ MODE_MMAP_EXT = 1
5
+ MODE_MMAP = 2
6
+ MODE_FILE = 4
7
+ MODE_MEMORY = 8
8
+ MODE_FD = 16
maxminddb/decoder.py ADDED
@@ -0,0 +1,207 @@
1
+ """Decoder for the MaxMind DB data section."""
2
+
3
+ import struct
4
+ from typing import ClassVar, Union, cast
5
+
6
+ try:
7
+ import mmap
8
+ except ImportError:
9
+ mmap = None # type: ignore[assignment]
10
+
11
+
12
+ from maxminddb.errors import InvalidDatabaseError
13
+ from maxminddb.file import FileBuffer
14
+ from maxminddb.types import Record
15
+
16
+
17
+ class Decoder:
18
+ """Decoder for the data section of the MaxMind DB."""
19
+
20
+ def __init__(
21
+ self,
22
+ database_buffer: Union[FileBuffer, "mmap.mmap", bytes],
23
+ pointer_base: int = 0,
24
+ pointer_test: bool = False, # noqa: FBT001, FBT002
25
+ ) -> None:
26
+ """Create a Decoder for a MaxMind DB.
27
+
28
+ Arguments:
29
+ database_buffer: an mmap'd MaxMind DB file.
30
+ pointer_base: the base number to use when decoding a pointer
31
+ pointer_test: used for internal unit testing of pointer code
32
+
33
+ """
34
+ self._pointer_test = pointer_test
35
+ self._buffer = database_buffer
36
+ self._pointer_base = pointer_base
37
+
38
+ def _decode_array(self, size: int, offset: int) -> tuple[list[Record], int]:
39
+ array = []
40
+ for _ in range(size):
41
+ (value, offset) = self.decode(offset)
42
+ array.append(value)
43
+ return array, offset
44
+
45
+ def _decode_boolean(self, size: int, offset: int) -> tuple[bool, int]:
46
+ return size != 0, offset
47
+
48
+ def _decode_bytes(self, size: int, offset: int) -> tuple[bytes, int]:
49
+ new_offset = offset + size
50
+ return self._buffer[offset:new_offset], new_offset
51
+
52
+ def _decode_double(self, size: int, offset: int) -> tuple[float, int]:
53
+ self._verify_size(size, 8)
54
+ new_offset = offset + size
55
+ packed_bytes = self._buffer[offset:new_offset]
56
+ (value,) = struct.unpack(b"!d", packed_bytes)
57
+ return value, new_offset
58
+
59
+ def _decode_float(self, size: int, offset: int) -> tuple[float, int]:
60
+ self._verify_size(size, 4)
61
+ new_offset = offset + size
62
+ packed_bytes = self._buffer[offset:new_offset]
63
+ (value,) = struct.unpack(b"!f", packed_bytes)
64
+ return value, new_offset
65
+
66
+ def _decode_int32(self, size: int, offset: int) -> tuple[int, int]:
67
+ if size == 0:
68
+ return 0, offset
69
+ new_offset = offset + size
70
+ packed_bytes = self._buffer[offset:new_offset]
71
+
72
+ if size != 4:
73
+ packed_bytes = packed_bytes.rjust(4, b"\x00")
74
+ (value,) = struct.unpack(b"!i", packed_bytes)
75
+ return value, new_offset
76
+
77
+ def _decode_map(self, size: int, offset: int) -> tuple[dict[str, Record], int]:
78
+ container: dict[str, Record] = {}
79
+ for _ in range(size):
80
+ (key, offset) = self.decode(offset)
81
+ (value, offset) = self.decode(offset)
82
+ container[cast("str", key)] = value
83
+ return container, offset
84
+
85
+ def _decode_pointer(self, size: int, offset: int) -> tuple[Record, int]:
86
+ pointer_size = (size >> 3) + 1
87
+
88
+ buf = self._buffer[offset : offset + pointer_size]
89
+ new_offset = offset + pointer_size
90
+
91
+ if pointer_size == 1:
92
+ buf = bytes([size & 0x7]) + buf
93
+ pointer = struct.unpack(b"!H", buf)[0] + self._pointer_base
94
+ elif pointer_size == 2:
95
+ buf = b"\x00" + bytes([size & 0x7]) + buf
96
+ pointer = struct.unpack(b"!I", buf)[0] + 2048 + self._pointer_base
97
+ elif pointer_size == 3:
98
+ buf = bytes([size & 0x7]) + buf
99
+ pointer = struct.unpack(b"!I", buf)[0] + 526336 + self._pointer_base
100
+ else:
101
+ pointer = struct.unpack(b"!I", buf)[0] + self._pointer_base
102
+
103
+ if self._pointer_test:
104
+ return pointer, new_offset
105
+ (value, _) = self.decode(pointer)
106
+ return value, new_offset
107
+
108
+ def _decode_uint(self, size: int, offset: int) -> tuple[int, int]:
109
+ new_offset = offset + size
110
+ uint_bytes = self._buffer[offset:new_offset]
111
+ return int.from_bytes(uint_bytes, "big"), new_offset
112
+
113
+ def _decode_utf8_string(self, size: int, offset: int) -> tuple[str, int]:
114
+ new_offset = offset + size
115
+ return self._buffer[offset:new_offset].decode("utf-8"), new_offset
116
+
117
+ _type_decoder: ClassVar = {
118
+ 1: _decode_pointer,
119
+ 2: _decode_utf8_string,
120
+ 3: _decode_double,
121
+ 4: _decode_bytes,
122
+ 5: _decode_uint, # uint16
123
+ 6: _decode_uint, # uint32
124
+ 7: _decode_map,
125
+ 8: _decode_int32,
126
+ 9: _decode_uint, # uint64
127
+ 10: _decode_uint, # uint128
128
+ 11: _decode_array,
129
+ 14: _decode_boolean,
130
+ 15: _decode_float,
131
+ }
132
+
133
+ def decode(self, offset: int) -> tuple[Record, int]:
134
+ """Decode a section of the data section starting at offset.
135
+
136
+ Arguments:
137
+ offset: the location of the data structure to decode
138
+
139
+ """
140
+ new_offset = offset + 1
141
+ ctrl_byte = self._buffer[offset]
142
+ type_num = ctrl_byte >> 5
143
+ # Extended type
144
+ if not type_num:
145
+ (type_num, new_offset) = self._read_extended(new_offset)
146
+
147
+ try:
148
+ decoder = self._type_decoder[type_num]
149
+ except KeyError as ex:
150
+ msg = f"Unexpected type number ({type_num}) encountered"
151
+ raise InvalidDatabaseError(
152
+ msg,
153
+ ) from ex
154
+
155
+ (size, new_offset) = self._size_from_ctrl_byte(ctrl_byte, new_offset, type_num)
156
+ return decoder(self, size, new_offset)
157
+
158
+ def _read_extended(self, offset: int) -> tuple[int, int]:
159
+ next_byte = self._buffer[offset]
160
+ type_num = next_byte + 7
161
+ if type_num < 7:
162
+ msg = (
163
+ "Something went horribly wrong in the decoder. An "
164
+ f"extended type resolved to a type number < 8 ({type_num})"
165
+ )
166
+ raise InvalidDatabaseError(
167
+ msg,
168
+ )
169
+ return type_num, offset + 1
170
+
171
+ @staticmethod
172
+ def _verify_size(expected: int, actual: int) -> None:
173
+ if expected != actual:
174
+ msg = (
175
+ "The MaxMind DB file's data section contains bad data "
176
+ "(unknown data type or corrupt data)"
177
+ )
178
+ raise InvalidDatabaseError(
179
+ msg,
180
+ )
181
+
182
+ def _size_from_ctrl_byte(
183
+ self,
184
+ ctrl_byte: int,
185
+ offset: int,
186
+ type_num: int,
187
+ ) -> tuple[int, int]:
188
+ size = ctrl_byte & 0x1F
189
+ if type_num == 1 or size < 29:
190
+ return size, offset
191
+
192
+ if size == 29:
193
+ size = 29 + self._buffer[offset]
194
+ return size, offset + 1
195
+
196
+ # Using unpack rather than int_from_bytes as it is faster
197
+ # here and below.
198
+ if size == 30:
199
+ new_offset = offset + 2
200
+ size_bytes = self._buffer[offset:new_offset]
201
+ size = 285 + struct.unpack(b"!H", size_bytes)[0]
202
+ return size, new_offset
203
+
204
+ new_offset = offset + 3
205
+ size_bytes = self._buffer[offset:new_offset]
206
+ size = struct.unpack(b"!I", b"\x00" + size_bytes)[0] + 65821
207
+ return size, new_offset
maxminddb/errors.py ADDED
@@ -0,0 +1,5 @@
1
+ """Typed errors thrown by this library."""
2
+
3
+
4
+ class InvalidDatabaseError(RuntimeError):
5
+ """An error thrown when unexpected data is found in the database."""
@@ -0,0 +1,115 @@
1
+ """C extension database reader and related classes."""
2
+
3
+ from ipaddress import IPv4Address, IPv6Address
4
+ from os import PathLike
5
+ from typing import IO, Any, AnyStr
6
+
7
+ from typing_extensions import Self
8
+
9
+ from maxminddb.types import Record
10
+
11
+ class Reader:
12
+ """A C extension implementation of a reader for the MaxMind DB format.
13
+
14
+ IP addresses can be looked up using the ``get`` method.
15
+ """
16
+
17
+ closed: bool = ...
18
+
19
+ def __init__(
20
+ self,
21
+ database: AnyStr | int | PathLike | IO,
22
+ mode: int = ...,
23
+ ) -> None:
24
+ """Reader for the MaxMind DB file format.
25
+
26
+ Arguments:
27
+ database: A path to a valid MaxMind DB file such as a GeoIP2 database
28
+ file, or a file descriptor in the case of MODE_FD.
29
+ mode: mode to open the database with. The only supported modes are
30
+ MODE_AUTO and MODE_MMAP_EXT.
31
+
32
+ """
33
+
34
+ def close(self) -> None:
35
+ """Close the MaxMind DB file and returns the resources to the system."""
36
+
37
+ def get(self, ip_address: str | IPv6Address | IPv4Address) -> Record | None:
38
+ """Return the record for the ip_address in the MaxMind DB.
39
+
40
+ Arguments:
41
+ ip_address: an IP address in the standard string notation
42
+
43
+ """
44
+
45
+ def get_with_prefix_len(
46
+ self,
47
+ ip_address: str | IPv6Address | IPv4Address,
48
+ ) -> tuple[Record | None, int]:
49
+ """Return a tuple with the record and the associated prefix length.
50
+
51
+ Arguments:
52
+ ip_address: an IP address in the standard string notation
53
+
54
+ """
55
+
56
+ def metadata(self) -> Metadata:
57
+ """Return the metadata associated with the MaxMind DB file."""
58
+
59
+ def __enter__(self) -> Self: ...
60
+ def __exit__(self, *args) -> None: ... # noqa: ANN002
61
+
62
+ class Metadata:
63
+ """Metadata for the MaxMind DB reader."""
64
+
65
+ binary_format_major_version: int
66
+ """
67
+ The major version number of the binary format used when creating the
68
+ database.
69
+ """
70
+
71
+ binary_format_minor_version: int
72
+ """
73
+ The minor version number of the binary format used when creating the
74
+ database.
75
+ """
76
+
77
+ build_epoch: int
78
+ """
79
+ The Unix epoch for the build time of the database.
80
+ """
81
+
82
+ database_type: str
83
+ """
84
+ A string identifying the database type, e.g., "GeoIP2-City".
85
+ """
86
+
87
+ description: dict[str, str]
88
+ """
89
+ A map from locales to text descriptions of the database.
90
+ """
91
+
92
+ ip_version: int
93
+ """
94
+ The IP version of the data in a database. A value of "4" means the
95
+ database only supports IPv4. A database with a value of "6" may support
96
+ both IPv4 and IPv6 lookups.
97
+ """
98
+
99
+ languages: list[str]
100
+ """
101
+ A list of locale codes supported by the database.
102
+ """
103
+
104
+ node_count: int
105
+ """
106
+ The number of nodes in the database.
107
+ """
108
+
109
+ record_size: int
110
+ """
111
+ The bit size of a record in the search tree.
112
+ """
113
+
114
+ def __init__(self, **kwargs: Any) -> None: # noqa: ANN401
115
+ """Create new Metadata object. kwargs are key/value pairs from spec."""
maxminddb/file.py ADDED
@@ -0,0 +1,74 @@
1
+ """For internal use only. It provides a slice-like file reader."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import overload
7
+
8
+ try:
9
+ from multiprocessing import Lock
10
+ except ImportError:
11
+ from threading import Lock # type: ignore[assignment]
12
+
13
+
14
+ class FileBuffer:
15
+ """A slice-able file reader."""
16
+
17
+ def __init__(self, database: str) -> None:
18
+ """Create FileBuffer."""
19
+ self._handle = open(database, "rb") # noqa: SIM115
20
+ self._size = os.fstat(self._handle.fileno()).st_size
21
+ if not hasattr(os, "pread"):
22
+ self._lock = Lock()
23
+
24
+ @overload
25
+ def __getitem__(self, index: int) -> int: ...
26
+
27
+ @overload
28
+ def __getitem__(self, index: slice) -> bytes: ...
29
+
30
+ def __getitem__(self, index: slice | int) -> bytes | int:
31
+ """Get item by index."""
32
+ if isinstance(index, slice):
33
+ return self._read(index.stop - index.start, index.start)
34
+ if isinstance(index, int):
35
+ return self._read(1, index)[0]
36
+ msg = "Invalid argument type."
37
+ raise TypeError(msg)
38
+
39
+ def rfind(self, needle: bytes, start: int) -> int:
40
+ """Reverse find needle from start."""
41
+ pos = self._read(self._size - start - 1, start).rfind(needle)
42
+ if pos == -1:
43
+ return pos
44
+ return start + pos
45
+
46
+ def size(self) -> int:
47
+ """Size of file."""
48
+ return self._size
49
+
50
+ def close(self) -> None:
51
+ """Close file."""
52
+ self._handle.close()
53
+
54
+ if hasattr(os, "pread"): # type: ignore[attr-defined]
55
+
56
+ def _read(self, buffersize: int, offset: int) -> bytes:
57
+ """Read that uses pread."""
58
+ return os.pread(self._handle.fileno(), buffersize, offset) # type: ignore[attr-defined]
59
+
60
+ else:
61
+
62
+ def _read(self, buffersize: int, offset: int) -> bytes:
63
+ """Read with a lock.
64
+
65
+ This lock is necessary as after a fork, the different processes
66
+ will share the same file table entry, even if we dup the fd, and
67
+ as such the same offsets. There does not appear to be a way to
68
+ duplicate the file table entry and we cannot re-open based on the
69
+ original path as that file may have replaced with another or
70
+ unlinked.
71
+ """
72
+ with self._lock:
73
+ self._handle.seek(offset)
74
+ return self._handle.read(buffersize)
maxminddb/py.typed ADDED
File without changes
maxminddb/reader.py ADDED
@@ -0,0 +1,380 @@
1
+ """Pure-Python reader for the MaxMind DB file format."""
2
+
3
+ from __future__ import annotations
4
+
5
+ try:
6
+ import mmap
7
+ except ImportError:
8
+ mmap = None # type: ignore[assignment]
9
+
10
+ import contextlib
11
+ import ipaddress
12
+ import struct
13
+ from ipaddress import IPv4Address, IPv6Address
14
+ from typing import IO, TYPE_CHECKING, Any, AnyStr
15
+
16
+ from maxminddb.const import MODE_AUTO, MODE_FD, MODE_FILE, MODE_MEMORY, MODE_MMAP
17
+ from maxminddb.decoder import Decoder
18
+ from maxminddb.errors import InvalidDatabaseError
19
+ from maxminddb.file import FileBuffer
20
+
21
+ if TYPE_CHECKING:
22
+ from collections.abc import Iterator
23
+ from os import PathLike
24
+
25
+ from typing_extensions import Self
26
+
27
+ from maxminddb.types import Record
28
+
29
+ _IPV4_MAX_NUM = 2**32
30
+
31
+
32
+ class Reader:
33
+ """A pure Python implementation of a reader for the MaxMind DB format.
34
+
35
+ IP addresses can be looked up using the ``get`` method.
36
+ """
37
+
38
+ _DATA_SECTION_SEPARATOR_SIZE = 16
39
+ _METADATA_START_MARKER = b"\xab\xcd\xefMaxMind.com"
40
+
41
+ _buffer: bytes | FileBuffer | "mmap.mmap" # noqa: UP037
42
+ _buffer_size: int
43
+ closed: bool
44
+ _decoder: Decoder
45
+ _metadata: Metadata
46
+ _ipv4_start: int
47
+
48
+ def __init__(
49
+ self,
50
+ database: AnyStr | int | PathLike | IO,
51
+ mode: int = MODE_AUTO,
52
+ ) -> None:
53
+ """Reader for the MaxMind DB file format.
54
+
55
+ Arguments:
56
+ database: A path to a valid MaxMind DB file such as a GeoIP2 database
57
+ file, or a file descriptor in the case of MODE_FD.
58
+ mode: mode to open the database with. Valid mode are:
59
+ * MODE_MMAP - read from memory map.
60
+ * MODE_FILE - read database as standard file.
61
+ * MODE_MEMORY - load database into memory.
62
+ * MODE_AUTO - tries MODE_MMAP and then MODE_FILE. Default.
63
+ * MODE_FD - the param passed via database is a file descriptor, not
64
+ a path. This mode implies MODE_MEMORY.
65
+
66
+ """
67
+ filename: Any
68
+ if (mode == MODE_AUTO and mmap) or mode == MODE_MMAP:
69
+ with open(database, "rb") as db_file: # type: ignore[arg-type]
70
+ self._buffer = mmap.mmap(db_file.fileno(), 0, access=mmap.ACCESS_READ)
71
+ self._buffer_size = self._buffer.size()
72
+ filename = database
73
+ elif mode in (MODE_AUTO, MODE_FILE):
74
+ self._buffer = FileBuffer(database) # type: ignore[arg-type]
75
+ self._buffer_size = self._buffer.size()
76
+ filename = database
77
+ elif mode == MODE_MEMORY:
78
+ with open(database, "rb") as db_file: # type: ignore[arg-type]
79
+ buf = db_file.read()
80
+ self._buffer = buf
81
+ self._buffer_size = len(buf)
82
+ filename = database
83
+ elif mode == MODE_FD:
84
+ self._buffer = database.read() # type: ignore[union-attr]
85
+ self._buffer_size = len(self._buffer) # type: ignore[arg-type]
86
+ filename = database.name # type: ignore[union-attr]
87
+ else:
88
+ msg = (
89
+ f"Unsupported open mode ({mode}). Only MODE_AUTO, MODE_FILE, "
90
+ "MODE_MEMORY and MODE_FD are supported by the pure Python "
91
+ "Reader"
92
+ )
93
+ raise ValueError(
94
+ msg,
95
+ )
96
+
97
+ metadata_start = self._buffer.rfind(
98
+ self._METADATA_START_MARKER,
99
+ max(0, self._buffer_size - 128 * 1024),
100
+ )
101
+
102
+ if metadata_start == -1:
103
+ self.close()
104
+ msg = (
105
+ f"Error opening database file ({filename}). "
106
+ "Is this a valid MaxMind DB file?"
107
+ )
108
+ raise InvalidDatabaseError(
109
+ msg,
110
+ )
111
+
112
+ metadata_start += len(self._METADATA_START_MARKER)
113
+ metadata_decoder = Decoder(self._buffer, metadata_start)
114
+ (metadata, _) = metadata_decoder.decode(metadata_start)
115
+
116
+ if not isinstance(metadata, dict):
117
+ msg = f"Error reading metadata in database file ({filename})."
118
+ raise InvalidDatabaseError(
119
+ msg,
120
+ )
121
+
122
+ self._metadata = Metadata(**metadata)
123
+
124
+ self._decoder = Decoder(
125
+ self._buffer,
126
+ self._metadata.search_tree_size + self._DATA_SECTION_SEPARATOR_SIZE,
127
+ )
128
+ self.closed = False
129
+
130
+ ipv4_start = 0
131
+ if self._metadata.ip_version == 6:
132
+ # We store the IPv4 starting node as an optimization for IPv4 lookups
133
+ # in IPv6 trees. This allows us to skip over the first 96 nodes in
134
+ # this case.
135
+ node = 0
136
+ for _ in range(96):
137
+ if node >= self._metadata.node_count:
138
+ break
139
+ node = self._read_node(node, 0)
140
+ ipv4_start = node
141
+ self._ipv4_start = ipv4_start
142
+
143
+ def metadata(self) -> Metadata:
144
+ """Return the metadata associated with the MaxMind DB file."""
145
+ return self._metadata
146
+
147
+ def get(self, ip_address: str | IPv6Address | IPv4Address) -> Record | None:
148
+ """Return the record for the ip_address in the MaxMind DB.
149
+
150
+ Arguments:
151
+ ip_address: an IP address in the standard string notation
152
+
153
+ """
154
+ (record, _) = self.get_with_prefix_len(ip_address)
155
+ return record
156
+
157
+ def get_with_prefix_len(
158
+ self,
159
+ ip_address: str | IPv6Address | IPv4Address,
160
+ ) -> tuple[Record | None, int]:
161
+ """Return a tuple with the record and the associated prefix length.
162
+
163
+ Arguments:
164
+ ip_address: an IP address in the standard string notation
165
+
166
+ """
167
+ if isinstance(ip_address, str):
168
+ address = ipaddress.ip_address(ip_address)
169
+ else:
170
+ address = ip_address
171
+
172
+ try:
173
+ packed_address = bytearray(address.packed)
174
+ except AttributeError as ex:
175
+ msg = "argument 1 must be a string or ipaddress object"
176
+ raise TypeError(msg) from ex
177
+
178
+ if address.version == 6 and self._metadata.ip_version == 4:
179
+ msg = (
180
+ f"Error looking up {ip_address}. You attempted to look up "
181
+ "an IPv6 address in an IPv4-only database."
182
+ )
183
+ raise ValueError(
184
+ msg,
185
+ )
186
+
187
+ (pointer, prefix_len) = self._find_address_in_tree(packed_address)
188
+
189
+ if pointer:
190
+ return self._resolve_data_pointer(pointer), prefix_len
191
+ return None, prefix_len
192
+
193
+ def __iter__(self) -> Iterator:
194
+ return self._generate_children(0, 0, 0)
195
+
196
+ def _generate_children(self, node: int, depth: int, ip_acc: int) -> Iterator:
197
+ if ip_acc != 0 and node == self._ipv4_start:
198
+ # Skip nodes aliased to IPv4
199
+ return
200
+
201
+ node_count = self._metadata.node_count
202
+ if node > node_count:
203
+ bits = 128 if self._metadata.ip_version == 6 else 32
204
+ ip_acc <<= bits - depth
205
+ if ip_acc <= _IPV4_MAX_NUM and bits == 128:
206
+ depth -= 96
207
+ yield (
208
+ ipaddress.ip_network((ip_acc, depth)),
209
+ self._resolve_data_pointer(
210
+ node,
211
+ ),
212
+ )
213
+ elif node < node_count:
214
+ left = self._read_node(node, 0)
215
+ ip_acc <<= 1
216
+ depth += 1
217
+ yield from self._generate_children(left, depth, ip_acc)
218
+ right = self._read_node(node, 1)
219
+ yield from self._generate_children(right, depth, ip_acc | 1)
220
+
221
+ def _find_address_in_tree(self, packed: bytearray) -> tuple[int, int]:
222
+ bit_count = len(packed) * 8
223
+ node = self._start_node(bit_count)
224
+ node_count = self._metadata.node_count
225
+
226
+ i = 0
227
+ while i < bit_count and node < node_count:
228
+ bit = 1 & (packed[i >> 3] >> 7 - (i % 8))
229
+ node = self._read_node(node, bit)
230
+ i = i + 1
231
+
232
+ if node == node_count:
233
+ # Record is empty
234
+ return 0, i
235
+ if node > node_count:
236
+ return node, i
237
+
238
+ msg = "Invalid node in search tree"
239
+ raise InvalidDatabaseError(msg)
240
+
241
+ def _start_node(self, length: int) -> int:
242
+ if self._metadata.ip_version == 6 and length == 32:
243
+ return self._ipv4_start
244
+ return 0
245
+
246
+ def _read_node(self, node_number: int, index: int) -> int:
247
+ base_offset = node_number * self._metadata.node_byte_size
248
+
249
+ record_size = self._metadata.record_size
250
+ if record_size == 24:
251
+ offset = base_offset + index * 3
252
+ node_bytes = b"\x00" + self._buffer[offset : offset + 3]
253
+ elif record_size == 28:
254
+ offset = base_offset + 3 * index
255
+ node_bytes = bytearray(self._buffer[offset : offset + 4])
256
+ if index:
257
+ node_bytes[0] = 0x0F & node_bytes[0]
258
+ else:
259
+ middle = (0xF0 & node_bytes.pop()) >> 4
260
+ node_bytes.insert(0, middle)
261
+ elif record_size == 32:
262
+ offset = base_offset + index * 4
263
+ node_bytes = self._buffer[offset : offset + 4]
264
+ else:
265
+ msg = f"Unknown record size: {record_size}"
266
+ raise InvalidDatabaseError(msg)
267
+ return struct.unpack(b"!I", node_bytes)[0]
268
+
269
+ def _resolve_data_pointer(self, pointer: int) -> Record:
270
+ resolved = pointer - self._metadata.node_count + self._metadata.search_tree_size
271
+
272
+ if resolved >= self._buffer_size:
273
+ msg = "The MaxMind DB file's search tree is corrupt"
274
+ raise InvalidDatabaseError(msg)
275
+
276
+ (data, _) = self._decoder.decode(resolved)
277
+ return data
278
+
279
+ def close(self) -> None:
280
+ """Close the MaxMind DB file and returns the resources to the system."""
281
+ with contextlib.suppress(AttributeError):
282
+ self._buffer.close() # type: ignore[union-attr]
283
+
284
+ self.closed = True
285
+
286
+ def __exit__(self, *_) -> None: # noqa: ANN002
287
+ self.close()
288
+
289
+ def __enter__(self) -> Self:
290
+ if self.closed:
291
+ msg = "Attempt to reopen a closed MaxMind DB"
292
+ raise ValueError(msg)
293
+ return self
294
+
295
+
296
+ class Metadata:
297
+ """Metadata for the MaxMind DB reader."""
298
+
299
+ binary_format_major_version: int
300
+ """
301
+ The major version number of the binary format used when creating the
302
+ database.
303
+ """
304
+
305
+ binary_format_minor_version: int
306
+ """
307
+ The minor version number of the binary format used when creating the
308
+ database.
309
+ """
310
+
311
+ build_epoch: int
312
+ """
313
+ The Unix epoch for the build time of the database.
314
+ """
315
+
316
+ database_type: str
317
+ """
318
+ A string identifying the database type, e.g., "GeoIP2-City".
319
+ """
320
+
321
+ description: dict[str, str]
322
+ """
323
+ A map from locales to text descriptions of the database.
324
+ """
325
+
326
+ ip_version: int
327
+ """
328
+ The IP version of the data in a database. A value of "4" means the
329
+ database only supports IPv4. A database with a value of "6" may support
330
+ both IPv4 and IPv6 lookups.
331
+ """
332
+
333
+ languages: list[str]
334
+ """
335
+ A list of locale codes supported by the database.
336
+ """
337
+
338
+ node_count: int
339
+ """
340
+ The number of nodes in the database.
341
+ """
342
+
343
+ record_size: int
344
+ """
345
+ The bit size of a record in the search tree.
346
+ """
347
+
348
+ def __init__(self, **kwargs) -> None:
349
+ """Create new Metadata object. kwargs are key/value pairs from spec."""
350
+ # Although I could just update __dict__, that is less obvious and it
351
+ # doesn't work well with static analysis tools and some IDEs
352
+ self.node_count = kwargs["node_count"]
353
+ self.record_size = kwargs["record_size"]
354
+ self.ip_version = kwargs["ip_version"]
355
+ self.database_type = kwargs["database_type"]
356
+ self.languages = kwargs["languages"]
357
+ self.binary_format_major_version = kwargs["binary_format_major_version"]
358
+ self.binary_format_minor_version = kwargs["binary_format_minor_version"]
359
+ self.build_epoch = kwargs["build_epoch"]
360
+ self.description = kwargs["description"]
361
+
362
+ @property
363
+ def node_byte_size(self) -> int:
364
+ """The size of a node in bytes.
365
+
366
+ :type: int
367
+ """
368
+ return self.record_size // 4
369
+
370
+ @property
371
+ def search_tree_size(self) -> int:
372
+ """The size of the search tree.
373
+
374
+ :type: int
375
+ """
376
+ return self.node_count * self.node_byte_size
377
+
378
+ def __repr__(self) -> str:
379
+ args = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
380
+ return f"{self.__module__}.{self.__class__.__name__}({args})"
maxminddb/types.py ADDED
@@ -0,0 +1,14 @@
1
+ """Types representing database records."""
2
+
3
+ from typing import AnyStr, Union
4
+
5
+ Primitive = Union[AnyStr, bool, float, int]
6
+ Record = Union[Primitive, "RecordList", "RecordDict"]
7
+
8
+
9
+ class RecordList(list[Record]):
10
+ """RecordList is a type for lists in a database record."""
11
+
12
+
13
+ class RecordDict(dict[str, Record]):
14
+ """RecordDict is a type for dicts in a database record."""
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: maxminddb
3
+ Version: 2.8.2
4
+ Summary: Reader for the MaxMind DB format
5
+ Author-email: Gregory Oschwald <goschwald@maxmind.com>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://www.maxmind.com/
8
+ Project-URL: Documentation, https://maxminddb.readthedocs.org/
9
+ Project-URL: Source Code, https://github.com/maxmind/MaxMind-DB-Reader-python
10
+ Project-URL: Issue Tracker, https://github.com/maxmind/MaxMind-DB-Reader-python/issues
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Environment :: Web Environment
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Internet
24
+ Classifier: Topic :: Internet :: Proxy Servers
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/x-rst
27
+ License-File: LICENSE
28
+ Dynamic: license-file
29
+
30
+ ========================
31
+ MaxMind DB Python Module
32
+ ========================
33
+
34
+ Description
35
+ -----------
36
+
37
+ This is a Python module for reading MaxMind DB files. The module includes both
38
+ a pure Python reader and an optional C extension.
39
+
40
+ MaxMind DB is a binary file format that stores data indexed by IP address
41
+ subnets (IPv4 or IPv6).
42
+
43
+ Installation
44
+ ------------
45
+
46
+ To install ``maxminddb``, type:
47
+
48
+ .. code-block:: bash
49
+
50
+ $ pip install maxminddb
51
+
52
+ If you are not able to install from PyPI, you may also use ``pip`` from the
53
+ source directory:
54
+
55
+ .. code-block:: bash
56
+
57
+ $ python -m pip install .
58
+
59
+ The installer will attempt to build the C extension. If this fails, the
60
+ module will fall-back to the pure Python implementation.
61
+
62
+ Usage
63
+ -----
64
+
65
+ To use this module, you must first download or create a MaxMind DB file. We
66
+ provide `free GeoLite2 databases
67
+ <https://dev.maxmind.com/geoip/geolocate-an-ip/databases?lang=en>`_. These
68
+ files must be decompressed with ``gunzip``.
69
+
70
+ After you have obtained a database and imported the module, call
71
+ ``open_database`` with a path, or file descriptor (in the case of ``MODE_FD``),
72
+ to the database as the first argument. Optionally, you may pass a mode as the
73
+ second argument. The modes are exported from ``maxminddb``. Valid modes are:
74
+
75
+ * ``MODE_MMAP_EXT`` - use the C extension with memory map.
76
+ * ``MODE_MMAP`` - read from memory map. Pure Python.
77
+ * ``MODE_FILE`` - read database as standard file. Pure Python.
78
+ * ``MODE_MEMORY`` - load database into memory. Pure Python.
79
+ * ``MODE_FD`` - load database into memory from a file descriptor. Pure Python.
80
+ * ``MODE_AUTO`` - try ``MODE_MMAP_EXT``, ``MODE_MMAP``, ``MODE_FILE`` in that
81
+ order. Default.
82
+
83
+ **NOTE**: When using ``MODE_FD``, it is the *caller's* responsibility to be
84
+ sure that the file descriptor gets closed properly. The caller may close the
85
+ file descriptor immediately after the ``Reader`` object is created.
86
+
87
+ The ``open_database`` function returns a ``Reader`` object. To look up an IP
88
+ address, use the ``get`` method on this object. The method will return the
89
+ corresponding values for the IP address from the database (e.g., a dictionary
90
+ for GeoIP2/GeoLite2 databases). If the database does not contain a record for
91
+ that IP address, the method will return ``None``.
92
+
93
+ If you wish to also retrieve the prefix length for the record, use the
94
+ ``get_with_prefix_len`` method. This returns a tuple containing the record
95
+ followed by the network prefix length associated with the record.
96
+
97
+ You may also iterate over the whole database. The ``Reader`` class implements
98
+ the ``__iter__`` method that returns an iterator. This iterator yields a
99
+ tuple containing the network and the record.
100
+
101
+ Example
102
+ -------
103
+
104
+ .. code-block:: pycon
105
+
106
+ >>> import maxminddb
107
+ >>>
108
+ >>> with maxminddb.open_database('GeoLite2-City.mmdb') as reader:
109
+ >>>
110
+ >>> reader.get('152.216.7.110')
111
+ {'country': ... }
112
+ >>>
113
+ >>> reader.get_with_prefix_len('152.216.7.110')
114
+ ({'country': ... }, 24)
115
+ >>>
116
+ >>> for network, record in reader:
117
+ >>> ...
118
+
119
+ Exceptions
120
+ ----------
121
+
122
+ The module will return an ``InvalidDatabaseError`` if the database is corrupt
123
+ or otherwise invalid. A ``ValueError`` will be thrown if you look up an
124
+ invalid IP address or an IPv6 address in an IPv4 database.
125
+
126
+ Requirements
127
+ ------------
128
+
129
+ This code requires Python 3.9+. Older versions are not supported. The C
130
+ extension requires CPython.
131
+
132
+ Versioning
133
+ ----------
134
+
135
+ The MaxMind DB Python module uses `Semantic Versioning <https://semver.org/>`_.
136
+
137
+ Support
138
+ -------
139
+
140
+ Please report all issues with this code using the `GitHub issue tracker
141
+ <https://github.com/maxmind/MaxMind-DB-Reader-python/issues>`_
142
+
143
+ If you are having an issue with a MaxMind service that is not specific to this
144
+ API, please contact `MaxMind support <https://www.maxmind.com/en/support>`_ for
145
+ assistance.
@@ -0,0 +1,15 @@
1
+ maxminddb/__init__.py,sha256=ESNJkA5VoLZsDCHGkU7Rhe7-ulJLxvtFbSV59XjYU8o,2656
2
+ maxminddb/const.py,sha256=X8W_Zic36qfZRRxmZO2GmBUdWGMHLDqXh0d2MfpaYiM,123
3
+ maxminddb/decoder.py,sha256=ljoIBp8V1gFcSweb9c_5H1_eYh04vwUghFVDpa303bw,7034
4
+ maxminddb/errors.py,sha256=RCKtM1525SQgSY4ncztf8tXveQJX4nJ0VzA4uaIARes,160
5
+ maxminddb/extension.cpython-313-aarch64-linux-android.so,sha256=LzR-Oa3jANl2j0Mi1o5sxkNqATsH8gwIucEm_wIVT8c,47288
6
+ maxminddb/extension.pyi,sha256=0QOwgEK48_6XX1sdFzeUWUGJF9ct7m30PRSWTbFG5Zs,3056
7
+ maxminddb/file.py,sha256=8jP0yM5q6FSpRkUw8U1nNJUcpJYnYpgUCNUgvP6xqCk,2401
8
+ maxminddb/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ maxminddb/reader.py,sha256=-d7JeTgCnn6BW4JT99MVJXZ89s80QJf_YFmjEb5h-g4,12566
10
+ maxminddb/types.py,sha256=EsiHjODJHMBdfZ3UwWE9BuN2wxM7tKCnh1RdTmkkV_s,375
11
+ maxminddb-2.8.2.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
12
+ maxminddb-2.8.2.dist-info/METADATA,sha256=1xCaVJaDkqcDvVUvJgu9dDEQHXIgfwTAiGEc2uiNuHw,5063
13
+ maxminddb-2.8.2.dist-info/WHEEL,sha256=5I58AbR9O_ArnZQueeIBJrrQjT_lZqa3wHwMOp-EJ_E,112
14
+ maxminddb-2.8.2.dist-info/top_level.txt,sha256=AKInnuDXa1wn83qmLWp8u5KzNupVHFWR8eSuSgsjTpc,10
15
+ maxminddb-2.8.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: false
4
+ Tag: cp313-cp313-android_21_arm64_v8a
5
+
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
@@ -0,0 +1 @@
1
+ maxminddb