maxminddb-rust 0.2.0__tar.gz → 0.3.0__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.
Files changed (38) hide show
  1. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/.github/workflows/codeql.yml +3 -3
  2. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/.github/workflows/lint.yml +7 -7
  3. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/.github/workflows/release.yml +7 -7
  4. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/.github/workflows/test.yml +3 -3
  5. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/.github/workflows/zizmor.yml +1 -1
  6. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/CHANGELOG.md +16 -0
  7. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/Cargo.lock +32 -25
  8. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/Cargo.toml +3 -3
  9. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/PKG-INFO +29 -3
  10. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/README.md +28 -2
  11. maxminddb_rust-0.3.0/examples/benchmark_path.py +56 -0
  12. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/maxminddb_rust.pyi +27 -1
  13. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/pyproject.toml +1 -1
  14. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/src/lib.rs +124 -1
  15. maxminddb_rust-0.3.0/tests/path_test.py +91 -0
  16. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/uv.lock +1 -1
  17. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/.githooks/pre-commit +0 -0
  18. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/.github/dependabot.yml +0 -0
  19. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/.gitignore +0 -0
  20. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/.gitmodules +0 -0
  21. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/CONTRIBUTING.md +0 -0
  22. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/LICENSE +0 -0
  23. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/MIGRATION.md +0 -0
  24. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/dev-bin/release.sh +0 -0
  25. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/examples/basic_usage.py +0 -0
  26. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/examples/batch_processing.py +0 -0
  27. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/examples/benchmark.py +0 -0
  28. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/examples/benchmark_batch.py +0 -0
  29. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0/examples}/benchmark_comprehensive.py +0 -0
  30. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/examples/context_manager.py +0 -0
  31. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/examples/iterator_demo.py +0 -0
  32. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/precious.toml +0 -0
  33. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/py.typed +0 -0
  34. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/tests/__init__.py +0 -0
  35. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/tests/maxmind/README.md +0 -0
  36. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/tests/maxmind/UPSTREAM_LICENSE +0 -0
  37. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/tests/maxmind/__init__.py +0 -0
  38. {maxminddb_rust-0.2.0 → maxminddb_rust-0.3.0}/tests/maxmind/reader_test.py +0 -0
@@ -21,7 +21,7 @@ jobs:
21
21
 
22
22
  steps:
23
23
  - name: Checkout repository
24
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
24
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
25
25
  with:
26
26
  fetch-depth: 2
27
27
  persist-credentials: false
@@ -31,12 +31,12 @@ jobs:
31
31
  run: git checkout HEAD^2
32
32
 
33
33
  - name: Set up Python
34
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
34
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
35
35
  with:
36
36
  python-version: "3.x"
37
37
 
38
38
  - name: Install Rust
39
- uses: dtolnay/rust-toolchain@0b1efabc08b657293548b77fb76cc02d26091c7e # master
39
+ uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
40
40
  with:
41
41
  toolchain: stable
42
42
 
@@ -13,12 +13,12 @@ jobs:
13
13
  name: Clippy
14
14
  runs-on: ubuntu-latest
15
15
  steps:
16
- - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
16
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
17
17
  with:
18
18
  persist-credentials: false
19
19
 
20
20
  - name: Install Rust
21
- uses: dtolnay/rust-toolchain@0b1efabc08b657293548b77fb76cc02d26091c7e # master
21
+ uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
22
22
  with:
23
23
  toolchain: stable
24
24
  components: clippy
@@ -30,12 +30,12 @@ jobs:
30
30
  name: Rustfmt
31
31
  runs-on: ubuntu-latest
32
32
  steps:
33
- - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
33
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
34
34
  with:
35
35
  persist-credentials: false
36
36
 
37
37
  - name: Install Rust
38
- uses: dtolnay/rust-toolchain@0b1efabc08b657293548b77fb76cc02d26091c7e # master
38
+ uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
39
39
  with:
40
40
  toolchain: stable
41
41
  components: rustfmt
@@ -47,7 +47,7 @@ jobs:
47
47
  name: Ruff
48
48
  runs-on: ubuntu-latest
49
49
  steps:
50
- - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
50
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
51
51
  with:
52
52
  persist-credentials: false
53
53
 
@@ -64,12 +64,12 @@ jobs:
64
64
  name: Prettier
65
65
  runs-on: ubuntu-latest
66
66
  steps:
67
- - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
67
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
68
68
  with:
69
69
  persist-credentials: false
70
70
 
71
71
  - name: Setup Node.js
72
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
72
+ uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
73
73
  with:
74
74
  node-version: "20"
75
75
 
@@ -37,12 +37,12 @@ jobs:
37
37
  python_versions: "3.11 3.12 3.13 3.14"
