gunsnprices 0.1.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.
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ .pytest_cache/
8
+ .DS_Store
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-10-12)
4
+
5
+ First release.
6
+
7
+ - `parse_title()`: firearm or ammo, brand, model, calibers, magazine capacity, barrel length, rounds per box, cost per round.
8
+ - `identify_model()`: 50 brands, the same model dictionaries that GunsNPrices uses on its site; decodes Glock part numbers.
9
+ - `find_calibers()` / `normalize_caliber()`: 293 calibers with retailer spellings.
10
+ - `magazine_capacity()`, `barrel_length()`, `rounds_per_box()`, `cost_per_round()`.
11
+ - Command line: `gunsnprices "title"` or `python -m gunsnprices`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GunsNPrices (https://gunsnprices.com)
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,12 @@
1
+ The data files in src/gunsnprices/data/ (models.json, calibers.json) are
2
+ Copyright (c) 2026 GunsNPrices (https://gunsnprices.com) and are licensed under
3
+ the Creative Commons Attribution 4.0 International License (CC BY 4.0).
4
+
5
+ You are free to share and adapt them for any purpose, including commercially,
6
+ as long as you give appropriate credit: "GunsNPrices (gunsnprices.com)" with a
7
+ link to https://gunsnprices.com, and indicate if changes were made.
8
+
9
+ Full license text: https://creativecommons.org/licenses/by/4.0/legalcode
10
+
11
+ The same data is published as datasets at
12
+ https://huggingface.co/gunsnprices and https://github.com/gunsnprices/firearm-datasets
@@ -0,0 +1,211 @@
1
+ Metadata-Version: 2.5
2
+ Name: gunsnprices
3
+ Version: 0.1.0
4
+ Summary: Parse firearm and ammunition product titles: brand, model, caliber, magazine capacity, barrel length, rounds per box.
5
+ Project-URL: Homepage, https://gunsnprices.com
6
+ Project-URL: Source, https://github.com/gunsnprices/gunsnprices-python
7
+ Project-URL: Issues, https://github.com/gunsnprices/gunsnprices-python/issues
8
+ Project-URL: Datasets, https://huggingface.co/gunsnprices
9
+ Author-email: GunsNPrices <app@gunsnprices.com>
10
+ License-Expression: MIT AND CC-BY-4.0
11
+ License-File: LICENSE
12
+ License-File: LICENSE-DATA
13
+ Keywords: ammo,ammunition,caliber,ecommerce,firearms,glock,guns,price-comparison,product-matching,title-parsing
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Natural Language :: English
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Topic :: Office/Business
26
+ Classifier: Topic :: Text Processing :: General
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.9
29
+ Description-Content-Type: text/markdown
30
+
31
+ # gunsnprices
32
+
33
+ [![PyPI](https://img.shields.io/pypi/v/gunsnprices)](https://pypi.org/project/gunsnprices/)
34
+ [![Python](https://img.shields.io/pypi/pyversions/gunsnprices)](https://pypi.org/project/gunsnprices/)
35
+ [![Tests](https://github.com/gunsnprices/gunsnprices-python/actions/workflows/ci.yml/badge.svg)](https://github.com/gunsnprices/gunsnprices-python/actions/workflows/ci.yml)
36
+ [![License](https://img.shields.io/badge/license-MIT%20%2B%20CC%20BY%204.0%20data-blue)](#license)
37
+
38
+ Parse firearm and ammunition product titles into structured data: brand, model, caliber, magazine capacity, barrel length,
39
+ rounds per box and cost per round.
40
+
41
+ ```python
42
+ >>> from gunsnprices import parse_title
43
+ >>> info = parse_title("Glock 19 Gen 5 MOS 9mm 4.02in 15rd Pistol PA195S203MOS")
44
+ >>> info.model.model, info.caliber.name, info.capacity, info.barrel_length
45
+ ('Glock 19 Gen 5 MOS', '9×19mm Parabellum', 15, 4.02)
46
+ ```
47
+
48
+ This is the title parser behind [GunsNPrices](https://gunsnprices.com), a price comparison site that tracks 40+ US gun
49
+ and ammo stores. Store titles are messy (`G19 GEN5 9MM 15RD FS`, `O/U 12/28 BL/WD 3"`, `Federal 308 150gr ... 20rds`),
50
+ and matching the same product across stores starts with reading them reliably. The model dictionaries and the caliber
51
+ list in this package are the ones the site uses to group listings, so you get the same model names you see on
52
+ gunsnprices.com.
53
+
54
+ No dependencies, pure Python 3.9+, about 1 ms per title.
55
+
56
+ ## Install
57
+
58
+ ```bash
59
+ pip install gunsnprices
60
+ ```
61
+
62
+ A JavaScript version with the same API and the same test cases is on npm: `npm install gunsnprices`
63
+ ([source](https://github.com/gunsnprices/gunsnprices-js)).
64
+
65
+ ## What it recognizes
66
+
67
+ | Field | Example in a title | Result |
68
+ |---|---|---|
69
+ | Firearm or ammo | `... 150gr ... 20rds` | `kind="ammo"` |
70
+ | Brand and model (50 brands) | `G19 Gen5 MOS`, part number `PA195S203` | `Glock 19 Gen 5 MOS` |
71
+ | Calibers (293) | `.357 Mag / .38 Spl`, `6.5 CM`, `12ga`, `300 BLK`, `7MM RM` | `.357 Mag`, `.38 S&W Special`, ... |
72
+ | Magazine capacity | `15+1`, `17RD`, `13rd/15rd`, `10-shot` | `15`, `17`, `15`, `10` |
73
+ | Barrel length, inches | `4.02in`, `16 1/2"`, `22 BBL`, `O/U 12/28` | `4.02`, `16.5`, `22.0`, `28.0` |
74
+ | Rounds per box | `50/Box`, `Box of 20`, `1,000 Rounds`, `20/25` | `50`, `20`, `1000`, `20` |
75
+ | Cost per round | price `449.99` + `1000 Rounds` | `0.45` |
76
+
77
+ Brands with model dictionaries: Barrett, Benelli, Beretta, Bergara, Browning, Cadex, Canik, Charles Daly, Chiappa,
78
+ Christensen Arms, Cimarron, CMMG, Colt, CVA, CZ, Daniel Defense, Desert Eagle, Diamondback, Fierce, FN, Glock,
79
+ Great Lakes, Henry, Heritage, HK, Howa, Kel-Tec, Kimber, Kriss, Marlin, Mossberg, Remington, Retay, Rossi, Ruger, Savage,
80
+ SCCY, Seekins, Shadow Systems, Sig Sauer, Smith & Wesson, Springfield, Stevens, Stoeger, Taurus, Tikka, Tristar, Walther,
81
+ Weatherby, Winchester.
82
+
83
+ ## Usage
84
+
85
+ ### Everything at once
86
+
87
+ ```python
88
+ from gunsnprices import parse_title
89
+
90
+ info = parse_title("Federal American Eagle 5.56x45mm 55gr FMJ 1000 Rounds", price=449.99)
91
+ info.kind # 'ammo'
92
+ info.caliber.name # '5.56×45mm NATO'
93
+ info.rounds # 1000
94
+ info.cost_per_round # 0.45
95
+
96
+ info.to_dict() # plain dict, ready for json.dumps
97
+ ```
98
+
99
+ `parse_title(title, brand=None, mpn="", sku="", price=None, kind=None)` returns a `ProductInfo`:
100
+
101
+ | Attribute | Type | Notes |
102
+ |---|---|---|
103
+ | `kind` | `str` | `"firearm"` or `"ammo"`; pass `kind=` to skip the guess |
104
+ | `brand` | `str` | brand of the identified model, else the `brand` you passed |
105
+ | `model` | `ModelMatch` or `None` | firearms only |
106
+ | `caliber` | `Caliber` or `None` | first caliber in the title |
107
+ | `calibers` | `list[Caliber]` | all calibers, in order |
108
+ | `capacity` | `int` or `None` | magazine capacity (for `15+1` it is 15), firearms only |
109
+ | `barrel_length` | `float` or `None` | inches, firearms only |
110
+ | `rounds` | `int` or `None` | rounds per box, ammo only |
111
+ | `cost_per_round` | `float` or `None` | ammo with `price` only |
112
+
113
+ Passing `brand` (the manufacturer field from a feed) helps with titles that don't name the brand. Passing `mpn` lets it
114
+ decode Glock part numbers when the title says nothing useful:
115
+
116
+ ```python
117
+ >>> from gunsnprices import identify_model
118
+ >>> identify_model("GLOCK 9MM PISTOL 15RD", brand="Glock", mpn="UA4350201")
119
+ ModelMatch(brand='Glock', family='Glock 43', model='Glock 43', generation='', configs=[], features=[], source='mpn')
120
+ ```
121
+
122
+ ### Models
123
+
124
+ ```python
125
+ from gunsnprices import identify_model, list_brands
126
+
127
+ m = identify_model("Sig Sauer P365 XL 9mm 3.7\" 12+1 Optic Ready")
128
+ m.brand, m.family, m.model # ('Sig Sauer', 'Sig Sauer P365', 'Sig Sauer P365 XL')
129
+ ```
130
+
131
+ `ModelMatch` has `brand`, `family`, `model` (the most specific name), `generation`, `configs` (such as `MOS`,
132
+ `Compact`), `features` (such as `Threaded Barrel`, `FDE`) and `source` (`"title"` or `"mpn"`). Accessories, parts,
133
+ airguns and airsoft are rejected, so it returns `None` for "Glock 19 holster" and "Umarex Glock 19 CO2 BB pistol".
134
+
135
+ ### Calibers
136
+
137
+ ```python
138
+ from gunsnprices import find_calibers, normalize_caliber, list_calibers
139
+
140
+ [c.name for c in find_calibers("Henry Big Boy .44 Mag/.44 Spl 20in")]
141
+ # ['.44 Magnum', '.44 S&W Special']
142
+
143
+ normalize_caliber("308 WIN (7.62X51 NATO)")
144
+ # Caliber(id='308win', name='.308 Win', type='rifle', url='https://gunsnprices.com/ammo/rifle-ammo/308-win')
145
+ ```
146
+
147
+ Each `Caliber` has a stable `id`, a display `name`, a `type` (`handgun`, `rifle`, `rimfire`, `shotgun`, `airgun`,
148
+ `muzzleloader`) and the `url` of its GunsNPrices price comparison page. `normalize_caliber` is meant for caliber fields
149
+ in product feeds; `find_calibers` searches free text.
150
+
151
+ ### Numbers
152
+
153
+ ```python
154
+ from gunsnprices import magazine_capacity, barrel_length, rounds_per_box, cost_per_round
155
+
156
+ magazine_capacity("Springfield Hellcat Pro OSP 9mm 3.7in 15rd/17rd") # 17
157
+ barrel_length('Mossberg 590 Persuader 12/20 BL/SY 3"') # 20.0 (3" is the chamber)
158
+ rounds_per_box("Winchester 308 150gr PowerMax Bonded 20rds") # 20
159
+ cost_per_round(31.99, 20) # 1.5995
160
+ ```
161
+
162
+ ### Command line
163
+
164
+ ```bash
165
+ $ gunsnprices "Ruger GP100 .357 Mag / .38 Special 4.2in 6-shot Revolver"
166
+ $ cat titles.txt | gunsnprices > parsed.jsonl
167
+ $ gunsnprices --price 18.99 "Federal 9mm 115gr FMJ 50/Box"
168
+ ```
169
+
170
+ ## Accuracy
171
+
172
+ Measured on 50,202 firearm and 13,555 ammunition listings from 40+ stores on gunsnprices.com (October 2026),
173
+ against the attributes stored on the site:
174
+
175
+ | What | Found in the title | Right when found |
176
+ |---|---|---|
177
+ | Model (32,317 listings with a known model) | 99.97% | 100% same model as the site |
178
+ | Caliber, firearms | 81% | 97.8% |
179
+ | Caliber, ammo | 91% | 94.9% |
180
+ | Magazine capacity | 46% | 95.6% |
181
+ | Barrel length | 58% | 94.4% |
182
+ | Rounds per box | 80% | 96.6% |
183
+
184
+ "Found" is limited by the titles themselves: many stores don't put the caliber or barrel length in the title at all.
185
+ Most of the "wrong" cases we checked by hand are errors in the store data, not in the parser: a 6mm Creedmoor rifle
186
+ labeled 6.5 Creedmoor, a "1000 Rounds" case stored as a box of 20.
187
+
188
+ ## Data
189
+
190
+ The model dictionaries and caliber list ship inside the package (`gunsnprices/data/`). The same data, with listing
191
+ counts and price levels, is published as open datasets:
192
+
193
+ - Hugging Face: [gunsnprices/firearm-calibers](https://huggingface.co/datasets/gunsnprices/firearm-calibers),
194
+ [gunsnprices/firearm-models](https://huggingface.co/datasets/gunsnprices/firearm-models)
195
+ - GitHub: [gunsnprices/firearm-datasets](https://github.com/gunsnprices/firearm-datasets)
196
+ - Kaggle: [gunsnprices](https://www.kaggle.com/gunsnprices)
197
+
198
+ ## Limitations
199
+
200
+ - Titles from US retailers, in English.
201
+ - Models are covered for the 50 brands above. Other brands still get calibers and numbers.
202
+ - Built for firearms and ammunition. Magazines, parts and accessories get calibers and numbers but no model.
203
+
204
+ ## License
205
+
206
+ The code is under the [MIT License](LICENSE). The data files in `gunsnprices/data/` are under
207
+ [CC BY 4.0](LICENSE-DATA): you can use them for anything, including commercial use, if you credit
208
+ **GunsNPrices (gunsnprices.com)** with a link.
209
+
210
+ Questions, corrections and missing models: open an issue or write to app@gunsnprices.com.
211
+ Current gun and ammo prices: [gunsnprices.com](https://gunsnprices.com).
@@ -0,0 +1,181 @@
1
+ # gunsnprices
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/gunsnprices)](https://pypi.org/project/gunsnprices/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/gunsnprices)](https://pypi.org/project/gunsnprices/)
5
+ [![Tests](https://github.com/gunsnprices/gunsnprices-python/actions/workflows/ci.yml/badge.svg)](https://github.com/gunsnprices/gunsnprices-python/actions/workflows/ci.yml)
6
+ [![License](https://img.shields.io/badge/license-MIT%20%2B%20CC%20BY%204.0%20data-blue)](#license)
7
+
8
+ Parse firearm and ammunition product titles into structured data: brand, model, caliber, magazine capacity, barrel length,
9
+ rounds per box and cost per round.
10
+
11
+ ```python
12
+ >>> from gunsnprices import parse_title
13
+ >>> info = parse_title("Glock 19 Gen 5 MOS 9mm 4.02in 15rd Pistol PA195S203MOS")
14
+ >>> info.model.model, info.caliber.name, info.capacity, info.barrel_length
15
+ ('Glock 19 Gen 5 MOS', '9×19mm Parabellum', 15, 4.02)
16
+ ```
17
+
18
+ This is the title parser behind [GunsNPrices](https://gunsnprices.com), a price comparison site that tracks 40+ US gun
19
+ and ammo stores. Store titles are messy (`G19 GEN5 9MM 15RD FS`, `O/U 12/28 BL/WD 3"`, `Federal 308 150gr ... 20rds`),
20
+ and matching the same product across stores starts with reading them reliably. The model dictionaries and the caliber
21
+ list in this package are the ones the site uses to group listings, so you get the same model names you see on
22
+ gunsnprices.com.
23
+
24
+ No dependencies, pure Python 3.9+, about 1 ms per title.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install gunsnprices
30
+ ```
31
+
32
+ A JavaScript version with the same API and the same test cases is on npm: `npm install gunsnprices`
33
+ ([source](https://github.com/gunsnprices/gunsnprices-js)).
34
+
35
+ ## What it recognizes
36
+
37
+ | Field | Example in a title | Result |
38
+ |---|---|---|
39
+ | Firearm or ammo | `... 150gr ... 20rds` | `kind="ammo"` |
40
+ | Brand and model (50 brands) | `G19 Gen5 MOS`, part number `PA195S203` | `Glock 19 Gen 5 MOS` |
41
+ | Calibers (293) | `.357 Mag / .38 Spl`, `6.5 CM`, `12ga`, `300 BLK`, `7MM RM` | `.357 Mag`, `.38 S&W Special`, ... |
42
+ | Magazine capacity | `15+1`, `17RD`, `13rd/15rd`, `10-shot` | `15`, `17`, `15`, `10` |
43
+ | Barrel length, inches | `4.02in`, `16 1/2"`, `22 BBL`, `O/U 12/28` | `4.02`, `16.5`, `22.0`, `28.0` |
44
+ | Rounds per box | `50/Box`, `Box of 20`, `1,000 Rounds`, `20/25` | `50`, `20`, `1000`, `20` |
45
+ | Cost per round | price `449.99` + `1000 Rounds` | `0.45` |
46
+
47
+ Brands with model dictionaries: Barrett, Benelli, Beretta, Bergara, Browning, Cadex, Canik, Charles Daly, Chiappa,
48
+ Christensen Arms, Cimarron, CMMG, Colt, CVA, CZ, Daniel Defense, Desert Eagle, Diamondback, Fierce, FN, Glock,
49
+ Great Lakes, Henry, Heritage, HK, Howa, Kel-Tec, Kimber, Kriss, Marlin, Mossberg, Remington, Retay, Rossi, Ruger, Savage,
50
+ SCCY, Seekins, Shadow Systems, Sig Sauer, Smith & Wesson, Springfield, Stevens, Stoeger, Taurus, Tikka, Tristar, Walther,
51
+ Weatherby, Winchester.
52
+
53
+ ## Usage
54
+
55
+ ### Everything at once
56
+
57
+ ```python
58
+ from gunsnprices import parse_title
59
+
60
+ info = parse_title("Federal American Eagle 5.56x45mm 55gr FMJ 1000 Rounds", price=449.99)
61
+ info.kind # 'ammo'
62
+ info.caliber.name # '5.56×45mm NATO'
63
+ info.rounds # 1000
64
+ info.cost_per_round # 0.45
65
+
66
+ info.to_dict() # plain dict, ready for json.dumps
67
+ ```
68
+
69
+ `parse_title(title, brand=None, mpn="", sku="", price=None, kind=None)` returns a `ProductInfo`:
70
+
71
+ | Attribute | Type | Notes |
72
+ |---|---|---|
73
+ | `kind` | `str` | `"firearm"` or `"ammo"`; pass `kind=` to skip the guess |
74
+ | `brand` | `str` | brand of the identified model, else the `brand` you passed |
75
+ | `model` | `ModelMatch` or `None` | firearms only |
76
+ | `caliber` | `Caliber` or `None` | first caliber in the title |
77
+ | `calibers` | `list[Caliber]` | all calibers, in order |
78
+ | `capacity` | `int` or `None` | magazine capacity (for `15+1` it is 15), firearms only |
79
+ | `barrel_length` | `float` or `None` | inches, firearms only |
80
+ | `rounds` | `int` or `None` | rounds per box, ammo only |
81
+ | `cost_per_round` | `float` or `None` | ammo with `price` only |
82
+
83
+ Passing `brand` (the manufacturer field from a feed) helps with titles that don't name the brand. Passing `mpn` lets it
84
+ decode Glock part numbers when the title says nothing useful:
85
+
86
+ ```python
87
+ >>> from gunsnprices import identify_model
88
+ >>> identify_model("GLOCK 9MM PISTOL 15RD", brand="Glock", mpn="UA4350201")
89
+ ModelMatch(brand='Glock', family='Glock 43', model='Glock 43', generation='', configs=[], features=[], source='mpn')
90
+ ```
91
+
92
+ ### Models
93
+
94
+ ```python
95
+ from gunsnprices import identify_model, list_brands
96
+
97
+ m = identify_model("Sig Sauer P365 XL 9mm 3.7\" 12+1 Optic Ready")
98
+ m.brand, m.family, m.model # ('Sig Sauer', 'Sig Sauer P365', 'Sig Sauer P365 XL')
99
+ ```
100
+
101
+ `ModelMatch` has `brand`, `family`, `model` (the most specific name), `generation`, `configs` (such as `MOS`,
102
+ `Compact`), `features` (such as `Threaded Barrel`, `FDE`) and `source` (`"title"` or `"mpn"`). Accessories, parts,
103
+ airguns and airsoft are rejected, so it returns `None` for "Glock 19 holster" and "Umarex Glock 19 CO2 BB pistol".
104
+
105
+ ### Calibers
106
+
107
+ ```python
108
+ from gunsnprices import find_calibers, normalize_caliber, list_calibers
109
+
110
+ [c.name for c in find_calibers("Henry Big Boy .44 Mag/.44 Spl 20in")]
111
+ # ['.44 Magnum', '.44 S&W Special']
112
+
113
+ normalize_caliber("308 WIN (7.62X51 NATO)")
114
+ # Caliber(id='308win', name='.308 Win', type='rifle', url='https://gunsnprices.com/ammo/rifle-ammo/308-win')
115
+ ```
116
+
117
+ Each `Caliber` has a stable `id`, a display `name`, a `type` (`handgun`, `rifle`, `rimfire`, `shotgun`, `airgun`,
118
+ `muzzleloader`) and the `url` of its GunsNPrices price comparison page. `normalize_caliber` is meant for caliber fields
119
+ in product feeds; `find_calibers` searches free text.
120
+
121
+ ### Numbers
122
+
123
+ ```python
124
+ from gunsnprices import magazine_capacity, barrel_length, rounds_per_box, cost_per_round
125
+
126
+ magazine_capacity("Springfield Hellcat Pro OSP 9mm 3.7in 15rd/17rd") # 17
127
+ barrel_length('Mossberg 590 Persuader 12/20 BL/SY 3"') # 20.0 (3" is the chamber)
128
+ rounds_per_box("Winchester 308 150gr PowerMax Bonded 20rds") # 20
129
+ cost_per_round(31.99, 20) # 1.5995
130
+ ```
131
+
132
+ ### Command line
133
+
134
+ ```bash
135
+ $ gunsnprices "Ruger GP100 .357 Mag / .38 Special 4.2in 6-shot Revolver"
136
+ $ cat titles.txt | gunsnprices > parsed.jsonl
137
+ $ gunsnprices --price 18.99 "Federal 9mm 115gr FMJ 50/Box"
138
+ ```
139
+
140
+ ## Accuracy
141
+
142
+ Measured on 50,202 firearm and 13,555 ammunition listings from 40+ stores on gunsnprices.com (October 2026),
143
+ against the attributes stored on the site:
144
+
145
+ | What | Found in the title | Right when found |
146
+ |---|---|---|
147
+ | Model (32,317 listings with a known model) | 99.97% | 100% same model as the site |
148
+ | Caliber, firearms | 81% | 97.8% |
149
+ | Caliber, ammo | 91% | 94.9% |
150
+ | Magazine capacity | 46% | 95.6% |
151
+ | Barrel length | 58% | 94.4% |
152
+ | Rounds per box | 80% | 96.6% |
153
+
154
+ "Found" is limited by the titles themselves: many stores don't put the caliber or barrel length in the title at all.
155
+ Most of the "wrong" cases we checked by hand are errors in the store data, not in the parser: a 6mm Creedmoor rifle
156
+ labeled 6.5 Creedmoor, a "1000 Rounds" case stored as a box of 20.
157
+
158
+ ## Data
159
+
160
+ The model dictionaries and caliber list ship inside the package (`gunsnprices/data/`). The same data, with listing
161
+ counts and price levels, is published as open datasets:
162
+
163
+ - Hugging Face: [gunsnprices/firearm-calibers](https://huggingface.co/datasets/gunsnprices/firearm-calibers),
164
+ [gunsnprices/firearm-models](https://huggingface.co/datasets/gunsnprices/firearm-models)
165
+ - GitHub: [gunsnprices/firearm-datasets](https://github.com/gunsnprices/firearm-datasets)
166
+ - Kaggle: [gunsnprices](https://www.kaggle.com/gunsnprices)
167
+
168
+ ## Limitations
169
+
170
+ - Titles from US retailers, in English.
171
+ - Models are covered for the 50 brands above. Other brands still get calibers and numbers.
172
+ - Built for firearms and ammunition. Magazines, parts and accessories get calibers and numbers but no model.
173
+
174
+ ## License
175
+
176
+ The code is under the [MIT License](LICENSE). The data files in `gunsnprices/data/` are under
177
+ [CC BY 4.0](LICENSE-DATA): you can use them for anything, including commercial use, if you credit
178
+ **GunsNPrices (gunsnprices.com)** with a link.
179
+
180
+ Questions, corrections and missing models: open an issue or write to app@gunsnprices.com.
181
+ Current gun and ammo prices: [gunsnprices.com](https://gunsnprices.com).
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "gunsnprices"
7
+ dynamic = ["version"]
8
+ description = "Parse firearm and ammunition product titles: brand, model, caliber, magazine capacity, barrel length, rounds per box."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT AND CC-BY-4.0"
12
+ license-files = ["LICENSE", "LICENSE-DATA"]
13
+ authors = [{ name = "GunsNPrices", email = "app@gunsnprices.com" }]
14
+ keywords = ["firearms", "guns", "ammunition", "ammo", "caliber", "glock", "title-parsing", "product-matching", "ecommerce", "price-comparison"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Natural Language :: English",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Topic :: Text Processing :: General",
28
+ "Topic :: Office/Business",
29
+ "Typing :: Typed",
30
+ ]
31
+ dependencies = []
32
+
33
+ [project.urls]
34
+ Homepage = "https://gunsnprices.com"
35
+ Source = "https://github.com/gunsnprices/gunsnprices-python"
36
+ Issues = "https://github.com/gunsnprices/gunsnprices-python/issues"
37
+ Datasets = "https://huggingface.co/gunsnprices"
38
+
39
+ [project.scripts]
40
+ gunsnprices = "gunsnprices.__main__:main"
41
+
42
+ [tool.hatch.version]
43
+ path = "src/gunsnprices/__init__.py"
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["src/gunsnprices"]
47
+
48
+ [tool.hatch.build.targets.sdist]
49
+ include = ["src", "tests", "README.md", "LICENSE", "LICENSE-DATA", "CHANGELOG.md"]
50
+
51
+ [tool.pytest.ini_options]
52
+ testpaths = ["tests"]
53
+ addopts = "--import-mode=importlib"
@@ -0,0 +1,18 @@
1
+ """gunsnprices - parse firearm and ammunition product titles.
2
+
3
+ Brand and model (Glock 19 Gen 5 MOS), caliber, magazine capacity, barrel length, rounds per box and cost per round.
4
+ Built on the model dictionaries and caliber list behind https://gunsnprices.com.
5
+ """
6
+ from .calibers import Caliber, find_caliber, find_calibers, list_calibers, normalize_caliber
7
+ from .models import ModelMatch, identify_model, list_brands
8
+ from .parse import ProductInfo, parse_title
9
+ from .specs import barrel_length, cost_per_round, looks_like_ammo, magazine_capacity, rounds_per_box
10
+
11
+ __version__ = "0.1.0"
12
+ __all__ = [
13
+ "parse_title", "ProductInfo",
14
+ "identify_model", "ModelMatch", "list_brands",
15
+ "find_calibers", "find_caliber", "normalize_caliber", "list_calibers", "Caliber",
16
+ "magazine_capacity", "barrel_length", "rounds_per_box", "cost_per_round", "looks_like_ammo",
17
+ "__version__",
18
+ ]
@@ -0,0 +1,29 @@
1
+ """Command line: ``python -m gunsnprices "Glock 19 Gen 5 9mm 15rd"`` prints the parsed title as JSON.
2
+
3
+ Without arguments, titles are read from standard input, one per line (JSON Lines out).
4
+ """
5
+ import argparse
6
+ import json
7
+ import sys
8
+
9
+ from . import __version__, parse_title
10
+
11
+
12
+ def main(argv=None) -> int:
13
+ ap = argparse.ArgumentParser(prog="gunsnprices", description="Parse firearm and ammunition product titles.")
14
+ ap.add_argument("title", nargs="*", help="product title (reads stdin when omitted)")
15
+ ap.add_argument("--brand", help="manufacturer, if known")
16
+ ap.add_argument("--price", type=float, help="listing price (gives cost per round for ammo)")
17
+ ap.add_argument("--kind", choices=["firearm", "ammo"], help="skip the firearm/ammo guess")
18
+ ap.add_argument("--version", action="version", version=f"gunsnprices {__version__}")
19
+ a = ap.parse_args(argv)
20
+ titles = [" ".join(a.title)] if a.title else (line.strip() for line in sys.stdin)
21
+ for t in titles:
22
+ if t:
23
+ info = parse_title(t, brand=a.brand, price=a.price, kind=a.kind)
24
+ print(json.dumps(info.to_dict(), ensure_ascii=False, indent=None if not a.title else 2))
25
+ return 0
26
+
27
+
28
+ if __name__ == "__main__":
29
+ sys.exit(main())