mmdb-writer 0.1.1__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 VimT
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.1
2
+ Name: mmdb-writer
3
+ Version: 0.1.1
4
+ Summary: Make mmdb format ip library file which can be read by maxmind official language reader
5
+ Home-page: https://github.com/vimt/MaxMind-DB-Writer-python
6
+ License: MIT
7
+ Author: VimT
8
+ Requires-Python: >=3.7,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.7
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Requires-Dist: maxminddb (>=2.0,<3.0)
17
+ Requires-Dist: netaddr (>=0.7.0,<1.0)
18
+ Description-Content-Type: text/markdown
19
+
20
+ # MaxMind-DB-Writer-python
21
+
22
+ Make `mmdb` format ip library file which can be read by [`maxmind` official language reader](https://dev.maxmind.com/geoip/geoip2/downloadable/)
23
+
24
+ [The official perl writer](https://github.com/maxmind/MaxMind-DB-Writer-perl) was written in perl, which was difficult to customize. So I implemented the `MaxmindDB format` ip library in python language
25
+ ## Install
26
+ ```shell script
27
+ pip install -U git+https://github.com/VimT/MaxMind-DB-Writer-python
28
+ ```
29
+
30
+ ## Usage
31
+ ```python
32
+ from netaddr import IPSet
33
+
34
+ from mmdb_writer import MMDBWriter
35
+ writer = MMDBWriter()
36
+
37
+ writer.insert_network(IPSet(['1.1.0.0/24', '1.1.1.0/24']), {'country': 'COUNTRY', 'isp': 'ISP'})
38
+ writer.to_db_file('test.mmdb')
39
+
40
+ import maxminddb
41
+ m = maxminddb.open_database('test.mmdb')
42
+ r = m.get('1.1.1.1')
43
+ assert r == {'country': 'COUNTRY', 'isp': 'ISP'}
44
+ ```
45
+
46
+ ## Examples
47
+ see [csv_to_mmdb.py](./examples/csv_to_mmdb.py)
48
+
49
+
50
+ ## Reference:
51
+ - [MaxmindDB format](http://maxmind.github.io/MaxMind-DB/)
52
+ - [geoip-mmdb](https://github.com/i-rinat/geoip-mmdb)
53
+
@@ -0,0 +1,33 @@
1
+ # MaxMind-DB-Writer-python
2
+
3
+ Make `mmdb` format ip library file which can be read by [`maxmind` official language reader](https://dev.maxmind.com/geoip/geoip2/downloadable/)
4
+
5
+ [The official perl writer](https://github.com/maxmind/MaxMind-DB-Writer-perl) was written in perl, which was difficult to customize. So I implemented the `MaxmindDB format` ip library in python language
6
+ ## Install
7
+ ```shell script
8
+ pip install -U git+https://github.com/VimT/MaxMind-DB-Writer-python
9
+ ```
10
+
11
+ ## Usage
12
+ ```python
13
+ from netaddr import IPSet
14
+
15
+ from mmdb_writer import MMDBWriter
16
+ writer = MMDBWriter()
17
+
18
+ writer.insert_network(IPSet(['1.1.0.0/24', '1.1.1.0/24']), {'country': 'COUNTRY', 'isp': 'ISP'})
19
+ writer.to_db_file('test.mmdb')
20
+
21
+ import maxminddb
22
+ m = maxminddb.open_database('test.mmdb')
23
+ r = m.get('1.1.1.1')
24
+ assert r == {'country': 'COUNTRY', 'isp': 'ISP'}
25
+ ```
26
+
27
+ ## Examples
28
+ see [csv_to_mmdb.py](./examples/csv_to_mmdb.py)
29
+
30
+
31
+ ## Reference:
32
+ - [MaxmindDB format](http://maxmind.github.io/MaxMind-DB/)
33
+ - [geoip-mmdb](https://github.com/i-rinat/geoip-mmdb)
@@ -0,0 +1,460 @@
1
+ # coding: utf-8
2
+ __version__ = '0.1.1'
3
+
4
+ import logging
5
+ import math
6
+ import struct
7
+ import time
8
+ from typing import Union
9
+
10
+ from netaddr import IPSet, IPNetwork
11
+
12
+ MMDBType = Union[dict, list, str, bytes, int, bool]
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ METADATA_MAGIC = b'\xab\xcd\xefMaxMind.com'
17
+
18
+
19
+ class SearchTreeNode(object):
20
+ def __init__(self, left=None, right=None):
21
+ self.left = left
22
+ self.right = right
23
+
24
+ def get_or_create(self, item):
25
+ if item == 0:
26
+ self.left = self.left or SearchTreeNode()
27
+ return self.left
28
+ elif item == 1:
29
+ self.right = self.right or SearchTreeNode()
30
+ return self.right
31
+
32
+ def __getitem__(self, item):
33
+ if item == 0:
34
+ return self.left
35
+ elif item == 1:
36
+ return self.right
37
+
38
+ def __setitem__(self, key, value):
39
+ if key == 0:
40
+ self.left = value
41
+ elif key == 1:
42
+ self.right = value
43
+
44
+
45
+ class SearchTreeLeaf(object):
46
+ def __init__(self, value):
47
+ self.value = value
48
+
49
+ def __repr__(self):
50
+ return "SearchTreeLeaf(value={value})".format(value=self.value)
51
+
52
+ __str__ = __repr__
53
+
54
+
55
+ class Encoder(object):
56
+
57
+ def __init__(self, cache=True):
58
+ self.data_cache = {}
59
+ self.data_list = []
60
+ self.data_pointer = 0
61
+
62
+ self.cache = cache
63
+
64
+ def _encode_pointer(self, value):
65
+ pointer = value
66
+ if pointer >= 134744064:
67
+ res = struct.pack('>BI', 0x38, pointer)
68
+ elif pointer >= 526336:
69
+ pointer -= 526336
70
+ res = struct.pack('>BBBB', 0x30 + ((pointer >> 24) & 0x07),
71
+ (pointer >> 16) & 0xff, (pointer >> 8) & 0xff,
72
+ pointer & 0xff)
73
+ elif pointer >= 2048:
74
+ pointer -= 2048
75
+ res = struct.pack('>BBB', 0x28 + ((pointer >> 16) & 0x07),
76
+ (pointer >> 8) & 0xff, pointer & 0xff)
77
+ else:
78
+ res = struct.pack('>BB', 0x20 + ((pointer >> 8) & 0x07),
79
+ pointer & 0xff)
80
+
81
+ return res
82
+
83
+ def _encode_utf8_string(self, value):
84
+ encoded_value = value.encode('utf-8')
85
+ res = self._make_header(2, len(encoded_value))
86
+ res += encoded_value
87
+ return res
88
+
89
+ def _encode_bytes(self, value):
90
+ return self._make_header(4, len(value)) + value
91
+
92
+ def _encode_uint(self, type_id, max_len):
93
+ def _encode_unsigned_value(value):
94
+ res = b''
95
+ while value != 0 and len(res) < max_len:
96
+ res = struct.pack('>B', value & 0xff) + res
97
+ value = value >> 8
98
+ return self._make_header(type_id, len(res)) + res
99
+
100
+ return _encode_unsigned_value
101
+
102
+ def _encode_map(self, value):
103
+ res = self._make_header(7, len(value))
104
+ for k, v in list(value.items()):
105
+ # Keys are always stored by value.
106
+ res += self.encode(k)
107
+ res += self.encode(v)
108
+ return res
109
+
110
+ def _encode_array(self, value):
111
+ res = self._make_header(11, len(value))
112
+ for k in value:
113
+ res += self.encode(k)
114
+ return res
115
+
116
+ def _encode_boolean(self, value):
117
+ return self._make_header(14, 1 if value else 0)
118
+
119
+ def _encode_pack_type(self, type_id, fmt):
120
+ def pack_type(value):
121
+ res = struct.pack(fmt, value)
122
+ return self._make_header(type_id, len(res)) + res
123
+
124
+ return pack_type
125
+
126
+ _type_decoder = None
127
+
128
+ @property
129
+ def type_decoder(self):
130
+ if self._type_decoder is None:
131
+ self._type_decoder = {
132
+ 1: self._encode_pointer,
133
+ 2: self._encode_utf8_string,
134
+ 3: self._encode_pack_type(3, '>d'), # double,
135
+ 4: self._encode_bytes,
136
+ 5: self._encode_uint(5, 2), # uint16
137
+ 6: self._encode_uint(6, 4), # uint32
138
+ 7: self._encode_map,
139
+ 8: self._encode_pack_type(8, '>i'), # int32
140
+ 9: self._encode_uint(9, 8), # uint64
141
+ 10: self._encode_uint(10, 16), # uint128
142
+ 11: self._encode_array,
143
+ 14: self._encode_boolean,
144
+ 15: self._encode_pack_type(15, '>f'), # float,
145
+ }
146
+ return self._type_decoder
147
+
148
+ def _make_header(self, type_id, length):
149
+ if length >= 16843036:
150
+ raise Exception('length >= 16843036')
151
+
152
+ elif length >= 65821:
153
+ five_bits = 31
154
+ length -= 65821
155
+ b3 = length & 0xff
156
+ b2 = (length >> 8) & 0xff
157
+ b1 = (length >> 16) & 0xff
158
+ additional_length_bytes = struct.pack('>BBB', b1, b2, b3)
159
+
160
+ elif length >= 285:
161
+ five_bits = 30
162
+ length -= 285
163
+ b2 = length & 0xff
164
+ b1 = (length >> 8) & 0xff
165
+ additional_length_bytes = struct.pack('>BB', b1, b2)
166
+
167
+ elif length >= 29:
168
+ five_bits = 29
169
+ length -= 29
170
+ additional_length_bytes = struct.pack('>B', length & 0xff)
171
+
172
+ else:
173
+ five_bits = length
174
+ additional_length_bytes = b''
175
+
176
+ if type_id <= 7:
177
+ res = struct.pack('>B', (type_id << 5) + five_bits)
178
+ else:
179
+ res = struct.pack('>BB', five_bits, type_id - 7)
180
+
181
+ return res + additional_length_bytes
182
+
183
+ _python_type_id = {
184
+ float: 15,
185
+ bool: 14,
186
+ list: 11,
187
+ dict: 7,
188
+ bytes: 4,
189
+ str: 2
190
+ }
191
+
192
+ def python_type_id(self, value):
193
+ value_type = type(value)
194
+ type_id = self._python_type_id.get(value_type)
195
+ if type_id:
196
+ return type_id
197
+ if value_type is int:
198
+ if value > 0xffffffffffffffff:
199
+ return 10
200
+ elif value > 0xffffffff:
201
+ return 9
202
+ elif value > 0xffff:
203
+ return 6
204
+ elif value < 0:
205
+ return 8
206
+ else:
207
+ return 5
208
+ raise TypeError("unknown type {value_type}".format(value_type=value_type))
209
+
210
+ def encode_meta(self, meta):
211
+ res = self._make_header(7, len(meta))
212
+ meta_type = {'node_count': 6, 'record_size': 5, 'ip_version': 5,
213
+ 'binary_format_major_version': 5, 'binary_format_minor_version': 5,
214
+ 'build_epoch': 9}
215
+ for k, v in list(meta.items()):
216
+ # Keys are always stored by value.
217
+ res += self.encode(k)
218
+ res += self.encode(v, meta_type.get(k))
219
+ return res
220
+
221
+ def encode(self, value, type_id=None):
222
+ if self.cache:
223
+ try:
224
+ return self.data_cache[id(value)]
225
+ except KeyError:
226
+ pass
227
+
228
+ if not type_id:
229
+ type_id = self.python_type_id(value)
230
+
231
+ try:
232
+ encoder = self.type_decoder[type_id]
233
+ except KeyError:
234
+ raise ValueError("unknown type_id={type_id}".format(type_id=type_id))
235
+ res = encoder(value)
236
+
237
+ if self.cache:
238
+ # add to cache
239
+ if type_id == 1:
240
+ self.data_list.append(res)
241
+ self.data_pointer += len(res)
242
+ return res
243
+ else:
244
+ self.data_list.append(res)
245
+ pointer_position = self.data_pointer
246
+ self.data_pointer += len(res)
247
+ pointer = self.encode(pointer_position, 1)
248
+ self.data_cache[id(value)] = pointer
249
+ return pointer
250
+ return res
251
+
252
+
253
+ class TreeWriter(object):
254
+ encoder_cls = Encoder
255
+
256
+ def __init__(self, tree, meta):
257
+ self._node_idx = {}
258
+ self._leaf_offset = {}
259
+ self._node_list = []
260
+ self._node_counter = 0
261
+ self._record_size = 0
262
+
263
+ self.tree = tree
264
+ self.meta = meta
265
+
266
+ self.encoder = self.encoder_cls(cache=True)
267
+
268
+ @property
269
+ def _data_list(self):
270
+ return self.encoder.data_list
271
+
272
+ @property
273
+ def _data_pointer(self):
274
+ return self.encoder.data_pointer + 16
275
+
276
+ def _build_meta(self):
277
+ return {
278
+ "node_count": self._node_counter,
279
+ "record_size": self.record_size,
280
+ **self.meta
281
+ }
282
+
283
+ def _adjust_record_size(self):
284
+ # Tree records should be large enough to contain either tree node index
285
+ # or data offset.
286
+ max_id = self._node_counter + self._data_pointer + 1
287
+
288
+ # Estimate required bit count.
289
+ bit_count = int(math.ceil(math.log(max_id, 2)))
290
+ if bit_count <= 24:
291
+ self.record_size = 24
292
+ elif bit_count <= 28:
293
+ self.record_size = 28
294
+ elif bit_count <= 32:
295
+ self.record_size = 32
296
+ else:
297
+ raise Exception('record_size > 32')
298
+
299
+ self.data_offset = self.record_size * 2 / 8 * self._node_counter
300
+
301
+ def _enumerate_nodes(self, node):
302
+ if type(node) is SearchTreeNode:
303
+ node_id = id(node)
304
+ if node_id not in self._node_idx:
305
+ self._node_idx[node_id] = self._node_counter
306
+ self._node_counter += 1
307
+ self._node_list.append(node)
308
+
309
+ self._enumerate_nodes(node.left)
310
+ self._enumerate_nodes(node.right)
311
+
312
+ elif type(node) is SearchTreeLeaf:
313
+ node_id = id(node)
314
+ if node_id not in self._leaf_offset:
315
+ res = self.encoder.encode(node.value)
316
+ self._leaf_offset[node_id] = self._data_pointer - len(res)
317
+ else: # == None
318
+ return
319
+
320
+ def _calc_record_idx(self, node):
321
+ if node is None:
322
+ return self._node_counter
323
+ elif type(node) is SearchTreeNode:
324
+ return self._node_idx[id(node)]
325
+ elif type(node) is SearchTreeLeaf:
326
+ return self._leaf_offset[id(node)] + self._node_counter
327
+ else:
328
+ raise Exception("unexpected type")
329
+
330
+ def _cal_node_bytes(self, node) -> bytes:
331
+ left_idx = self._calc_record_idx(node.left)
332
+ right_idx = self._calc_record_idx(node.right)
333
+
334
+ if self.record_size == 24:
335
+ b1 = (left_idx >> 16) & 0xff
336
+ b2 = (left_idx >> 8) & 0xff
337
+ b3 = left_idx & 0xff
338
+ b4 = (right_idx >> 16) & 0xff
339
+ b5 = (right_idx >> 8) & 0xff
340
+ b6 = right_idx & 0xff
341
+ return struct.pack('>BBBBBB', b1, b2, b3, b4, b5, b6)
342
+
343
+ elif self.record_size == 28:
344
+ b1 = (left_idx >> 16) & 0xff
345
+ b2 = (left_idx >> 8) & 0xff
346
+ b3 = left_idx & 0xff
347
+ b4 = ((left_idx >> 24) & 0xf) * 16 + \
348
+ ((right_idx >> 24) & 0xf)
349
+ b5 = (right_idx >> 16) & 0xff
350
+ b6 = (right_idx >> 8) & 0xff
351
+ b7 = right_idx & 0xff
352
+ return struct.pack('>BBBBBBB', b1, b2, b3, b4, b5, b6, b7)
353
+
354
+ elif self.record_size == 32:
355
+ return struct.pack('>II', left_idx, right_idx)
356
+
357
+ else:
358
+ raise Exception('self.record_size > 32')
359
+
360
+ def write(self, fname):
361
+ self._enumerate_nodes(self.tree)
362
+ self._adjust_record_size()
363
+
364
+ with open(fname, 'wb') as f:
365
+ for node in self._node_list:
366
+ f.write(self._cal_node_bytes(node))
367
+
368
+ f.write(b'\x00' * 16)
369
+
370
+ for element in self._data_list:
371
+ f.write(element)
372
+
373
+ f.write(METADATA_MAGIC)
374
+ f.write(self.encoder_cls(cache=False).encode_meta(self._build_meta()))
375
+
376
+
377
+ def bits_rstrip(n, length=None, keep=0):
378
+ return map(int, bin(n)[2:].rjust(length, '0')[:keep])
379
+
380
+
381
+ class MMDBWriter(object):
382
+
383
+ def __init__(self, ip_version=4, database_type='GeoIP',
384
+ languages=None, description='GeoIP db',
385
+ ipv4_compatible=False):
386
+ self.tree = SearchTreeNode()
387
+ self.ipv4_compatible = ipv4_compatible
388
+
389
+ if languages is None:
390
+ languages = []
391
+ self.description = description
392
+ self.database_type = database_type
393
+ self.ip_version = ip_version
394
+ self.languages = languages
395
+ self.binary_format_major_version = 2
396
+ self.binary_format_minor_version = 0
397
+
398
+ self._bit_length = 128 if ip_version == 6 else 32
399
+
400
+ if ip_version not in [4, 6]:
401
+ raise ValueError("ip_version should be 4 or 6, {} is incorrect".format(ip_version))
402
+ if ip_version == 4 and ipv4_compatible:
403
+ raise ValueError("ipv4_compatible=True can set when ip_version=6")
404
+ if not self.binary_format_major_version:
405
+ raise ValueError("major_version can't be empty or 0: {}".format(self.binary_format_major_version))
406
+ if isinstance(description, str):
407
+ self.description = {i: description for i in languages}
408
+ for i in languages:
409
+ if i not in self.description:
410
+ raise ValueError("language {} must have description!")
411
+
412
+ def insert_network(self, network: IPSet, content: MMDBType):
413
+ leaf = SearchTreeLeaf(content)
414
+ if not isinstance(network, IPSet):
415
+ raise ValueError("network type should be netaddr.IPSet.")
416
+ network = network.iter_cidrs()
417
+ for cidr in network:
418
+ if self.ip_version == 4 and cidr.version == 6:
419
+ raise ValueError('You inserted a IPv6 address {} '
420
+ 'to an IPv4-only database.'.format(cidr))
421
+ if self.ip_version == 6 and cidr.version == 4:
422
+ if not self.ipv4_compatible:
423
+ raise ValueError("You inserted a IPv4 address {} to an IPv6 database."
424
+ "Please use ipv4_compatible=True option store "
425
+ "IPv4 address in IPv6 database as ::/96 format".format(cidr))
426
+ cidr = cidr.ipv6(True)
427
+ node = self.tree
428
+ bits = list(bits_rstrip(cidr.value, self._bit_length, cidr.prefixlen))
429
+ current_node = node
430
+ supernet_leaf = None # Tracks whether we are inserting into a subnet
431
+ for (index, ip_bit) in enumerate(bits[:-1]):
432
+ previous_node = current_node
433
+ current_node = previous_node.get_or_create(ip_bit)
434
+
435
+ if isinstance(current_node, SearchTreeLeaf):
436
+ current_cidr = IPNetwork((int(''.join(map(str, bits[:index + 1])).ljust(self._bit_length, '0'), 2), index + 1))
437
+ logger.info(f"Inserting {cidr} ({content}) into subnet of {current_cidr} ({current_node.value})")
438
+ supernet_leaf = current_node
439
+ current_node = SearchTreeNode()
440
+ previous_node[ip_bit] = current_node
441
+
442
+ if supernet_leaf:
443
+ next_bit = bits[index + 1]
444
+ # Insert supernet information on each inverse bit of the current subnet
445
+ current_node[1 - next_bit] = supernet_leaf
446
+ current_node[bits[-1]] = leaf
447
+
448
+ def to_db_file(self, filename: str):
449
+ return TreeWriter(self.tree, self._build_meta()).write(filename)
450
+
451
+ def _build_meta(self):
452
+ return {
453
+ "ip_version": self.ip_version,
454
+ "database_type": self.database_type,
455
+ "languages": self.languages,
456
+ "binary_format_major_version": self.binary_format_major_version,
457
+ "binary_format_minor_version": self.binary_format_minor_version,
458
+ "build_epoch": int(time.time()),
459
+ "description": self.description,
460
+ }
@@ -0,0 +1,18 @@
1
+ [tool.poetry]
2
+ name = "mmdb-writer"
3
+ version = "0.1.1"
4
+ description = "Make mmdb format ip library file which can be read by maxmind official language reader"
5
+ authors = ["VimT"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+ packages = [{include = "mmdb_writer.py"}]
9
+ homepage = "https://github.com/vimt/MaxMind-DB-Writer-python"
10
+
11
+ [tool.poetry.dependencies]
12
+ python = ">=3.7,<4.0"
13
+ maxminddb = ">=2.0,<3.0"
14
+ netaddr = ">=0.7.0,<1.0"
15
+
16
+ [build-system]
17
+ requires = ["poetry-core"]
18
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,25 @@
1
+ # -*- coding: utf-8 -*-
2
+ from setuptools import setup
3
+
4
+ modules = \
5
+ ['mmdb_writer']
6
+ install_requires = \
7
+ ['maxminddb>=2.0,<3.0', 'netaddr>=0.7.0,<1.0']
8
+
9
+ setup_kwargs = {
10
+ 'name': 'mmdb-writer',
11
+ 'version': '0.1.1',
12
+ 'description': 'Make mmdb format ip library file which can be read by maxmind official language reader',
13
+ 'long_description': "# MaxMind-DB-Writer-python\n\nMake `mmdb` format ip library file which can be read by [`maxmind` official language reader](https://dev.maxmind.com/geoip/geoip2/downloadable/)\n\n[The official perl writer](https://github.com/maxmind/MaxMind-DB-Writer-perl) was written in perl, which was difficult to customize. So I implemented the `MaxmindDB format` ip library in python language\n## Install\n```shell script\npip install -U git+https://github.com/VimT/MaxMind-DB-Writer-python\n```\n\n## Usage\n```python\nfrom netaddr import IPSet\n\nfrom mmdb_writer import MMDBWriter\nwriter = MMDBWriter()\n\nwriter.insert_network(IPSet(['1.1.0.0/24', '1.1.1.0/24']), {'country': 'COUNTRY', 'isp': 'ISP'})\nwriter.to_db_file('test.mmdb')\n\nimport maxminddb\nm = maxminddb.open_database('test.mmdb')\nr = m.get('1.1.1.1')\nassert r == {'country': 'COUNTRY', 'isp': 'ISP'}\n```\n\n## Examples\nsee [csv_to_mmdb.py](./examples/csv_to_mmdb.py)\n\n\n## Reference: \n- [MaxmindDB format](http://maxmind.github.io/MaxMind-DB/)\n- [geoip-mmdb](https://github.com/i-rinat/geoip-mmdb)\n",
14
+ 'author': 'VimT',
15
+ 'author_email': 'None',
16
+ 'maintainer': 'None',
17
+ 'maintainer_email': 'None',
18
+ 'url': 'https://github.com/vimt/MaxMind-DB-Writer-python',
19
+ 'py_modules': modules,
20
+ 'install_requires': install_requires,
21
+ 'python_requires': '>=3.7,<4.0',
22
+ }
23
+
24
+
25
+ setup(**setup_kwargs)