38
38
 
39
39
  steps:
40
- - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
40
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
41
41
  with:
42
42
  persist-credentials: false
43
43
 
44
44
  - name: Set up Python
45
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
45
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
46
46
  with:
47
47
  python-version: "3.x"
48
48
 
@@ -51,7 +51,7 @@ jobs:
51
51
 
52
52
  - name: Set up Zig
53
53
  if: runner.os == 'Windows'
54
- uses: mlugg/setup-zig@8d6198c65fb0feaa111df26e6b467fea8345e46f # v2.0.5
54
+ uses: mlugg/setup-zig@fa65c4058643678a4e4a9a60513944a7d8d35440 # v2.1.0
55
55
 
56
56
  - name: Set up QEMU
57
57
  if: runner.os == 'Linux' && matrix.target == 'aarch64'
@@ -68,7 +68,7 @@ jobs:
68
68
  manylinux: auto
69
69
 
70
70
  - name: Upload wheels
71
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
71
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
72
72
  with:
73
73
  name: maxminddb-rust-whl-${{ matrix.os }}-${{ matrix.target }}
74
74
  path: dist
@@ -77,7 +77,7 @@ jobs:
77
77
  name: Build source distribution
78
78
  runs-on: ubuntu-latest
79
79
  steps:
80
- - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
80
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
81
81
  with:
82
82
  persist-credentials: false
83
83
 
@@ -88,7 +88,7 @@ jobs:
88
88
  args: --out dist
89
89
 
90
90
  - name: Upload sdist
91
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
91
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
92
92
  with:
93
93
  name: maxminddb-rust-sdist
94
94
  path: dist
@@ -103,7 +103,7 @@ jobs:
103
103
  id-token: write
104
104
  steps:
105
105
  - name: Download artifacts
106
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
106
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
107
107
  with:
108
108
  pattern: maxminddb-rust-*
109
109
  path: dist
@@ -21,18 +21,18 @@ jobs:
21
21
  os: [ubuntu-latest, macos-latest, windows-latest]
22
22
 
23
23
  steps:
24
- - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
24
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
25
25
  with:
26
26
  persist-credentials: false
27
27
  submodules: recursive
28
28
 
29
29
  - name: Set up Python ${{ matrix.python-version }}
30
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
30
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
31
31
  with:
32
32
  python-version: ${{ matrix.python-version }}
33
33
 
34
34
  - name: Install Rust
35
- uses: dtolnay/rust-toolchain@0b1efabc08b657293548b77fb76cc02d26091c7e # master
35
+ uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
36
36
  with:
37
37
  toolchain: stable
38
38
 
@@ -17,7 +17,7 @@ jobs:
17
17
  security-events: write
18
18
  steps:
19
19
  - name: Checkout repository
20
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
20
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
21
21
  with:
22
22
  persist-credentials: false
23
23
 
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.0] - 2025-12-18
9
+
10
+ ### Added
11
+
12
+ - Added `get_path()` method to `Reader` class. This allows high-performance
13
+ retrieval of specific fields (e.g.,
14
+ `reader.get_path(ip, ('country', 'iso_code'))`) without decoding the entire
15
+ record. This can be over 6x faster than `get()` when only partial data is
16
+ needed.
17
+
18
+ ### Changed
19
+
20
+ - Upgraded maxminddb crate from 0.27.0 to 0.27.1.
21
+ - Enabled `simdutf8` feature in `maxminddb` crate. This improves lookup
22
+ performance by using SIMD instructions for faster UTF-8 validation.
23
+
8
24
  ## [0.2.0] - 2025-11-28
9
25
 
10
26
  ### Changed
@@ -10,9 +10,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
10
10
 
11
11
  [[package]]
12
12
  name = "cc"
13
- version = "1.2.37"
13
+ version = "1.2.49"
14
14
  source = "registry+https://github.com/rust-lang/crates.io-index"
