├── .gitignore ├── Python Brasil 2022 - Queries performáticas com ORM em Python, Django e Postgres.pdf ├── README.md ├── app ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py ├── docker-compose.yml ├── manage.py ├── poetry.lock ├── pyproject.toml └── shop ├── __init__.py ├── admin.py ├── apps.py ├── factory.py ├── helpers.py ├── management ├── __init__.py └── commands │ ├── __init__.py │ └── create_test_records.py ├── managers.py ├── migrations ├── 0001_initial.py ├── 0002_customer_email.py ├── 0003_customer_email_non_indexed_alter_customer_email.py └── __init__.py ├── models.py ├── samples.py ├── tests.py └── views.py /.gitignore: -------------------------------------------------------------------------------- 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 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /Python Brasil 2022 - Queries performáticas com ORM em Python, Django e Postgres.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiagoferreiraw/django-orm-optimization-talk/e92804aa97ee8bbf6be4d4e399f330547357b04f/Python Brasil 2022 - Queries performáticas com ORM em Python, Django e Postgres.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-orm-optimization-talk 2 | 3 | ## Presentation slides 4 | 5 | - [Python Brasil 2022 - Queries performáticas com ORM em Python, Django e Postgres.pdf](https://github.com/thiagoferreiraw/django-orm-optimization-talk/blob/main/Python%20Brasil%202022%20-%20Queries%20perform%C3%A1ticas%20com%20ORM%20em%20Python%2C%20Django%20e%20Postgres.pdf) 6 | - 2nd option: [slideshare](https://pt.slideshare.net/ThiagoFerreira237/python-brasil-2022-queries-performaticas-com-orm-em-python-django-e-postgrespdf) 7 | 8 | 9 | ## Installing 10 | Make sure you have poetry installed, then run: 11 | ``` 12 | poetry install 13 | poetry run python manage.py migrate 14 | ``` 15 | 16 | ## Creating a sample database 17 | 18 | ``` 19 | poetry run python manage.py create_test_records 20 | ``` 21 | 22 | See available params with: 23 | ``` 24 | poetry run python manage.py create_test_records --help 25 | ``` 26 | 27 | ![Screen Shot 2022-09-03 at 15 20 26](https://user-images.githubusercontent.com/9268203/188283454-683d145e-70f3-42e2-9f54-4f3ff20c7566.png) 28 | 29 | 30 | ## Running sample queries 31 | 32 | In file `shop/samples.py`, we can find several good/bad examples of ORM queries. 33 | 34 | To run those queries, do: 35 | ```sh 36 | $ poetry run python manage.py shell_plus 37 | ``` 38 | 39 | Then, in the python shell, run: 40 | ```python 41 | # Using a "bad" example - this will run several unucessary queries 42 | from shop.samples import list_orders_bad 43 | list_orders_bad() 44 | ``` 45 | image 46 | 47 | 48 | 49 | Another example: 50 | 51 | ```python 52 | # Using a "good" example - this is optimized 53 | from shop.samples import list_orders_good 54 | list_orders_good() 55 | ``` 56 | 57 | image 58 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiagoferreiraw/django-orm-optimization-talk/e92804aa97ee8bbf6be4d4e399f330547357b04f/app/__init__.py -------------------------------------------------------------------------------- /app/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for app project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /app/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for app project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.1/ref/settings/ 11 | """ 12 | 13 | import os 14 | from pathlib import Path 15 | 16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 17 | BASE_DIR = Path(__file__).resolve().parent.parent 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = "django-insecure-o*=$as&xos2w!ks=%v6#ms58bio@(1^8fg*esx_9neq+rimc%3" 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = [] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = [ 35 | "django.contrib.admin", 36 | "django.contrib.auth", 37 | "django.contrib.contenttypes", 38 | "django.contrib.sessions", 39 | "django.contrib.messages", 40 | "django.contrib.staticfiles", 41 | "shop", 42 | "django_extensions", 43 | ] 44 | 45 | MIDDLEWARE = [ 46 | "django.middleware.security.SecurityMiddleware", 47 | "django.contrib.sessions.middleware.SessionMiddleware", 48 | "django.middleware.common.CommonMiddleware", 49 | "django.middleware.csrf.CsrfViewMiddleware", 50 | "django.contrib.auth.middleware.AuthenticationMiddleware", 51 | "django.contrib.messages.middleware.MessageMiddleware", 52 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 53 | ] 54 | 55 | ROOT_URLCONF = "app.urls" 56 | 57 | TEMPLATES = [ 58 | { 59 | "BACKEND": "django.template.backends.django.DjangoTemplates", 60 | "DIRS": [], 61 | "APP_DIRS": True, 62 | "OPTIONS": { 63 | "context_processors": [ 64 | "django.template.context_processors.debug", 65 | "django.template.context_processors.request", 66 | "django.contrib.auth.context_processors.auth", 67 | "django.contrib.messages.context_processors.messages", 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = "app.wsgi.application" 74 | 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/4.1/ref/settings/#databases 78 | 79 | DATABASES = { 80 | "default": { 81 | "ENGINE": "django.db.backends.postgresql", 82 | "NAME": "postgres", 83 | "USER": "postgres", 84 | "PASSWORD": "123456", 85 | "HOST": os.environ.get("DATABASE_HOST", "0.0.0.0"), 86 | "PORT": "5432", 87 | } 88 | } 89 | 90 | # Password validation 91 | # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators 92 | 93 | AUTH_PASSWORD_VALIDATORS = [ 94 | { 95 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 96 | }, 97 | { 98 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 99 | }, 100 | { 101 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 102 | }, 103 | { 104 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 105 | }, 106 | ] 107 | 108 | 109 | # Internationalization 110 | # https://docs.djangoproject.com/en/4.1/topics/i18n/ 111 | 112 | LANGUAGE_CODE = "en-us" 113 | 114 | TIME_ZONE = "UTC" 115 | 116 | USE_I18N = True 117 | 118 | USE_TZ = True 119 | 120 | 121 | # Static files (CSS, JavaScript, Images) 122 | # https://docs.djangoproject.com/en/4.1/howto/static-files/ 123 | 124 | STATIC_URL = "static/" 125 | 126 | # Default primary key field type 127 | # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field 128 | 129 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 130 | -------------------------------------------------------------------------------- /app/urls.py: -------------------------------------------------------------------------------- 1 | """app URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/4.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path 18 | 19 | urlpatterns = [ 20 | path("admin/", admin.site.urls), 21 | ] 22 | -------------------------------------------------------------------------------- /app/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for app project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | db: 4 | image: postgres 5 | restart: always 6 | environment: 7 | POSTGRES_PASSWORD: 123456 8 | ports: 9 | - 5432:5432 -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "appnope" 3 | version = "0.1.3" 4 | description = "Disable App Nap on macOS >= 10.9" 5 | category = "main" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "asgiref" 11 | version = "3.5.2" 12 | description = "ASGI specs, helper code, and adapters" 13 | category = "main" 14 | optional = false 15 | python-versions = ">=3.7" 16 | 17 | [package.extras] 18 | tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] 19 | 20 | [[package]] 21 | name = "asttokens" 22 | version = "2.0.8" 23 | description = "Annotate AST trees with source code positions" 24 | category = "main" 25 | optional = false 26 | python-versions = "*" 27 | 28 | [package.dependencies] 29 | six = "*" 30 | 31 | [package.extras] 32 | test = ["astroid (<=2.5.3)", "pytest"] 33 | 34 | [[package]] 35 | name = "backcall" 36 | version = "0.2.0" 37 | description = "Specifications for callback functions passed in to an API" 38 | category = "main" 39 | optional = false 40 | python-versions = "*" 41 | 42 | [[package]] 43 | name = "black" 44 | version = "22.6.0" 45 | description = "The uncompromising code formatter." 46 | category = "dev" 47 | optional = false 48 | python-versions = ">=3.6.2" 49 | 50 | [package.dependencies] 51 | click = ">=8.0.0" 52 | mypy-extensions = ">=0.4.3" 53 | pathspec = ">=0.9.0" 54 | platformdirs = ">=2" 55 | tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} 56 | typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} 57 | 58 | [package.extras] 59 | colorama = ["colorama (>=0.4.3)"] 60 | d = ["aiohttp (>=3.7.4)"] 61 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 62 | uvloop = ["uvloop (>=0.15.2)"] 63 | 64 | [[package]] 65 | name = "click" 66 | version = "8.1.3" 67 | description = "Composable command line interface toolkit" 68 | category = "dev" 69 | optional = false 70 | python-versions = ">=3.7" 71 | 72 | [package.dependencies] 73 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 74 | 75 | [[package]] 76 | name = "colorama" 77 | version = "0.4.5" 78 | description = "Cross-platform colored terminal text." 79 | category = "main" 80 | optional = false 81 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 82 | 83 | [[package]] 84 | name = "decorator" 85 | version = "5.1.1" 86 | description = "Decorators for Humans" 87 | category = "main" 88 | optional = false 89 | python-versions = ">=3.5" 90 | 91 | [[package]] 92 | name = "django" 93 | version = "4.1" 94 | description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." 95 | category = "main" 96 | optional = false 97 | python-versions = ">=3.8" 98 | 99 | [package.dependencies] 100 | asgiref = ">=3.5.2,<4" 101 | sqlparse = ">=0.2.2" 102 | tzdata = {version = "*", markers = "sys_platform == \"win32\""} 103 | 104 | [package.extras] 105 | argon2 = ["argon2-cffi (>=19.1.0)"] 106 | bcrypt = ["bcrypt"] 107 | 108 | [[package]] 109 | name = "django-extensions" 110 | version = "3.2.0" 111 | description = "Extensions for Django" 112 | category = "main" 113 | optional = false 114 | python-versions = ">=3.6" 115 | 116 | [package.dependencies] 117 | Django = ">=3.2" 118 | 119 | [[package]] 120 | name = "executing" 121 | version = "1.0.0" 122 | description = "Get the currently executing AST node of a frame, and other information" 123 | category = "main" 124 | optional = false 125 | python-versions = "*" 126 | 127 | [[package]] 128 | name = "factory-boy" 129 | version = "3.2.1" 130 | description = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby." 131 | category = "main" 132 | optional = false 133 | python-versions = ">=3.6" 134 | 135 | [package.dependencies] 136 | Faker = ">=0.7.0" 137 | 138 | [package.extras] 139 | dev = ["coverage", "django", "flake8", "isort", "pillow", "sqlalchemy", "mongoengine", "wheel (>=0.32.0)", "tox", "zest.releaser"] 140 | doc = ["sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"] 141 | 142 | [[package]] 143 | name = "faker" 144 | version = "14.1.0" 145 | description = "Faker is a Python package that generates fake data for you." 146 | category = "main" 147 | optional = false 148 | python-versions = ">=3.6" 149 | 150 | [package.dependencies] 151 | python-dateutil = ">=2.4" 152 | 153 | [[package]] 154 | name = "ipython" 155 | version = "8.4.0" 156 | description = "IPython: Productive Interactive Computing" 157 | category = "main" 158 | optional = false 159 | python-versions = ">=3.8" 160 | 161 | [package.dependencies] 162 | appnope = {version = "*", markers = "sys_platform == \"darwin\""} 163 | backcall = "*" 164 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 165 | decorator = "*" 166 | jedi = ">=0.16" 167 | matplotlib-inline = "*" 168 | pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} 169 | pickleshare = "*" 170 | prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" 171 | pygments = ">=2.4.0" 172 | stack-data = "*" 173 | traitlets = ">=5" 174 | 175 | [package.extras] 176 | all = ["black", "Sphinx (>=1.3)", "ipykernel", "nbconvert", "nbformat", "ipywidgets", "notebook", "ipyparallel", "qtconsole", "pytest (<7.1)", "pytest-asyncio", "testpath", "curio", "matplotlib (!=3.2.0)", "numpy (>=1.19)", "pandas", "trio"] 177 | black = ["black"] 178 | doc = ["Sphinx (>=1.3)"] 179 | kernel = ["ipykernel"] 180 | nbconvert = ["nbconvert"] 181 | nbformat = ["nbformat"] 182 | notebook = ["ipywidgets", "notebook"] 183 | parallel = ["ipyparallel"] 184 | qtconsole = ["qtconsole"] 185 | test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] 186 | test_extra = ["pytest (<7.1)", "pytest-asyncio", "testpath", "curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.19)", "pandas", "trio"] 187 | 188 | [[package]] 189 | name = "isort" 190 | version = "5.10.1" 191 | description = "A Python utility / library to sort Python imports." 192 | category = "dev" 193 | optional = false 194 | python-versions = ">=3.6.1,<4.0" 195 | 196 | [package.extras] 197 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 198 | requirements_deprecated_finder = ["pipreqs", "pip-api"] 199 | colors = ["colorama (>=0.4.3,<0.5.0)"] 200 | plugins = ["setuptools"] 201 | 202 | [[package]] 203 | name = "jedi" 204 | version = "0.18.1" 205 | description = "An autocompletion tool for Python that can be used for text editors." 206 | category = "main" 207 | optional = false 208 | python-versions = ">=3.6" 209 | 210 | [package.dependencies] 211 | parso = ">=0.8.0,<0.9.0" 212 | 213 | [package.extras] 214 | qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] 215 | testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<7.0.0)"] 216 | 217 | [[package]] 218 | name = "matplotlib-inline" 219 | version = "0.1.6" 220 | description = "Inline Matplotlib backend for Jupyter" 221 | category = "main" 222 | optional = false 223 | python-versions = ">=3.5" 224 | 225 | [package.dependencies] 226 | traitlets = "*" 227 | 228 | [[package]] 229 | name = "mypy-extensions" 230 | version = "0.4.3" 231 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 232 | category = "dev" 233 | optional = false 234 | python-versions = "*" 235 | 236 | [[package]] 237 | name = "parso" 238 | version = "0.8.3" 239 | description = "A Python Parser" 240 | category = "main" 241 | optional = false 242 | python-versions = ">=3.6" 243 | 244 | [package.extras] 245 | qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] 246 | testing = ["docopt", "pytest (<6.0.0)"] 247 | 248 | [[package]] 249 | name = "pathspec" 250 | version = "0.9.0" 251 | description = "Utility library for gitignore style pattern matching of file paths." 252 | category = "dev" 253 | optional = false 254 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 255 | 256 | [[package]] 257 | name = "pexpect" 258 | version = "4.8.0" 259 | description = "Pexpect allows easy control of interactive console applications." 260 | category = "main" 261 | optional = false 262 | python-versions = "*" 263 | 264 | [package.dependencies] 265 | ptyprocess = ">=0.5" 266 | 267 | [[package]] 268 | name = "pickleshare" 269 | version = "0.7.5" 270 | description = "Tiny 'shelve'-like database with concurrency support" 271 | category = "main" 272 | optional = false 273 | python-versions = "*" 274 | 275 | [[package]] 276 | name = "platformdirs" 277 | version = "2.5.2" 278 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 279 | category = "dev" 280 | optional = false 281 | python-versions = ">=3.7" 282 | 283 | [package.extras] 284 | docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] 285 | test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] 286 | 287 | [[package]] 288 | name = "prompt-toolkit" 289 | version = "3.0.30" 290 | description = "Library for building powerful interactive command lines in Python" 291 | category = "main" 292 | optional = false 293 | python-versions = ">=3.6.2" 294 | 295 | [package.dependencies] 296 | wcwidth = "*" 297 | 298 | [[package]] 299 | name = "psycopg2-binary" 300 | version = "2.9.3" 301 | description = "psycopg2 - Python-PostgreSQL Database Adapter" 302 | category = "main" 303 | optional = false 304 | python-versions = ">=3.6" 305 | 306 | [[package]] 307 | name = "ptyprocess" 308 | version = "0.7.0" 309 | description = "Run a subprocess in a pseudo terminal" 310 | category = "main" 311 | optional = false 312 | python-versions = "*" 313 | 314 | [[package]] 315 | name = "pure-eval" 316 | version = "0.2.2" 317 | description = "Safely evaluate AST nodes without side effects" 318 | category = "main" 319 | optional = false 320 | python-versions = "*" 321 | 322 | [package.extras] 323 | tests = ["pytest"] 324 | 325 | [[package]] 326 | name = "pygments" 327 | version = "2.13.0" 328 | description = "Pygments is a syntax highlighting package written in Python." 329 | category = "main" 330 | optional = false 331 | python-versions = ">=3.6" 332 | 333 | [package.extras] 334 | plugins = ["importlib-metadata"] 335 | 336 | [[package]] 337 | name = "python-dateutil" 338 | version = "2.8.2" 339 | description = "Extensions to the standard Python datetime module" 340 | category = "main" 341 | optional = false 342 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 343 | 344 | [package.dependencies] 345 | six = ">=1.5" 346 | 347 | [[package]] 348 | name = "six" 349 | version = "1.16.0" 350 | description = "Python 2 and 3 compatibility utilities" 351 | category = "main" 352 | optional = false 353 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 354 | 355 | [[package]] 356 | name = "sqlparse" 357 | version = "0.4.2" 358 | description = "A non-validating SQL parser." 359 | category = "main" 360 | optional = false 361 | python-versions = ">=3.5" 362 | 363 | [[package]] 364 | name = "stack-data" 365 | version = "0.5.0" 366 | description = "Extract data from python stack frames and tracebacks for informative displays" 367 | category = "main" 368 | optional = false 369 | python-versions = "*" 370 | 371 | [package.dependencies] 372 | asttokens = "*" 373 | executing = "*" 374 | pure-eval = "*" 375 | 376 | [package.extras] 377 | tests = ["cython", "littleutils", "pygments", "typeguard", "pytest"] 378 | 379 | [[package]] 380 | name = "tomli" 381 | version = "2.0.1" 382 | description = "A lil' TOML parser" 383 | category = "dev" 384 | optional = false 385 | python-versions = ">=3.7" 386 | 387 | [[package]] 388 | name = "traitlets" 389 | version = "5.3.0" 390 | description = "" 391 | category = "main" 392 | optional = false 393 | python-versions = ">=3.7" 394 | 395 | [package.extras] 396 | test = ["pre-commit", "pytest"] 397 | 398 | [[package]] 399 | name = "typing-extensions" 400 | version = "4.3.0" 401 | description = "Backported and Experimental Type Hints for Python 3.7+" 402 | category = "dev" 403 | optional = false 404 | python-versions = ">=3.7" 405 | 406 | [[package]] 407 | name = "tzdata" 408 | version = "2022.2" 409 | description = "Provider of IANA time zone data" 410 | category = "main" 411 | optional = false 412 | python-versions = ">=2" 413 | 414 | [[package]] 415 | name = "wcwidth" 416 | version = "0.2.5" 417 | description = "Measures the displayed width of unicode strings in a terminal" 418 | category = "main" 419 | optional = false 420 | python-versions = "*" 421 | 422 | [metadata] 423 | lock-version = "1.1" 424 | python-versions = "^3.9" 425 | content-hash = "e50a9fb2b1aaf3e0d30945cdab112092819bc5c8bdae1b78485a2ed5d54b83b6" 426 | 427 | [metadata.files] 428 | appnope = [] 429 | asgiref = [] 430 | asttokens = [] 431 | backcall = [ 432 | {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, 433 | {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, 434 | ] 435 | black = [] 436 | click = [] 437 | colorama = [] 438 | decorator = [ 439 | {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, 440 | {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, 441 | ] 442 | django = [] 443 | django-extensions = [] 444 | executing = [] 445 | factory-boy = [ 446 | {file = "factory_boy-3.2.1-py2.py3-none-any.whl", hash = "sha256:eb02a7dd1b577ef606b75a253b9818e6f9eaf996d94449c9d5ebb124f90dc795"}, 447 | {file = "factory_boy-3.2.1.tar.gz", hash = "sha256:a98d277b0c047c75eb6e4ab8508a7f81fb03d2cb21986f627913546ef7a2a55e"}, 448 | ] 449 | faker = [] 450 | ipython = [] 451 | isort = [ 452 | {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, 453 | {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, 454 | ] 455 | jedi = [ 456 | {file = "jedi-0.18.1-py2.py3-none-any.whl", hash = "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d"}, 457 | {file = "jedi-0.18.1.tar.gz", hash = "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab"}, 458 | ] 459 | matplotlib-inline = [] 460 | mypy-extensions = [ 461 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 462 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 463 | ] 464 | parso = [ 465 | {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, 466 | {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, 467 | ] 468 | pathspec = [ 469 | {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, 470 | {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, 471 | ] 472 | pexpect = [ 473 | {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, 474 | {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, 475 | ] 476 | pickleshare = [ 477 | {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, 478 | {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, 479 | ] 480 | platformdirs = [] 481 | prompt-toolkit = [] 482 | psycopg2-binary = [ 483 | {file = "psycopg2-binary-2.9.3.tar.gz", hash = "sha256:761df5313dc15da1502b21453642d7599d26be88bff659382f8f9747c7ebea4e"}, 484 | {file = "psycopg2_binary-2.9.3-cp310-cp310-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:539b28661b71da7c0e428692438efbcd048ca21ea81af618d845e06ebfd29478"}, 485 | {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e82d38390a03da28c7985b394ec3f56873174e2c88130e6966cb1c946508e65"}, 486 | {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57804fc02ca3ce0dbfbef35c4b3a4a774da66d66ea20f4bda601294ad2ea6092"}, 487 | {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:083a55275f09a62b8ca4902dd11f4b33075b743cf0d360419e2051a8a5d5ff76"}, 488 | {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:0a29729145aaaf1ad8bafe663131890e2111f13416b60e460dae0a96af5905c9"}, 489 | {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a79d622f5206d695d7824cbf609a4f5b88ea6d6dab5f7c147fc6d333a8787e4"}, 490 | {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:090f3348c0ab2cceb6dfbe6bf721ef61262ddf518cd6cc6ecc7d334996d64efa"}, 491 | {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a9e1f75f96ea388fbcef36c70640c4efbe4650658f3d6a2967b4cc70e907352e"}, 492 | {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c3ae8e75eb7160851e59adc77b3a19a976e50622e44fd4fd47b8b18208189d42"}, 493 | {file = "psycopg2_binary-2.9.3-cp310-cp310-win32.whl", hash = "sha256:7b1e9b80afca7b7a386ef087db614faebbf8839b7f4db5eb107d0f1a53225029"}, 494 | {file = "psycopg2_binary-2.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:8b344adbb9a862de0c635f4f0425b7958bf5a4b927c8594e6e8d261775796d53"}, 495 | {file = "psycopg2_binary-2.9.3-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:e847774f8ffd5b398a75bc1c18fbb56564cda3d629fe68fd81971fece2d3c67e"}, 496 | {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68641a34023d306be959101b345732360fc2ea4938982309b786f7be1b43a4a1"}, 497 | {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3303f8807f342641851578ee7ed1f3efc9802d00a6f83c101d21c608cb864460"}, 498 | {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:e3699852e22aa68c10de06524a3721ade969abf382da95884e6a10ff798f9281"}, 499 | {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:526ea0378246d9b080148f2d6681229f4b5964543c170dd10bf4faaab6e0d27f"}, 500 | {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:b1c8068513f5b158cf7e29c43a77eb34b407db29aca749d3eb9293ee0d3103ca"}, 501 | {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:15803fa813ea05bef089fa78835118b5434204f3a17cb9f1e5dbfd0b9deea5af"}, 502 | {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:152f09f57417b831418304c7f30d727dc83a12761627bb826951692cc6491e57"}, 503 | {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:404224e5fef3b193f892abdbf8961ce20e0b6642886cfe1fe1923f41aaa75c9d"}, 504 | {file = "psycopg2_binary-2.9.3-cp36-cp36m-win32.whl", hash = "sha256:1f6b813106a3abdf7b03640d36e24669234120c72e91d5cbaeb87c5f7c36c65b"}, 505 | {file = "psycopg2_binary-2.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:2d872e3c9d5d075a2e104540965a1cf898b52274a5923936e5bfddb58c59c7c2"}, 506 | {file = "psycopg2_binary-2.9.3-cp37-cp37m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:10bb90fb4d523a2aa67773d4ff2b833ec00857f5912bafcfd5f5414e45280fb1"}, 507 | {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a52ecab70af13e899f7847b3e074eeb16ebac5615665db33bce8a1009cf33"}, 508 | {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a29b3ca4ec9defec6d42bf5feb36bb5817ba3c0230dd83b4edf4bf02684cd0ae"}, 509 | {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:12b11322ea00ad8db8c46f18b7dfc47ae215e4df55b46c67a94b4effbaec7094"}, 510 | {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:53293533fcbb94c202b7c800a12c873cfe24599656b341f56e71dd2b557be063"}, 511 | {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c381bda330ddf2fccbafab789d83ebc6c53db126e4383e73794c74eedce855ef"}, 512 | {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d29409b625a143649d03d0fd7b57e4b92e0ecad9726ba682244b73be91d2fdb"}, 513 | {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:183a517a3a63503f70f808b58bfbf962f23d73b6dccddae5aa56152ef2bcb232"}, 514 | {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:15c4e4cfa45f5a60599d9cec5f46cd7b1b29d86a6390ec23e8eebaae84e64554"}, 515 | {file = "psycopg2_binary-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:adf20d9a67e0b6393eac162eb81fb10bc9130a80540f4df7e7355c2dd4af9fba"}, 516 | {file = "psycopg2_binary-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:2f9ffd643bc7349eeb664eba8864d9e01f057880f510e4681ba40a6532f93c71"}, 517 | {file = "psycopg2_binary-2.9.3-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:def68d7c21984b0f8218e8a15d514f714d96904265164f75f8d3a70f9c295667"}, 518 | {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dffc08ca91c9ac09008870c9eb77b00a46b3378719584059c034b8945e26b272"}, 519 | {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:280b0bb5cbfe8039205c7981cceb006156a675362a00fe29b16fbc264e242834"}, 520 | {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:af9813db73395fb1fc211bac696faea4ca9ef53f32dc0cfa27e4e7cf766dcf24"}, 521 | {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:63638d875be8c2784cfc952c9ac34e2b50e43f9f0a0660b65e2a87d656b3116c"}, 522 | {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ffb7a888a047696e7f8240d649b43fb3644f14f0ee229077e7f6b9f9081635bd"}, 523 | {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0c9d5450c566c80c396b7402895c4369a410cab5a82707b11aee1e624da7d004"}, 524 | {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:d1c1b569ecafe3a69380a94e6ae09a4789bbb23666f3d3a08d06bbd2451f5ef1"}, 525 | {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8fc53f9af09426a61db9ba357865c77f26076d48669f2e1bb24d85a22fb52307"}, 526 | {file = "psycopg2_binary-2.9.3-cp38-cp38-win32.whl", hash = "sha256:6472a178e291b59e7f16ab49ec8b4f3bdada0a879c68d3817ff0963e722a82ce"}, 527 | {file = "psycopg2_binary-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:35168209c9d51b145e459e05c31a9eaeffa9a6b0fd61689b48e07464ffd1a83e"}, 528 | {file = "psycopg2_binary-2.9.3-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:47133f3f872faf28c1e87d4357220e809dfd3fa7c64295a4a148bcd1e6e34ec9"}, 529 | {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91920527dea30175cc02a1099f331aa8c1ba39bf8b7762b7b56cbf54bc5cce42"}, 530 | {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887dd9aac71765ac0d0bac1d0d4b4f2c99d5f5c1382d8b770404f0f3d0ce8a39"}, 531 | {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:1f14c8b0942714eb3c74e1e71700cbbcb415acbc311c730370e70c578a44a25c"}, 532 | {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:7af0dd86ddb2f8af5da57a976d27cd2cd15510518d582b478fbb2292428710b4"}, 533 | {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93cd1967a18aa0edd4b95b1dfd554cf15af657cb606280996d393dadc88c3c35"}, 534 | {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bda845b664bb6c91446ca9609fc69f7db6c334ec5e4adc87571c34e4f47b7ddb"}, 535 | {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:01310cf4cf26db9aea5158c217caa92d291f0500051a6469ac52166e1a16f5b7"}, 536 | {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:99485cab9ba0fa9b84f1f9e1fef106f44a46ef6afdeec8885e0b88d0772b49e8"}, 537 | {file = "psycopg2_binary-2.9.3-cp39-cp39-win32.whl", hash = "sha256:46f0e0a6b5fa5851bbd9ab1bc805eef362d3a230fbdfbc209f4a236d0a7a990d"}, 538 | {file = "psycopg2_binary-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:accfe7e982411da3178ec690baaceaad3c278652998b2c45828aaac66cd8285f"}, 539 | ] 540 | ptyprocess = [ 541 | {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, 542 | {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, 543 | ] 544 | pure-eval = [] 545 | pygments = [] 546 | python-dateutil = [ 547 | {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, 548 | {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, 549 | ] 550 | six = [ 551 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 552 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 553 | ] 554 | sqlparse = [ 555 | {file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"}, 556 | {file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"}, 557 | ] 558 | stack-data = [] 559 | tomli = [ 560 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 561 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 562 | ] 563 | traitlets = [] 564 | typing-extensions = [] 565 | tzdata = [] 566 | wcwidth = [ 567 | {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, 568 | {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, 569 | ] 570 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "django-orm-optimization-talk" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Thiago Ferreira "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.9" 9 | Django = "^4.1" 10 | factory-boy = "^3.2.1" 11 | django-extensions = "^3.2.0" 12 | ipython = "^8.4.0" 13 | psycopg2-binary = "^2.9.3" 14 | 15 | [tool.poetry.dev-dependencies] 16 | factory-boy = "^3.2.1" 17 | black = "^22.6.0" 18 | isort = "^5.10.1" 19 | 20 | [build-system] 21 | requires = ["poetry-core>=1.0.0"] 22 | build-backend = "poetry.core.masonry.api" 23 | -------------------------------------------------------------------------------- /shop/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiagoferreiraw/django-orm-optimization-talk/e92804aa97ee8bbf6be4d4e399f330547357b04f/shop/__init__.py -------------------------------------------------------------------------------- /shop/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /shop/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ShopConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "shop" 7 | -------------------------------------------------------------------------------- /shop/factory.py: -------------------------------------------------------------------------------- 1 | import factory 2 | import factory.fuzzy 3 | 4 | from shop.models import Customer, Order, OrderItem, Product, ProductCategory 5 | 6 | 7 | class CustomerFactory(factory.django.DjangoModelFactory): 8 | class Meta: 9 | model = Customer 10 | 11 | name = factory.Faker("name") 12 | birth_date = factory.Faker("date") 13 | country = factory.Iterator(["US", "BR", "AR"]) 14 | email = factory.Faker("email") 15 | 16 | 17 | class ProductCategoryFactory(factory.django.DjangoModelFactory): 18 | class Meta: 19 | model = ProductCategory 20 | 21 | name = factory.Faker("name") 22 | minumum_age_allowed = factory.Iterator([21, 18, 18, 0, 0, 0]) 23 | 24 | 25 | class ProductFactory(factory.django.DjangoModelFactory): 26 | class Meta: 27 | model = Product 28 | 29 | name = factory.Faker("name") 30 | description = factory.Faker("sentence") 31 | category = factory.SubFactory(ProductCategoryFactory) 32 | price = factory.fuzzy.FuzzyDecimal(0.5, 15) 33 | 34 | 35 | class OrderFactory(factory.django.DjangoModelFactory): 36 | class Meta: 37 | model = Order 38 | 39 | customer = factory.SubFactory(CustomerFactory) 40 | order_date = factory.Faker("date") 41 | is_paid = factory.Iterator([True, False]) 42 | delivery_method = factory.Iterator( 43 | map(lambda x: x[0], Order.delivery_method.field.choices) 44 | ) 45 | total = 0 46 | 47 | 48 | class OrderItemFactory(factory.django.DjangoModelFactory): 49 | class Meta: 50 | model = OrderItem 51 | 52 | order = factory.SubFactory(OrderFactory) 53 | product = factory.SubFactory(ProductFactory) 54 | amount = factory.fuzzy.FuzzyDecimal(0.1, 10) 55 | -------------------------------------------------------------------------------- /shop/helpers.py: -------------------------------------------------------------------------------- 1 | import time 2 | from functools import wraps 3 | 4 | from django.db import connection, reset_queries 5 | 6 | 7 | class DebugTypes: 8 | FULL = "full" 9 | SHORT = "short" 10 | 11 | 12 | def debug_queries(print_mode=DebugTypes.SHORT): 13 | def inner_decorator(func): 14 | @wraps(func) 15 | def wrapper(*args, **kwargs): 16 | reset_queries() 17 | start_time = time.perf_counter() 18 | func(*args, **kwargs) 19 | time_elapsed_ms = (time.perf_counter() - start_time) * 1000 20 | _print_sql_statements(print_mode, time_elapsed_ms) 21 | 22 | return wrapper 23 | 24 | return inner_decorator 25 | 26 | 27 | def _print_sql_statements(print_mode, time_elapsed_ms): 28 | total_sql_time_ms = sum(map(lambda q: float(q["time"]), connection.queries)) * 1000 29 | print( 30 | f"\n{'-'*60}\n[{len(connection.queries)}] SQL statements executed " 31 | f"(Total time = {time_elapsed_ms:0.1f}ms, " 32 | f"SQL time = {total_sql_time_ms:0.1f}ms):\n" 33 | ) 34 | for idx, query in enumerate(connection.queries, start=1): 35 | print(idx, _get_formatted_sql_statement(query["sql"], print_mode), "\n") 36 | print(f"\n{'-'*60}") 37 | 38 | 39 | def _get_formatted_sql_statement(sql, print_mode): 40 | if print_mode == DebugTypes.SHORT: 41 | return sql[sql.find("FROM") :] 42 | return sql 43 | -------------------------------------------------------------------------------- /shop/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiagoferreiraw/django-orm-optimization-talk/e92804aa97ee8bbf6be4d4e399f330547357b04f/shop/management/__init__.py -------------------------------------------------------------------------------- /shop/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiagoferreiraw/django-orm-optimization-talk/e92804aa97ee8bbf6be4d4e399f330547357b04f/shop/management/commands/__init__.py -------------------------------------------------------------------------------- /shop/management/commands/create_test_records.py: -------------------------------------------------------------------------------- 1 | from random import choice, randint 2 | 3 | from django.core.management.base import BaseCommand, CommandError 4 | from django.db.models import Avg, Count, Max, Min, Sum 5 | from factory import LazyFunction 6 | 7 | from shop.factory import ( 8 | CustomerFactory, 9 | OrderFactory, 10 | OrderItemFactory, 11 | ProductCategoryFactory, 12 | ProductFactory, 13 | ) 14 | from shop.models import Customer, Order 15 | 16 | 17 | class Command(BaseCommand): 18 | def add_arguments(self, parser): 19 | parser.add_argument("--total_customers", type=int, default=1000, required=False) 20 | parser.add_argument("--total_categories", type=int, default=50, required=False) 21 | parser.add_argument("--total_products", type=int, default=1000, required=False) 22 | parser.add_argument("--total_orders", type=int, default=5000, required=False) 23 | parser.add_argument( 24 | "--max_items_per_order", type=int, default=6, required=False 25 | ) 26 | 27 | def handle(self, *args, **options): 28 | customers, orders, categories, products = [], [], [], [] 29 | 30 | self.stdout.write(f"Inserting test records with options = {options}") 31 | 32 | self.stdout.write(f"Inserting {options['total_customers']} Customers") 33 | customers = CustomerFactory.create_batch(options["total_customers"]) 34 | 35 | self.stdout.write(f"Inserting {options['total_categories']} Categories") 36 | categories = ProductCategoryFactory.create_batch(options["total_categories"]) 37 | 38 | self.stdout.write(f"Inserting {options['total_products']} Products") 39 | products = ProductFactory.create_batch( 40 | options["total_products"], category=LazyFunction(lambda: choice(categories)) 41 | ) 42 | 43 | self.stdout.write(f"Inserting {options['total_orders']} Orders") 44 | orders = OrderFactory.create_batch( 45 | options["total_orders"], customer=LazyFunction(lambda: choice(customers)) 46 | ) 47 | 48 | self.stdout.write(f"Creating order items...") 49 | for order in orders: 50 | OrderItemFactory.create_batch( 51 | randint(1, options["max_items_per_order"]), 52 | order=order, 53 | product=LazyFunction(lambda: choice(products)), 54 | ) 55 | order.save(should_calculate_total=True) 56 | 57 | self.stdout.write(self.style.SUCCESS("Done!")) 58 | 59 | average_item_count_per_order = Order.objects.annotate(Count("items")).aggregate( 60 | items_count=Avg("items__count") 61 | ) 62 | order_stats = Order.objects.aggregate( 63 | Avg("total"), Max("total"), Min("total"), Sum("total"), Count("id") 64 | ) 65 | top_five_customers = ( 66 | Customer.objects.annotate(orders_placed=Count("orders")) 67 | .order_by("-orders_placed") 68 | .values("name", "orders_placed") 69 | )[:5] 70 | 71 | self.stdout.write("*" * 100) 72 | self.stdout.write( 73 | f"average_item_count_per_order => {average_item_count_per_order}" 74 | ) 75 | self.stdout.write(f"top_five_customers => {list(top_five_customers)}") 76 | self.stdout.write("Order totals") 77 | for key, value in order_stats.items(): 78 | self.stdout.write(f"-- {key} => {value}") 79 | 80 | self.stdout.write("*" * 100) 81 | -------------------------------------------------------------------------------- /shop/managers.py: -------------------------------------------------------------------------------- 1 | from django.apps import apps 2 | from django.db import models 3 | from django.db.models import Prefetch 4 | 5 | 6 | class OrderManager(models.Manager): 7 | def with_items(self): 8 | OrderItem = apps.get_model("shop", "OrderItem") 9 | 10 | prefetch_items = Prefetch( 11 | "items", queryset=OrderItem.objects.select_related("product__category") 12 | ) 13 | return ( 14 | self.get_queryset() 15 | .prefetch_related(prefetch_items) 16 | .select_related("customer") 17 | ) 18 | -------------------------------------------------------------------------------- /shop/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1 on 2022-08-29 20:28 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="Customer", 16 | fields=[ 17 | ( 18 | "id", 19 | models.BigAutoField( 20 | auto_created=True, 21 | primary_key=True, 22 | serialize=False, 23 | verbose_name="ID", 24 | ), 25 | ), 26 | ("created_at", models.DateTimeField(auto_now_add=True)), 27 | ("updated_at", models.DateTimeField(auto_now=True)), 28 | ("name", models.CharField(max_length=100)), 29 | ("birth_date", models.DateField()), 30 | ("country", models.CharField(max_length=2)), 31 | ], 32 | options={ 33 | "abstract": False, 34 | }, 35 | ), 36 | migrations.CreateModel( 37 | name="Order", 38 | fields=[ 39 | ( 40 | "id", 41 | models.BigAutoField( 42 | auto_created=True, 43 | primary_key=True, 44 | serialize=False, 45 | verbose_name="ID", 46 | ), 47 | ), 48 | ("created_at", models.DateTimeField(auto_now_add=True)), 49 | ("updated_at", models.DateTimeField(auto_now=True)), 50 | ("order_date", models.DateField()), 51 | ("is_paid", models.BooleanField(default=False)), 52 | ( 53 | "delivery_method", 54 | models.CharField( 55 | choices=[ 56 | ("pickup", "Pickup"), 57 | ("delivery", "Delivery"), 58 | ("other", "other"), 59 | ], 60 | max_length=20, 61 | ), 62 | ), 63 | ("total", models.DecimalField(decimal_places=2, max_digits=16)), 64 | ( 65 | "customer", 66 | models.ForeignKey( 67 | on_delete=django.db.models.deletion.PROTECT, 68 | related_name="orders", 69 | to="shop.customer", 70 | ), 71 | ), 72 | ], 73 | options={ 74 | "abstract": False, 75 | }, 76 | ), 77 | migrations.CreateModel( 78 | name="ProductCategory", 79 | fields=[ 80 | ( 81 | "id", 82 | models.BigAutoField( 83 | auto_created=True, 84 | primary_key=True, 85 | serialize=False, 86 | verbose_name="ID", 87 | ), 88 | ), 89 | ("created_at", models.DateTimeField(auto_now_add=True)), 90 | ("updated_at", models.DateTimeField(auto_now=True)), 91 | ("name", models.CharField(max_length=100)), 92 | ("minumum_age_allowed", models.IntegerField(default=0)), 93 | ("active", models.BooleanField(default=True)), 94 | ], 95 | options={ 96 | "abstract": False, 97 | }, 98 | ), 99 | migrations.CreateModel( 100 | name="Product", 101 | fields=[ 102 | ( 103 | "id", 104 | models.BigAutoField( 105 | auto_created=True, 106 | primary_key=True, 107 | serialize=False, 108 | verbose_name="ID", 109 | ), 110 | ), 111 | ("created_at", models.DateTimeField(auto_now_add=True)), 112 | ("updated_at", models.DateTimeField(auto_now=True)), 113 | ("name", models.CharField(max_length=100)), 114 | ("description", models.TextField()), 115 | ("price", models.DecimalField(decimal_places=2, max_digits=16)), 116 | ("active", models.BooleanField(default=True)), 117 | ( 118 | "category", 119 | models.ForeignKey( 120 | on_delete=django.db.models.deletion.PROTECT, 121 | related_name="products", 122 | to="shop.productcategory", 123 | ), 124 | ), 125 | ], 126 | options={ 127 | "abstract": False, 128 | }, 129 | ), 130 | migrations.CreateModel( 131 | name="OrderItem", 132 | fields=[ 133 | ( 134 | "id", 135 | models.BigAutoField( 136 | auto_created=True, 137 | primary_key=True, 138 | serialize=False, 139 | verbose_name="ID", 140 | ), 141 | ), 142 | ("created_at", models.DateTimeField(auto_now_add=True)), 143 | ("updated_at", models.DateTimeField(auto_now=True)), 144 | ("amount", models.DecimalField(decimal_places=2, max_digits=16)), 145 | ("unit_price", models.DecimalField(decimal_places=2, max_digits=16)), 146 | ("subtotal", models.DecimalField(decimal_places=2, max_digits=16)), 147 | ( 148 | "order", 149 | models.ForeignKey( 150 | on_delete=django.db.models.deletion.PROTECT, 151 | related_name="items", 152 | to="shop.order", 153 | ), 154 | ), 155 | ( 156 | "product", 157 | models.ForeignKey( 158 | on_delete=django.db.models.deletion.PROTECT, 159 | related_name="items_sold", 160 | to="shop.product", 161 | ), 162 | ), 163 | ], 164 | options={ 165 | "abstract": False, 166 | }, 167 | ), 168 | ] 169 | -------------------------------------------------------------------------------- /shop/migrations/0002_customer_email.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1 on 2022-09-05 20:09 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("shop", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="customer", 15 | name="email", 16 | field=models.EmailField(blank=True, max_length=254, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /shop/migrations/0003_customer_email_non_indexed_alter_customer_email.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1 on 2022-09-05 21:47 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("shop", "0002_customer_email"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="customer", 15 | name="email_non_indexed", 16 | field=models.EmailField(blank=True, max_length=254, null=True), 17 | ), 18 | migrations.AlterField( 19 | model_name="customer", 20 | name="email", 21 | field=models.EmailField( 22 | blank=True, db_index=True, max_length=254, null=True 23 | ), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /shop/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiagoferreiraw/django-orm-optimization-talk/e92804aa97ee8bbf6be4d4e399f330547357b04f/shop/migrations/__init__.py -------------------------------------------------------------------------------- /shop/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from shop.managers import OrderManager 4 | 5 | 6 | class BaseModel(models.Model): 7 | created_at = models.DateTimeField(auto_now_add=True) 8 | updated_at = models.DateTimeField(auto_now=True) 9 | 10 | class Meta: 11 | abstract = True 12 | 13 | 14 | class Customer(BaseModel): 15 | name = models.CharField(max_length=100) 16 | birth_date = models.DateField() 17 | email = models.EmailField(null=True, blank=True, db_index=True) 18 | email_non_indexed = models.EmailField(null=True, blank=True) 19 | country = models.CharField(max_length=2) 20 | 21 | def save(self, *args, **kwargs): 22 | self.email_non_indexed = self.email 23 | return super().save(*args, **kwargs) 24 | 25 | 26 | class ProductCategory(BaseModel): 27 | name = models.CharField(max_length=100) 28 | minumum_age_allowed = models.IntegerField(default=0) 29 | active = models.BooleanField(default=True) 30 | 31 | 32 | class Product(BaseModel): 33 | name = models.CharField(max_length=100) 34 | description = models.TextField() 35 | category = models.ForeignKey( 36 | ProductCategory, on_delete=models.PROTECT, related_name="products" 37 | ) 38 | price = models.DecimalField(max_digits=16, decimal_places=2) 39 | active = models.BooleanField(default=True) 40 | 41 | 42 | class Order(BaseModel): 43 | customer = models.ForeignKey( 44 | Customer, on_delete=models.PROTECT, related_name="orders" 45 | ) 46 | order_date = models.DateField() 47 | is_paid = models.BooleanField(default=False) 48 | delivery_method = models.CharField( 49 | choices=( 50 | ("pickup", "Pickup"), 51 | ("delivery", "Delivery"), 52 | ("other", "other"), 53 | ), 54 | max_length=20, 55 | ) 56 | total = models.DecimalField(max_digits=16, decimal_places=2) 57 | 58 | objects = OrderManager() 59 | 60 | def _calculate_total(self): 61 | self.total = self.items.all().aggregate(models.Sum("subtotal"))["subtotal__sum"] 62 | 63 | def save(self, *args, should_calculate_total=False, **kwargs): 64 | if should_calculate_total: 65 | self._calculate_total() 66 | return super().save(*args, **kwargs) 67 | 68 | 69 | class OrderItem(BaseModel): 70 | order = models.ForeignKey(Order, on_delete=models.PROTECT, related_name="items") 71 | product = models.ForeignKey( 72 | Product, on_delete=models.PROTECT, related_name="items_sold" 73 | ) 74 | amount = models.DecimalField(max_digits=16, decimal_places=2) 75 | unit_price = models.DecimalField(max_digits=16, decimal_places=2) 76 | subtotal = models.DecimalField(max_digits=16, decimal_places=2) 77 | 78 | def save(self, *args, **kwargs): 79 | if not self.unit_price: 80 | self.unit_price = self.product.price 81 | 82 | self.subtotal = self.unit_price * self.amount 83 | return super().save(*args, **kwargs) 84 | -------------------------------------------------------------------------------- /shop/samples.py: -------------------------------------------------------------------------------- 1 | from django.db import connection 2 | from django.db.models import Avg, Count, Exists, Max, Min, OuterRef, Prefetch, Sum 3 | 4 | from shop.helpers import DebugTypes, debug_queries 5 | from shop.models import Customer, Order, OrderItem, Product 6 | 7 | 8 | @debug_queries() 9 | def list_orders_bad(limit=5): 10 | orders = Order.objects.filter()[:limit] 11 | for order in orders: 12 | print(f"Order #{order.id} - {order.customer.name} - ${order.total}") 13 | 14 | 15 | @debug_queries() 16 | def list_orders_without_orm(limit=5): 17 | with connection.cursor() as cursor: 18 | cursor.execute( 19 | 'select "shop_order"."id", ' 20 | ' "shop_customer"."name", ' 21 | ' "shop_order"."total" ' 22 | 'FROM "shop_order" ' 23 | 'INNER JOIN "shop_customer" ' 24 | ' ON ("shop_order"."customer_id" = "shop_customer"."id") ' 25 | "LIMIT 5" 26 | ) 27 | orders = cursor.fetchall() 28 | 29 | for (order_id, customer_name, total) in orders: 30 | print(f"Order #{order_id} - {customer_name} - ${total}") 31 | 32 | 33 | @debug_queries() 34 | def list_orders_good(limit=5): 35 | orders = Order.objects.filter().select_related("customer")[:limit] 36 | for order in orders: 37 | print(f"Order #{order.id} - {order.customer.name} - ${order.total}") 38 | 39 | 40 | @debug_queries() 41 | def list_order_items_bad(order_id=1): 42 | order = Order.objects.get(id=order_id) 43 | print(f"Printing Order #{order.id} - {order.customer.name}", "\n", "-" * 60) 44 | for item in order.items.all(): 45 | print("Product: ", item.product.name) 46 | print("Category: ", item.product.category.name) 47 | print("Subtotal: ", item.subtotal, "\n") 48 | 49 | 50 | @debug_queries() 51 | def list_order_items_good(order_id=1): 52 | order = Order.objects.select_related("customer").get(id=order_id) 53 | print(f"Printing Order #{order.id} - {order.customer.name}", "\n", "-" * 60) 54 | for item in order.items.all().select_related("product__category"): 55 | print("Product: ", item.product.name) 56 | print("Category: ", item.product.category.name) 57 | print("Subtotal: ", item.subtotal, "\n") 58 | 59 | 60 | @debug_queries() 61 | def list_multiple_orders_and_items_bad(limit=5): 62 | orders = Order.objects.filter()[:limit] 63 | for order in orders: 64 | print(f"Printing Order #{order.id} - {order.customer.name}", "\n", "-" * 60) 65 | for item in order.items.all(): 66 | print("Product: ", item.product.name) 67 | print("Category: ", item.product.category.name) 68 | print("Subtotal: ", item.subtotal, "\n") 69 | 70 | 71 | @debug_queries() 72 | def list_multiple_orders_and_items_medium(limit=5): 73 | orders = Order.objects.select_related("customer").filter()[:limit] 74 | for order in orders: 75 | print(f"Printing Order #{order.id} - {order.customer.name}", "\n", "-" * 60) 76 | for item in order.items.all().select_related("product__category"): 77 | print("Product: ", item.product.name) 78 | print("Category: ", item.product.category.name) 79 | print("Subtotal: ", item.subtotal, "\n") 80 | 81 | 82 | @debug_queries() 83 | def list_multiple_orders_and_items_good(limit=5): 84 | prefetch_items = Prefetch( 85 | "items", queryset=OrderItem.objects.select_related("product__category") 86 | ) 87 | orders = ( 88 | Order.objects.select_related("customer") 89 | .prefetch_related(prefetch_items) 90 | .filter()[:limit] 91 | ) 92 | for order in orders: 93 | print(f"Printing Order #{order.id} - {order.customer.name}", "\n", "-" * 60) 94 | for item in order.items.all(): 95 | print("Product: ", item.product.name) 96 | print("Category: ", item.product.category.name) 97 | print("Subtotal: ", item.subtotal, "\n") 98 | 99 | 100 | @debug_queries() 101 | def list_multiple_orders_and_items_good_refactored(limit=5): 102 | orders = Order.objects.with_items()[:limit] 103 | for order in orders: 104 | print(f"Printing Order #{order.id} - {order.customer.name}", "\n", "-" * 60) 105 | for item in order.items.all(): 106 | print("Product: ", item.product.name) 107 | print("Category: ", item.product.category.name) 108 | print("Subtotal: ", item.subtotal, "\n") 109 | 110 | 111 | @debug_queries(DebugTypes.FULL) 112 | def list_total_sold_for_email_good(email="bramirez@example.com"): 113 | # Uses the indexed email field 114 | total_sold_for_email = Order.objects.filter(customer__email=email).aggregate( 115 | total_sold=Sum("total") 116 | ) 117 | 118 | print(f"Total sold for {email}: ${total_sold_for_email['total_sold']}") 119 | 120 | 121 | @debug_queries(DebugTypes.FULL) 122 | def list_total_sold_for_email_bad(email="bramirez@example.com"): 123 | # Uses the non indexed email field 124 | total_sold_for_email = Order.objects.filter( 125 | customer__email_non_indexed=email 126 | ).aggregate(total_sold=Sum("total")) 127 | 128 | print(f"Total sold for {email}: ${total_sold_for_email['total_sold']}") 129 | 130 | 131 | @debug_queries(DebugTypes.FULL) 132 | def list_top_paying_customers(limit=5): 133 | top_customers = ( 134 | Customer.objects.annotate( 135 | orders_placed=Count("orders"), total_value=Sum("orders__total") 136 | ) 137 | .order_by("-total_value") 138 | .values_list("name", "orders_placed", "total_value")[:limit] 139 | ) 140 | for name, orders_placed, total_value in top_customers: 141 | print("Customer: ", name) 142 | print("Orders placed: ", orders_placed) 143 | print("Total Value: ", total_value) 144 | 145 | 146 | @debug_queries(DebugTypes.FULL) 147 | def list_top_paying_customers(limit=5): 148 | top_customers = ( 149 | Customer.objects.annotate( 150 | orders_placed=Count("orders"), total_value=Sum("orders__total") 151 | ) 152 | .order_by("-total_value") 153 | .values_list("name", "orders_placed", "total_value")[:limit] 154 | ) 155 | for name, orders_placed, total_value in top_customers: 156 | print("Customer: ", name) 157 | print("Orders placed: ", orders_placed) 158 | print("Total Value: ", total_value) 159 | 160 | 161 | @debug_queries(DebugTypes.FULL) 162 | def list_product_top_sales(has_sales=True, limit=5): 163 | produts_with_sales = ( 164 | Product.objects.annotate( 165 | has_sales=Exists(OrderItem.objects.filter(product_id=OuterRef("id"))), 166 | total_sold=Sum("items_sold__subtotal"), 167 | ) 168 | .filter(has_sales=has_sales) 169 | .order_by("-total_sold") 170 | .values_list("name", "total_sold")[:limit] 171 | ) 172 | 173 | for name, total_sold in produts_with_sales: 174 | print("Product: ", name) 175 | print("Total Sold: ", total_sold, "\n") 176 | 177 | 178 | def run_all(): 179 | from shop import samples 180 | 181 | for name, sample in samples.__dict__.items(): 182 | if not name.startswith("list_"): 183 | continue 184 | 185 | print(f"======== {name} ==========") 186 | sample() 187 | -------------------------------------------------------------------------------- /shop/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /shop/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | --------------------------------------------------------------------------------