django-queryset-feeler


Namedjango-queryset-feeler JSON
Version 0.0.6 PyPI version JSON
download
home_page
Summarytools to help understand how django querysets are executed
upload_time2023-10-04 13:31:46
maintainer
docs_urlNone
author
requires_python>=3.7
licenseMIT License Copyright (c) 2022 Lukas Schillinger Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
keywords django query queryset
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            [![This is an image](https://img.shields.io/pypi/v/django-queryset-feeler.svg?style=flat-square)](https://pypi.python.org/pypi/django-queryset-feeler)

# django-queryset-feeler

Get a better feel for how your django views and serializers are accessing your app’s database. Use django-queryset-feeler (dqf) to measure the count, execution time, and raw SQL of your queries from the command line, ipython shell, or jupyter notebook without any configuration.

This extension is used differently than the popular [django-debug-toolbar](https://github.com/jazzband/django-debug-toolbar) in a few ways. First, dqf can be used to profile more than just views. You can pass functions, querysets, model instances, views, class based views, and [django-rest-framework](https://github.com/encode/django-rest-framework/) serializers to dqf for profiling. Second, dqf profiles queries with only one object and can be used in the command line, ipython shell, or jupyter notebook. This is especially useful for prototyping or learning how django querysets are executed. 

## Usage
```
pip install django-queryset-feeler
```
```python
from django_queryset_feeler import Feel
```
Create a `Feel()` instance by passing it any one of the following objects from your django project. No other configuration is required. 
| Query Type | About |
| :--- | :--- |
| `Feel(view)`| Execute a view using an empty HttpRequest. Add a `request` key word argument to supply your own request. | 
| `Feel(ClassBasedView)` | Execute an eligible class based view using an empty HttpRequest with a `GET` method. Add a `request` key word argument to supply your own request. |
| `Feel(serializer)` | Execute a serializer on the model specified by the serializer's Meta class. |
| `Feel(queryset)` | Execute a queryset |
| `Feel(model_instance)` | Execute a model instance by calling it again from the database using `.refresh_from_db()` |
| `Feel(function)` | Execute a function |

Profile your queries using any of the following properties. 

| Property | About 
| :--- | :---
| `feel.query_time`           | Repeat the query 100 times (adjust iterations with the `iterations` key word argument) and return the average query duration in seconds.  
| `feel.query_count` | Execute the query and return the number of times that the database was accessed. 
| `feel.sql_queries` | Execute the query and return a formatted copy of the raw SQL. 
| `feel.table_counts` | Execute the query and return a dictionary containing each table and how many times it was accessed. 
|`feel.report` | Print the query time, count, and table count summary.  

## Example
The below example illustrates an easy to make django queryset mistake called an 'n + 1 query' and how to use dqf to find it.   
#### `project / app / models.py`
```python
class Topping(models.Model):
    name = CharField()
    vegetarian = BooleanField()

class Pizza(models.Model):
    name = CharField()
    toppings = ManyToManyField(Topping)
```
#### `project / app / views.py`
```python
def pizza_list(request):
    pizzas = Pizza.objects.all()
    return render(request, 'pizza_list.html' context={'pizzas': pizzas})
```
#### `project / app / templates / app / pizza_list.html`
```html
{% for pizza in pizzas %}
<tr>
    <td>{{ pizza.name }}</td>
    <td>
    {% for topping in pizza.toppings %}
        {{ topping.name }}
    {% endfor %}
    </td>
    <td>
    {% with last=pizza.toppings|dictsort:'vegetarian'|last %}
        {% if last.vegetarian %}
            🌱
        {% else %}
            🥩
        {% endif %}
    {% endwith %}
    </td>
<tr>
{% endfor %}
```

| Pizza | Toppings | |
| ---: | --- | ---
| mediterranean | roasted eggplant, balsamic glaze | 🌱
| hawaiian | pineapple, smoked ham | 🥩
| meat lovers | pepperoni, andouille sausage, capicola | 🥩


#### `project / dqf.ipynb`
Note that the `DEBUG` setting in `project / settings.py` must be `True` for dqf to work. `DEBUG` is enabled by default when you create a django project. 
```python
from django_queryset_feeler import Feel
from app.views import pizza_list

feel = Feel(pizza_list)

print(f'query count: {feel.query_count}')
print(f'average duration: {feel.duration} s')
print(feel.sql_queries)
```

```python
'query count: 4'
'average duration: 0.00023 s'

SELECT "app_pizza"."id",
       "app_pizza"."name",
FROM "app_pizza"

SELECT "app_topping"."id",
       "app_topping"."name",
       "app_topping"."vegetarian"
FROM "app_topping"
WHERE "app_topping"."id" = '0'

SELECT "app_topping"."id",
       "app_topping"."name",
       "app_topping"."vegetarian"
FROM "app_topping"
WHERE "app_topping"."id" = '1'

SELECT "app_topping"."id",
       "app_topping"."name",
       "app_topping"."vegetarian"
FROM "app_topping"
WHERE "app_topping"."id" = '2'
```
In the above example django queried the database a total of 4 times: once to get a list of pizzas and then again for each pizza to find its toppings. As more pizzas are added to the menu n + 1 queries would be made to the database where n is the number of pizzas. 

Note that even though the pizza's toppings are accessed once in column 2 for the name and again in column 3 to determine if the pizza is vegetarian the database is still accessed only once in this period. This is because after evaluation the results are stored in the queryset object and used for subsequent calls. 

A more efficient way to render this template would be to fetch the list of pizzas and then query the toppings table once to get all the toppings for all the pizzas. Django makes this easy using [prefetch_related()](https://docs.djangoproject.com/en/4.0/ref/models/querysets/#prefetch-related). 
#### `project / app / views.py` 
```python
def pizza_list(request):
    pizzas = Pizza.objects.all().prefetch_related('toppings')
    return render(request, 'pizza_list.html' context={'pizzas': pizzas})
```
#### `project / dqf.ipynb`
```python
feel = Feel(pizza_list)
feel.report
```
```python
     query count: 2         
average duration: 0.069 ms                
   unique tables: 2         
        accessed   
```

## Run Django in a Jupyter Notebook

#### `project / dqf.ipynb`
```python 
# re-import modules when a cell is run. This ensures that changes made to
# the django app are synced with your notebook
%load_ext autoreload
%autoreload 2

import django
import os

# change 'project.settings' to '{your_project}.settings'
os.environ['DJANGO_SETTINGS_MODULE'] = 'project.settings'
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
django.setup()
```

            

Raw data

            {
    "_id": null,
    "home_page": "",
    "name": "django-queryset-feeler",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.7",
    "maintainer_email": "",
    "keywords": "django,query,queryset",
    "author": "",
    "author_email": "Lukas Schillinger <lukas@schillingertools.com>",
    "download_url": "https://files.pythonhosted.org/packages/79/5e/3c5cacda2a7f282dc01ea7726142427d17ee9a65161f8db3bd50a5532eac/django-queryset-feeler-0.0.6.tar.gz",
    "platform": null,
    "description": "[![This is an image](https://img.shields.io/pypi/v/django-queryset-feeler.svg?style=flat-square)](https://pypi.python.org/pypi/django-queryset-feeler)\n\n# django-queryset-feeler\n\nGet a better feel for how your django views and serializers are accessing your app\u2019s database. Use django-queryset-feeler (dqf) to measure the count, execution time, and raw SQL of your queries from the command line, ipython shell, or jupyter notebook without any configuration.\n\nThis extension is used differently than the popular [django-debug-toolbar](https://github.com/jazzband/django-debug-toolbar) in a few ways. First, dqf can be used to profile more than just views. You can pass functions, querysets, model instances, views, class based views, and [django-rest-framework](https://github.com/encode/django-rest-framework/) serializers to dqf for profiling. Second, dqf profiles queries with only one object and can be used in the command line, ipython shell, or jupyter notebook. This is especially useful for prototyping or learning how django querysets are executed. \n\n## Usage\n```\npip install django-queryset-feeler\n```\n```python\nfrom django_queryset_feeler import Feel\n```\nCreate a `Feel()` instance by passing it any one of the following objects from your django project. No other configuration is required. \n| Query Type | About |\n| :--- | :--- |\n| `Feel(view)`| Execute a view using an empty HttpRequest. Add a `request` key word argument to supply your own request. | \n| `Feel(ClassBasedView)` | Execute an eligible class based view using an empty HttpRequest with a `GET` method. Add a `request` key word argument to supply your own request. |\n| `Feel(serializer)` | Execute a serializer on the model specified by the serializer's Meta class. |\n| `Feel(queryset)` | Execute a queryset |\n| `Feel(model_instance)` | Execute a model instance by calling it again from the database using `.refresh_from_db()` |\n| `Feel(function)` | Execute a function |\n\nProfile your queries using any of the following properties. \n\n| Property | About \n| :--- | :---\n| `feel.query_time`&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  | Repeat the query 100 times (adjust iterations with the `iterations` key word argument) and return the average query duration in seconds.  \n| `feel.query_count` | Execute the query and return the number of times that the database was accessed. \n| `feel.sql_queries` | Execute the query and return a formatted copy of the raw SQL. \n| `feel.table_counts` | Execute the query and return a dictionary containing each table and how many times it was accessed. \n|`feel.report` | Print the query time, count, and table count summary.  \n\n## Example\nThe below example illustrates an easy to make django queryset mistake called an 'n + 1 query' and how to use dqf to find it.   \n#### `project / app / models.py`\n```python\nclass Topping(models.Model):\n    name = CharField()\n    vegetarian = BooleanField()\n\nclass Pizza(models.Model):\n    name = CharField()\n    toppings = ManyToManyField(Topping)\n```\n#### `project / app / views.py`\n```python\ndef pizza_list(request):\n    pizzas = Pizza.objects.all()\n    return render(request, 'pizza_list.html' context={'pizzas': pizzas})\n```\n#### `project / app / templates / app / pizza_list.html`\n```html\n{% for pizza in pizzas %}\n<tr>\n    <td>{{ pizza.name }}</td>\n    <td>\n    {% for topping in pizza.toppings %}\n        {{ topping.name }}\n    {% endfor %}\n    </td>\n    <td>\n    {% with last=pizza.toppings|dictsort:'vegetarian'|last %}\n        {% if last.vegetarian %}\n            \ud83c\udf31\n        {% else %}\n            \ud83e\udd69\n        {% endif %}\n    {% endwith %}\n    </td>\n<tr>\n{% endfor %}\n```\n\n| Pizza | Toppings | |\n| ---: | --- | ---\n| mediterranean | roasted eggplant, balsamic glaze | \ud83c\udf31\n| hawaiian | pineapple, smoked ham | \ud83e\udd69\n| meat lovers | pepperoni, andouille sausage, capicola | \ud83e\udd69\n\n\n#### `project / dqf.ipynb`\nNote that the `DEBUG` setting in `project / settings.py` must be `True` for dqf to work. `DEBUG` is enabled by default when you create a django project. \n```python\nfrom django_queryset_feeler import Feel\nfrom app.views import pizza_list\n\nfeel = Feel(pizza_list)\n\nprint(f'query count: {feel.query_count}')\nprint(f'average duration: {feel.duration} s')\nprint(feel.sql_queries)\n```\n\n```python\n'query count: 4'\n'average duration: 0.00023 s'\n\nSELECT \"app_pizza\".\"id\",\n       \"app_pizza\".\"name\",\nFROM \"app_pizza\"\n\nSELECT \"app_topping\".\"id\",\n       \"app_topping\".\"name\",\n       \"app_topping\".\"vegetarian\"\nFROM \"app_topping\"\nWHERE \"app_topping\".\"id\" = '0'\n\nSELECT \"app_topping\".\"id\",\n       \"app_topping\".\"name\",\n       \"app_topping\".\"vegetarian\"\nFROM \"app_topping\"\nWHERE \"app_topping\".\"id\" = '1'\n\nSELECT \"app_topping\".\"id\",\n       \"app_topping\".\"name\",\n       \"app_topping\".\"vegetarian\"\nFROM \"app_topping\"\nWHERE \"app_topping\".\"id\" = '2'\n```\nIn the above example django queried the database a total of 4 times: once to get a list of pizzas and then again for each pizza to find its toppings. As more pizzas are added to the menu n + 1 queries would be made to the database where n is the number of pizzas. \n\nNote that even though the pizza's toppings are accessed once in column 2 for the name and again in column 3 to determine if the pizza is vegetarian the database is still accessed only once in this period. This is because after evaluation the results are stored in the queryset object and used for subsequent calls. \n\nA more efficient way to render this template would be to fetch the list of pizzas and then query the toppings table once to get all the toppings for all the pizzas. Django makes this easy using [prefetch_related()](https://docs.djangoproject.com/en/4.0/ref/models/querysets/#prefetch-related). \n#### `project / app / views.py` \n```python\ndef pizza_list(request):\n    pizzas = Pizza.objects.all().prefetch_related('toppings')\n    return render(request, 'pizza_list.html' context={'pizzas': pizzas})\n```\n#### `project / dqf.ipynb`\n```python\nfeel = Feel(pizza_list)\nfeel.report\n```\n```python\n     query count: 2         \naverage duration: 0.069 ms                \n   unique tables: 2         \n        accessed   \n```\n\n## Run Django in a Jupyter Notebook\n\n#### `project / dqf.ipynb`\n```python \n# re-import modules when a cell is run. This ensures that changes made to\n# the django app are synced with your notebook\n%load_ext autoreload\n%autoreload 2\n\nimport django\nimport os\n\n# change 'project.settings' to '{your_project}.settings'\nos.environ['DJANGO_SETTINGS_MODULE'] = 'project.settings'\nos.environ[\"DJANGO_ALLOW_ASYNC_UNSAFE\"] = \"true\"\ndjango.setup()\n```\n",
    "bugtrack_url": null,
    "license": "MIT License  Copyright (c) 2022 Lukas Schillinger  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ",
    "summary": "tools to help understand how django querysets are executed",
    "version": "0.0.6",
    "project_urls": {
        "homepage": "https://github.com/lukas-schillinger/django-queryset-feeler"
    },
    "split_keywords": [
        "django",
        "query",
        "queryset"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "fbdc76f0bb479b40a718cf9ec2026d91975b537fef706e1976b82b475037a31f",
                "md5": "c6e7df7483a9da85b6c7aa4dd21c933e",
                "sha256": "a522340bc102beae777ff54cca78dc350a3dd233684a5046e1b1a525f8a1f13c"
            },
            "downloads": -1,
            "filename": "django_queryset_feeler-0.0.6-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "c6e7df7483a9da85b6c7aa4dd21c933e",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.7",
            "size": 9120,
            "upload_time": "2023-10-04T13:31:45",
            "upload_time_iso_8601": "2023-10-04T13:31:45.271099Z",
            "url": "https://files.pythonhosted.org/packages/fb/dc/76f0bb479b40a718cf9ec2026d91975b537fef706e1976b82b475037a31f/django_queryset_feeler-0.0.6-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "795e3c5cacda2a7f282dc01ea7726142427d17ee9a65161f8db3bd50a5532eac",
                "md5": "d1b8e959471b9590834a37ceeec74c02",
                "sha256": "1d8066b08f010c94aec0ddedaa91851a4334fe663e01c8e558c2e7dbdced0219"
            },
            "downloads": -1,
            "filename": "django-queryset-feeler-0.0.6.tar.gz",
            "has_sig": false,
            "md5_digest": "d1b8e959471b9590834a37ceeec74c02",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.7",
            "size": 11039,
            "upload_time": "2023-10-04T13:31:46",
            "upload_time_iso_8601": "2023-10-04T13:31:46.803668Z",
            "url": "https://files.pythonhosted.org/packages/79/5e/3c5cacda2a7f282dc01ea7726142427d17ee9a65161f8db3bd50a5532eac/django-queryset-feeler-0.0.6.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-10-04 13:31:46",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "lukas-schillinger",
    "github_project": "django-queryset-feeler",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "lcname": "django-queryset-feeler"
}
        
Elapsed time: 0.14234s