15
- checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44"
15
+ checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215"
16
16
  dependencies = [
17
17
  "find-msvc-tools",
18
18
  "shlex",
@@ -20,9 +20,9 @@ dependencies = [
20
20
 
21
21
  [[package]]
22
22
  name = "find-msvc-tools"
23
- version = "0.1.4"
23
+ version = "0.1.5"
24
24
  source = "registry+https://github.com/rust-lang/crates.io-index"
25
- checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
25
+ checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
26
26
 
27
27
  [[package]]
28
28
  name = "heck"
@@ -47,32 +47,33 @@ checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763"
47
47
 
48
48
  [[package]]
49
49
  name = "libc"
50
- version = "0.2.177"
50
+ version = "0.2.178"
51
51
  source = "registry+https://github.com/rust-lang/crates.io-index"
52
- checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
52
+ checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
53
53
 
54
54
  [[package]]
55
55
  name = "log"
56
- version = "0.4.28"
56
+ version = "0.4.29"
57
57
  source = "registry+https://github.com/rust-lang/crates.io-index"
58
- checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
58
+ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
59
59
 
60
60
  [[package]]
61
61
  name = "maxminddb"
62
- version = "0.27.0"
62
+ version = "0.27.1"
63
63
  source = "registry+https://github.com/rust-lang/crates.io-index"
64
- checksum = "7ef0551fc3e7345a6c854c1026b0ddada1e443e51f4fb4cdcf86cc1a71d4b337"
64
+ checksum = "99681a80368084e68fff1a4ec657b09ae6a04f1107762ec6346a82b8cc19d8eb"
65
65
  dependencies = [
66
66
  "ipnetwork",
67
67
  "log",
68
68
  "memchr",
69
69
  "serde",
70
+ "simdutf8",
70
71
  "thiserror",
71
72
  ]
72
73
 
73
74
  [[package]]
74
75
  name = "maxminddb-rust"
75
- version = "0.2.0"
76
+ version = "0.3.0"
76
77
  dependencies = [
77
78
  "ipnetwork",
78
79
  "maxminddb",
@@ -128,9 +129,9 @@ dependencies = [
128
129
 
129
130
  [[package]]
130
131
  name = "pyo3"
131
- version = "0.27.1"
132
+ version = "0.27.2"
132
133
  source = "registry+https://github.com/rust-lang/crates.io-index"
133
- checksum = "37a6df7eab65fc7bee654a421404947e10a0f7085b6951bf2ea395f4659fb0cf"
134
+ checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d"
134
135
  dependencies = [
135
136
  "indoc",
136
137
  "libc",
@@ -145,9 +146,9 @@ dependencies = [
145
146
 
146
147
  [[package]]
147
148
  name = "pyo3-build-config"
148
- version = "0.27.1"
149
+ version = "0.27.2"
149
150
  source = "registry+https://github.com/rust-lang/crates.io-index"
150
- checksum = "f77d387774f6f6eec64a004eac0ed525aab7fa1966d94b42f743797b3e395afb"
151
+ checksum = "b455933107de8642b4487ed26d912c2d899dec6114884214a0b3bb3be9261ea6"
151
152
  dependencies = [
152
153
  "python3-dll-a",
153
154
  "target-lexicon",
@@ -155,9 +156,9 @@ dependencies = [
155
156
 
156
157
  [[package]]
157
158
  name = "pyo3-ffi"
158
- version = "0.27.1"
159
+ version = "0.27.2"
159
160
  source = "registry+https://github.com/rust-lang/crates.io-index"
160
- checksum = "2dd13844a4242793e02df3e2ec093f540d948299a6a77ea9ce7afd8623f542be"
161
+ checksum = "1c85c9cbfaddf651b1221594209aed57e9e5cff63c4d11d1feead529b872a089"
161
162
  dependencies = [
162
163
  "libc",
163
164
  "pyo3-build-config",
@@ -165,9 +166,9 @@ dependencies = [
165
166
 
166
167
  [[package]]
167
168
  name = "pyo3-macros"
168
- version = "0.27.1"
169
+ version = "0.27.2"
169
170
  source = "registry+https://github.com/rust-lang/crates.io-index"
170
- checksum = "eaf8f9f1108270b90d3676b8679586385430e5c0bb78bb5f043f95499c821a71"
171
+ checksum = "0a5b10c9bf9888125d917fb4d2ca2d25c8df94c7ab5a52e13313a07e050a3b02"
171
172
  dependencies = [
172
173
  "proc-macro2",
173
174
  "pyo3-macros-backend",
@@ -177,9 +178,9 @@ dependencies = [
177
178
 
178
179
  [[package]]
179
180
  name = "pyo3-macros-backend"
180
- version = "0.27.1"
181
+ version = "0.27.2"
181
182
  source = "registry+https://github.com/rust-lang/crates.io-index"
182
- checksum = "70a3b2274450ba5288bc9b8c1b69ff569d1d61189d4bff38f8d22e03d17f932b"
183
+ checksum = "03b51720d314836e53327f5871d4c0cfb4fb37cc2c4a11cc71907a86342c40f9"
183
184
  dependencies = [
184
185
  "heck",
185
186
  "proc-macro2",
@@ -199,9 +200,9 @@ dependencies = [
199
200
 
200
201
  [[package]]
201
202
  name = "quote"
202
- version = "1.0.41"
203
+ version = "1.0.42"
203
204
  source = "registry+https://github.com/rust-lang/crates.io-index"
204
- checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
205
+ checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
205
206
  dependencies = [
206
207
  "proc-macro2",
207
208
  ]
@@ -248,11 +249,17 @@ version = "1.3.0"
248
249
  source = "registry+https://github.com/rust-lang/crates.io-index"
249
250
  checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
250
251
 
252
+ [[package]]
253
+ name = "simdutf8"
254
+ version = "0.1.5"
255
+ source = "registry+https://github.com/rust-lang/crates.io-index"
256
+ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
257
+
251
258
  [[package]]
252
259
  name = "syn"
253
- version = "2.0.109"
260
+ version = "2.0.111"
254
261
  source = "registry+https://github.com/rust-lang/crates.io-index"
255
- checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f"
262
+ checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
256
263
  dependencies = [
257
264
  "proc-macro2",
258
265
  "quote",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "maxminddb-rust"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  edition = "2021"
5
5
  authors = ["Gregory Oschwald <oschwald@gmail.com>"]
6
6
  description = "Rust-based Python binding for MaxMind DB - high-performance alternative to maxminddb"
@@ -14,9 +14,9 @@ crate-type = ["cdylib"]
14
14
 
15
15
  [dependencies]
16
16
  ipnetwork = "0.21"
17
- maxminddb = "0.27.0"
17
+ maxminddb = { version = "0.27.0", features = ["simdutf8"] }
18
18
  memmap2 = "0.9"
19
- pyo3 = { version = "0.27.1", features = ["extension-module", "generate-import-lib"] }
19
+ pyo3 = { version = "0.27.2", features = ["extension-module", "generate-import-lib"] }
20
20
  serde = "1.0"
21
21
 
22
22
  [features]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxminddb-rust
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: ISC License (ISCL)
@@ -72,6 +72,9 @@ This package provides **100% API compatibility** with the official [`maxminddb`]
72
72
  **Extensions (not in original):**
73
73
 
74
74
  - ⭐ `get_many()` - Batch lookup method for processing multiple IPs efficiently
75
+ - ⭐ `get_path()` - Efficiently retrieve a specific field from a record (e.g.,
76
+ `('country', 'iso_code')`) without decoding the entire record. This can be
77
+ over 6x faster than `get()` when only partial data is needed.
75
78
 
76
79
  **Not Yet Implemented:**
77
80
 
@@ -143,6 +146,24 @@ for ip, result in zip(ips, results):
143
146
  print(f"{ip}: {result}")
144
147
  ```
145
148
 
149
+ ### Selective Field Lookup (Extension)
150
+
151
+ The `get_path()` method allows you to efficiently retrieve a specific field from a record:
152
+
153
+ ```python
154
+ import maxminddb_rust
155
+
156
+ reader = maxminddb_rust.open_database("/var/lib/GeoIP/GeoIP2-City.mmdb")
157
+
158
+ # Retrieve specific field without decoding the entire record
159
+ # Path elements can be strings (map keys) or integers (array indices)
160
+ iso_code = reader.get_path("8.8.8.8", ("country", "iso_code"))
161
+ print(f"ISO Code: {iso_code}")
162
+
163
+ # Accessing arrays by index
164
+ # e.g., reader.get_path("8.8.8.8", ("subdivisions", 0, "iso_code"))
165
+ ```
166
+
146
167
  ### Iterator Support
147
168
 
148
169
  Iterate over all networks in the database:
@@ -170,8 +191,11 @@ reader = maxminddb_rust.open_database("/var/lib/GeoIP/GeoIP2-City.mmdb", mode=ma
170
191
  # MODE_MMAP: Explicitly use memory-mapped files
171
192
  reader = maxminddb_rust.open_database("/var/lib/GeoIP/GeoIP2-City.mmdb", mode=maxminddb_rust.MODE_MMAP)
172
193
 
173
- # MODE_MEMORY: Load entire database into memory (useful for embedded systems or when file handle limits are a concern)
174
- reader = maxminddb_rust.open_database("/var/lib/GeoIP/GeoIP2-City.mmdb", mode=maxminddb_rust.MODE_MEMORY)
194
+ # MODE_MEMORY: Load entire database into memory (useful for embedded systems
195
+ # or when file handle limits are a concern)
196
+ reader = maxminddb_rust.open_database(
197
+ "/var/lib/GeoIP/GeoIP2-City.mmdb", mode=maxminddb_rust.MODE_MEMORY
198
+ )
175
199
  ```
176
200
 
177
201
  ## Examples
@@ -182,6 +206,8 @@ The `examples/` directory contains complete working examples demonstrating vario
182
206
  - **[context_manager.py](https://github.com/oschwald/maxminddb-rust-python/blob/main/examples/context_manager.py)** - Using `with` statement for automatic resource cleanup
183
207
  - **[iterator_demo.py](https://github.com/oschwald/maxminddb-rust-python/blob/main/examples/iterator_demo.py)** - Iterating over all networks in the database
184
208
  - **[batch_processing.py](https://github.com/oschwald/maxminddb-rust-python/blob/main/examples/batch_processing.py)** - High-performance batch lookups with `get_many()`
209
+ - **[benchmark_path.py](https://github.com/oschwald/maxminddb-rust-python/blob/main/examples/benchmark_path.py)**
210
+ - Performance comparison between `get()` and `get_path()`
185
211
 
186
212
  Run any example:
187
213
 
@@ -37,6 +37,9 @@ This package provides **100% API compatibility** with the official [`maxminddb`]
37
37
  **Extensions (not in original):**
38
38
 
39
39
  - ⭐ `get_many()` - Batch lookup method for processing multiple IPs efficiently
40
+ - ⭐ `get_path()` - Efficiently retrieve a specific field from a record (e.g.,
41
+ `('country', 'iso_code')`) without decoding the entire record. This can be
42
+ over 6x faster than `get()` when only partial data is needed.
40
43
 
41
44
  **Not Yet Implemented:**
42
45
 
@@ -108,6 +111,24 @@ for ip, result in zip(ips, results):
108
111
  print(f"{ip}: {result}")
109
112
  ```
110
113
 
114
+ ### Selective Field Lookup (Extension)
115
+
116
+ The `get_path()` method allows you to efficiently retrieve a specific field from a record:
117
+
118
+ ```python
119
+ import maxminddb_rust
120
+
121
+ reader = maxminddb_rust.open_database("/var/lib/GeoIP/GeoIP2-City.mmdb")
122
+
123
+ # Retrieve specific field without decoding the entire record
124
+ # Path elements can be strings (map keys) or integers (array indices)
125
+ iso_code = reader.get_path("8.8.8.8", ("country", "iso_code"))
126
+ print(f"ISO Code: {iso_code}")
127
+
128
+ # Accessing arrays by index
129
+ # e.g., reader.get_path("8.8.8.8", ("subdivisions", 0, "iso_code"))
130
+ ```
131
+
111
132
  ### Iterator Support
112
133
 
113
134
  Iterate over all networks in the database:
@@ -135,8 +156,11 @@ reader = maxminddb_rust.open_database("/var/lib/GeoIP/GeoIP2-City.mmdb", mode=ma
135
156
  # MODE_MMAP: Explicitly use memory-mapped files
136
157
  reader = maxminddb_rust.open_database("/var/lib/GeoIP/GeoIP2-City.mmdb", mode=maxminddb_rust.MODE_MMAP)
137
158
 
138
- # MODE_MEMORY: Load entire database into memory (useful for embedded systems or when file handle limits are a concern)
139
- reader = maxminddb_rust.open_database("/var/lib/GeoIP/GeoIP2-City.mmdb", mode=maxminddb_rust.MODE_MEMORY)
159
+ # MODE_MEMORY: Load entire database into memory (useful for embedded systems
160
+ # or when file handle limits are a concern)
161
+ reader = maxminddb_rust.open_database(
162
+ "/var/lib/GeoIP/GeoIP2-City.mmdb", mode=maxminddb_rust.MODE_MEMORY
163
+ )
140
164
  ```
141
165
 
142
166
  ## Examples
@@ -147,6 +171,8 @@ The `examples/` directory contains complete working examples demonstrating vario
147
171
  - **[context_manager.py](https://github.com/oschwald/maxminddb-rust-python/blob/main/examples/context_manager.py)** - Using `with` statement for automatic resource cleanup
148
172
  - **[iterator_demo.py](https://github.com/oschwald/maxminddb-rust-python/blob/main/examples/iterator_demo.py)** - Iterating over all networks in the database
149
173
  - **[batch_processing.py](https://github.com/oschwald/maxminddb-rust-python/blob/main/examples/batch_processing.py)** - High-performance batch lookups with `get_many()`
174
+ - **[benchmark_path.py](https://github.com/oschwald/maxminddb-rust-python/blob/main/examples/benchmark_path.py)**
175
+ - Performance comparison between `get()` and `get_path()`
150
176
 
151
177
  Run any example:
152
178
 
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/python
2
+
3
+ import argparse
4
+ import random
5
+ import socket
6
+ import struct
7
+ import timeit
8
+
9
+ import maxminddb_rust
10
+
11
+ parser = argparse.ArgumentParser(description="Benchmark maxminddb get vs get_path.")
12
+ parser.add_argument("--count", default=250000, type=int, help="number of lookups")
13
+ parser.add_argument(
14
+ "--file", default="/var/lib/GeoIP/GeoLite2-City.mmdb", help="path to mmdb file"
15
+ )
16
+
17
+ args = parser.parse_args()
18
+
19
+ random.seed(0)
20
+ reader = maxminddb_rust.open_database(args.file)
21
+
22
+ # Pre-generate IPs to ensure fair comparison (though random lookup overhead is small)
23
+ ips = [
24
+ socket.inet_ntoa(struct.pack("!L", random.getrandbits(32)))
25
+ for _ in range(args.count)
26
+ ]
27
+
28
+
29
+ def lookup_full():
30
+ for ip in ips:
31
+ try:
32
+ res = reader.get(ip)
33
+ if res:
34
+ res.get("country", {}).get("iso_code")
35
+ except ValueError:
36
+ pass
37
+
38
+
39
+ def lookup_path():
40
+ path = ("country", "iso_code")
41
+ for ip in ips:
42
+ try:
43
+ reader.get_path(ip, path)
44
+ except ValueError:
45
+ pass
46
+
47
+
48
+ print(f"Benchmarking with {args.count:,} lookups...")
49
+
50
+ time_full = timeit.timeit(lookup_full, number=1)
51
+ print(f"Full record decode: {int(args.count / time_full):,} lookups per second")
52
+
53
+ time_path = timeit.timeit(lookup_path, number=1)
54
+ print(f"Path decode (get_path): {int(args.count / time_path):,} lookups per second")
55
+
56
+ print(f"Speedup: {time_full / time_path:.2f}x")
@@ -8,7 +8,7 @@ implemented in Rust using PyO3 with 100% API compatibility.
8
8
  from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network
9
9
  from os import PathLike
10
10
  from types import TracebackType
11
- from typing import Any, Iterator, Literal, Optional, Union
11
+ from typing import Any, Iterator, Literal, Optional, Sequence, Union
12
12
 
13
13
  __all__ = [
14
14
  "Reader",
@@ -152,6 +152,32 @@ class Reader:
152
152
  """
153
153
  ...
154
154
 
155
+ def get_path(
156
+ self,
157
+ ip_address: Union[str, IPv4Address, IPv6Address],
158
+ path: Sequence[Union[str, int]],
159
+ ) -> Optional[Any]:
160
+ """
161
+ Query the database for a specific path within the record.
162
+
163
+ This method is more efficient than get() when you only need a specific field
164
+ (e.g., country code) from the record, as it avoids decoding the entire record.
165
+
166
+ Args:
167
+ ip_address: The IP address to look up.
168
+ path: A sequence (tuple or list) of strings or integers representing the path to the data.
169
+
170
+ Returns:
171
+ The value at the specified path, or None if the IP address or path is not found.
172
+
173
+ Example:
174
+ >>> reader.get_path('8.8.8.8', ('country', 'iso_code'))
175
+ 'US'
176
+ >>> reader.get_path('8.8.8.8', ('subdivisions', 0, 'iso_code'))
177
+ 'CA'
178
+ """
179
+ ...
180
+
155
181
  def get_many(self, ips: list[str]) -> list[Optional[dict[str, Any]]]:
156
182
  """
157
183
  Query the database for multiple IP addresses in a single batch operation.
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "maxminddb-rust"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Rust-based Python binding for MaxMind DB - high-performance alternative to maxminddb"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -1,5 +1,7 @@
1
1
  use ::maxminddb as maxminddb_crate;
2
- use maxminddb_crate::{MaxMindDbError, Reader as MaxMindReader, Within, WithinOptions};
2
+ use maxminddb_crate::{
3
+ MaxMindDbError, PathElement, Reader as MaxMindReader, Within, WithinOptions,
4
+ };
3
5
  use memmap2::Mmap;
4
6
  use pyo3::{
5
7
  conversion::IntoPyObjectExt,
@@ -240,6 +242,11 @@ const ERR_CLOSED_DB: &str = "Attempt to read from a closed MaxMind DB.";
240
242
  const ERR_BAD_DATA: &str =
241
243
  "The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)";
242
244
 
245
+ enum OwnedPathElement {
246
+ Key(String),
247
+ Index(usize),
248
+ }
249
+
243
250
  /// Enum to handle different reader source types
244
251
  enum ReaderSource {
245
252
  Mmap(MaxMindReader<Mmap>),
@@ -287,6 +294,36 @@ impl ReaderSource {
287
294
  }
288
295
  }
289
296
 
297
+ #[inline]
298
+ fn lookup_path(
299
+ &self,
300
+ ip: IpAddr,
301
+ path: &[OwnedPathElement],
302
+ ) -> Result<Option<PyDecodedValue>, maxminddb_crate::MaxMindDbError> {
303
+ let path_elements: Vec<PathElement> = path
304
+ .iter()
305
+ .map(|e| match e {
306
+ OwnedPathElement::Key(s) => PathElement::Key(s.as_str()),
307
+
308
+ OwnedPathElement::Index(i) => PathElement::Index(*i),
309
+ })
310
+ .collect();
311
+
312
+ match self {
313
+ ReaderSource::Mmap(reader) => {
314
+ let result = reader.lookup(ip)?;
315
+
316
+ result.decode_path(&path_elements)
317
+ }
318
+
319
+ ReaderSource::Memory(reader) => {
320
+ let result = reader.lookup(ip)?;
321
+
322
+ result.decode_path(&path_elements)
323
+ }
324
+ }
325
+ }
326
+
290
327
  #[inline]
291
328
  fn metadata(&self) -> &maxminddb_crate::Metadata {
292
329
  match self {
@@ -490,6 +527,64 @@ impl Reader {
490
527
  }
491
528
  }
492
529
 
530
+ /// Query the database for a specific path within the record.
531
+ ///
532
+ /// This method is more efficient than get() when you only need a specific field
533
+ /// (e.g., country code) from the record, as it avoids decoding the entire record.
534
+ ///
535
+ /// Args:
536
+ /// ip_address: The IP address to look up. May be a string (e.g., '1.2.3.4')
537
+ /// or an ipaddress.IPv4Address or ipaddress.IPv6Address object.
538
+ /// path: A sequence (tuple or list) of strings or integers representing the
539
+ /// path to the data.
540
+ ///
541
+ /// Returns:
542
+ /// The value at the specified path, or None if the IP address or path is
543
+ /// not found.
544
+ ///
545
+ /// Example:
546
+ /// >>> reader.get_path('8.8.8.8', ('country', 'iso_code'))
547
+ /// 'US'
548
+ fn get_path(
549
+ &self,
550
+ py: Python,
551
+ ip_address: &Bound<'_, PyAny>,
552
+ path: &Bound<'_, PyAny>,
553
+ ) -> PyResult<Py<PyAny>> {
554
+ // Quick check if database is closed
555
+ if self.closed.load(Ordering::Acquire) {
556
+ return Err(PyValueError::new_err(ERR_CLOSED_DB));
557
+ }
558
+
559
+ // Parse IP address
560
+ let parsed_ip = parse_ip_address(ip_address)?;
561
+
562
+ if self.ip_version == 4 && matches!(parsed_ip.addr, IpAddr::V6(_)) {
563
+ return Err(PyValueError::new_err(ipv6_in_ipv4_error(&parsed_ip.addr)));
564
+ }
565
+
566
+ // Parse path
567
+ let owned_path = parse_path(path)?;
568
+
569
+ let reader = self.get_reader()?;
570
+
571
+ // We need to keep the Strings alive while we use them as &str
572
+ // This is a bit tricky because we're crossing the thread boundary with py.detach
573
+ // But since we are passing ownership of `path` (Vec<String>) into the closure,
574
+ // and constructing the Vec<&str> inside, it should work.
575
+
576
+ let result = py.detach(move || reader.lookup_path(parsed_ip.addr, &owned_path));
577
+
578
+ match result {
579
+ Ok(Some(data)) => Ok(data.into_py()),
580
+ Ok(None) => Ok(py.None()),
581
+ Err(MaxMindDbError::InvalidDatabase { .. } | MaxMindDbError::Decoding { .. }) => {
582
+ Err(InvalidDatabaseError::new_err(ERR_BAD_DATA))
583
+ }
584
+ Err(err) => Err(PyValueError::new_err(format!("Database error: {err}"))),
585
+ }
586
+ }
587
+
493
588
  /// Query the database for information about an IP address and return the network prefix length.
494
589
  ///
495
590
  /// Args:
@@ -895,6 +990,34 @@ impl ReaderWithin {
895
990
  }
896
991
  }
897
992
 
993
+ fn parse_path(path: &Bound<'_, PyAny>) -> PyResult<Vec<OwnedPathElement>> {
994
+ let mut owned_path = Vec::new();
995
+ if path.is_instance_of::<PyString>() {
996
+ return Err(PyTypeError::new_err(
997
+ "Path must be a sequence (list or tuple)",
998
+ ));
999
+ }
1000
+ if let Ok(iterator) = path.try_iter() {
1001
+ for item in iterator {
1002
+ let item = item?;
1003
+ if let Ok(s) = item.extract::<String>() {
1004
+ owned_path.push(OwnedPathElement::Key(s));
1005
+ } else if let Ok(i) = item.extract::<usize>() {
1006
+ owned_path.push(OwnedPathElement::Index(i));
1007
+ } else {
1008
+ return Err(PyTypeError::new_err(
1009
+ "Path elements must be strings or integers",
1010
+ ));
1011
+ }
1012
+ }
1013
+ } else {
1014
+ return Err(PyTypeError::new_err(
1015
+ "Path must be a sequence (list or tuple)",
1016
+ ));
1017
+ }
1018
+ Ok(owned_path)
1019
+ }
1020
+
898
1021
  struct ParsedIp {
899
1022
  addr: IpAddr,
900
1023
  }
@@ -0,0 +1,91 @@
1
+ import os
2
+ import pytest
3
+ import maxminddb_rust
4
+
5
+
6
+ def test_get_path():
7
+ db_path = os.path.join(
8
+ os.path.dirname(__file__), "data", "test-data", "GeoIP2-City-Test.mmdb"
9
+ )
10
+ with maxminddb_rust.open_database(db_path) as reader:
11
+ ip = "81.2.69.142"
12
+
13
+ # Verify full record first
14
+ record = reader.get(ip)
15
+ assert record["country"]["iso_code"] == "GB"
16
+ assert record["city"]["names"]["en"] == "London"
17
+
18
+ # Test get_path for various depths
19
+ assert reader.get_path(ip, ("country", "iso_code")) == "GB"
20
+ assert reader.get_path(ip, ("city", "names", "en")) == "London"
21
+ assert (
22
+ reader.get_path(ip, ("location", "latitude"))
23
+ == record["location"]["latitude"]
24
+ )
25
+
26
+ # Test non-existent path
27
+ assert reader.get_path(ip, ("non", "existent")) is None
28
+
29
+ # Test non-existent IP
30
+ assert reader.get_path("1.1.1.1", ("country", "iso_code")) is None
31
+
32
+ # Test array indexing (if subdivisions exist)
33
+ if "subdivisions" in record and len(record["subdivisions"]) > 0:
34
+ assert (
35
+ reader.get_path(ip, ("subdivisions", 0, "iso_code"))
36
+ == record["subdivisions"][0]["iso_code"]
37
+ )
38
+
39
+
40
+ def test_get_path_ipv6():
41
+ db_path = os.path.join(
42
+ os.path.dirname(__file__), "data", "test-data", "GeoIP2-City-Test.mmdb"
43
+ )
44
+ with maxminddb_rust.open_database(db_path) as reader:
45
+ ip = "2001:2b8::"
46
+
47
+ assert reader.get_path(ip, ("country", "iso_code")) == "KR"
48
+ assert reader.get_path(ip, ("continent", "names", "en")) == "Asia"
49
+
50
+
51
+ def test_get_path_invalid_types():
52
+ db_path = os.path.join(
53
+ os.path.dirname(__file__), "data", "test-data", "GeoIP2-City-Test.mmdb"
54
+ )
55
+ with maxminddb_rust.open_database(db_path) as reader:
56
+ ip = "81.2.69.142"
57
+
58
+ # Invalid path element type (float)
59
+ with pytest.raises(
60
+ TypeError, match="Path elements must be strings or integers"
61
+ ):
62
+ reader.get_path(ip, ("country", 3.14))
63
+
64
+ # Invalid path argument type (not a sequence)
65
+ with pytest.raises(TypeError, match="Path must be a sequence"):
66
+ reader.get_path(ip, "country") # type: ignore
67
+
68
+
69
+ def test_get_path_closed_db():
70
+ db_path = os.path.join(
71
+ os.path.dirname(__file__), "data", "test-data", "GeoIP2-City-Test.mmdb"
72
+ )
73
+ reader = maxminddb_rust.open_database(db_path)
74
+ reader.close()
75
+
76
+ with pytest.raises(ValueError, match="closed"):
77
+ reader.get_path("81.2.69.142", ("country", "iso_code"))
78
+
79
+
80
+ def test_get_path_mixed_invalid():
81
+ db_path = os.path.join(
82
+ os.path.dirname(__file__), "data", "test-data", "GeoIP2-City-Test.mmdb"
83
+ )
84
+ with maxminddb_rust.open_database(db_path) as reader:
85
+ ip = "81.2.69.142"
86
+
87
+ # Mixed valid and invalid types
88
+ with pytest.raises(
89
+ TypeError, match="Path elements must be strings or integers"
90
+ ):
91
+ reader.get_path(ip, ("country", 3.14, "iso_code"))
@@ -55,7 +55,7 @@ wheels = [
55
55
 
56
56
  [[package]]
57
57
  name = "maxminddb-rust"
58
- version = "0.2.0"
58
+ version = "0.3.0"
59
59
  source = { editable = "." }
60
60
 
61
61
  [package.optional-dependencies]
File without changes
File without changes