nplus1 1.0.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,109 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ env/
12
+ build/
13
+ develop-eggs/
14
+ dist/
15
+ downloads/
16
+ eggs/
17
+ .eggs/
18
+ lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+
28
+ # PyInstaller
29
+ # Usually these files are written by a python script from a template
30
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
31
+ *.manifest
32
+ *.spec
33
+
34
+ # Installer logs
35
+ pip-log.txt
36
+ pip-delete-this-directory.txt
37
+
38
+ # Unit test / coverage reports
39
+ htmlcov/
40
+ .tox/
41
+ .coverage
42
+ .coverage.*
43
+ .cache
44
+ nosetests.xml
45
+ coverage.xml
46
+ *.cover
47
+ .hypothesis/
48
+ .pytest_cache/
49
+
50
+ # Translations
51
+ *.mo
52
+ *.pot
53
+
54
+ # Django stuff:
55
+ *.log
56
+ local_settings.py
57
+
58
+ # Flask stuff:
59
+ instance/
60
+ .webassets-cache
61
+
62
+ # Scrapy stuff:
63
+ .scrapy
64
+
65
+ # Sphinx documentation
66
+ docs/_build/
67
+ html/
68
+
69
+ # PyBuilder
70
+ target/
71
+
72
+ # Jupyter Notebook
73
+ .ipynb_checkpoints
74
+
75
+ # Dask worker cache
76
+ dask-worker-space/
77
+
78
+ # celery beat schedule file
79
+ celerybeat-schedule
80
+
81
+ # SageMath parsed files
82
+ *.sage.py
83
+
84
+ # dotenv
85
+ .env
86
+
87
+ # virtualenv
88
+ .venv
89
+ .venv-*/
90
+ venv/
91
+ ENV/
92
+
93
+ # Spyder project settings
94
+ .spyderproject
95
+ .spyproject
96
+
97
+ # Rope project settings
98
+ .ropeproject
99
+
100
+ # mkdocs documentation
101
+ /site
102
+
103
+ # mypy
104
+ .mypy_cache/
105
+
106
+ # IDE settings
107
+ .vscode/
108
+ .idea/
109
+
@@ -0,0 +1,13 @@
1
+ =======
2
+ Credits
3
+ =======
4
+
5
+ Development Lead
6
+ ----------------
7
+
8
+ * Huy Nguyen <ndhgl99@gmail.com>
9
+
10
+ Contributors
11
+ ------------
12
+
13
+ None yet. Why not be the first?
nplus1-1.0.0/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026, Huy Nguyen
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.
22
+
nplus1-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,346 @@
1
+ Metadata-Version: 2.4
2
+ Name: nplus1
3
+ Version: 1.0.0
4
+ Summary: nplusone is a library for detecting the n+1 queries problem in Python ORMs, including SQLAlchemy, Peewee, and the Django ORM
5
+ Project-URL: Homepage, https://github.com/huynguyengl99/nplus1
6
+ Project-URL: Repository, https://github.com/huynguyengl99/nplus1
7
+ Author-email: Huy Nguyen <ndhgl99@gmail.com>
8
+ License: MIT License
9
+
10
+ Copyright (c) 2026, Huy Nguyen
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+
30
+ License-File: AUTHORS.rst
31
+ License-File: LICENSE
32
+ Classifier: Environment :: Web Environment
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: Intended Audience :: Information Technology
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Operating System :: OS Independent
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3 :: Only
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Programming Language :: Python :: 3.13
42
+ Classifier: Programming Language :: Python :: 3.14
43
+ Classifier: Topic :: Internet :: WWW/HTTP
44
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
45
+ Classifier: Topic :: Software Development
46
+ Classifier: Topic :: Software Development :: Libraries
47
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
48
+ Requires-Python: <4.0,>=3.11
49
+ Requires-Dist: blinker>=1.9
50
+ Provides-Extra: celery
51
+ Requires-Dist: celery>=5.3; extra == 'celery'
52
+ Provides-Extra: django
53
+ Requires-Dist: django>=4.2; extra == 'django'
54
+ Provides-Extra: flask
55
+ Requires-Dist: flask-sqlalchemy>=3.0; extra == 'flask'
56
+ Requires-Dist: flask>=2.0; extra == 'flask'
57
+ Provides-Extra: peewee
58
+ Requires-Dist: peewee>=3.15; extra == 'peewee'
59
+ Provides-Extra: sqlalchemy
60
+ Requires-Dist: sqlalchemy>=2.0; extra == 'sqlalchemy'
61
+ Description-Content-Type: text/markdown
62
+
63
+ # nplusone
64
+
65
+ Detect the N+1 queries problem in Python ORMs — SQLAlchemy, Peewee, and Django ORM.
66
+
67
+ A modern rewrite of the [original nplusone](https://github.com/jmcarp/nplusone) library, targeting Python 3.11+ with full type annotations, SQLAlchemy 2.0 support, and fixes for false positives found in production Django+DRF codebases.
68
+
69
+ [![PyPI](https://img.shields.io/pypi/v/nplus1)](https://pypi.org/project/nplus1/)
70
+ [![Python](https://img.shields.io/pypi/pyversions/nplus1)](https://pypi.org/project/nplus1/)
71
+ [![Tests](https://github.com/huynguyengl99/nplus1/actions/workflows/test.yml/badge.svg)](https://github.com/huynguyengl99/nplus1/actions)
72
+ [![Coverage](https://codecov.io/gh/huynguyengl99/nplus1/branch/main/graph/badge.svg)](https://codecov.io/gh/huynguyengl99/nplus1)
73
+
74
+ ## Installation
75
+
76
+ ```bash
77
+ pip install nplus1
78
+ ```
79
+
80
+ With optional ORM/framework dependencies:
81
+
82
+ ```bash
83
+ pip install nplus1[django] # Django 4.2+
84
+ pip install nplus1[sqlalchemy] # SQLAlchemy 2.0+
85
+ pip install nplus1[flask] # Flask + Flask-SQLAlchemy
86
+ pip install nplus1[peewee] # Peewee 3.15+
87
+ ```
88
+
89
+ ## Quick Start
90
+
91
+ ### Django
92
+
93
+ Add the middleware to your **dev** settings:
94
+
95
+ ```python
96
+ # settings/dev.py
97
+ MIDDLEWARE = [
98
+ ...
99
+ "nplusone.ext.django.NPlusOneMiddleware",
100
+ ...
101
+ ]
102
+
103
+ NPLUSONE_ENABLED = True # set False in prod (zero overhead)
104
+ NPLUSONE_LOG = True # log detections (default)
105
+ NPLUSONE_RAISE = True # raise exceptions in dev/test
106
+ ```
107
+
108
+ ### Flask + SQLAlchemy
109
+
110
+ ```python
111
+ from nplusone.ext.flask_sqlalchemy import NPlusOne
112
+
113
+ app = Flask(__name__)
114
+ NPlusOne(app)
115
+ ```
116
+
117
+ ### Standalone (any code)
118
+
119
+ ```python
120
+ from nplusone.core.profiler import Profiler
121
+
122
+ with Profiler():
123
+ users = session.query(User).all()
124
+ for user in users:
125
+ user.addresses # NPlusOneError raised
126
+ ```
127
+
128
+ ### Celery
129
+
130
+ ```python
131
+ from nplusone.ext.celery import NPlusOneCelery
132
+
133
+ app = Celery("myapp")
134
+ NPlusOneCelery(app)
135
+ ```
136
+
137
+ Or manually with signals:
138
+
139
+ ```python
140
+ from celery.signals import task_prerun, task_postrun
141
+ from nplusone.core.profiler import setup, teardown
142
+
143
+ @task_prerun.connect()
144
+ def on_prerun(**kwargs):
145
+ setup()
146
+
147
+ @task_postrun.connect()
148
+ def on_postrun(**kwargs):
149
+ teardown()
150
+ ```
151
+
152
+ ## What It Detects
153
+
154
+ ### N+1 lazy loads
155
+
156
+ ```python
157
+ users = User.objects.all() # 1 query
158
+ for user in users:
159
+ print(user.addresses) # N queries — flagged!
160
+ ```
161
+
162
+ **Fix:** use `select_related` or `prefetch_related`:
163
+
164
+ ```python
165
+ users = User.objects.select_related("addresses").all()
166
+ ```
167
+
168
+ ### Unnecessary eager loads
169
+
170
+ ```python
171
+ users = User.objects.select_related("occupation").all()
172
+ for user in users:
173
+ print(user.name) # occupation never accessed — flagged!
174
+ ```
175
+
176
+ ## Configuration
177
+
178
+ All settings work across Django, Flask, and Celery:
179
+
180
+ | Setting | Default | Description |
181
+ |---------|---------|-------------|
182
+ | `NPLUSONE_ENABLED` | `True` | Master switch. Set `False` in prod for zero overhead. |
183
+ | `NPLUSONE_LOG` | `True` | Log detections to the `nplusone` logger. |
184
+ | `NPLUSONE_RAISE` | `False` | Raise `NPlusOneError` on detection. |
185
+ | `NPLUSONE_WHITELIST` | `[]` | List of rule dicts to suppress specific warnings. |
186
+ | `NPLUSONE_LOGGER` | `logging.getLogger("nplusone")` | Custom logger instance. |
187
+ | `NPLUSONE_LOG_LEVEL` | `DEBUG` | Log level for detections. |
188
+ | `NPLUSONE_DEBUG` | `False` | Verbose signal logging to `nplusone.debug` logger. |
189
+ | `NPLUSONE_REPORT_MODE` | `"immediate"` | `"immediate"` or `"batch"`. Batch collects all detections and reports at end of request. |
190
+ | `NPLUSONE_SKIP_EAGER_ON_ERROR` | `True` | Skip eager load checks on error responses (>= 400). |
191
+ | `NPLUSONE_EAGER_LOAD_SKIP` | `None` | Callable `(request, response) -> bool` for custom skip logic. |
192
+ | `NPLUSONE_SKIP_EMPTY_PREFETCH` | `False` | Skip flagging `prefetch_related` that returns zero rows. |
193
+
194
+ ### Whitelisting
195
+
196
+ Suppress specific warnings by model, field, or pattern:
197
+
198
+ ```python
199
+ NPLUSONE_WHITELIST = [
200
+ {"model": "User", "field": "profile"}, # exact match
201
+ {"model": "myapp.User"}, # Django app_label.Model format
202
+ {"model": "User*"}, # fnmatch wildcard
203
+ {"label": "unused_eager_load"}, # suppress all eager load warnings
204
+ ]
205
+ ```
206
+
207
+ ### Prod/Dev Split
208
+
209
+ Only add the middleware in dev/test settings — no need for it in production:
210
+
211
+ ```python
212
+ # settings/dev.py (or settings/test.py)
213
+ MIDDLEWARE = [
214
+ ...
215
+ "nplusone.ext.django.NPlusOneMiddleware",
216
+ ...
217
+ ]
218
+ NPLUSONE_RAISE = True
219
+ ```
220
+
221
+ No `INSTALLED_APPS` entry is needed — the ORM patches are applied
222
+ automatically when the middleware is imported.
223
+
224
+ For Celery, use `NPLUSONE_ENABLED` to control whether detection runs:
225
+
226
+ ```python
227
+ # settings/base.py
228
+ NPLUSONE_ENABLED = False # Celery setup() is a no-op
229
+
230
+ # settings/dev.py
231
+ NPLUSONE_ENABLED = True # Celery detection active
232
+ ```
233
+
234
+ ## Debug Mode
235
+
236
+ Enable `NPLUSONE_DEBUG = True` to see every signal fire during a request:
237
+
238
+ ```
239
+ [nplusone.debug] REQUEST START: GET /api/orders/
240
+ [nplusone.debug] EAGER_REGISTER: Order.customer (5 instances) at views.py:42 in get_queryset
241
+ [nplusone.debug] EAGER_ACCESS: Order.customer (1 instances) at serializers.py:18 in to_representation
242
+ [nplusone.debug] DETECTED: Potential unnecessary eager load on Order.shipping_address
243
+ [nplusone.debug] REQUEST END: GET /api/orders/ → 200
244
+ ```
245
+
246
+ Detection messages include the registration site (inspired by
247
+ [django-zeal](https://github.com/taobojlen/django-zeal)'s `ZEAL_SHOW_ALL_CALLERS`):
248
+
249
+ ```
250
+ Potential unnecessary eager load detected on `Order.shipping_address`
251
+ Registered at: myapp/views.py:42 in get_queryset
252
+ qs.select_related("customer", "shipping_address")
253
+ ```
254
+
255
+ ## Comparison
256
+
257
+ ### vs. [jmcarp/nplusone](https://github.com/jmcarp/nplusone) (original)
258
+
259
+ This library is a ground-up rewrite of the original nplusone, which has been
260
+ unmaintained since 2020. We preserve the same detection architecture
261
+ (blinker signals + ORM monkey-patching) but modernize everything else:
262
+
263
+ | | Original nplusone | This library |
264
+ |---|---|---|
265
+ | Python | 2.7+ / 3.3+ | 3.11+ |
266
+ | Type hints | None | Full (mypy strict + pyright strict) |
267
+ | SQLAlchemy | 1.x only | 2.0+ |
268
+ | Django | 1.8+ (compat code) | 4.2 – 5.2 (clean) |
269
+ | Nullable FK | False positive | Skipped (valid optimization) |
270
+ | MTI / Polymorphic | False positives | PK-based cross-model matching |
271
+ | Error responses | False positive | Skipped on 4xx/5xx (configurable) |
272
+ | Celery | Not supported | `NPlusOneCelery(app)` + `setup()`/`teardown()` |
273
+ | Debug/trace mode | Not available | `NPLUSONE_DEBUG` with full signal logging |
274
+ | Stack traces | Not in messages | Registration site in every detection |
275
+ | Batch reporting | Not available | `NPLUSONE_REPORT_MODE = "batch"` |
276
+ | Prod switch | Not available | `NPLUSONE_ENABLED = False` (zero overhead) |
277
+ | Dependencies | `six`, `blinker` | `blinker` only |
278
+
279
+ ### vs. [django-zeal](https://github.com/taobojlen/django-zeal)
280
+
281
+ django-zeal is a Django-only N+1 detector with a different approach.
282
+
283
+ | | django-zeal | This library |
284
+ |---|---|---|
285
+ | ORMs | Django only | Django, SQLAlchemy, Peewee |
286
+ | Detect N+1 lazy loads | Yes | Yes |
287
+ | Detect unused eager loads | No | Yes |
288
+ | Detect `.defer()`/`.only()` issues | Yes | No |
289
+ | Configurable threshold | Yes (`ZEAL_NPLUSONE_THRESHOLD`) | No (flags on first repeat) |
290
+ | Non-invasive in prod | Yes (no patching when inactive) | Yes (`NPLUSONE_ENABLED = False` skips all setup) |
291
+ | Stack traces | Yes (`ZEAL_SHOW_ALL_CALLERS`) | Yes (always included) |
292
+ | Celery | Manual `setup()`/`teardown()` | `NPlusOneCelery(app)` or manual |
293
+ | Batch reporting | No | Yes |
294
+
295
+ **Choose nplusone** if you need multi-ORM support, unused eager load detection,
296
+ or work with complex Django patterns (MTI, polymorphic models, DRF).
297
+
298
+ **Choose django-zeal** if you only use Django and want `.defer()`/`.only()`
299
+ detection or configurable thresholds.
300
+
301
+ ## Development
302
+
303
+ ```bash
304
+ # Setup
305
+ uv sync
306
+
307
+ # Run tests
308
+ python -m pytest tests/
309
+
310
+ # Run tests for specific ORM
311
+ tox -e py311-django52
312
+ tox -e py311-sqlalchemy
313
+ tox -e py311-peewee
314
+ tox -e py311-flask
315
+
316
+ # Lint and type check
317
+ ruff check nplusone/ tests/
318
+ python -m mypy nplusone/
319
+ npx pyright
320
+
321
+ # Coverage
322
+ python -m pytest tests/ --cov=nplusone --cov-report=term-missing
323
+ ```
324
+
325
+ ### Multi-version Testing
326
+
327
+ ```bash
328
+ # Full matrix
329
+ tox
330
+
331
+ # Specific Python + Django version
332
+ tox -e py312-django51
333
+ tox -e py313-django42
334
+ ```
335
+
336
+ ### Docker (PostgreSQL)
337
+
338
+ ```bash
339
+ docker compose up -d
340
+ cp .env.EXAMPLE .env
341
+ python -m pytest tests/testapp/
342
+ ```
343
+
344
+ ## License
345
+
346
+ MIT. See [LICENSE](LICENSE).