├── .flake8
├── .gitattributes
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .pre-commit-config.yaml
├── LICENSE
├── Makefile
├── README.md
├── django_insights
├── __init__.py
├── apps.py
├── charts.py
├── choices.py
├── database.py
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ └── collect_insights.py
├── managers.py
├── metrics.py
├── metrics_types.py
├── migrations
│ ├── 0001_initial.py
│ └── __init__.py
├── models.py
├── packages
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── src
│ │ ├── index.js
│ │ └── input.css
│ └── tailwind.config.js
├── pytest.py
├── registry.py
├── settings.py
├── static
│ └── insights
│ │ ├── css
│ │ ├── insights.css
│ │ └── pdf.css
│ │ ├── fonts
│ │ └── ubuntu.ttf
│ │ └── js
│ │ └── insights.js
├── templates
│ └── insights
│ │ ├── app.html
│ │ ├── base.html
│ │ ├── dashboard.html
│ │ └── pdf.html
├── templatetags
│ ├── __init__.py
│ ├── insight_chart.py
│ └── nice_format.py
├── urls.py
├── utils.py
└── views.py
├── docs
├── alternatives.md
├── assets
│ └── images
│ │ ├── banner.png
│ │ ├── screen_1.png
│ │ └── screen_2.png
├── index.md
├── installation.md
├── license.md
├── stylesheets
│ └── extra.css
└── types.md
├── manage.py
├── mkdocs.yml
├── project
├── __init__.py
├── settings
│ ├── __init__.py
│ ├── base.py
│ ├── dev.py
│ └── test.py
├── testapp
│ ├── __init__.py
│ ├── apps.py
│ ├── insights.py
│ ├── management
│ │ ├── __init__.py
│ │ └── commands
│ │ │ ├── __init__.py
│ │ │ └── seed_db.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ └── users
│ │ ├── __init__.py
│ │ ├── apps.py
│ │ ├── insights.py
│ │ ├── migrations
│ │ ├── 0001_initial.py
│ │ ├── 0002_alter_appuser_created.py
│ │ └── __init__.py
│ │ └── models.py
└── urls.py
├── pyproject.toml
├── requirements
├── compile.py
├── py310-django32.txt
├── py310-django40.txt
├── py310-django41.txt
├── py310-django42.txt
├── py311-django41.txt
├── py311-django42.txt
├── py38-django32.txt
├── py38-django40.txt
├── py38-django41.txt
├── py38-django42.txt
├── py39-django32.txt
├── py39-django40.txt
├── py39-django41.txt
├── py39-django42.txt
└── requirements.in
├── tests
├── __init__.py
├── test_api.py
└── utils.py
└── tox.ini
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length = 88
3 | extend-ignore = E203,E501
4 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | django_insights/static/* linguist-vendored
2 | django_insights/packages/* linguist-vendored
3 | docs/* linguist-documentation
4 |
5 | *.py linguist-detectable=true
6 | *.js linguist-detectable=false
7 | *.html linguist-detectable=false
8 | *.css linguist-detectable=false
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | concurrency:
10 | group: ${{ github.head_ref || github.run_id }}
11 | cancel-in-progress: true
12 |
13 | jobs:
14 | tests:
15 | name: Python ${{ matrix.python-version }}
16 | runs-on: ubuntu-22.04
17 |
18 | strategy:
19 | matrix:
20 | python-version:
21 | - 3.8
22 | - 3.9
23 | - "3.10"
24 | - "3.11"
25 |
26 | steps:
27 | - uses: actions/checkout@v3
28 |
29 | - uses: actions/setup-python@v4
30 | with:
31 | python-version: ${{ matrix.python-version }}
32 | cache: pip
33 | cache-dependency-path: "requirements/*.txt"
34 |
35 | - name: Install dependencies
36 | run: |
37 | python -m pip install --upgrade pip setuptools wheel
38 | python -m pip install --upgrade tox
39 | - name: Run tox targets for ${{ matrix.python-version }}
40 | run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d .)
41 |
--------------------------------------------------------------------------------
/.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 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
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 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 |
112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113 | __pypackages__/
114 |
115 | # Celery stuff
116 | celerybeat-schedule
117 | celerybeat.pid
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Environments
123 | .env
124 | .venv
125 | env/
126 | venv/
127 | ENV/
128 | env.bak/
129 | venv.bak/
130 |
131 | # Spyder project settings
132 | .spyderproject
133 | .spyproject
134 |
135 | # Rope project settings
136 | .ropeproject
137 |
138 | # mkdocs documentation
139 | /site
140 |
141 | # mypy
142 | .mypy_cache/
143 | .dmypy.json
144 | dmypy.json
145 |
146 | # Pyre type checker
147 | .pyre/
148 |
149 | # pytype static type analyzer
150 | .pytype/
151 |
152 | # Cython debug symbols
153 | cython_debug/
154 |
155 | # Node modules
156 | node_modules/
157 |
158 | # PyCharm
159 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
160 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
161 | # and can be added to the global gitignore or merged into this file. For a more nuclear
162 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
163 | #.idea/
164 |
165 | # Project specifics
166 | insights.db
167 | testapp.db
168 | db/
169 |
170 | project/static/
171 | project/media/
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/psf/black
3 | rev: 23.1.0
4 | hooks:
5 | - id: black
6 | - repo: https://github.com/PyCQA/isort
7 | rev: 5.12.0
8 | hooks:
9 | - id: isort
10 | - repo: https://github.com/PyCQA/flake8
11 | rev: 6.0.0
12 | hooks:
13 | - id: flake8
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright (c) 2023 terminalkitten and individual contributors.
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy of
5 | this software and associated documentation files (the "Software"), to deal in
6 | the Software without restriction, including without limitation the rights to
7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
8 | of the Software, and to permit persons to whom the Software is furnished to do
9 | so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | SOFTWARE.
21 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: rs migrate dump shell seed collect_insights compilereqtxt clean
2 |
3 | rs:
4 | python manage.py runserver
5 |
6 | migrate:
7 | python manage.py migrate
8 | python manage.py migrate insights --database=insights
9 |
10 | shell:
11 | python manage.py shell
12 |
13 | seed:
14 | python manage.py seed_db
15 |
16 | collect_insights:
17 | python manage.py collect_insights
18 |
19 | compilereq:
20 | python requirements/compile.py
21 |
22 | clean:
23 | rm -f db/*.db
24 | rm -rf django_insights/migrations
25 | rm -rf project/testapp/migrations
26 |
27 | python manage.py makemigrations testapp users
28 | python manage.py makemigrations insights
29 | python manage.py migrate
30 | python manage.py migrate insights --database=insights
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/terminalkitten/django-insights/actions/workflows/main.yml)
2 |
3 | 
4 |
5 | ## Features
6 |
7 | Create insights for your app, store them in a SQLite database for further processing, these insights are written right next to your application logic.
8 |
9 | ### Note:
10 |
11 | Still working on some small things, extending tests and improving the documentation.
12 |
13 | For now focus is on:
14 |
15 | - Django 3.2 (LTS), 4.0,4.1 and 4.2;
16 | - Python ≥ 3.8
17 |
18 | ## Installation
19 |
20 | Installing with:
21 |
22 | ```bash
23 | pip install 'django-insights'
24 | ```
25 |
26 | ## Usage
27 |
28 | First create a `insights.py` file in your app directory, for example:
29 |
30 | ```bash
31 | project
32 | └── testapp
33 | └── insights.py
34 | ```
35 |
36 | Each app can have it's own `insights.py` file, these files are auto-discovered by Django Insights, so at any given location it would pick up your metrics.
37 |
38 | In these insights files you write out any metric you would like to track. Each metric starts with a question and some values to store. Below is a example of the `@metrics.counter` function:
39 |
40 | ```python
41 | # project/testapp/insights.py
42 | from django_insights.metrics import metrics
43 | from project.testapp.models import Author
44 |
45 | label = "Bookstore"
46 |
47 | @metrics.counter(question="How many authors are there?")
48 | def count_authors() -> int:
49 | return Author.objects.count()
50 |
51 | ```
52 |
53 | Insight apps can have a `label`, this is used in the dashboard or can be read from `insights_app` table later on.
54 |
55 | ### Settings
56 |
57 | Add django_insights package, insights database and router to your settings
58 |
59 | ```python
60 |
61 | INSTALLED_APPS = [
62 | ...
63 | "django_insights",
64 | ]
65 |
66 |
67 | DATABASES = {
68 | ...
69 | "insights": {
70 | "ENGINE": "django.db.backends.sqlite3",
71 | "NAME": os.path.join(BASE_DIR,"db/insights.db")
72 | },
73 | ...
74 | }
75 |
76 | DATABASE_ROUTERS = ['django_insights.database.Router']
77 |
78 | ```
79 |
80 | Note: please make sure you exclude the database in your `.gitignore` file
81 |
82 | Migrate insights database:
83 |
84 | ```bash
85 | workon myapp
86 | python manage.py migrate insights --database=insights
87 | ```
88 |
89 | Now collect your insights
90 |
91 | ```bash
92 | python manage.py collect_insights
93 | ```
94 |
95 | You now have a database containing all insights from your application.
96 |
97 | Note: You need to run the `python manage.py collect_insights` command each time you want to update data on your dashboard. In production, you can setup a cron job to call this command at regular intervals depending on your use case.
98 |
99 | You can inspect this database yourself with `sqlite3 db/insights.db` - or - you can use the Django Insights dashboard.
100 |
101 | ### Dashboard
102 |
103 | 
104 |
105 | To enable this dashboard, add the following settings:
106 |
107 | ```python
108 | from django.urls import include, path
109 |
110 | urlpatterns = [
111 | path(
112 | 'insights/',
113 | include('django_insights.urls', namespace='insights'),
114 | ),
115 | ]
116 | ```
117 |
118 | 
119 |
120 | Now you can visit https://localhost:8000/insights to inspect your Django Insights database.
121 |
122 | ## Metrics
123 |
124 | Django insights contains 5 types of metrics it can collect:
125 |
126 | - `@metrics.counter`
127 | - `@metrics.gauge`
128 | - `@metrics.timeseries`
129 | - `@metrics.scatterplot`
130 | - `@metrics.barchart`
131 |
132 | ### Counter:
133 |
134 | ```python
135 | from django_insights.metrics import metrics
136 | from project.testapp.models import Author
137 |
138 |
139 | @metrics.counter(question="How many authors are there?")
140 | def count_authors() -> int:
141 | return Author.objects.count()
142 |
143 | ```
144 |
145 | ### Gauge:
146 |
147 | ```python
148 |
149 | from django.db.models import Avg, Count
150 |
151 | from django_insights.metrics import metrics
152 | from project.testapp.models import Author
153 |
154 |
155 | @metrics.gauge(question="Average book(s) per author?")
156 | def avg_books_per_author() -> int:
157 | avg_total_books = (
158 | Author.objects.prefetch_related('books')
159 | .annotate(total_books=Count('books'))
160 | .aggregate(Avg('total_books'))
161 | .get('total_books__avg')
162 | )
163 |
164 | return avg_total_books
165 | ```
166 |
167 | ### Timeseries:
168 |
169 | ```python
170 | from datetime import datetime
171 |
172 | from django.db.models import Count
173 | from django.db.models.functions import TruncMonth
174 |
175 | from django_insights.metrics import metrics
176 | from project.testapp.models import Book
177 |
178 |
179 | @metrics.timeseries(
180 | question="Num of books created per month?",
181 | desc="How many books are added each month, since the opening of our store",
182 | xlabel="Month",
183 | xformat='%m',
184 | ylabel="Num of books",
185 | )
186 | def num_of_books_per_month() -> list[tuple[datetime, int]]:
187 | return (
188 | Book.objects.all()
189 | .annotate(month=TruncMonth('created'))
190 | .values('month')
191 | .filter(month__isnull=False)
192 | .annotate(total=Count('pk'))
193 | .values_list('month', 'total')
194 | .order_by('month')
195 | )
196 | ```
197 |
198 | ### Scatterplot:
199 |
200 | ```python
201 | from datetime import datetime
202 |
203 | from django.db.models import Count, Value
204 |
205 | from django_insights.metrics import metrics
206 | from project.testapp.models import Author
207 |
208 |
209 | @metrics.scatterplot(
210 | question="Num of books by age of author?",
211 | xlabel="Age",
212 | ylabel="Num of books",
213 | )
214 | def author_age_vs_num_of_books() -> list[tuple[float, float, Any]]:
215 | return (
216 | Author.objects.values('age')
217 | .annotate(num_of_books=Count('books'), category=Value("author"))
218 | .values_list('num_of_books', 'age', 'category')
219 | )
220 | ```
221 |
222 | ### Barchart:
223 |
224 | ```python
225 | from datetime import datetime
226 |
227 | from django.db.models import Case, Count, Value, When
228 |
229 | from django_insights.metrics import metrics
230 | from project.testapp.models import Author
231 |
232 |
233 | @metrics.barchart(
234 | question="Num of books by gender of author?",
235 | xlabel="Gender",
236 | ylabel="Num of books",
237 | )
238 | def author_gender_vs_num_of_books() -> list[tuple[float, float, str]]:
239 | return (
240 | Author.objects.values('gender')
241 | .annotate(
242 | num_of_books=Count('books'),
243 | gender_category=Case(
244 | When(gender=1, then=Value('Male')),
245 | When(gender=2, then=Value('Female')),
246 | ),
247 | )
248 | .values_list('num_of_books', 'gender', 'gender_category')
249 | )
250 |
251 | ```
252 |
253 | ## Settings
254 |
255 | ```python
256 | # Custom app name
257 | INSIGHTS_APP_NAME = "Bezamon"
258 |
259 | # Quality of chart images
260 | INSIGHTS_CHART_DPI = 180
261 |
262 | # Default theme for dashboard
263 | INSIGHTS_THEME = "dark"
264 |
265 | # Change primary color of dashboard
266 | INSIGHTS_CHART_LIGHT_PRIMARY_COLOR = "#2563EB"
267 | INSIGHTS_CHART_DARK_PRIMARY_COLOR = "#BFDBFE"
268 |
269 | ```
270 |
271 | ## Use-cases
272 |
273 | Insights are gathered from your current application state, Django Insights is not intentend to be used as a realtime, incremementing data-source. You should be able to re-gather these insights from your actual data at any moment in time.
274 |
275 | Yes:
276 |
277 | - How many users, how many users invited a year
278 | - How many reports a day, how many messages send on Wednesday's
279 |
280 | No:
281 |
282 | - How many GET request for url XXX a second
283 | - Realtime profit target percentage
284 |
285 | ## Background
286 |
287 | I'm currently working at a small company that is in the process of renewing some parts of our product. To gain insight into the usage over different periods, we have tried a few solutions. We initially attempted to periodically generate CSV files from queries, as well as send data to a dashboard at regular intervals.
288 |
289 | We ended up with many exports that where spread out over multiple people. Additionally, exporting data directly from the database also posed a security risk, as it required constant movement of possible sensitive information. After several months of working with CSV files, which were often outdated and required conversion by other (paid) tools, we where looking for a better solution.
290 |
291 | I wanted an easy-to-configure file within our various apps that would allow me to create "insights" easily, so Django Insights was born. I decided to switch to a local SQLite database which could be share on request, as a plus these files can be tracked by a security officer.
292 |
293 | ## Documentation
294 |
295 | Write more about where to find documentation
296 |
297 | ## Ideas
298 |
299 | - Connect to other datasources and export to different file-formats ( ArrowFile?, NDJSON )
300 |
301 | ## Is it any good?
302 |
303 | [Yes.](http://news.ycombinator.com/item?id=3067434)
304 |
305 | ## License
306 |
307 | The MIT License
308 |
--------------------------------------------------------------------------------
/django_insights/__init__.py:
--------------------------------------------------------------------------------
1 | """ Django Insights """
2 |
3 | __version__ = "0.2.12-alpha"
4 |
--------------------------------------------------------------------------------
/django_insights/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class InsightAppConfig(AppConfig):
5 | name = 'django_insights'
6 | label = 'insights'
7 | verbose_name = 'Django Insights'
8 |
9 | default_auto_field = 'django.db.models.AutoField'
10 |
--------------------------------------------------------------------------------
/django_insights/charts.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import base64
4 | import io
5 | import os
6 | from dataclasses import dataclass
7 | from enum import Enum
8 |
9 | import matplotlib
10 | import matplotlib.dates as mdates
11 | import matplotlib.font_manager as fm
12 | import matplotlib.pyplot as plt
13 | from asgiref.sync import sync_to_async
14 |
15 | from django_insights.models import Bucket
16 | from django_insights.settings import settings
17 |
18 | matplotlib.use('Agg')
19 | dir_path = os.path.dirname(os.path.realpath(__file__))
20 | fm.fontManager.addfont(f"{dir_path}/static/insights/fonts/ubuntu.ttf")
21 |
22 |
23 | plt.rcParams['font.family'] = 'sans-serif'
24 | plt.rcParams['font.sans-serif'] = 'Ubuntu'
25 |
26 |
27 | class ChartSize(Enum):
28 | SMALL = 'small'
29 | MEDIUM = 'medium'
30 | LARGE = 'large'
31 |
32 |
33 | @dataclass
34 | class ThemeColor:
35 | primary: str
36 | face: str
37 | tick: str
38 | grid: str
39 | edge: str
40 |
41 |
42 | @dataclass
43 | class Theme:
44 | dark: ThemeColor
45 | light: ThemeColor
46 |
47 |
48 | themes = Theme(
49 | light=ThemeColor(
50 | primary=settings.INSIGHTS_CHART_LIGHT_PRIMARY_COLOR,
51 | face="#FEFEFE",
52 | tick="#333333",
53 | grid="#333333",
54 | edge="#FFFFFF",
55 | ),
56 | dark=ThemeColor(
57 | primary=settings.INSIGHTS_CHART_DARK_PRIMARY_COLOR,
58 | face="#0d1117",
59 | tick="#FEFEFE",
60 | grid="#FEFEFE",
61 | edge="#0d1117",
62 | ),
63 | )
64 |
65 |
66 | def get_figsize(size: ChartSize) -> tuple[int, int]:
67 | """Size of charts
68 |
69 | Args:
70 | size (ChartSize): SMALL, MEDIUM, LARGE
71 |
72 | Returns:
73 | tuple[int, int]: return tuple of figure siza
74 | """
75 |
76 | if size == ChartSize.SMALL:
77 | return (8, 4)
78 | if size == ChartSize.MEDIUM:
79 | return (16, 8)
80 | if size == ChartSize.LARGE:
81 | return (32, 16)
82 |
83 |
84 | def to_bytes_io(fig) -> bytes:
85 | """Render Mathplotlib figure to file-like object"""
86 | flike = io.BytesIO()
87 | fig.savefig(flike, dpi=settings.INSIGHTS_CHART_DPI)
88 |
89 | return flike.getvalue()
90 |
91 |
92 | def to_base64img(fig) -> str:
93 | """Render Mathplotlib figure to Base64 encoded image"""
94 | return base64.b64encode(to_bytes_io(fig)).decode()
95 |
96 |
97 | def prepare_plot(bucket, theme, size: ChartSize = ChartSize.SMALL):
98 | """Default plot options, used for all charts"""
99 |
100 | figsize = get_figsize(size)
101 |
102 | fig, ax = plt.subplots(figsize=figsize)
103 | fig.set_facecolor(theme.face)
104 | ax.tick_params(axis='x', colors=theme.tick)
105 | ax.tick_params(axis='y', colors=theme.tick)
106 | ax.set_facecolor(theme.face)
107 | ax.set_title(bucket.title)
108 | ax.set_ylabel(bucket.ylabel)
109 | ax.set_xlabel(bucket.xlabel)
110 | ax.yaxis.label.set_color(theme.tick)
111 | ax.xaxis.label.set_color(theme.tick)
112 | ax.grid(
113 | linestyle="dotted",
114 | linewidth=0.1,
115 | color=theme.grid,
116 | zorder=-10,
117 | visible=False,
118 | )
119 |
120 | for spine in ax.spines.values():
121 | spine.set_edgecolor(theme.edge)
122 |
123 | return fig, ax
124 |
125 |
126 | def render_barchart(yaxis, xaxis, labels, bucket, theme, size) -> str:
127 | """Render barchart"""
128 | theme = getattr(themes, theme)
129 | fig, ax = prepare_plot(bucket, theme, size)
130 | ax.bar(labels, xaxis, color=theme.primary)
131 |
132 | return fig
133 |
134 |
135 | def render_hbarchart(yaxis, xaxis, labels, combined_labels, bucket, theme, size) -> str:
136 | """Render horizontal barchart"""
137 | theme = getattr(themes, theme)
138 | fig, ax = prepare_plot(bucket, theme, size)
139 | bars = ax.barh(
140 | combined_labels,
141 | xaxis,
142 | color=theme.primary,
143 | align='center',
144 | height=0.9,
145 | )
146 |
147 | for bar in bars:
148 | width = bar.get_width()
149 | label_y_pos = bar.get_y() + bar.get_height() / 2
150 | ax.text(width, label_y_pos, s=f'{width}', va='center', size=4)
151 |
152 | for tick in ax.yaxis.get_major_ticks():
153 | tick.label1.set_fontsize(6)
154 | tick.label2.set_fontsize(6)
155 |
156 | ax.set_ylabel(bucket.xlabel)
157 | ax.set_xlabel(bucket.ylabel)
158 |
159 | _, xmax = plt.xlim()
160 | plt.subplots_adjust(left=0.3, right=0.9)
161 | plt.xlim(0, xmax + 100)
162 | return fig
163 |
164 |
165 | def render_scatterplot(xaxis, yaxis, bucket, theme, size) -> str:
166 | """Render scatterplot"""
167 | theme = getattr(themes, theme)
168 |
169 | fig, ax = prepare_plot(bucket, theme, size)
170 | ax.scatter(xaxis, yaxis, color=theme.primary)
171 |
172 | return fig
173 |
174 |
175 | def render_timeseries(xaxis, yaxis, bucket, theme, size) -> str:
176 | """Render timeseries"""
177 | theme = getattr(themes, theme)
178 |
179 | fig, ax = prepare_plot(bucket, theme, size)
180 | ax.plot(xaxis, yaxis, '--o', markersize=5, color=theme.primary)
181 |
182 | # Date formatting
183 | ax.fmt_xdata = mdates.DateFormatter(bucket.xformat)
184 |
185 | return fig
186 |
187 |
188 | @sync_to_async
189 | def barchart(bucket: Bucket, theme: str, size: str = ChartSize.SMALL) -> str:
190 | """Barchart"""
191 | values = bucket.values.all()
192 | yvalues = [bucket_value.yvalue for bucket_value in values]
193 | xvalues = [bucket_value.xvalue for bucket_value in values]
194 | labels = [bucket_value.category for bucket_value in values]
195 |
196 | return render_barchart(yvalues, xvalues, labels, bucket, theme, size)
197 |
198 |
199 | @sync_to_async
200 | def hbarchart(bucket: Bucket, theme: str, size: str = ChartSize.MEDIUM) -> str:
201 | """Horizontal Barchart"""
202 | values = bucket.values.all()
203 | yvalues = [bucket_value.yvalue for bucket_value in values]
204 | xvalues = [bucket_value.xvalue for bucket_value in values]
205 | labels = [bucket_value.category for bucket_value in values]
206 |
207 | combined_labels = [
208 | f"{bucket_value.category[0:30]}..:{int(bucket_value.yvalue)}"
209 | for bucket_value in values
210 | ]
211 |
212 | return render_hbarchart(
213 | yvalues, xvalues, labels, combined_labels, bucket, theme, size
214 | )
215 |
216 |
217 | @sync_to_async
218 | def scatterplot(bucket: Bucket, theme: str, size: str = ChartSize.SMALL) -> str:
219 | """Scatterplot chart"""
220 | values = bucket.values.all()
221 | yvalues = [bucket_value.yvalue for bucket_value in values]
222 | xvalues = [bucket_value.xvalue for bucket_value in values]
223 |
224 | return render_scatterplot(yvalues, xvalues, bucket, theme, size)
225 |
226 |
227 | @sync_to_async
228 | def timeseries(bucket: Bucket, theme: str, size: str = ChartSize.SMALL) -> str:
229 | """Timeseries chart"""
230 | values = bucket.values.all()
231 |
232 | yvalues = [bucket_value.timestamp for bucket_value in values]
233 | xvalues = [bucket_value.xvalue for bucket_value in values]
234 |
235 | return render_timeseries(yvalues, xvalues, bucket, theme, size)
236 |
--------------------------------------------------------------------------------
/django_insights/choices.py:
--------------------------------------------------------------------------------
1 | class BucketType:
2 | TIMESERIES = 1
3 | HISTOGRAM = 2
4 | SCATTERPLOT = 3
5 | BARCHART = 4
6 | HBARCHART = 5
7 | BUCKET_TYPES = (
8 | (TIMESERIES, 'timeseries'),
9 | (HISTOGRAM, 'histogram'),
10 | (SCATTERPLOT, 'scatterplot'),
11 | (BARCHART, 'barchart'),
12 | (HBARCHART, 'hbarchart'),
13 | )
14 |
--------------------------------------------------------------------------------
/django_insights/database.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.core.exceptions import ImproperlyConfigured
3 |
4 | app_labels = {"insights"}
5 | database_entry = "insights"
6 |
7 |
8 | def check_settings():
9 | if database_entry not in settings.DATABASES:
10 | raise ImproperlyConfigured()
11 |
12 |
13 | class Router:
14 | def db_for_read(self, model, **hints):
15 | if model._meta.app_label in app_labels:
16 | return "insights"
17 | return None
18 |
19 | def db_for_write(self, model, **hints):
20 | if model._meta.app_label in app_labels:
21 | return "insights"
22 |
23 | def allow_relation(self, obj1, obj2, **hints):
24 | if obj1._meta.app_label in app_labels or obj2._meta.app_label in app_labels:
25 | return True
26 | return None
27 |
28 | def allow_migrate(self, db, app_label, **hints):
29 | if app_label in app_labels:
30 | return db == database_entry
31 | return None
32 |
--------------------------------------------------------------------------------
/django_insights/management/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/terminalkitten/django-insights/054cbd8e50c1c4ce21166d875251631583f679b9/django_insights/management/__init__.py
--------------------------------------------------------------------------------
/django_insights/management/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/terminalkitten/django-insights/054cbd8e50c1c4ce21166d875251631583f679b9/django_insights/management/commands/__init__.py
--------------------------------------------------------------------------------
/django_insights/management/commands/collect_insights.py:
--------------------------------------------------------------------------------
1 | from django.core.management.base import BaseCommand
2 |
3 | from django_insights.metrics import metrics
4 | from django_insights.registry import registry
5 |
6 |
7 | class Command(BaseCommand):
8 |
9 | """Show all registered audit events"""
10 |
11 | def handle(self, *args, **options):
12 | self.stdout.write(self.style.HTTP_INFO('[Django Insights] - Collect insights'))
13 |
14 | # Auto-discover
15 | registry.autodiscover_insights()
16 |
17 | # Save metrics
18 | metrics.collect(reset=True)
19 |
--------------------------------------------------------------------------------
/django_insights/managers.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | from django_insights.choices import BucketType
4 |
5 |
6 | class BucketQuerySet(models.QuerySet):
7 | def timeseries(self):
8 | return self.filter(type=BucketType.TIMESERIES)
9 |
10 | def histograms(self):
11 | return self.filter(type=BucketType.HISTOGRAM)
12 |
13 | def scatterplots(self):
14 | return self.filter(type=BucketType.SCATTERPLOT)
15 |
16 | def barcharts(self):
17 | return self.filter(type=BucketType.BARCHART)
18 |
19 | def hbarcharts(self):
20 | return self.filter(type=BucketType.HBARCHART)
21 |
22 |
23 | class BucketManager(models.Manager):
24 | def get_queryset(self):
25 | return BucketQuerySet(self.model, using=self._db)
26 |
27 | def timeseries(self):
28 | return self.get_queryset().timeseries()
29 |
30 | def histograms(self):
31 | return self.get_queryset().histograms()
32 |
33 | def scatterplots(self):
34 | return self.get_queryset().scatterplots()
35 |
36 | def barcharts(self):
37 | return self.get_queryset().barcharts()
38 |
39 | def hbarcharts(self):
40 | return self.get_queryset().hbarcharts()
41 |
--------------------------------------------------------------------------------
/django_insights/metrics.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import functools
4 | import importlib
5 |
6 | from django.db import IntegrityError
7 |
8 | from django_insights.choices import BucketType
9 | from django_insights.metrics_types import (
10 | BarChartAnswer,
11 | BarChartType,
12 | CounterType,
13 | GaugeType,
14 | ScatterPlotAnswer,
15 | ScatterPlotType,
16 | TimeSeriesAnswer,
17 | TimeSeriesType,
18 | )
19 | from django_insights.models import App, Bucket, BucketValue, Counter, Gauge
20 | from django_insights.registry import registry
21 | from django_insights.utils import rebuild_chart_media_cache
22 |
23 |
24 | class InsightMetrics:
25 | """Auto-generate metrics from multiple apps at once"""
26 |
27 | create_counters: list[Counter]
28 | create_gauges: list[Gauge]
29 | create_bucket_values: list[BucketValue]
30 |
31 | apps: dict[str, App] = {}
32 |
33 | def __init__(self) -> None:
34 | self.create_counters = []
35 | self.create_gauges = []
36 | self.create_bucket_values = []
37 |
38 | def delete_metrics(self) -> None:
39 | """Reset current dataset if metrics are generated"""
40 | Counter.objects.all().delete()
41 | Gauge.objects.all().delete()
42 | Bucket.objects.all().delete()
43 | BucketValue.objects.all().delete()
44 |
45 | def get_memoized_app(self, module: str, label: str = None) -> App:
46 | """Memoize apps so we reduce query count"""
47 | if app := self.apps.get(module):
48 | return app
49 |
50 | # get or create new app
51 | app, _ = App.objects.get_or_create(module=module, defaults={'label': label})
52 | self.apps.update({module: app})
53 |
54 | return app
55 |
56 | def get_app(self, func) -> tuple[str, App]:
57 | """
58 | Get memoized app
59 |
60 | """
61 |
62 | label = func.__name__
63 | module = func.__module__
64 | insights_module = importlib.import_module(module)
65 |
66 | app_label = getattr(insights_module, 'label', None)
67 | app = self.get_memoized_app(module=module, label=app_label)
68 |
69 | return label, app
70 |
71 | def counter(self, question: str = None, desc: str = None):
72 | """
73 | Decorator to collect Counter metrics
74 |
75 | """
76 |
77 | def decorator(func):
78 | label, app = self.get_app(func)
79 |
80 | @functools.wraps(func)
81 | def inner(*args, **kwargs):
82 | counter_type = CounterType(value=func(*args, **kwargs))
83 | counter = Counter(
84 | app=app,
85 | label=label,
86 | value=counter_type.value,
87 | question=question,
88 | desc=desc,
89 | )
90 | self.create_counters.append(counter)
91 |
92 | registry.register_insight(
93 | label=label,
94 | module=app.module,
95 | question=question,
96 | func=inner,
97 | )
98 |
99 | return func
100 |
101 | return decorator
102 |
103 | def gauge(self, question: str = None, desc: str = None):
104 | """
105 | Decorator to collect Gauge metric
106 |
107 | """
108 |
109 | def decorator(func):
110 | label, app = self.get_app(func)
111 |
112 | @functools.wraps(func)
113 | def inner(*args, **kwargs):
114 | gauge_type = GaugeType(value=func(*args, **kwargs))
115 | gauge = Gauge(
116 | app=app,
117 | label=label,
118 | value=gauge_type.value,
119 | question=question,
120 | desc=desc,
121 | )
122 | self.create_gauges.append(gauge)
123 |
124 | registry.register_insight(
125 | label=label,
126 | module=app.module,
127 | question=question,
128 | func=inner,
129 | )
130 |
131 | return func
132 |
133 | return decorator
134 |
135 | def timeseries(
136 | self,
137 | question: str = None,
138 | desc: str = None,
139 | xlabel: str = None,
140 | xformat: str = None,
141 | ylabel: str = None,
142 | yformat: str = None,
143 | title=None,
144 | ):
145 | """
146 | Decorator to collect TimeSeries metrics
147 |
148 | """
149 |
150 | def decorator(func):
151 | label, app = self.get_app(func)
152 |
153 | @functools.wraps(func)
154 | def inner(*args, **kwargs):
155 | results = func(*args, **kwargs)
156 | ts_type = TimeSeriesType(
157 | values=[TimeSeriesAnswer(*result) for result in results]
158 | )
159 |
160 | bucket = Bucket.objects.create(
161 | app=app,
162 | label=label,
163 | question=question,
164 | desc=desc,
165 | xlabel=xlabel,
166 | xformat=xformat,
167 | ylabel=ylabel,
168 | yformat=yformat,
169 | title=title,
170 | type=BucketType.TIMESERIES,
171 | )
172 |
173 | for row in ts_type.values:
174 | bucket_value = BucketValue(
175 | timestamp=row.timestamp, xvalue=row.xvalue, bucket=bucket
176 | )
177 | self.create_bucket_values.append(bucket_value)
178 |
179 | registry.register_insight(
180 | label=label,
181 | module=app.module,
182 | question=question,
183 | func=inner,
184 | )
185 |
186 | return func
187 |
188 | return decorator
189 |
190 | def scatterplot(
191 | self,
192 | question: str = None,
193 | desc: str = None,
194 | xlabel: str = None,
195 | xformat: str = None,
196 | ylabel: str = None,
197 | yformat: str = None,
198 | title=None,
199 | ):
200 | """
201 | Decorator to collect Scatterplot metrics
202 |
203 | """
204 |
205 | def decorator(func):
206 | label, app = self.get_app(func)
207 |
208 | @functools.wraps(func)
209 | def inner(*args, **kwargs):
210 | results = func(*args, **kwargs)
211 | scp_type = ScatterPlotType(
212 | values=[ScatterPlotAnswer(*result) for result in results]
213 | )
214 |
215 | bucket = Bucket.objects.create(
216 | app=app,
217 | label=label,
218 | question=question,
219 | desc=desc,
220 | xlabel=xlabel,
221 | xformat=xformat,
222 | ylabel=ylabel,
223 | yformat=yformat,
224 | title=title,
225 | type=BucketType.SCATTERPLOT,
226 | )
227 |
228 | for row in scp_type.values:
229 | bucket_value = BucketValue(
230 | xvalue=row.xvalue,
231 | yvalue=row.yvalue,
232 | category=row.category,
233 | bucket=bucket,
234 | )
235 | self.create_bucket_values.append(bucket_value)
236 |
237 | registry.register_insight(
238 | label=label,
239 | module=app.module,
240 | question=question,
241 | func=inner,
242 | )
243 |
244 | return func
245 |
246 | return decorator
247 |
248 | def barchart(
249 | self,
250 | question: str = None,
251 | desc: str = None,
252 | xlabel: str = None,
253 | xformat: str = None,
254 | ylabel: str = None,
255 | yformat: str = None,
256 | title=None,
257 | ):
258 | """
259 | Decorator to collect Barchart metrics
260 |
261 | """
262 |
263 | def decorator(func):
264 | label, app = self.get_app(func)
265 |
266 | @functools.wraps(func)
267 | def inner(*args, **kwargs):
268 | results = func(*args, **kwargs)
269 | bar_type = BarChartType(
270 | values=[BarChartAnswer(*result) for result in results]
271 | )
272 |
273 | bucket = Bucket.objects.create(
274 | app=app,
275 | label=label,
276 | question=question,
277 | desc=desc,
278 | xlabel=xlabel,
279 | xformat=xformat,
280 | ylabel=ylabel,
281 | yformat=yformat,
282 | title=title,
283 | type=BucketType.BARCHART,
284 | )
285 |
286 | for row in bar_type.values:
287 | bucket_value = BucketValue(
288 | xvalue=row.xvalue,
289 | yvalue=row.yvalue,
290 | category=row.category,
291 | bucket=bucket,
292 | )
293 | self.create_bucket_values.append(bucket_value)
294 |
295 | registry.register_insight(
296 | label=label,
297 | module=app.module,
298 | question=question,
299 | func=inner,
300 | )
301 |
302 | return func
303 |
304 | return decorator
305 |
306 | def hbarchart(
307 | self,
308 | question: str = None,
309 | desc: str = None,
310 | xlabel: str = None,
311 | xformat: str = None,
312 | ylabel: str = None,
313 | yformat: str = None,
314 | title=None,
315 | ):
316 | """
317 | Decorator to collect Horizontal Barchart metrics
318 |
319 | """
320 |
321 | def decorator(func):
322 | label, app = self.get_app(func)
323 |
324 | @functools.wraps(func)
325 | def inner(*args, **kwargs):
326 | results = func(*args, **kwargs)
327 | bar_type = BarChartType(
328 | values=[BarChartAnswer(*result) for result in results]
329 | )
330 |
331 | bucket = Bucket.objects.create(
332 | app=app,
333 | label=label,
334 | question=question,
335 | desc=desc,
336 | xlabel=xlabel,
337 | xformat=xformat,
338 | ylabel=ylabel,
339 | yformat=yformat,
340 | title=title,
341 | type=BucketType.HBARCHART,
342 | )
343 |
344 | for row in bar_type.values:
345 | bucket_value = BucketValue(
346 | xvalue=row.xvalue,
347 | yvalue=row.yvalue,
348 | category=row.category,
349 | bucket=bucket,
350 | )
351 | self.create_bucket_values.append(bucket_value)
352 |
353 | registry.register_insight(
354 | label=label,
355 | module=app.module,
356 | question=question,
357 | func=inner,
358 | )
359 |
360 | return func
361 |
362 | return decorator
363 |
364 | def collect(self, reset: bool = False):
365 | """
366 | Collect insights
367 |
368 | """
369 |
370 | # Reset metrics
371 | self.delete_metrics() if reset else None
372 |
373 | # Break chart cache
374 | rebuild_chart_media_cache()
375 |
376 | # Collect insights
377 | registry.collect_insights()
378 |
379 | try:
380 | Counter.objects.bulk_create(self.create_counters)
381 | Gauge.objects.bulk_create(self.create_gauges)
382 | BucketValue.objects.bulk_create(self.create_bucket_values)
383 |
384 | except IntegrityError as e:
385 | print(
386 | "Something went wrong, most likely a you've redefined a insights method with the same name."
387 | )
388 | print("Stacktrace:", e)
389 | exit()
390 |
391 |
392 | metrics = InsightMetrics()
393 |
--------------------------------------------------------------------------------
/django_insights/metrics_types.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import datetime
4 | from dataclasses import dataclass
5 | from typing import Any, NamedTuple, Optional
6 |
7 |
8 | @dataclass
9 | class CounterType:
10 | value: int
11 |
12 |
13 | @dataclass
14 | class GaugeType:
15 | value: float
16 |
17 |
18 | class TimeSeriesAnswer(NamedTuple):
19 | timestamp: datetime.datetime
20 | xvalue: float
21 |
22 |
23 | @dataclass
24 | class TimeSeriesType:
25 | values: list[TimeSeriesAnswer]
26 |
27 |
28 | class ScatterPlotAnswer(NamedTuple):
29 | xvalue: float
30 | yvalue: float
31 | category: Optional[Any]
32 |
33 |
34 | @dataclass
35 | class ScatterPlotType:
36 | values: list[ScatterPlotAnswer]
37 |
38 |
39 | class BarChartAnswer(NamedTuple):
40 | xvalue: float
41 | yvalue: float
42 | category: Optional[Any]
43 |
44 |
45 | @dataclass
46 | class BarChartType:
47 | values: list[BarChartAnswer]
48 |
--------------------------------------------------------------------------------
/django_insights/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.7 on 2023-03-28 07:45
2 |
3 | import uuid
4 |
5 | import django.db.models.deletion
6 | import django.utils.timezone
7 | from django.db import migrations, models
8 |
9 |
10 | class Migration(migrations.Migration):
11 | initial = True
12 |
13 | dependencies = []
14 |
15 | operations = [
16 | migrations.CreateModel(
17 | name='App',
18 | fields=[
19 | (
20 | 'uuid',
21 | models.UUIDField(
22 | default=uuid.uuid4,
23 | primary_key=True,
24 | serialize=False,
25 | unique=True,
26 | ),
27 | ),
28 | (
29 | 'module',
30 | models.CharField(db_index=True, max_length=254, unique=True),
31 | ),
32 | ('label', models.CharField(max_length=254, null=True)),
33 | ],
34 | ),
35 | migrations.CreateModel(
36 | name='Bucket',
37 | fields=[
38 | (
39 | 'id',
40 | models.AutoField(
41 | auto_created=True,
42 | primary_key=True,
43 | serialize=False,
44 | verbose_name='ID',
45 | ),
46 | ),
47 | (
48 | 'created_at',
49 | models.DateTimeField(
50 | db_index=True, default=django.utils.timezone.now
51 | ),
52 | ),
53 | ('label', models.CharField(db_index=True, max_length=254, unique=True)),
54 | ('question', models.TextField(blank=True, null=True)),
55 | ('desc', models.TextField(blank=True, null=True)),
56 | (
57 | 'type',
58 | models.IntegerField(
59 | choices=[
60 | (1, 'timeseries'),
61 | (2, 'histogram'),
62 | (3, 'scatterplot'),
63 | (4, 'barchart'),
64 | ],
65 | default=1,
66 | ),
67 | ),
68 | ('xlabel', models.CharField(blank=True, max_length=254, null=True)),
69 | ('xformat', models.CharField(blank=True, max_length=254, null=True)),
70 | ('ylabel', models.CharField(blank=True, max_length=254, null=True)),
71 | ('yformat', models.CharField(blank=True, max_length=254, null=True)),
72 | ('title', models.CharField(blank=True, max_length=254, null=True)),
73 | (
74 | 'app',
75 | models.ForeignKey(
76 | on_delete=django.db.models.deletion.CASCADE,
77 | related_name='buckets',
78 | to='insights.app',
79 | ),
80 | ),
81 | ],
82 | ),
83 | migrations.CreateModel(
84 | name='ExecutionDelta',
85 | fields=[
86 | (
87 | 'id',
88 | models.AutoField(
89 | auto_created=True,
90 | primary_key=True,
91 | serialize=False,
92 | verbose_name='ID',
93 | ),
94 | ),
95 | (
96 | 'executed_at',
97 | models.DateTimeField(
98 | db_index=True, default=django.utils.timezone.now
99 | ),
100 | ),
101 | ('uuid', models.UUIDField(default=uuid.uuid4)),
102 | ],
103 | ),
104 | migrations.CreateModel(
105 | name='Gauge',
106 | fields=[
107 | (
108 | 'id',
109 | models.AutoField(
110 | auto_created=True,
111 | primary_key=True,
112 | serialize=False,
113 | verbose_name='ID',
114 | ),
115 | ),
116 | (
117 | 'created_at',
118 | models.DateTimeField(
119 | db_index=True, default=django.utils.timezone.now
120 | ),
121 | ),
122 | ('label', models.CharField(db_index=True, max_length=254, unique=True)),
123 | ('question', models.TextField(blank=True, null=True)),
124 | ('desc', models.TextField(blank=True, null=True)),
125 | ('value', models.FloatField(default=0.0)),
126 | (
127 | 'app',
128 | models.ForeignKey(
129 | on_delete=django.db.models.deletion.CASCADE,
130 | related_name='gauges',
131 | to='insights.app',
132 | ),
133 | ),
134 | ],
135 | ),
136 | migrations.CreateModel(
137 | name='Counter',
138 | fields=[
139 | (
140 | 'id',
141 | models.AutoField(
142 | auto_created=True,
143 | primary_key=True,
144 | serialize=False,
145 | verbose_name='ID',
146 | ),
147 | ),
148 | (
149 | 'created_at',
150 | models.DateTimeField(
151 | db_index=True, default=django.utils.timezone.now
152 | ),
153 | ),
154 | ('label', models.CharField(db_index=True, max_length=254, unique=True)),
155 | ('question', models.TextField(blank=True, null=True)),
156 | ('desc', models.TextField(blank=True, null=True)),
157 | ('value', models.IntegerField(default=0)),
158 | (
159 | 'app',
160 | models.ForeignKey(
161 | on_delete=django.db.models.deletion.CASCADE,
162 | related_name='counters',
163 | to='insights.app',
164 | ),
165 | ),
166 | ],
167 | ),
168 | migrations.CreateModel(
169 | name='BucketValue',
170 | fields=[
171 | (
172 | 'id',
173 | models.AutoField(
174 | auto_created=True,
175 | primary_key=True,
176 | serialize=False,
177 | verbose_name='ID',
178 | ),
179 | ),
180 | (
181 | 'timestamp',
182 | models.DateTimeField(blank=True, db_index=True, null=True),
183 | ),
184 | ('xvalue', models.FloatField(default=0.0)),
185 | ('yvalue', models.FloatField(default=0.0)),
186 | (
187 | 'category',
188 | models.CharField(db_index=True, max_length=254, null=True),
189 | ),
190 | (
191 | 'bucket',
192 | models.ForeignKey(
193 | on_delete=django.db.models.deletion.CASCADE,
194 | related_name='values',
195 | to='insights.bucket',
196 | ),
197 | ),
198 | ],
199 | ),
200 | migrations.AddConstraint(
201 | model_name='gauge',
202 | constraint=models.UniqueConstraint(
203 | fields=('app', 'label'), name='gauge_unique_app_label'
204 | ),
205 | ),
206 | migrations.AddConstraint(
207 | model_name='counter',
208 | constraint=models.UniqueConstraint(
209 | fields=('app', 'label'), name='counter_unique_app_label'
210 | ),
211 | ),
212 | migrations.AddConstraint(
213 | model_name='bucketvalue',
214 | constraint=models.UniqueConstraint(
215 | fields=('bucket', 'timestamp'),
216 | name='bucketvalue_unique_bucket_timestamp',
217 | ),
218 | ),
219 | migrations.AddConstraint(
220 | model_name='bucket',
221 | constraint=models.UniqueConstraint(
222 | fields=('app', 'label'), name='bucket_unique_app_label_timestamp'
223 | ),
224 | ),
225 | ]
226 |
--------------------------------------------------------------------------------
/django_insights/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/terminalkitten/django-insights/054cbd8e50c1c4ce21166d875251631583f679b9/django_insights/migrations/__init__.py
--------------------------------------------------------------------------------
/django_insights/models.py:
--------------------------------------------------------------------------------
1 | import uuid
2 |
3 | from django.db import models
4 | from django.utils import timezone
5 |
6 | from django_insights.choices import BucketType
7 | from django_insights.database import database_entry
8 | from django_insights.managers import BucketManager
9 |
10 |
11 | class App(models.Model):
12 | """
13 | Apps that have insights
14 |
15 | """
16 |
17 | uuid = models.UUIDField(default=uuid.uuid4, unique=True, primary_key=True)
18 | module = models.CharField(max_length=254, db_index=True, unique=True)
19 | label = models.CharField(max_length=254, null=True)
20 |
21 | @property
22 | def name(self) -> str:
23 | return self.label or self.module
24 |
25 |
26 | class ExecutionDelta(models.Model):
27 | """
28 | Delta is stored on every run and used for filtering, these values should never change
29 |
30 | """
31 |
32 | executed_at = models.DateTimeField(db_index=True, default=timezone.now)
33 | uuid = models.UUIDField(default=uuid.uuid4)
34 |
35 |
36 | class MetricModel(models.Model):
37 | created_at = models.DateTimeField(db_index=True, default=timezone.now)
38 | label = models.CharField(max_length=254, db_index=True, unique=True)
39 |
40 | question = models.TextField(blank=True, null=True)
41 | desc = models.TextField(blank=True, null=True)
42 |
43 | class Meta:
44 | abstract = True
45 |
46 |
47 | class Counter(MetricModel):
48 | """
49 | Simple counter, can incr or decr, label and app are unique
50 |
51 | """
52 |
53 | value = models.IntegerField(default=0)
54 | app = models.ForeignKey(App, related_name='counters', on_delete=models.CASCADE)
55 |
56 | class Meta:
57 | app_label = database_entry
58 | constraints = [
59 | models.UniqueConstraint(
60 | fields=['app', 'label'], name='counter_unique_app_label'
61 | )
62 | ]
63 |
64 | def __str__(self):
65 | return f"Insight Counter: {self.label} = {self.value}"
66 |
67 |
68 | class Gauge(MetricModel):
69 | """
70 | Simple gauge, can be set or set with delta, label and app are unique
71 |
72 | """
73 |
74 | value = models.FloatField(default=float(0.0))
75 | app = models.ForeignKey(App, related_name='gauges', on_delete=models.CASCADE)
76 |
77 | def gauge(self, value=1, delta=False) -> int:
78 | """Set gauge value"""
79 | if not delta:
80 | self.value = value
81 | else:
82 | self.value = self.value + value
83 |
84 | class Meta:
85 | app_label = database_entry
86 | constraints = [
87 | models.UniqueConstraint(
88 | fields=['app', 'label'], name='gauge_unique_app_label'
89 | )
90 | ]
91 |
92 | def __str__(self):
93 | return f"Insight Gauge: {self.label} = {self.value}"
94 |
95 |
96 | class Bucket(MetricModel):
97 | app = models.ForeignKey(App, related_name='buckets', on_delete=models.CASCADE)
98 |
99 | type = models.IntegerField(
100 | choices=BucketType.BUCKET_TYPES, default=BucketType.TIMESERIES
101 | )
102 |
103 | # Bucket specific plot options
104 | xlabel = models.CharField(max_length=254, blank=True, null=True)
105 | xformat = models.CharField(max_length=254, blank=True, null=True)
106 | ylabel = models.CharField(max_length=254, blank=True, null=True)
107 | yformat = models.CharField(max_length=254, blank=True, null=True)
108 |
109 | # title for plot
110 | title = models.CharField(max_length=254, blank=True, null=True)
111 |
112 | @property
113 | def is_timeseries(self) -> bool:
114 | return self.type == BucketType.TIMESERIES
115 |
116 | @property
117 | def is_histogram(self) -> bool:
118 | return self.type == BucketType.HISTOGRAM
119 |
120 | @property
121 | def is_scatterplot(self) -> bool:
122 | return self.type == BucketType.SCATTERPLOT
123 |
124 | @property
125 | def is_barchart(self) -> bool:
126 | return self.type == BucketType.BARCHART
127 |
128 | @property
129 | def is_hbarchart(self) -> bool:
130 | return self.type == BucketType.HBARCHART
131 |
132 | objects = BucketManager()
133 |
134 | class Meta:
135 | app_label = database_entry
136 | constraints = [
137 | models.UniqueConstraint(
138 | fields=[
139 | 'app',
140 | 'label',
141 | ],
142 | name='bucket_unique_app_label_timestamp',
143 | )
144 | ]
145 |
146 |
147 | class BucketValue(models.Model):
148 | """
149 | Bucket values matrix: used for time-series, histogram and scatterpltos.
150 | Combination of timestamp, label and app are unique.
151 |
152 | """
153 |
154 | # Timestamp for timeseries
155 | timestamp = models.DateTimeField(db_index=True, blank=True, null=True)
156 |
157 | # Matrix
158 | xvalue = models.FloatField(default=float(0.0))
159 | yvalue = models.FloatField(default=float(0.0))
160 | category = models.CharField(db_index=True, max_length=254, null=True)
161 |
162 | bucket = models.ForeignKey(Bucket, related_name='values', on_delete=models.CASCADE)
163 |
164 | class Meta:
165 | app_label = database_entry
166 | constraints = [
167 | models.UniqueConstraint(
168 | fields=['bucket', 'timestamp'],
169 | name='bucketvalue_unique_bucket_timestamp',
170 | )
171 | ]
172 |
173 | def __str__(self):
174 | return (
175 | "Insight Bucket value: "
176 | f" timestamp={self.bucket.label}.{self.timestamp} x={self.xvalue} y={self.yvalue}"
177 | )
178 |
--------------------------------------------------------------------------------
/django_insights/packages/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@django-insights/dashboard",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "dev": "tailwindcss -i ./src/input.css -o ../static/insights/css/insights.css --watch",
8 | "bundle": "esbuild src/index.js --minify --bundle --outfile=../static/insights/js/insights.js"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "esbuild": "^0.17.15",
15 | "preline": "^1.7.0",
16 | "tailwindcss": "^3.2.7"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/django_insights/packages/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: 5.4
2 |
3 | specifiers:
4 | esbuild: ^0.17.15
5 | preline: ^1.7.0
6 | tailwindcss: ^3.2.7
7 |
8 | dependencies:
9 | esbuild: 0.17.15
10 | preline: 1.7.0
11 | tailwindcss: 3.2.7
12 |
13 | packages:
14 |
15 | /@esbuild/android-arm/0.17.15:
16 | resolution: {integrity: sha512-sRSOVlLawAktpMvDyJIkdLI/c/kdRTOqo8t6ImVxg8yT7LQDUYV5Rp2FKeEosLr6ZCja9UjYAzyRSxGteSJPYg==}
17 | engines: {node: '>=12'}
18 | cpu: [arm]
19 | os: [android]
20 | requiresBuild: true
21 | dev: false
22 | optional: true
23 |
24 | /@esbuild/android-arm64/0.17.15:
25 | resolution: {integrity: sha512-0kOB6Y7Br3KDVgHeg8PRcvfLkq+AccreK///B4Z6fNZGr/tNHX0z2VywCc7PTeWp+bPvjA5WMvNXltHw5QjAIA==}
26 | engines: {node: '>=12'}
27 | cpu: [arm64]
28 | os: [android]
29 | requiresBuild: true
30 | dev: false
31 | optional: true
32 |
33 | /@esbuild/android-x64/0.17.15:
34 | resolution: {integrity: sha512-MzDqnNajQZ63YkaUWVl9uuhcWyEyh69HGpMIrf+acR4otMkfLJ4sUCxqwbCyPGicE9dVlrysI3lMcDBjGiBBcQ==}
35 | engines: {node: '>=12'}
36 | cpu: [x64]
37 | os: [android]
38 | requiresBuild: true
39 | dev: false
40 | optional: true
41 |
42 | /@esbuild/darwin-arm64/0.17.15:
43 | resolution: {integrity: sha512-7siLjBc88Z4+6qkMDxPT2juf2e8SJxmsbNVKFY2ifWCDT72v5YJz9arlvBw5oB4W/e61H1+HDB/jnu8nNg0rLA==}
44 | engines: {node: '>=12'}
45 | cpu: [arm64]
46 | os: [darwin]
47 | requiresBuild: true
48 | dev: false
49 | optional: true
50 |
51 | /@esbuild/darwin-x64/0.17.15:
52 | resolution: {integrity: sha512-NbImBas2rXwYI52BOKTW342Tm3LTeVlaOQ4QPZ7XuWNKiO226DisFk/RyPk3T0CKZkKMuU69yOvlapJEmax7cg==}
53 | engines: {node: '>=12'}
54 | cpu: [x64]
55 | os: [darwin]
56 | requiresBuild: true
57 | dev: false
58 | optional: true
59 |
60 | /@esbuild/freebsd-arm64/0.17.15:
61 | resolution: {integrity: sha512-Xk9xMDjBVG6CfgoqlVczHAdJnCs0/oeFOspFap5NkYAmRCT2qTn1vJWA2f419iMtsHSLm+O8B6SLV/HlY5cYKg==}
62 | engines: {node: '>=12'}
63 | cpu: [arm64]
64 | os: [freebsd]
65 | requiresBuild: true
66 | dev: false
67 | optional: true
68 |
69 | /@esbuild/freebsd-x64/0.17.15:
70 | resolution: {integrity: sha512-3TWAnnEOdclvb2pnfsTWtdwthPfOz7qAfcwDLcfZyGJwm1SRZIMOeB5FODVhnM93mFSPsHB9b/PmxNNbSnd0RQ==}
71 | engines: {node: '>=12'}
72 | cpu: [x64]
73 | os: [freebsd]
74 | requiresBuild: true
75 | dev: false
76 | optional: true
77 |
78 | /@esbuild/linux-arm/0.17.15:
79 | resolution: {integrity: sha512-MLTgiXWEMAMr8nmS9Gigx43zPRmEfeBfGCwxFQEMgJ5MC53QKajaclW6XDPjwJvhbebv+RzK05TQjvH3/aM4Xw==}
80 | engines: {node: '>=12'}
81 | cpu: [arm]
82 | os: [linux]
83 | requiresBuild: true
84 | dev: false
85 | optional: true
86 |
87 | /@esbuild/linux-arm64/0.17.15:
88 | resolution: {integrity: sha512-T0MVnYw9KT6b83/SqyznTs/3Jg2ODWrZfNccg11XjDehIved2oQfrX/wVuev9N936BpMRaTR9I1J0tdGgUgpJA==}
89 | engines: {node: '>=12'}
90 | cpu: [arm64]
91 | os: [linux]
92 | requiresBuild: true
93 | dev: false
94 | optional: true
95 |
96 | /@esbuild/linux-ia32/0.17.15:
97 | resolution: {integrity: sha512-wp02sHs015T23zsQtU4Cj57WiteiuASHlD7rXjKUyAGYzlOKDAjqK6bk5dMi2QEl/KVOcsjwL36kD+WW7vJt8Q==}
98 | engines: {node: '>=12'}
99 | cpu: [ia32]
100 | os: [linux]
101 | requiresBuild: true
102 | dev: false
103 | optional: true
104 |
105 | /@esbuild/linux-loong64/0.17.15:
106 | resolution: {integrity: sha512-k7FsUJjGGSxwnBmMh8d7IbObWu+sF/qbwc+xKZkBe/lTAF16RqxRCnNHA7QTd3oS2AfGBAnHlXL67shV5bBThQ==}
107 | engines: {node: '>=12'}
108 | cpu: [loong64]
109 | os: [linux]
110 | requiresBuild: true
111 | dev: false
112 | optional: true
113 |
114 | /@esbuild/linux-mips64el/0.17.15:
115 | resolution: {integrity: sha512-ZLWk6czDdog+Q9kE/Jfbilu24vEe/iW/Sj2d8EVsmiixQ1rM2RKH2n36qfxK4e8tVcaXkvuV3mU5zTZviE+NVQ==}
116 | engines: {node: '>=12'}
117 | cpu: [mips64el]
118 | os: [linux]
119 | requiresBuild: true
120 | dev: false
121 | optional: true
122 |
123 | /@esbuild/linux-ppc64/0.17.15:
124 | resolution: {integrity: sha512-mY6dPkIRAiFHRsGfOYZC8Q9rmr8vOBZBme0/j15zFUKM99d4ILY4WpOC7i/LqoY+RE7KaMaSfvY8CqjJtuO4xg==}
125 | engines: {node: '>=12'}
126 | cpu: [ppc64]
127 | os: [linux]
128 | requiresBuild: true
129 | dev: false
130 | optional: true
131 |
132 | /@esbuild/linux-riscv64/0.17.15:
133 | resolution: {integrity: sha512-EcyUtxffdDtWjjwIH8sKzpDRLcVtqANooMNASO59y+xmqqRYBBM7xVLQhqF7nksIbm2yHABptoioS9RAbVMWVA==}
134 | engines: {node: '>=12'}
135 | cpu: [riscv64]
136 | os: [linux]
137 | requiresBuild: true
138 | dev: false
139 | optional: true
140 |
141 | /@esbuild/linux-s390x/0.17.15:
142 | resolution: {integrity: sha512-BuS6Jx/ezxFuHxgsfvz7T4g4YlVrmCmg7UAwboeyNNg0OzNzKsIZXpr3Sb/ZREDXWgt48RO4UQRDBxJN3B9Rbg==}
143 | engines: {node: '>=12'}
144 | cpu: [s390x]
145 | os: [linux]
146 | requiresBuild: true
147 | dev: false
148 | optional: true
149 |
150 | /@esbuild/linux-x64/0.17.15:
151 | resolution: {integrity: sha512-JsdS0EgEViwuKsw5tiJQo9UdQdUJYuB+Mf6HxtJSPN35vez1hlrNb1KajvKWF5Sa35j17+rW1ECEO9iNrIXbNg==}
152 | engines: {node: '>=12'}
153 | cpu: [x64]
154 | os: [linux]
155 | requiresBuild: true
156 | dev: false
157 | optional: true
158 |
159 | /@esbuild/netbsd-x64/0.17.15:
160 | resolution: {integrity: sha512-R6fKjtUysYGym6uXf6qyNephVUQAGtf3n2RCsOST/neIwPqRWcnc3ogcielOd6pT+J0RDR1RGcy0ZY7d3uHVLA==}
161 | engines: {node: '>=12'}
162 | cpu: [x64]
163 | os: [netbsd]
164 | requiresBuild: true
165 | dev: false
166 | optional: true
167 |
168 | /@esbuild/openbsd-x64/0.17.15:
169 | resolution: {integrity: sha512-mVD4PGc26b8PI60QaPUltYKeSX0wxuy0AltC+WCTFwvKCq2+OgLP4+fFd+hZXzO2xW1HPKcytZBdjqL6FQFa7w==}
170 | engines: {node: '>=12'}
171 | cpu: [x64]
172 | os: [openbsd]
173 | requiresBuild: true
174 | dev: false
175 | optional: true
176 |
177 | /@esbuild/sunos-x64/0.17.15:
178 | resolution: {integrity: sha512-U6tYPovOkw3459t2CBwGcFYfFRjivcJJc1WC8Q3funIwX8x4fP+R6xL/QuTPNGOblbq/EUDxj9GU+dWKX0oWlQ==}
179 | engines: {node: '>=12'}
180 | cpu: [x64]
181 | os: [sunos]
182 | requiresBuild: true
183 | dev: false
184 | optional: true
185 |
186 | /@esbuild/win32-arm64/0.17.15:
187 | resolution: {integrity: sha512-W+Z5F++wgKAleDABemiyXVnzXgvRFs+GVKThSI+mGgleLWluv0D7Diz4oQpgdpNzh4i2nNDzQtWbjJiqutRp6Q==}
188 | engines: {node: '>=12'}
189 | cpu: [arm64]
190 | os: [win32]
191 | requiresBuild: true
192 | dev: false
193 | optional: true
194 |
195 | /@esbuild/win32-ia32/0.17.15:
196 | resolution: {integrity: sha512-Muz/+uGgheShKGqSVS1KsHtCyEzcdOn/W/Xbh6H91Etm+wiIfwZaBn1W58MeGtfI8WA961YMHFYTthBdQs4t+w==}
197 | engines: {node: '>=12'}
198 | cpu: [ia32]
199 | os: [win32]
200 | requiresBuild: true
201 | dev: false
202 | optional: true
203 |
204 | /@esbuild/win32-x64/0.17.15:
205 | resolution: {integrity: sha512-DjDa9ywLUUmjhV2Y9wUTIF+1XsmuFGvZoCmOWkli1XcNAh5t25cc7fgsCx4Zi/Uurep3TTLyDiKATgGEg61pkA==}
206 | engines: {node: '>=12'}
207 | cpu: [x64]
208 | os: [win32]
209 | requiresBuild: true
210 | dev: false
211 | optional: true
212 |
213 | /@nodelib/fs.scandir/2.1.5:
214 | resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
215 | engines: {node: '>= 8'}
216 | dependencies:
217 | '@nodelib/fs.stat': 2.0.5
218 | run-parallel: 1.2.0
219 | dev: false
220 |
221 | /@nodelib/fs.stat/2.0.5:
222 | resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
223 | engines: {node: '>= 8'}
224 | dev: false
225 |
226 | /@nodelib/fs.walk/1.2.8:
227 | resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
228 | engines: {node: '>= 8'}
229 | dependencies:
230 | '@nodelib/fs.scandir': 2.1.5
231 | fastq: 1.15.0
232 | dev: false
233 |
234 | /@popperjs/core/2.11.6:
235 | resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==}
236 | dev: false
237 |
238 | /acorn-node/1.8.2:
239 | resolution: {integrity: sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==}
240 | dependencies:
241 | acorn: 7.4.1
242 | acorn-walk: 7.2.0
243 | xtend: 4.0.2
244 | dev: false
245 |
246 | /acorn-walk/7.2.0:
247 | resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==}
248 | engines: {node: '>=0.4.0'}
249 | dev: false
250 |
251 | /acorn/7.4.1:
252 | resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==}
253 | engines: {node: '>=0.4.0'}
254 | hasBin: true
255 | dev: false
256 |
257 | /anymatch/3.1.3:
258 | resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
259 | engines: {node: '>= 8'}
260 | dependencies:
261 | normalize-path: 3.0.0
262 | picomatch: 2.3.1
263 | dev: false
264 |
265 | /arg/5.0.2:
266 | resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
267 | dev: false
268 |
269 | /binary-extensions/2.2.0:
270 | resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
271 | engines: {node: '>=8'}
272 | dev: false
273 |
274 | /braces/3.0.2:
275 | resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
276 | engines: {node: '>=8'}
277 | dependencies:
278 | fill-range: 7.0.1
279 | dev: false
280 |
281 | /camelcase-css/2.0.1:
282 | resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
283 | engines: {node: '>= 6'}
284 | dev: false
285 |
286 | /chokidar/3.5.3:
287 | resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
288 | engines: {node: '>= 8.10.0'}
289 | dependencies:
290 | anymatch: 3.1.3
291 | braces: 3.0.2
292 | glob-parent: 5.1.2
293 | is-binary-path: 2.1.0
294 | is-glob: 4.0.3
295 | normalize-path: 3.0.0
296 | readdirp: 3.6.0
297 | optionalDependencies:
298 | fsevents: 2.3.2
299 | dev: false
300 |
301 | /color-name/1.1.4:
302 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
303 | dev: false
304 |
305 | /cssesc/3.0.0:
306 | resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
307 | engines: {node: '>=4'}
308 | hasBin: true
309 | dev: false
310 |
311 | /defined/1.0.1:
312 | resolution: {integrity: sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==}
313 | dev: false
314 |
315 | /detective/5.2.1:
316 | resolution: {integrity: sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==}
317 | engines: {node: '>=0.8.0'}
318 | hasBin: true
319 | dependencies:
320 | acorn-node: 1.8.2
321 | defined: 1.0.1
322 | minimist: 1.2.8
323 | dev: false
324 |
325 | /didyoumean/1.2.2:
326 | resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
327 | dev: false
328 |
329 | /dlv/1.1.3:
330 | resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
331 | dev: false
332 |
333 | /esbuild/0.17.15:
334 | resolution: {integrity: sha512-LBUV2VsUIc/iD9ME75qhT4aJj0r75abCVS0jakhFzOtR7TQsqQA5w0tZ+KTKnwl3kXE0MhskNdHDh/I5aCR1Zw==}
335 | engines: {node: '>=12'}
336 | hasBin: true
337 | requiresBuild: true
338 | optionalDependencies:
339 | '@esbuild/android-arm': 0.17.15
340 | '@esbuild/android-arm64': 0.17.15
341 | '@esbuild/android-x64': 0.17.15
342 | '@esbuild/darwin-arm64': 0.17.15
343 | '@esbuild/darwin-x64': 0.17.15
344 | '@esbuild/freebsd-arm64': 0.17.15
345 | '@esbuild/freebsd-x64': 0.17.15
346 | '@esbuild/linux-arm': 0.17.15
347 | '@esbuild/linux-arm64': 0.17.15
348 | '@esbuild/linux-ia32': 0.17.15
349 | '@esbuild/linux-loong64': 0.17.15
350 | '@esbuild/linux-mips64el': 0.17.15
351 | '@esbuild/linux-ppc64': 0.17.15
352 | '@esbuild/linux-riscv64': 0.17.15
353 | '@esbuild/linux-s390x': 0.17.15
354 | '@esbuild/linux-x64': 0.17.15
355 | '@esbuild/netbsd-x64': 0.17.15
356 | '@esbuild/openbsd-x64': 0.17.15
357 | '@esbuild/sunos-x64': 0.17.15
358 | '@esbuild/win32-arm64': 0.17.15
359 | '@esbuild/win32-ia32': 0.17.15
360 | '@esbuild/win32-x64': 0.17.15
361 | dev: false
362 |
363 | /fast-glob/3.2.12:
364 | resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==}
365 | engines: {node: '>=8.6.0'}
366 | dependencies:
367 | '@nodelib/fs.stat': 2.0.5
368 | '@nodelib/fs.walk': 1.2.8
369 | glob-parent: 5.1.2
370 | merge2: 1.4.1
371 | micromatch: 4.0.5
372 | dev: false
373 |
374 | /fastq/1.15.0:
375 | resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==}
376 | dependencies:
377 | reusify: 1.0.4
378 | dev: false
379 |
380 | /fill-range/7.0.1:
381 | resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
382 | engines: {node: '>=8'}
383 | dependencies:
384 | to-regex-range: 5.0.1
385 | dev: false
386 |
387 | /fsevents/2.3.2:
388 | resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
389 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
390 | os: [darwin]
391 | requiresBuild: true
392 | dev: false
393 | optional: true
394 |
395 | /function-bind/1.1.1:
396 | resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
397 | dev: false
398 |
399 | /glob-parent/5.1.2:
400 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
401 | engines: {node: '>= 6'}
402 | dependencies:
403 | is-glob: 4.0.3
404 | dev: false
405 |
406 | /glob-parent/6.0.2:
407 | resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
408 | engines: {node: '>=10.13.0'}
409 | dependencies:
410 | is-glob: 4.0.3
411 | dev: false
412 |
413 | /has/1.0.3:
414 | resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
415 | engines: {node: '>= 0.4.0'}
416 | dependencies:
417 | function-bind: 1.1.1
418 | dev: false
419 |
420 | /is-binary-path/2.1.0:
421 | resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
422 | engines: {node: '>=8'}
423 | dependencies:
424 | binary-extensions: 2.2.0
425 | dev: false
426 |
427 | /is-core-module/2.11.0:
428 | resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==}
429 | dependencies:
430 | has: 1.0.3
431 | dev: false
432 |
433 | /is-extglob/2.1.1:
434 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
435 | engines: {node: '>=0.10.0'}
436 | dev: false
437 |
438 | /is-glob/4.0.3:
439 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
440 | engines: {node: '>=0.10.0'}
441 | dependencies:
442 | is-extglob: 2.1.1
443 | dev: false
444 |
445 | /is-number/7.0.0:
446 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
447 | engines: {node: '>=0.12.0'}
448 | dev: false
449 |
450 | /lilconfig/2.0.6:
451 | resolution: {integrity: sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==}
452 | engines: {node: '>=10'}
453 | dev: false
454 |
455 | /merge2/1.4.1:
456 | resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
457 | engines: {node: '>= 8'}
458 | dev: false
459 |
460 | /micromatch/4.0.5:
461 | resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
462 | engines: {node: '>=8.6'}
463 | dependencies:
464 | braces: 3.0.2
465 | picomatch: 2.3.1
466 | dev: false
467 |
468 | /minimist/1.2.8:
469 | resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
470 | dev: false
471 |
472 | /nanoid/3.3.4:
473 | resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==}
474 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
475 | hasBin: true
476 | dev: false
477 |
478 | /normalize-path/3.0.0:
479 | resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
480 | engines: {node: '>=0.10.0'}
481 | dev: false
482 |
483 | /object-hash/3.0.0:
484 | resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
485 | engines: {node: '>= 6'}
486 | dev: false
487 |
488 | /path-parse/1.0.7:
489 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
490 | dev: false
491 |
492 | /picocolors/1.0.0:
493 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
494 | dev: false
495 |
496 | /picomatch/2.3.1:
497 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
498 | engines: {node: '>=8.6'}
499 | dev: false
500 |
501 | /pify/2.3.0:
502 | resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
503 | engines: {node: '>=0.10.0'}
504 | dev: false
505 |
506 | /postcss-import/14.1.0_postcss@8.4.21:
507 | resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==}
508 | engines: {node: '>=10.0.0'}
509 | peerDependencies:
510 | postcss: ^8.0.0
511 | dependencies:
512 | postcss: 8.4.21
513 | postcss-value-parser: 4.2.0
514 | read-cache: 1.0.0
515 | resolve: 1.22.1
516 | dev: false
517 |
518 | /postcss-js/4.0.1_postcss@8.4.21:
519 | resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==}
520 | engines: {node: ^12 || ^14 || >= 16}
521 | peerDependencies:
522 | postcss: ^8.4.21
523 | dependencies:
524 | camelcase-css: 2.0.1
525 | postcss: 8.4.21
526 | dev: false
527 |
528 | /postcss-load-config/3.1.4_postcss@8.4.21:
529 | resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==}
530 | engines: {node: '>= 10'}
531 | peerDependencies:
532 | postcss: '>=8.0.9'
533 | ts-node: '>=9.0.0'
534 | peerDependenciesMeta:
535 | postcss:
536 | optional: true
537 | ts-node:
538 | optional: true
539 | dependencies:
540 | lilconfig: 2.0.6
541 | postcss: 8.4.21
542 | yaml: 1.10.2
543 | dev: false
544 |
545 | /postcss-nested/6.0.0_postcss@8.4.21:
546 | resolution: {integrity: sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==}
547 | engines: {node: '>=12.0'}
548 | peerDependencies:
549 | postcss: ^8.2.14
550 | dependencies:
551 | postcss: 8.4.21
552 | postcss-selector-parser: 6.0.11
553 | dev: false
554 |
555 | /postcss-selector-parser/6.0.11:
556 | resolution: {integrity: sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==}
557 | engines: {node: '>=4'}
558 | dependencies:
559 | cssesc: 3.0.0
560 | util-deprecate: 1.0.2
561 | dev: false
562 |
563 | /postcss-value-parser/4.2.0:
564 | resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
565 | dev: false
566 |
567 | /postcss/8.4.21:
568 | resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==}
569 | engines: {node: ^10 || ^12 || >=14}
570 | dependencies:
571 | nanoid: 3.3.4
572 | picocolors: 1.0.0
573 | source-map-js: 1.0.2
574 | dev: false
575 |
576 | /preline/1.7.0:
577 | resolution: {integrity: sha512-Zqca6I0Pj5C/yVSCgu3xilwuyNgPUSx1ZgbV5inP9OJmvVqEsOshE7zvo78M927f0vheTFIuinvkfNvAhduq6g==}
578 | dependencies:
579 | '@popperjs/core': 2.11.6
580 | dev: false
581 |
582 | /queue-microtask/1.2.3:
583 | resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
584 | dev: false
585 |
586 | /quick-lru/5.1.1:
587 | resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
588 | engines: {node: '>=10'}
589 | dev: false
590 |
591 | /read-cache/1.0.0:
592 | resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
593 | dependencies:
594 | pify: 2.3.0
595 | dev: false
596 |
597 | /readdirp/3.6.0:
598 | resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
599 | engines: {node: '>=8.10.0'}
600 | dependencies:
601 | picomatch: 2.3.1
602 | dev: false
603 |
604 | /resolve/1.22.1:
605 | resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==}
606 | hasBin: true
607 | dependencies:
608 | is-core-module: 2.11.0
609 | path-parse: 1.0.7
610 | supports-preserve-symlinks-flag: 1.0.0
611 | dev: false
612 |
613 | /reusify/1.0.4:
614 | resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
615 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
616 | dev: false
617 |
618 | /run-parallel/1.2.0:
619 | resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
620 | dependencies:
621 | queue-microtask: 1.2.3
622 | dev: false
623 |
624 | /source-map-js/1.0.2:
625 | resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
626 | engines: {node: '>=0.10.0'}
627 | dev: false
628 |
629 | /supports-preserve-symlinks-flag/1.0.0:
630 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
631 | engines: {node: '>= 0.4'}
632 | dev: false
633 |
634 | /tailwindcss/3.2.7:
635 | resolution: {integrity: sha512-B6DLqJzc21x7wntlH/GsZwEXTBttVSl1FtCzC8WP4oBc/NKef7kaax5jeihkkCEWc831/5NDJ9gRNDK6NEioQQ==}
636 | engines: {node: '>=12.13.0'}
637 | hasBin: true
638 | dependencies:
639 | arg: 5.0.2
640 | chokidar: 3.5.3
641 | color-name: 1.1.4
642 | detective: 5.2.1
643 | didyoumean: 1.2.2
644 | dlv: 1.1.3
645 | fast-glob: 3.2.12
646 | glob-parent: 6.0.2
647 | is-glob: 4.0.3
648 | lilconfig: 2.0.6
649 | micromatch: 4.0.5
650 | normalize-path: 3.0.0
651 | object-hash: 3.0.0
652 | picocolors: 1.0.0
653 | postcss: 8.4.21
654 | postcss-import: 14.1.0_postcss@8.4.21
655 | postcss-js: 4.0.1_postcss@8.4.21
656 | postcss-load-config: 3.1.4_postcss@8.4.21
657 | postcss-nested: 6.0.0_postcss@8.4.21
658 | postcss-selector-parser: 6.0.11
659 | postcss-value-parser: 4.2.0
660 | quick-lru: 5.1.1
661 | resolve: 1.22.1
662 | transitivePeerDependencies:
663 | - ts-node
664 | dev: false
665 |
666 | /to-regex-range/5.0.1:
667 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
668 | engines: {node: '>=8.0'}
669 | dependencies:
670 | is-number: 7.0.0
671 | dev: false
672 |
673 | /util-deprecate/1.0.2:
674 | resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
675 | dev: false
676 |
677 | /xtend/4.0.2:
678 | resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
679 | engines: {node: '>=0.4'}
680 | dev: false
681 |
682 | /yaml/1.10.2:
683 | resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
684 | engines: {node: '>= 6'}
685 | dev: false
686 |
--------------------------------------------------------------------------------
/django_insights/packages/src/index.js:
--------------------------------------------------------------------------------
1 | import 'preline';
2 |
3 | function setCookie(theme) {
4 | document.cookie = `theme=${theme}; path=/; max-age=${60 * 60 * 24 * 14};`;
5 | }
6 |
7 | function getCookie(name) {
8 | let value = `; ${document.cookie}`;
9 | let parts = value.split(`; ${name}=`);
10 | if (parts.length === 2) return parts.pop().split(';').shift();
11 | }
12 |
13 | function darkMode(theme) {
14 | const element = document.getElementById('html-root');
15 | if (theme == 'light') {
16 | element.classList.remove('dark');
17 | } else {
18 | element.classList.add('dark');
19 | }
20 | }
21 |
22 | function toggleDarkMode() {
23 | const element = document.getElementById('html-root');
24 | if (element.classList.contains('dark')) {
25 | element.classList.remove('dark');
26 | return 'light';
27 | } else {
28 | element.classList.add('dark');
29 | return 'dark';
30 | }
31 | }
32 |
33 | document.addEventListener('DOMContentLoaded', function () {
34 | // set theme
35 | const theme = getCookie('theme');
36 | darkMode(theme);
37 |
38 | const button = document.getElementById('mode-toggle');
39 |
40 | button.addEventListener('click', (event) => {
41 | const theme = toggleDarkMode();
42 | setCookie(theme);
43 | window.location.reload();
44 | });
45 |
46 | // Enable CSV copy
47 | const copyButtons = document.querySelectorAll('[data-insights-csv-copy]');
48 |
49 | copyButtons.forEach((btn) => {
50 | btn.addEventListener('click', () => {
51 | const tableId = btn.getAttribute('data-insights-csv-copy');
52 | copy_csv(tableId);
53 | });
54 | });
55 |
56 | // Enable CSV downloads
57 | const downloadButtons = document.querySelectorAll(
58 | '[data-insights-csv-download]'
59 | );
60 |
61 | downloadButtons.forEach((btn) => {
62 | btn.addEventListener('click', () => {
63 | const tableId = btn.getAttribute('data-insights-csv-download');
64 | download_csv(tableId);
65 | });
66 | });
67 | });
68 |
69 | function generate_csv(table_id, separator = ',') {
70 | // Select rows from table_id
71 | var rows = document.querySelectorAll(`${table_id} tr`);
72 | // Construct csv
73 | var csv = [];
74 | for (var i = 0; i < rows.length; i++) {
75 | var row = [],
76 | cols = rows[i].querySelectorAll('td, th');
77 | for (var j = 0; j < cols.length; j++) {
78 | // Clean innertext to remove multiple spaces and jumpline (break csv)
79 | var data = cols[j].innerText
80 | .replace(/(\r\n|\n|\r)/gm, '')
81 | .replace(/(\s\s)/gm, ' ');
82 | // Escape double-quote with double-double-quote (see https://stackoverflow.com/questions/17808511/properly-escape-a-double-quote-in-csv)
83 | data = data.replace(/"/g, '""');
84 | // Push escaped string
85 | row.push('"' + data + '"');
86 | }
87 | csv.push(row.join(separator));
88 | }
89 | return csv.join('\n');
90 | }
91 |
92 | function download_csv(tableId) {
93 | var csv_string = generate_csv(tableId);
94 |
95 | // Download it
96 | const filename =
97 | 'export_' +
98 | tableId.replace('#', '') +
99 | '_' +
100 | new Date().toLocaleDateString() +
101 | '.csv';
102 | const link = document.createElement('a');
103 | link.style.display = 'none';
104 | link.setAttribute('target', '_blank');
105 | link.setAttribute(
106 | 'href',
107 | 'data:text/csv;charset=utf-8,' + encodeURIComponent(csv_string)
108 | );
109 | link.setAttribute('download', filename);
110 | document.body.appendChild(link);
111 | link.click();
112 | document.body.removeChild(link);
113 | }
114 |
115 | function copy_csv(tableId) {
116 | var csv_string = generate_csv(tableId);
117 | navigator.clipboard.writeText(csv_string).then(
118 | () => {
119 | const dataCopiedMsg = document.getElementById('data-copied-msg');
120 | dataCopiedMsg.classList.remove('hidden');
121 | },
122 | (err) => {
123 | alert('Er ging iets mis', err);
124 | }
125 | );
126 | }
127 |
--------------------------------------------------------------------------------
/django_insights/packages/src/input.css:
--------------------------------------------------------------------------------
1 | @import url(https://fonts.bunny.net/css?family=ubuntu:300,300i,400,400i,500,500i,700,700i);
2 |
3 | @tailwind base;
4 | @tailwind components;
5 | @tailwind utilities;
--------------------------------------------------------------------------------
/django_insights/packages/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ['../templates/**/*.{html,js}', 'node_modules/preline/dist/*.js'],
4 | darkMode: 'class',
5 | theme: {
6 | fontFamily: {
7 | sans: ['Ubuntu', 'sans-serif'],
8 | },
9 | extend: {
10 | colors: {
11 | background: '#f0f3f6',
12 | foreground: '#0d1117',
13 | gray: {
14 | 50: '#f6f8fa',
15 | 100: '#eaeef2',
16 | 200: '#d0d7de',
17 | 300: '#afb8c1',
18 | 400: '#8c959f',
19 | 500: '#6e7781',
20 | 600: '#57606a',
21 | 700: '#424a53',
22 | 800: '#32383f',
23 | 900: '#24292f',
24 | },
25 | slate: {
26 | 50: '#f6f8fa',
27 | 100: '#eaeef2',
28 | 200: '#d0d7de',
29 | 300: '#afb8c1',
30 | 400: '#8c959f',
31 | 500: '#6e7781',
32 | 600: '#57606a',
33 | 700: '#424a53',
34 | 800: '#32383f',
35 | 900: '#24292f',
36 | },
37 | },
38 |
39 | // neutral: {
40 | // 50: "#f9fafb",
41 | // 100: "#f4f4f5",
42 | // 200: "#e6e6e6",
43 | // 300: "#d4d4d4",
44 | // 400: "#a3a2a3",
45 | // 500: "#737272",
46 | // 600: "#555353",
47 | // 700: "#424040",
48 | // 800: "#292727",
49 | // 900: "#1a1818",
50 | // },
51 | // blue: {
52 | // DEFAULT: "#355bb6",
53 | // 50: "#eff6ff",
54 | // 100: "#dceafd",
55 | // 200: "#c3dcfb",
56 | // 300: "#9cc6f6",
57 | // 400: "#70a7ec",
58 | // 500: "#5287df",
59 | // 600: "#3f6dcd",
60 | // 700: "#355bb6",
61 | // 800: "#2746a0",
62 | // 900: "#1e3a8a",
63 | // },
64 | // },
65 | },
66 | },
67 | plugins: [require('preline/plugin')],
68 | };
69 |
--------------------------------------------------------------------------------
/django_insights/pytest.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | from collections import namedtuple
3 | from importlib import import_module
4 | from typing import Callable
5 | from unittest.mock import patch
6 |
7 | import pytest
8 |
9 | App = namedtuple('App', ('module'))
10 |
11 |
12 | def register_insights_modules(*args, **kwargs) -> dict[str, Callable]:
13 | """
14 | Helper function to load insights functions into registry
15 | This way pytest can look them up and call them from a fixture,
16 | it also disables collecting parts with mocks
17 |
18 | Returns:
19 | dict[str, Callable]: registry with functions
20 | """
21 | from django.apps import apps
22 |
23 | registry: dict[str, Callable] = {}
24 |
25 | for app_config in apps.get_app_configs():
26 | for module_to_search in args:
27 | # Attempt to import the app's module.
28 | try:
29 | with patch('django_insights.metrics.InsightMetrics.get_app') as get_app:
30 | get_app.return_value = ("foo", App(module="bar"))
31 | mod = import_module("%s.%s" % (app_config.name, module_to_search))
32 | functions = inspect.getmembers(mod, inspect.isfunction)
33 | registry = {**registry, **dict(functions)}
34 | except Exception:
35 | continue
36 |
37 | return registry
38 |
39 |
40 | registry = register_insights_modules("insights")
41 |
42 |
43 | @pytest.fixture
44 | def insights_query(request) -> Callable | None:
45 | """
46 | Returns insights_query fixture, this function
47 | resolves wrapped function from registry
48 |
49 | Args:
50 | request (_type_): Current calling context
51 |
52 | Returns:
53 | Callable | None: Return function to call query
54 | """
55 | if func := registry.get(request.node.name.replace("test_", "")):
56 | return func
57 |
58 | return None
59 |
60 |
61 | def pytest_runtest_setup(item: pytest.Item) -> None:
62 | """
63 | Add insight fixture to insights marked tests
64 |
65 | Args:
66 | item (pytest.Item): pytest function description
67 | """
68 | marker = item.get_closest_marker("insights")
69 | if marker is None:
70 | return
71 | item.fixturenames.insert(0, 'insights_query')
72 | item.fixturenames.insert(0, 'insights_config')
73 |
--------------------------------------------------------------------------------
/django_insights/registry.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import Any, Callable
4 |
5 | from django.utils.module_loading import autodiscover_modules
6 | from tqdm import tqdm
7 |
8 |
9 | class InsightRegistry:
10 | registered_insights: list[tuple[str, str, str, Callable[[Any], Any]]] = list()
11 |
12 | @staticmethod
13 | def autodiscover_insights():
14 | autodiscover_modules("insights")
15 |
16 | def register_insight(
17 | self,
18 | label: str,
19 | module: str,
20 | question: str,
21 | func: Callable[[Any], Any],
22 | ):
23 | self.registered_insights.append((module, label, question, func))
24 |
25 | def collect_insights(self):
26 | progress_iterator = tqdm(self.registered_insights, desc='Collect insights')
27 |
28 | for module, name, question, metric in progress_iterator:
29 | progress_iterator.set_description(
30 | desc=f"Create insights for {module}.{name}", refresh=True
31 | )
32 |
33 | metric()
34 |
35 | progress_iterator.set_description(desc="Done!")
36 |
37 |
38 | registry = InsightRegistry()
39 |
--------------------------------------------------------------------------------
/django_insights/settings.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from django.conf import settings as django_settings
4 |
5 | __all__ = ["settings"]
6 |
7 |
8 | class CustomSettings:
9 | """
10 | Django Insight settings
11 |
12 | """
13 |
14 | INSIGHTS_APP_NAME: str = "MyApp"
15 | INSIGHTS_CHART_DPI: int = 180
16 | INSIGHTS_DEFAULT_THEME = "dark"
17 | INSIGHTS_CHART_LIGHT_PRIMARY_COLOR = "#2563EB"
18 | INSIGHTS_CHART_DARK_PRIMARY_COLOR = "#BFDBFE"
19 | INSIGHT_CHARTS_USE_MEDIA_CACHE = False
20 | INSIGHT_MEDIA_CACHE_ROOT = None
21 | INSIGHT_DASHBOARD_PERMISSIONS = None
22 |
23 | def __getattribute__(self, name):
24 | try:
25 | return getattr(django_settings, name)
26 | except AttributeError:
27 | return super().__getattribute__(name)
28 |
29 |
30 | settings = CustomSettings()
31 |
--------------------------------------------------------------------------------
/django_insights/static/insights/css/pdf.css:
--------------------------------------------------------------------------------
1 | @page {
2 | @top-left {
3 | background: #fff;
4 | content: counter(page);
5 | height: 1cm;
6 | text-align: center;
7 | width: 1cm;
8 | }
9 | @top-center {
10 | background: #fff;
11 | content: '';
12 | display: block;
13 | height: .05cm;
14 | opacity: .5;
15 | width: 100%;
16 | }
17 | @top-right {
18 | content: string(heading);
19 | font-size: 9pt;
20 | height: 1cm;
21 | vertical-align: middle;
22 | width: 100%;
23 | }
24 | }
25 | @page :blank {
26 | background: #fff;
27 | @top-left { background: none; content: '' }
28 | @top-center { content: none }
29 | @top-right { content: none }
30 | }
31 | @page no-chapter {
32 | @top-left { background: none; content: none }
33 | @top-center { content: none }
34 | @top-right { content: none }
35 | }
36 | @page {
37 | font-family: Ubuntu;
38 | background: #fff;
39 | color: black;
40 | }
41 |
42 | @page chapter {
43 | background: #fff;
44 | margin: 0;
45 | @top-left { content: none }
46 | @top-center { content: none }
47 | @top-right { content: none }
48 | }
49 |
50 | html {
51 | color: black;
52 | font-family: Ubuntu;
53 | font-size: 11pt;
54 | font-weight: 300;
55 | line-height: 1.5;
56 | }
57 |
58 | h1 {
59 | color: black;
60 | font-size: 38pt;
61 | margin: 5cm 2cm 0 2cm;
62 | page: no-chapter;
63 | width: 100%;
64 | }
65 | h2, h3, h4 {
66 | color: black;
67 | font-weight: 400;
68 | }
69 | h2 {
70 | break-before: always;
71 | font-size: 28pt;
72 | font-weight: 700;
73 | string-set: heading content();
74 | }
75 | h3 {
76 | font-weight: 600;
77 | font-size: 15pt;
78 | }
79 | h4 {
80 | font-size: 13pt;
81 | }
82 |
83 | .chart {
84 | width: 100%;
85 | }
86 |
87 | #cover {
88 | align-content: space-between;
89 | display: flex;
90 | flex-wrap: wrap;
91 | height: 297mm;
92 | }
93 | #cover address {
94 | background: #fff;
95 | flex: 1 50%;
96 | margin: 0 -2cm;
97 | padding: 1cm 0;
98 | white-space: pre-wrap;
99 | }
100 | #cover address:first-of-type {
101 | padding-left: 3cm;
102 | }
103 | #contents {
104 | break-before: right;
105 | break-after: left;
106 | page: no-chapter;
107 | }
108 | #contents h2 {
109 | font-size: 20pt;
110 | font-weight: 400;
111 | margin-bottom: 3cm;
112 | }
113 | #contents h3 {
114 | font-weight: 500;
115 | margin: 3em 0 1em;
116 | }
117 | #contents h3::before {
118 | background: #fff;
119 | content: '';
120 | display: block;
121 | height: .08cm;
122 | margin-bottom: .25cm;
123 | width: 2cm;
124 | }
125 | #contents ul {
126 | list-style: none;
127 | padding-left: 0;
128 | }
129 | #contents ul li {
130 | border-top: .25pt solid black;
131 | margin: .25cm 0;
132 | padding-top: .25cm;
133 | }
134 | #contents ul li::before {
135 | color: black;
136 | content: '• ';
137 | font-size: 40pt;
138 | line-height: 16pt;
139 | vertical-align: bottom;
140 | }
141 | #contents ul li a {
142 | color: inherit;
143 | text-decoration-line: inherit;
144 | }
145 | #contents ul li a::before {
146 | content: target-text(attr(href));
147 | }
148 | #contents ul li a::after {
149 | color: black;
150 | content: target-counter(attr(href), page);
151 | float: right;
152 | }
153 |
154 | #columns section {
155 | columns: 2;
156 | column-gap: 1cm;
157 | padding-top: 1cm;
158 | }
159 | #columns section p {
160 | text-align: justify;
161 | }
162 | #columns section p:first-of-type {
163 | font-weight: 700;
164 | }
165 |
166 | #skills h3 {
167 | background: #fff;
168 | margin: 0 -3cm 1cm;
169 | padding: 1cm 1cm 1cm 3cm;
170 | width: 21cm;
171 | }
172 | #skills section {
173 | padding: .5cm 0;
174 | }
175 | #skills section h4 {
176 | margin: 0;
177 | }
178 | #skills section p {
179 | margin-top: 0;
180 | }
181 |
182 | #offers {
183 | display: flex;
184 | flex-wrap: wrap;
185 | justify-content: space-between;
186 | }
187 | #offers h2, #offers h3 {
188 | width: 100%;
189 | }
190 | #offers section {
191 | width: 30%;
192 | }
193 | #offers section h4 {
194 | margin-bottom: 0;
195 | }
196 | #offers section ul {
197 | list-style: none;
198 | margin: 0;
199 | padding-left: 0;
200 | }
201 | #offers section ul li:not(:last-of-type) {
202 | margin: .5cm 0;
203 | }
204 | #offers section p {
205 | background: #fff;
206 | display: block;
207 | font-size: 15pt;
208 | font-weight: 700;
209 | margin-bottom: 0;
210 | padding: .25cm 0;
211 | text-align: center;
212 | }
213 |
214 | #chapter {
215 | align-items: center;
216 | display: flex;
217 | height: 297mm;
218 | justify-content: center;
219 | page: chapter;
220 | }
221 |
222 | #typography section {
223 | display: flex;
224 | flex-wrap: wrap;
225 | margin: 1cm 0;
226 | }
227 | #typography section h4 {
228 | border-top: 1pt solid;
229 | flex: 1 25%;
230 | margin: 0;
231 | }
232 | #typography section h4 + * {
233 | flex: 1 75%;
234 | margin: 0;
235 | padding-left: .5cm;
236 | }
237 | #typography section p {
238 | text-align: justify;
239 | }
240 | #typography section ul {
241 | line-height: 2;
242 | list-style: none;
243 | }
244 | #typography section#small-caps p {
245 | font-variant: small-caps;
246 | }
247 | #typography section#ligatures dl {
248 | display: flex;
249 | flex-wrap: wrap;
250 | }
251 | #typography section#ligatures dl dt {
252 | font-weight: 400;
253 | width: 30%;
254 | }
255 | #typography section#ligatures dl dd {
256 | flex: 1 70%;
257 | margin: 0;
258 | padding: 0;
259 | }
260 | #typography section#ligatures .none {
261 | font-variant-ligatures: none;
262 | }
263 | #typography section#ligatures .common {
264 | font-variant-ligatures: common-ligatures;
265 | }
266 | #typography section#ligatures .discretionary {
267 | font-variant-ligatures: discretionary-ligatures;
268 | }
269 | #typography section#ligatures .contextual {
270 | font-variant-ligatures: contextual;
271 | }
272 | #typography section#numbers dl {
273 | display: flex;
274 | flex-wrap: wrap;
275 | }
276 | #typography section#numbers dl dt {
277 | font-weight: 400;
278 | width: 30%;
279 | }
280 | #typography section#numbers dl dd {
281 | flex: 1 70%;
282 | margin: 0;
283 | padding: 0;
284 | }
285 | #typography section#numbers #fractions {
286 | font-variant-numeric: diagonal-fractions;
287 | }
288 | #typography section#numbers #ordinals {
289 | font-variant-numeric: ordinal;
290 | }
291 | #typography section#numbers #slashed {
292 | font-variant-numeric: slashed-zero;
293 | }
294 | #typography section#numbers #super {
295 | font-variant-position: super;
296 | }
297 | #typography section#numbers #sub {
298 | font-variant-position: sub;
299 | }
300 | #typography section#figures dl {
301 | columns: 4;
302 | }
303 | #typography section#figures dl dt {
304 | font-weight: 400;
305 | }
306 | #typography section#figures dl dd {
307 | display: flex;
308 | margin: 0;
309 | padding: 0;
310 | }
311 | #typography section#figures dl dd ul {
312 | padding: 0 1em 0 0;
313 | }
314 | #typography section#figures #oldstyle {
315 | font-variant-numeric: oldstyle-nums;
316 | }
317 | #typography section#figures #tabular {
318 | font-variant-numeric: tabular-nums;
319 | }
320 | #typography section#figures #old-tabular {
321 | font-variant-numeric: oldstyle-nums tabular-nums;
322 | }
323 |
324 | table {
325 | width: 100%;
326 | font-size: 9px;
327 | padding: 4px;
328 | }
329 |
330 | table thead {
331 | background-color: lightgrey;
332 | padding: 6px;
333 | vertical-align: bottom;
334 | }
335 |
336 | .pagebreak { page-break-before: always;}
337 |
338 |
339 | .counters {
340 | display: flex;
341 | flex-wrap: wrap;
342 | gap: 6px;
343 | }
344 |
345 | .counters > div {
346 | flex: 1 0 22%;
347 | margin: 4px;
348 | padding: 4px;
349 | border: 1px solid black;
350 | border-radius: 4px;
351 | }
352 |
353 | .counters p {
354 | margin: 0;
355 | padding: 4px;
356 | }
357 |
358 | .counters .stats {
359 | display: flex;
360 | padding: 4px;
361 | align-items: center;
362 | }
363 |
364 | .counters .counter {
365 | display: inline-block;
366 | }
--------------------------------------------------------------------------------
/django_insights/static/insights/fonts/ubuntu.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/terminalkitten/django-insights/054cbd8e50c1c4ce21166d875251631583f679b9/django_insights/static/insights/fonts/ubuntu.ttf
--------------------------------------------------------------------------------
/django_insights/templates/insights/base.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% url 'insights:insight_dashboard' as dashboard_url %}
3 |
4 |
5 |
{{ app_name }} - Insights
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
36 |
37 |
38 |
40 | -
41 | {{ app_name }}
42 |
50 |
51 | - {{ app_label }}
53 |
54 |
65 |
66 |
67 |
68 |
69 | Data copied copy of the data is now on your clipboard.
70 |
71 |
72 |
109 |
110 | {% block content %}
111 | {% endblock content %}
112 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/django_insights/templates/insights/dashboard.html:
--------------------------------------------------------------------------------
1 | {% extends "insights/base.html" %}
2 | {% block content %}
3 |
4 |
5 |
6 |
7 | {% for counter in counter_list %}
8 |
9 |
10 |
11 |
{{ counter.question }}
12 | {% if counter.desc %}
13 |
30 | {% endif %}
31 |
32 |
33 |
{{ counter.value }}
34 |
35 | x
36 |
37 |
38 |
39 |
40 | {% endfor %}
41 | {% for gauge in gauges %}
42 |
43 |
44 |
45 |
{{ gauge.question }}
46 |
63 |
64 |
65 |
{{ gauge.value }}
66 |
67 | %
68 |
69 |
78 |
79 |
80 |
81 | {% endfor %}
82 |
83 |
84 |
85 |
86 |
87 | {% endblock content %}
88 |
--------------------------------------------------------------------------------
/django_insights/templates/insights/pdf.html:
--------------------------------------------------------------------------------
1 | {% load static nice_format insight_chart %}
2 |
3 |
4 |
5 | Report {{ app_name }}
6 |
7 | {% if test_mode %}
8 |
9 | {% endif %}
10 |
11 |
12 |
13 | Insights {{ app_name }}
14 |
15 | {% for app in object_list %}
16 |
17 | {{ app.name }}
18 |
19 | {% for counter in app.counters.all %}
20 | {% if counter %}
21 |
22 |
23 |
{{ counter.question }}
24 |
25 |
{{ counter.value }}
26 |
27 | x
28 |
29 |
30 |
31 |
32 | {% endif %}
33 | {% endfor %}
34 | {% for gauge in app.gauges.all %}
35 | {% if gauge %}
36 |
37 |
38 |
{{ gauge.question }}
39 |
40 |
{{ gauge.value }}
41 |
42 | %
43 |
44 |
45 |
46 |
47 | {% endif %}
48 | {% endfor %}
49 |
50 |
51 | {% for bucket in app.buckets.timeseries %}
52 | {% if bucket %}
53 | {{ bucket.question }}
54 | {{ bucket.desc }}
55 | {% pdf_chart bucket.pk %}
56 |
57 |
58 |
59 |
60 |
61 | {{ bucket.xlabel }} |
62 | {{ bucket.ylabel }} |
63 |
64 |
65 |
66 | {% for bucket_value in bucket.values.all %}
67 |
68 | {{ bucket_value.timestamp|date:bucket.xformat|clean_str:"%" }} |
69 | {{ bucket_value.xvalue|nice_float }} |
70 |
71 | {% endfor %}
72 |
73 |
74 |
75 |
76 | {% if not forloop.last %}{% endif %}
77 | {% endif %}
78 | {% endfor %}
79 | {% for bucket in app.buckets.barcharts %}
80 | {% if bucket %}
81 | {{ bucket.question }}
82 | {{ bucket.desc }}
83 | {% pdf_chart bucket.pk %}
84 |
85 |
86 |
87 |
88 |
89 | {{ bucket.xlabel }} |
90 | {{ bucket.ylabel }} |
91 |
92 |
93 |
94 | {% for bucket_value in bucket.values.all %}
95 |
96 | {{ bucket_value.category }} |
97 | {{ bucket_value.xvalue|nice_float }} |
98 |
99 | {% endfor %}
100 |
101 |
102 |
103 |
104 | {% if not forloop.last %}{% endif %}
105 | {% endif %}
106 | {% endfor %}
107 | {% for bucket in app.buckets.hbarcharts %}
108 | {% if bucket %}
109 | {{ bucket.question }}
110 | {{ bucket.desc }}
111 | {% pdf_chart bucket.pk %}
112 |
113 |
114 |
115 |
116 |
117 | {{ bucket.xlabel }} |
118 | {{ bucket.ylabel }} |
119 |
120 |
121 |
122 | {% for bucket_value in bucket.values.all %}
123 |
124 | {{ bucket_value.category }} |
125 | {{ bucket_value.xvalue|nice_float }} |
126 |
127 | {% endfor %}
128 |
129 |
130 |
131 |
132 | {% if not forloop.last %}{% endif %}
133 | {% endif %}
134 | {% endfor %}
135 | {% if forloop.last %}{% endif %}
136 |
137 | {% endfor %}
138 |
139 |
140 |
--------------------------------------------------------------------------------
/django_insights/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/terminalkitten/django-insights/054cbd8e50c1c4ce21166d875251631583f679b9/django_insights/templatetags/__init__.py
--------------------------------------------------------------------------------
/django_insights/templatetags/insight_chart.py:
--------------------------------------------------------------------------------
1 | from asgiref.sync import async_to_sync
2 | from django import template
3 | from django.urls import reverse
4 | from django.utils.safestring import mark_safe
5 |
6 | from django_insights.charts import (
7 | barchart,
8 | hbarchart,
9 | scatterplot,
10 | timeseries,
11 | to_base64img,
12 | )
13 | from django_insights.models import Bucket
14 | from django_insights.settings import settings
15 |
16 | register = template.Library()
17 |
18 |
19 | @register.simple_tag
20 | def chart(bucket: Bucket, overlay: str = None):
21 | img_url = reverse("insights:insight_chart", args=[bucket.pk])
22 |
23 | overlay_attr = ""
24 | if overlay:
25 | overlay_attr = f'data-hs-overlay="#hs-full-screen-modal-{overlay}"'
26 |
27 | return mark_safe(
28 | f"""
29 |
35 | """
36 | )
37 |
38 |
39 | @async_to_sync
40 | async def base64img(bucket: Bucket, override_theme=None):
41 | theme = override_theme or settings.INSIGHTS_DEFAULT_THEME
42 | if bucket.is_timeseries:
43 | fig = await timeseries(bucket, theme=theme)
44 | if bucket.is_scatterplot:
45 | fig = await scatterplot(bucket, theme=theme)
46 | if bucket.is_barchart:
47 | fig = await barchart(bucket, theme=theme)
48 | if bucket.is_hbarchart:
49 | fig = await hbarchart(bucket, theme=theme)
50 |
51 | return to_base64img(fig)
52 |
53 |
54 | @register.simple_tag
55 | def pdf_chart(bucket_id: int):
56 | bucket = Bucket.objects.get(pk=bucket_id)
57 | img_data = base64img(bucket, override_theme="light")
58 |
59 | return mark_safe(
60 | f"""
61 |
65 | """
66 | )
67 |
--------------------------------------------------------------------------------
/django_insights/templatetags/nice_format.py:
--------------------------------------------------------------------------------
1 | from django import template
2 |
3 | register = template.Library()
4 |
5 |
6 | @register.filter
7 | def clean_str(string, char):
8 | return string.replace(char, '')
9 |
10 |
11 | @register.filter
12 | def nice_float(string):
13 | if isinstance(string, float):
14 | return f'{round(string):,}'.replace(',', '.')
15 | return string
16 |
--------------------------------------------------------------------------------
/django_insights/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from django_insights import views
4 |
5 | app_name = "insights"
6 |
7 | urlpatterns = [
8 | path(
9 | 'app//',
10 | views.InsightsAppView.as_view(),
11 | name="insight_app",
12 | ),
13 | path(
14 | 'pdf/test/',
15 | views.InsightsPDFTestView.as_view(),
16 | name="insight_pdf_test",
17 | ),
18 | path(
19 | 'pdf/',
20 | views.InsightsPDFView.as_view(),
21 | name="insight_pdf",
22 | ),
23 | path(
24 | 'charts//',
25 | views.InsightsChartView.as_view(),
26 | name="insight_chart",
27 | ),
28 | path(
29 | '',
30 | views.InsightsDashboardView.as_view(),
31 | name="insight_dashboard",
32 | ),
33 | ]
34 |
--------------------------------------------------------------------------------
/django_insights/utils.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 | from typing import Any, Callable
4 |
5 | from django.conf import settings
6 | from django.db.backends.base.base import BaseDatabaseWrapper
7 | from django.db.backends.signals import connection_created
8 |
9 |
10 | class DjangoReadOnlyError(Exception):
11 | pass
12 |
13 |
14 | def rebuild_chart_media_cache() -> None:
15 | """
16 | Delete and recreate cache if cache is enabled and cache dir exists
17 | """
18 | if settings.INSIGHT_CHARTS_USE_MEDIA_CACHE and os.path.exists(
19 | settings.INSIGHT_MEDIA_CACHE_ROOT
20 | ):
21 | shutil.rmtree(settings.INSIGHT_MEDIA_CACHE_ROOT)
22 | os.mkdir(settings.INSIGHT_MEDIA_CACHE_ROOT)
23 |
24 |
25 | def should_block(sql: str) -> bool:
26 | return not sql.lstrip(" \n(").startswith(
27 | (
28 | "EXPLAIN ",
29 | "PRAGMA ",
30 | "ROLLBACK TO SAVEPOINT ",
31 | "RELEASE SAVEPOINT ",
32 | "SAVEPOINT ",
33 | "SELECT ",
34 | "SET ",
35 | )
36 | ) and sql not in ("BEGIN", "COMMIT", "ROLLBACK")
37 |
38 |
39 | def blocker(
40 | execute: Callable[[str, str, bool, dict[str, Any]], Any],
41 | sql: str,
42 | params: str,
43 | many: bool,
44 | context: dict[str, Any],
45 | ) -> Any:
46 | if should_block(sql):
47 | msg = "Write queries are currently disabled. Metrics MUST be read_only"
48 | raise DjangoReadOnlyError(msg)
49 | return execute(sql, params, many, context)
50 |
51 |
52 | def install_hook(connection: BaseDatabaseWrapper, **kwargs: object) -> None:
53 | if blocker not in connection.execute_wrappers: # pragma: no branch
54 | connection.execute_wrappers.insert(0, blocker)
55 |
56 |
57 | class ReadOnly:
58 | def __init__(self, read_only_metric):
59 | self.read_only_metric = read_only_metric
60 |
61 | def __enter__(self):
62 | connection_created.connect(install_hook)
63 | return self.read_only_metric
64 |
65 | def __exit__(*args, **kwargs):
66 | connection_created.disconnect(install_hook)
67 |
68 |
69 | read_only_mode = ReadOnly
70 |
--------------------------------------------------------------------------------
/django_insights/views.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import asyncio
4 | import os
5 | from typing import Any, Callable
6 |
7 | from asgiref.sync import sync_to_async
8 | from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
9 | from django.db.models.query import QuerySet
10 | from django.http import HttpResponse
11 | from django.utils.decorators import classonlymethod
12 | from django.views.generic import DetailView, ListView, View
13 | from django_weasyprint import WeasyTemplateResponseMixin
14 |
15 | from django_insights.charts import (
16 | barchart,
17 | hbarchart,
18 | scatterplot,
19 | timeseries,
20 | to_bytes_io,
21 | )
22 | from django_insights.models import App, Bucket, Counter, Gauge
23 | from django_insights.settings import settings
24 |
25 |
26 | class CustomPermissionMixin(UserPassesTestMixin):
27 |
28 | """
29 | Run following permisssion
30 |
31 | class IsSomeAdminUser:
32 | def has_permission(self, request):
33 | return
34 | """
35 |
36 | def run_permissions(self, permissions: list[Callable]) -> list[bool]:
37 | """
38 | Run all permission checks from INSIGHT_DASHBOARD_PERMISSIONS settings
39 | and return output in array
40 | """
41 | return [
42 | permission().has_permission(request=self.request)
43 | for permission in permissions
44 | if hasattr(permission(), 'has_permission')
45 | ]
46 |
47 | def test_func(self):
48 | return (
49 | all(self.run_permissions(settings.INSIGHT_DASHBOARD_PERMISSIONS))
50 | if settings.INSIGHT_DASHBOARD_PERMISSIONS
51 | else True
52 | )
53 |
54 |
55 | class InsightAppMixin(
56 | LoginRequiredMixin,
57 | CustomPermissionMixin,
58 | View,
59 | ):
60 | apps: list[App] = []
61 |
62 | def get_apps(self) -> QuerySet[App]:
63 | if not self.apps:
64 | self.apps = App.objects.all()
65 | return self.apps
66 |
67 | def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
68 | context = super().get_context_data(**kwargs)
69 | context.update(
70 | {'app_name': settings.INSIGHTS_APP_NAME, 'apps': self.get_apps()}
71 | )
72 | return context
73 |
74 |
75 | class InsightsAppView(InsightAppMixin, DetailView):
76 | model = App
77 | slug_field = 'uuid'
78 | slug_url_kwarg = 'app_uuid'
79 | template_name = "insights/app.html"
80 |
81 | def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
82 | app = self.get_object()
83 |
84 | context = super().get_context_data(**kwargs)
85 | context.update({'app_label': app.name})
86 | return context
87 |
88 |
89 | class InsightsDashboardView(InsightAppMixin, ListView):
90 | template_name = "insights/dashboard.html"
91 |
92 | def get_queryset(self) -> list[Any]:
93 | # Get all metrics in one query?
94 | return Counter.objects.all()
95 |
96 | def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
97 | context = super().get_context_data(**kwargs)
98 | context.update({'gauges': Gauge.objects.all(), 'app_label': 'Dashboard'})
99 | return context
100 |
101 |
102 | @sync_to_async
103 | def get_bucket(bucket_id: int) -> Bucket:
104 | """Get bucket from async context"""
105 | return Bucket.objects.get(pk=bucket_id)
106 |
107 |
108 | class InsightsChartView(LoginRequiredMixin, View):
109 | @classonlymethod
110 | def as_view(cls, **initkwargs):
111 | view = super().as_view(**initkwargs)
112 | view._is_coroutine = asyncio.coroutines._is_coroutine
113 | return view
114 |
115 | async def get(self, request, bucket_id):
116 | theme = self.request.COOKIES.get('theme') or settings.INSIGHTS_DEFAULT_THEME
117 | bucket: Bucket = await get_bucket(bucket_id)
118 |
119 | filename: str = (
120 | f"{settings.INSIGHT_MEDIA_CACHE_ROOT}/{bucket.type}-{bucket.pk}-{theme}.png"
121 | )
122 |
123 | if os.path.exists(filename) and settings.INSIGHT_CHARTS_USE_MEDIA_CACHE:
124 | with open(filename, 'rb') as cached_image:
125 | buffer = cached_image.read()
126 |
127 | return HttpResponse(buffer, content_type='image/png')
128 |
129 | fig = None
130 |
131 | if bucket.is_timeseries:
132 | fig = await timeseries(bucket, theme=theme)
133 | if bucket.is_scatterplot:
134 | fig = await scatterplot(bucket, theme=theme)
135 | if bucket.is_barchart:
136 | fig = await barchart(bucket, theme=theme)
137 | if bucket.is_hbarchart:
138 | fig = await hbarchart(bucket, theme=theme)
139 |
140 | buffer = to_bytes_io(fig)
141 |
142 | if settings.INSIGHT_CHARTS_USE_MEDIA_CACHE:
143 | with open(filename, 'wb') as cached_image:
144 | cached_image.write(buffer)
145 |
146 | return HttpResponse(buffer, content_type='image/png')
147 |
148 |
149 | class InsightsPDFView(WeasyTemplateResponseMixin, InsightAppMixin, ListView):
150 | """Render PDF"""
151 |
152 | queryset = App.objects.all()
153 |
154 | pdf_stylesheets = [
155 | settings.STATIC_ROOT + 'insights/css/pdf.css',
156 | ]
157 |
158 | template_name = 'insights/pdf.html'
159 | pdf_attachment = False
160 | pdf_filename = 'foo.pdf'
161 |
162 |
163 | class InsightsPDFTestView(InsightAppMixin, ListView):
164 | """Render PDF HTML as test"""
165 |
166 | queryset = App.objects.all()
167 |
168 | pdf_stylesheets = [
169 | settings.STATIC_ROOT + 'insights/css/pdf.css',
170 | ]
171 |
172 | def get_template_names(self):
173 | return ['insights/pdf.html']
174 |
175 | def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
176 | data = super().get_context_data(**kwargs)
177 |
178 | data.update(
179 | {
180 | 'test_mode': True,
181 | 'test_stylesheet': 'insights/css/pdf.css',
182 | }
183 | )
184 |
185 | return data
186 |
--------------------------------------------------------------------------------
/docs/alternatives.md:
--------------------------------------------------------------------------------
1 | # Alternatives
2 |
--------------------------------------------------------------------------------
/docs/assets/images/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/terminalkitten/django-insights/054cbd8e50c1c4ce21166d875251631583f679b9/docs/assets/images/banner.png
--------------------------------------------------------------------------------
/docs/assets/images/screen_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/terminalkitten/django-insights/054cbd8e50c1c4ce21166d875251631583f679b9/docs/assets/images/screen_1.png
--------------------------------------------------------------------------------
/docs/assets/images/screen_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/terminalkitten/django-insights/054cbd8e50c1c4ce21166d875251631583f679b9/docs/assets/images/screen_2.png
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # Welcome to Django Insights
2 |
3 | ## Django Insights: Overview and Usage
4 |
5 | Django Insights is a Django app that provides a easy way to generate application insights and store them in a SQLite database. These insights can be used to inform decisions about application design and to identify trends over time.
6 |
7 | Examples of the types of insights you can gather include:
8 |
9 | - number of users
10 | - number of users invited in the past year
11 | - number of reports generated per day
12 | - number of messages sent on Wednesdays
13 |
14 | While Django Insights can be used gather insights from your current application state, it is not suitable for tracking real-time metrics such as:
15 |
16 | - number of GET requests for a specific URL per second
17 | - real-time profit target percentage
18 |
--------------------------------------------------------------------------------
/docs/installation.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | Installing with:
4 |
5 | ```bash
6 | pip install 'django-insights'
7 | ```
8 |
9 | ## Usage
10 |
11 | First create a `insights.py` file in your app directory, for example:
12 |
13 | ```bash
14 | project
15 | └── testapp
16 | └── insights.py
17 | ```
18 |
19 | Each app can have it's own `insights.py` file, these files are auto-discovered by Django Insights, so at any given location it would pick up your metrics.
20 |
21 | In these insights files you write out any metric you would like to track. Each metric starts with a question and some values to store. Below is a example of the `@metrics.counter` function:
22 |
23 | ```python
24 | # project/testapp/insights.py
25 | from django_insights.metrics import metrics
26 | from project.testapp.models import Author
27 |
28 | label = "Bookstore"
29 |
30 | @metrics.counter(question="How many authors are there?")
31 | def count_authors() -> int:
32 | return Author.objects.count()
33 |
34 | ```
35 |
36 | Insight apps can have a `label`, this is used in the dashboard or can be read from `insights_app` table later on.
37 |
38 | ### Settings
39 |
40 | Add django_insights package, insights database and router to your settings
41 |
42 | ```python
43 |
44 | INSTALLED_APPS = [
45 | ...
46 | "django_insights",
47 | ]
48 |
49 |
50 | DATABASES = {
51 | ...
52 | "insights": {
53 | "ENGINE": "django.db.backends.sqlite3",
54 | "NAME": os.path.join(BASE_DIR,"db/insights.db")
55 | },
56 | ...
57 | }
58 |
59 | DATABASE_ROUTERS = ['django_insights.database.Router']
60 |
61 | ```
62 |
63 | Note: please make sure you exclude the database in your `.gitignore` file
64 |
65 | ### Migrate database
66 |
67 | ```bash
68 | workon myapp
69 | python manage.py migrate insights --database=insights
70 | ```
71 |
72 | ### Collect your insights
73 |
74 | ```bash
75 | python manage.py collect_insights
76 | ```
77 |
78 | You now have a database containing all insights from your application.
79 |
80 | Note: You need to run the `python manage.py collect_insights` command each time you want to update data on your dashboard. In production, you can setup a cron job to call this command at regular intervals depending on your use case.
81 |
82 |
83 | You can inspect this database yourself with `sqlite3 db/insights.db` - or - you can use the Django Insights dashboard.
84 |
--------------------------------------------------------------------------------
/docs/license.md:
--------------------------------------------------------------------------------
1 | # License
2 |
3 | **The MIT License (MIT)**
4 |
5 | Copyright (c) 2023 terminalkitten and individual contributors.
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy of
8 | this software and associated documentation files (the "Software"), to deal in
9 | the Software without restriction, including without limitation the rights to
10 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
11 | of the Software, and to permit persons to whom the Software is furnished to do
12 | so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 |
--------------------------------------------------------------------------------
/docs/stylesheets/extra.css:
--------------------------------------------------------------------------------
1 | [data-md-color-scheme="slate"] {
2 | --md-hue: 210;
3 | }
--------------------------------------------------------------------------------
/docs/types.md:
--------------------------------------------------------------------------------
1 | # Metrics
2 |
3 | There are currently 4 types of metrics that can be collected with Django Insights:
4 |
5 | ## Counter
6 |
7 | ## Gauge
8 |
9 | ## Timeseries
10 |
11 | ## Scatterplot
12 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Define the Django Insights management entry."""
3 | import os
4 | import sys
5 |
6 | if __name__ == "__main__":
7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings.dev")
8 |
9 | from django.core.management import execute_from_command_line
10 |
11 | execute_from_command_line(sys.argv)
12 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Django Insights
2 | site_url: https://github.com/terminalkitten/django-insights/
3 | site_author: terminalkitten
4 | site_description: >-
5 | Simple and easy insights in your Django app
6 |
7 | # Repository
8 | repo_name: terminalkitten/django-insights
9 | repo_url: https://github.com/terminalkitten/django-insights
10 |
11 | # Theme
12 |
13 | theme:
14 | name: material
15 | palette:
16 | accent: orange
17 | primary: orange
18 | scheme: slate
19 | font:
20 | text: Roboto
21 | icon:
22 | logo: octicons/pulse-16
23 | features:
24 | - navigation.footer
25 |
26 | # Custom CSS
27 | extra_css:
28 | - stylesheets/extra.css
29 |
30 | # Pages
31 | nav:
32 | - Home: index.md
33 | - Getting started:
34 | - Installation: installation.md
35 | - Alternatives: alternatives.md
36 | - License: license.md
37 | - Insights:
38 | - Types: types.md
39 |
40 | # Extra
41 | extra:
42 | generator: false
43 | copyright: Copyright © 2023 terminalkitten
44 |
--------------------------------------------------------------------------------
/project/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/terminalkitten/django-insights/054cbd8e50c1c4ce21166d875251631583f679b9/project/__init__.py
--------------------------------------------------------------------------------
/project/settings/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/terminalkitten/django-insights/054cbd8e50c1c4ce21166d875251631583f679b9/project/settings/__init__.py
--------------------------------------------------------------------------------
/project/settings/base.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import os
4 |
5 | import django
6 |
7 | BASE_DIR = os.path.dirname(os.path.dirname(__file__))
8 |
9 | DEBUG = True
10 | TEMPLATE_DEBUG = DEBUG
11 |
12 | SECRET_KEY = "NOTASECRET"
13 |
14 |
15 | DATABASES = {
16 | "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "db/testapp.db"},
17 | "insights": {"ENGINE": "django.db.backends.sqlite3", "NAME": "db/insights.db"},
18 | }
19 |
20 | DATABASE_ROUTERS = ['django_insights.database.Router']
21 |
22 | CACHES = {
23 | "default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"},
24 | }
25 |
26 | ALLOWED_HOSTS: list[str] = []
27 |
28 | INSTALLED_APPS = [
29 | "django.contrib.auth",
30 | "django.contrib.contenttypes",
31 | "django.contrib.staticfiles",
32 | "project.testapp",
33 | "project.testapp.users",
34 | "django_insights",
35 | ]
36 |
37 | MIDDLEWARE_CLASSES = (
38 | "django.middleware.common.CommonMiddleware",
39 | "django.middleware.csrf.CsrfViewMiddleware",
40 | "django.contrib.sessions.middleware.SessionMiddleware",
41 | "django.contrib.auth.middleware.AuthenticationMiddleware",
42 | "django.contrib.messages.middleware.MessageMiddleware",
43 | "django.middleware.clickjacking.XFrameOptionsMiddleware",
44 | )
45 |
46 | STATIC_URL = 'static/'
47 | STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
48 | STATICFILES_FINDERS = (
49 | 'django.contrib.staticfiles.finders.FileSystemFinder',
50 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
51 | )
52 |
53 | MEDIA_URL = 'media/'
54 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
55 |
56 |
57 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
58 |
59 | AUTH_USER_MODEL = "users.AppUser"
60 |
61 | ROOT_URLCONF = "project.urls"
62 | LANGUAGE_CODE = "en-us"
63 | TIME_ZONE = "UTC"
64 | USE_I18N = True
65 |
66 | if django.VERSION < (4, 0):
67 | USE_L10N = True
68 |
69 | TEMPLATES = [
70 | {
71 | "BACKEND": "django.template.backends.django.DjangoTemplates",
72 | "DIRS": [],
73 | "APP_DIRS": True,
74 | "OPTIONS": {
75 | "context_processors": [
76 | "django.template.context_processors.debug",
77 | "django.template.context_processors.request",
78 | "django.contrib.auth.context_processors.auth",
79 | "django.contrib.messages.context_processors.messages",
80 | ]
81 | },
82 | }
83 | ]
84 |
85 | USE_TZ = True
86 |
87 |
88 | # Django Insights settings
89 |
90 | # Custom app name
91 | INSIGHTS_APP_NAME = "Finq"
92 |
93 | # Quality of chart images
94 | INSIGHTS_CHART_DPI = 180
95 |
96 | # Insight cache
97 | INSIGHT_MEDIA_CACHE_ROOT = MEDIA_ROOT
98 |
99 | # Change primary color
100 | INSIGHTS_CHART_LIGHT_PRIMARY_COLOR = "#2563EB"
101 | INSIGHTS_CHART_DARK_PRIMARY_COLOR = "#BFDBFE"
102 |
--------------------------------------------------------------------------------
/project/settings/dev.py:
--------------------------------------------------------------------------------
1 | from project.settings.base import * # noqa
2 |
--------------------------------------------------------------------------------
/project/settings/test.py:
--------------------------------------------------------------------------------
1 | from project.settings.base import * # noqa
2 |
3 | DATABASES = {
4 | "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"},
5 | "insights": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"},
6 | }
7 |
--------------------------------------------------------------------------------
/project/testapp/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/terminalkitten/django-insights/054cbd8e50c1c4ce21166d875251631583f679b9/project/testapp/__init__.py
--------------------------------------------------------------------------------
/project/testapp/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class TestAppConfig(AppConfig):
5 | name = 'project.testapp'
6 | verbose_name = 'Django Insights TestApp'
7 | default_auto_field = 'django.db.models.BigAutoField'
8 |
--------------------------------------------------------------------------------
/project/testapp/insights.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from datetime import datetime
4 | from typing import Any
5 |
6 | from django.db.models import Avg, Case, Count, Value, When
7 | from django.db.models.functions import Length, TruncMonth, TruncYear
8 |
9 | from django_insights.metrics import metrics
10 | from project.testapp.models import Author, Book
11 |
12 | label = "Books"
13 |
14 |
15 | @metrics.counter(
16 | question="How many authors are there in our store?",
17 | desc="Number of authors we sell books for in our store",
18 | )
19 | def count_authors() -> int:
20 | return Author.objects.count()
21 |
22 |
23 | @metrics.counter(question="How many books are there?")
24 | def count_books() -> int:
25 | return Book.objects.count()
26 |
27 |
28 | @metrics.counter(question="Books with title longer than 20 chars?")
29 | def count_books_title_gt_20() -> int:
30 | return (
31 | Book.objects.annotate(title_len=Length('title'))
32 | .filter(title_len__gt=20)
33 | .count()
34 | )
35 |
36 |
37 | @metrics.counter(question="Books with title less than 10 chars?")
38 | def count_books_title_lt_10() -> int:
39 | return (
40 | Book.objects.annotate(title_len=Length('title'))
41 | .filter(title_len__lt=10)
42 | .count()
43 | )
44 |
45 |
46 | @metrics.counter(question="Authors with two or more books?")
47 | def count_authors_with_two_or_more_books() -> int:
48 | return (
49 | Author.objects.prefetch_related('books')
50 | .annotate(total_books=Count('books'))
51 | .filter(total_books__gte=2)
52 | .count()
53 | )
54 |
55 |
56 | @metrics.counter(question="Authors without books?")
57 | def count_authors_without_books() -> int:
58 | return (
59 | Author.objects.prefetch_related('books')
60 | .annotate(total_books=Count('books'))
61 | .filter(total_books=0)
62 | .count()
63 | )
64 |
65 |
66 | @metrics.counter(question="Authors with name longer than 20 chars?")
67 | def count_authors_name_gt_20() -> int:
68 | return (
69 | Author.objects.annotate(name_len=Length('name')).filter(name_len__gt=20).count()
70 | )
71 |
72 |
73 | @metrics.gauge(question="Average book(s) per author?")
74 | def avg_books_per_author() -> int:
75 | avg_total_books = (
76 | Author.objects.prefetch_related('books')
77 | .annotate(total_books=Count('books'))
78 | .aggregate(Avg('total_books'))
79 | .get('total_books__avg')
80 | )
81 |
82 | return avg_total_books
83 |
84 |
85 | @metrics.timeseries(
86 | question="Num of books created per month?",
87 | desc="How many books are added each month, since the opening of our store",
88 | xlabel="Month",
89 | xformat='%m',
90 | ylabel="Num of books",
91 | )
92 | def num_of_books_per_month() -> list[tuple[datetime, int]]:
93 | return (
94 | Book.objects.all()
95 | .annotate(month=TruncMonth('created'))
96 | .values('month')
97 | .filter(month__isnull=False)
98 | .annotate(total=Count('pk'))
99 | .values_list('month', 'total')
100 | .order_by('month')
101 | )
102 |
103 |
104 | @metrics.timeseries(
105 | question="Num of books created per year?",
106 | xlabel="Year",
107 | xformat='%Y',
108 | ylabel="Num of books",
109 | )
110 | def num_of_books_per_year() -> list[tuple[datetime, int]]:
111 | return (
112 | Book.objects.all()
113 | .annotate(year=TruncYear('created'))
114 | .values('year')
115 | .filter(year__isnull=False)
116 | .annotate(total=Count('pk'))
117 | .values_list('year', 'total')
118 | .order_by('year')
119 | )
120 |
121 |
122 | @metrics.scatterplot(
123 | question="Num of books by age of author?",
124 | xlabel="Age",
125 | ylabel="Num of books",
126 | )
127 | def author_age_vs_num_of_books() -> list[tuple[float, float, Any]]:
128 | return (
129 | Author.objects.values('age')
130 | .annotate(num_of_books=Count('books'), category=Value("author"))
131 | .values_list('num_of_books', 'age', 'category')
132 | )
133 |
134 |
135 | @metrics.barchart(
136 | question="Num of books by gender of author?",
137 | xlabel="Gender",
138 | ylabel="Num of books",
139 | )
140 | def author_gender_vs_num_of_books() -> list[tuple[float, float, str]]:
141 | return (
142 | Author.objects.values('gender')
143 | .annotate(
144 | num_of_books=Count('books'),
145 | gender_category=Case(
146 | When(gender=1, then=Value('Male')),
147 | When(gender=2, then=Value('Female')),
148 | ),
149 | )
150 | .values_list('num_of_books', 'gender', 'gender_category')
151 | )
152 |
--------------------------------------------------------------------------------
/project/testapp/management/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/terminalkitten/django-insights/054cbd8e50c1c4ce21166d875251631583f679b9/project/testapp/management/__init__.py
--------------------------------------------------------------------------------
/project/testapp/management/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/terminalkitten/django-insights/054cbd8e50c1c4ce21166d875251631583f679b9/project/testapp/management/commands/__init__.py
--------------------------------------------------------------------------------
/project/testapp/management/commands/seed_db.py:
--------------------------------------------------------------------------------
1 | from random import choice, randrange
2 |
3 | from django.core.management.base import BaseCommand
4 | from django.utils import timezone
5 | from faker import Faker
6 |
7 | from project.testapp.models import Author, Book
8 | from project.testapp.users.models import AppUser
9 |
10 | fake = Faker()
11 |
12 |
13 | class Command(BaseCommand):
14 |
15 | """Seed TestApp database"""
16 |
17 | def handle(self, *args, **options):
18 | self.stdout.write(self.style.HTTP_INFO('[TestApp] - Database seed'))
19 |
20 | AppUser.objects.create(email="user@example.com")
21 |
22 | Author.objects.all().delete()
23 | Book.objects.all().delete()
24 |
25 | authors = []
26 |
27 | names = [fake.unique.name() for i in range(2000)]
28 | titles = [fake.sentence(nb_words=randrange(30)) for i in range(5000)]
29 |
30 | for name in names:
31 | author_created = fake.date_time_between_dates(
32 | datetime_start='-10y',
33 | datetime_end='now',
34 | tzinfo=timezone.get_current_timezone(),
35 | )
36 | authors.append(
37 | Author.objects.create(
38 | created=author_created,
39 | name=name,
40 | age=randrange(80),
41 | gender=fake.random.choice((1, 2)),
42 | )
43 | )
44 |
45 | for title in titles:
46 | book_created = fake.date_time_between_dates(
47 | datetime_start='-10y',
48 | datetime_end='now',
49 | tzinfo=timezone.get_current_timezone(),
50 | )
51 |
52 | Book.objects.create(
53 | created=book_created, author=choice(authors), title=title
54 | )
55 |
--------------------------------------------------------------------------------
/project/testapp/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.7 on 2023-03-28 07:45
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 | initial = True
9 |
10 | dependencies = []
11 |
12 | operations = [
13 | migrations.CreateModel(
14 | name='Author',
15 | fields=[
16 | (
17 | 'id',
18 | models.BigAutoField(
19 | auto_created=True,
20 | primary_key=True,
21 | serialize=False,
22 | verbose_name='ID',
23 | ),
24 | ),
25 | ('created', models.DateTimeField(verbose_name='created')),
26 | (
27 | 'modified',
28 | models.DateTimeField(auto_now=True, verbose_name='modified'),
29 | ),
30 | ('gender', models.IntegerField(choices=[('M', 1), ('F', 2)])),
31 | ('name', models.CharField(max_length=32, unique=True)),
32 | ('age', models.IntegerField()),
33 | ],
34 | ),
35 | migrations.CreateModel(
36 | name='Book',
37 | fields=[
38 | (
39 | 'id',
40 | models.BigAutoField(
41 | auto_created=True,
42 | primary_key=True,
43 | serialize=False,
44 | verbose_name='ID',
45 | ),
46 | ),
47 | ('created', models.DateTimeField(verbose_name='created')),
48 | (
49 | 'modified',
50 | models.DateTimeField(auto_now=True, verbose_name='modified'),
51 | ),
52 | ('title', models.CharField(max_length=128)),
53 | (
54 | 'author',
55 | models.ForeignKey(
56 | on_delete=django.db.models.deletion.CASCADE,
57 | related_name='books',
58 | to='testapp.author',
59 | ),
60 | ),
61 | ],
62 | ),
63 | ]
64 |
--------------------------------------------------------------------------------
/project/testapp/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/terminalkitten/django-insights/054cbd8e50c1c4ce21166d875251631583f679b9/project/testapp/migrations/__init__.py
--------------------------------------------------------------------------------
/project/testapp/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 |
4 | class Author(models.Model):
5 | created = models.DateTimeField('created')
6 | modified = models.DateTimeField('modified', auto_now=True)
7 |
8 | gender = models.IntegerField(choices=(('M', 1), ('F', 2)))
9 | name = models.CharField(max_length=32, unique=True)
10 | age = models.IntegerField()
11 |
12 |
13 | class Book(models.Model):
14 | created = models.DateTimeField('created')
15 | modified = models.DateTimeField('modified', auto_now=True)
16 |
17 | title = models.CharField(max_length=128)
18 | author = models.ForeignKey(Author, related_name='books', on_delete=models.CASCADE)
19 |
--------------------------------------------------------------------------------
/project/testapp/users/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/terminalkitten/django-insights/054cbd8e50c1c4ce21166d875251631583f679b9/project/testapp/users/__init__.py
--------------------------------------------------------------------------------
/project/testapp/users/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class TestAppUsersConfig(AppConfig):
5 | name = "project.testapp.users"
6 | label = "users"
7 |
--------------------------------------------------------------------------------
/project/testapp/users/insights.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from django_insights.metrics import metrics
4 | from project.testapp.users.models import AppUser
5 |
6 | label = "Users"
7 |
8 |
9 | @metrics.counter(
10 | question="How many app users we have in our store?",
11 | desc="Number of app users",
12 | )
13 | def count_app_users() -> int:
14 | return AppUser.objects.count()
15 |
--------------------------------------------------------------------------------
/project/testapp/users/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.7 on 2023-03-27 19:26
2 |
3 | import django.utils.timezone
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 | initial = True
9 |
10 | dependencies = [
11 | ('auth', '0012_alter_user_first_name_max_length'),
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name='AppUser',
17 | fields=[
18 | (
19 | 'id',
20 | models.BigAutoField(
21 | auto_created=True,
22 | primary_key=True,
23 | serialize=False,
24 | verbose_name='ID',
25 | ),
26 | ),
27 | ('password', models.CharField(max_length=128, verbose_name='password')),
28 | (
29 | 'is_superuser',
30 | models.BooleanField(
31 | default=False,
32 | help_text='Designates that this user has all permissions without explicitly assigning them.',
33 | verbose_name='superuser status',
34 | ),
35 | ),
36 | ('created', models.DateTimeField(verbose_name='created')),
37 | (
38 | 'modified',
39 | models.DateTimeField(auto_now=True, verbose_name='modified'),
40 | ),
41 | (
42 | 'email',
43 | models.EmailField(
44 | blank=True, default='', max_length=254, unique=True
45 | ),
46 | ),
47 | ('last_login', models.DateTimeField(blank=True, null=True)),
48 | (
49 | 'date_joined',
50 | models.DateTimeField(default=django.utils.timezone.now),
51 | ),
52 | (
53 | 'groups',
54 | models.ManyToManyField(
55 | blank=True,
56 | help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.',
57 | related_name='user_set',
58 | related_query_name='user',
59 | to='auth.group',
60 | verbose_name='groups',
61 | ),
62 | ),
63 | (
64 | 'user_permissions',
65 | models.ManyToManyField(
66 | blank=True,
67 | help_text='Specific permissions for this user.',
68 | related_name='user_set',
69 | related_query_name='user',
70 | to='auth.permission',
71 | verbose_name='user permissions',
72 | ),
73 | ),
74 | ],
75 | options={
76 | 'abstract': False,
77 | },
78 | ),
79 | ]
80 |
--------------------------------------------------------------------------------
/project/testapp/users/migrations/0002_alter_appuser_created.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.7 on 2023-03-27 19:29
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ('users', '0001_initial'),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterField(
13 | model_name='appuser',
14 | name='created',
15 | field=models.DateTimeField(auto_now_add=True, verbose_name='created'),
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/project/testapp/users/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/terminalkitten/django-insights/054cbd8e50c1c4ce21166d875251631583f679b9/project/testapp/users/migrations/__init__.py
--------------------------------------------------------------------------------
/project/testapp/users/models.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
2 | from django.db import models
3 | from django.utils import timezone
4 |
5 |
6 | class AppUser(AbstractBaseUser, PermissionsMixin):
7 | created = models.DateTimeField('created', auto_now_add=True)
8 | modified = models.DateTimeField('modified', auto_now=True)
9 |
10 | email = models.EmailField(blank=True, default="", unique=True)
11 | last_login = models.DateTimeField(blank=True, null=True)
12 | date_joined = models.DateTimeField(default=timezone.now)
13 |
14 | USERNAME_FIELD = "email"
15 | EMAIL_FIELD = "email"
16 | REQUIRED_FIELDS = []
17 |
--------------------------------------------------------------------------------
/project/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.conf.urls.static import static
3 | from django.urls import include, path
4 |
5 | urlpatterns = (
6 | [
7 | path(
8 | '',
9 | include('django_insights.urls', namespace='insights'),
10 | ),
11 | ]
12 | + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
13 | + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
14 | )
15 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["flit_core >=3.2,<4"]
3 | build-backend = "flit_core.buildapi"
4 |
5 | [distutils]
6 | index-servers = ["pypi"]
7 |
8 | [pypi]
9 | repository = "https://upload.pypi.org/legacy/"
10 | username = "terminalkitten"
11 |
12 | [project]
13 | name = "django-insights"
14 | authors = [{ name = "DK", email = "dk@terminalkitten.com" }]
15 | readme = "README.md"
16 | license = { file = "LICENSE" }
17 | classifiers = [
18 | "Development Status :: 3 - Alpha",
19 | "Environment :: Web Environment",
20 | "Framework :: Django",
21 | "Framework :: Django :: 3.2",
22 | "Framework :: Django :: 4.0",
23 | "Framework :: Django :: 4.1",
24 | "Framework :: Django :: 4.2",
25 | "Framework :: Matplotlib",
26 | "Intended Audience :: Developers",
27 | "Operating System :: OS Independent",
28 | "License :: OSI Approved :: MIT License",
29 | "Programming Language :: Python :: 3 :: Only",
30 | "Programming Language :: Python :: 3.8",
31 | "Programming Language :: Python :: 3.9",
32 | "Programming Language :: Python :: 3.10",
33 | "Programming Language :: Python :: 3.11",
34 | ]
35 | dynamic = ["version", "description"]
36 | dependencies = [
37 | "django",
38 | "matplotlib",
39 | "tqdm",
40 | "weasyprint>=53",
41 | "django-weasyprint>=2.3",
42 | ]
43 |
44 | [project.optional-dependencies]
45 | dev = [
46 | "black",
47 | "ipython",
48 | "flit",
49 | "flake8",
50 | "pytest",
51 | "pytest-django",
52 | "mypy",
53 | "isort",
54 | "wheel",
55 | "faker",
56 | ]
57 | doc = ["pdoc"]
58 |
59 | [project.scripts]
60 | insights = "django_insights.cli:cli"
61 |
62 | [project.urls]
63 | Home = "https://github.com/terminalkitten/django-insights"
64 |
65 | [tool.django-stubs]
66 | django_settings_module = "project.settings"
67 |
68 | [tool.black]
69 | line-length = 88
70 | skip-string-normalization = true
71 | target-version = ['py39', 'py310', 'py311']
72 |
73 | [tool.isort]
74 | profile = "black"
75 | line_length = 88
76 |
77 | [tool.pytest.ini_options]
78 | pythonpath = ["."]
79 | filterwarnings = ["error", "ignore::UserWarning"]
80 | addopts = """\
81 | --strict-config
82 | --strict-markers
83 | --ds=project.settings.test
84 | """
85 | django_find_project = false
86 |
--------------------------------------------------------------------------------
/requirements/compile.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from __future__ import annotations
3 |
4 | import os
5 | import subprocess
6 | import sys
7 | from pathlib import Path
8 |
9 | if __name__ == "__main__":
10 | os.chdir(Path(__file__).parent)
11 | os.environ["CUSTOM_COMPILE_COMMAND"] = "requirements/compile.py"
12 | os.environ["PIP_REQUIRE_VIRTUALENV"] = "0"
13 | common_args = [
14 | "-m",
15 | "piptools",
16 | "compile",
17 | "--generate-hashes",
18 | "--allow-unsafe",
19 | ] + sys.argv[1:]
20 | subprocess.run(
21 | [
22 | "python3.8",
23 | *common_args,
24 | "-P",
25 | "Django>=3.2a1,<3.3",
26 | "-o",
27 | "py38-django32.txt",
28 | ],
29 | check=True,
30 | capture_output=True,
31 | )
32 | subprocess.run(
33 | [
34 | "python3.8",
35 | *common_args,
36 | "-P",
37 | "Django>=4.0a1,<4.1",
38 | "-o",
39 | "py38-django40.txt",
40 | ],
41 | check=True,
42 | capture_output=True,
43 | )
44 | subprocess.run(
45 | [
46 | "python3.8",
47 | *common_args,
48 | "-P",
49 | "Django>=4.1a1,<4.2",
50 | "-o",
51 | "py38-django41.txt",
52 | ],
53 | check=True,
54 | capture_output=True,
55 | )
56 | subprocess.run(
57 | [
58 | "python3.8",
59 | *common_args,
60 | "-P",
61 | "Django>=4.2a1,<5.0",
62 | "-o",
63 | "py38-django42.txt",
64 | ],
65 | check=True,
66 | capture_output=True,
67 | )
68 | subprocess.run(
69 | [
70 | "python3.9",
71 | *common_args,
72 | "-P",
73 | "Django>=3.2a1,<3.3",
74 | "-o",
75 | "py39-django32.txt",
76 | ],
77 | check=True,
78 | capture_output=True,
79 | )
80 | subprocess.run(
81 | [
82 | "python3.9",
83 | *common_args,
84 | "-P",
85 | "Django>=4.0a1,<4.1",
86 | "-o",
87 | "py39-django40.txt",
88 | ],
89 | check=True,
90 | capture_output=True,
91 | )
92 | subprocess.run(
93 | [
94 | "python3.9",
95 | *common_args,
96 | "-P",
97 | "Django>=4.1a1,<4.2",
98 | "-o",
99 | "py39-django41.txt",
100 | ],
101 | check=True,
102 | capture_output=True,
103 | )
104 | subprocess.run(
105 | [
106 | "python3.9",
107 | *common_args,
108 | "-P",
109 | "Django>=4.2a1,<5.0",
110 | "-o",
111 | "py39-django42.txt",
112 | ],
113 | check=True,
114 | capture_output=True,
115 | )
116 | subprocess.run(
117 | [
118 | "python3.10",
119 | *common_args,
120 | "-P",
121 | "Django>=3.2a1,<3.3",
122 | "-o",
123 | "py310-django32.txt",
124 | ],
125 | check=True,
126 | capture_output=True,
127 | )
128 | subprocess.run(
129 | [
130 | "python3.10",
131 | *common_args,
132 | "-P",
133 | "Django>=4.0a1,<4.1",
134 | "-o",
135 | "py310-django40.txt",
136 | ],
137 | check=True,
138 | capture_output=True,
139 | )
140 | subprocess.run(
141 | [
142 | "python3.10",
143 | *common_args,
144 | "-P",
145 | "Django>=4.1a1,<4.2",
146 | "-o",
147 | "py310-django41.txt",
148 | ],
149 | check=True,
150 | capture_output=True,
151 | )
152 | subprocess.run(
153 | [
154 | "python3.10",
155 | *common_args,
156 | "-P",
157 | "Django>=4.2a1,<5.0",
158 | "-o",
159 | "py310-django42.txt",
160 | ],
161 | check=True,
162 | capture_output=True,
163 | )
164 | subprocess.run(
165 | [
166 | "python3.11",
167 | *common_args,
168 | "-P",
169 | "Django>=4.1a1,<4.2",
170 | "-o",
171 | "py311-django41.txt",
172 | ],
173 | check=True,
174 | capture_output=True,
175 | )
176 | subprocess.run(
177 | [
178 | "python3.11",
179 | *common_args,
180 | "-P",
181 | "Django>=4.2a1,<5.0",
182 | "-o",
183 | "py311-django42.txt",
184 | ],
185 | check=True,
186 | capture_output=True,
187 | )
188 |
--------------------------------------------------------------------------------
/requirements/py311-django41.txt:
--------------------------------------------------------------------------------
1 | #
2 | # This file is autogenerated by pip-compile with Python 3.11
3 | # by the following command:
4 | #
5 | # requirements/compile.py
6 | #
7 | asgiref==3.6.0 \
8 | --hash=sha256:71e68008da809b957b7ee4b43dbccff33d1b23519fb8344e33f049897077afac \
9 | --hash=sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506
10 | # via django
11 | attrs==22.2.0 \
12 | --hash=sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836 \
13 | --hash=sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99
14 | # via pytest
15 | contourpy==1.0.7 \
16 | --hash=sha256:031154ed61f7328ad7f97662e48660a150ef84ee1bc8876b6472af88bf5a9b98 \
17 | --hash=sha256:0f9d350b639db6c2c233d92c7f213d94d2e444d8e8fc5ca44c9706cf72193772 \
18 | --hash=sha256:130230b7e49825c98edf0b428b7aa1125503d91732735ef897786fe5452b1ec2 \
19 | --hash=sha256:152fd8f730c31fd67fe0ffebe1df38ab6a669403da93df218801a893645c6ccc \
20 | --hash=sha256:1c71fdd8f1c0f84ffd58fca37d00ca4ebaa9e502fb49825484da075ac0b0b803 \
21 | --hash=sha256:24847601071f740837aefb730e01bd169fbcaa610209779a78db7ebb6e6a7051 \
22 | --hash=sha256:2e9ebb4425fc1b658e13bace354c48a933b842d53c458f02c86f371cecbedecc \
23 | --hash=sha256:30676ca45084ee61e9c3da589042c24a57592e375d4b138bd84d8709893a1ba4 \
24 | --hash=sha256:31a55dccc8426e71817e3fe09b37d6d48ae40aae4ecbc8c7ad59d6893569c436 \
25 | --hash=sha256:366a0cf0fc079af5204801786ad7a1c007714ee3909e364dbac1729f5b0849e5 \
26 | --hash=sha256:38e2e577f0f092b8e6774459317c05a69935a1755ecfb621c0a98f0e3c09c9a5 \
27 | --hash=sha256:3c184ad2433635f216645fdf0493011a4667e8d46b34082f5a3de702b6ec42e3 \
28 | --hash=sha256:3caea6365b13119626ee996711ab63e0c9d7496f65641f4459c60a009a1f3e80 \
29 | --hash=sha256:3e927b3868bd1e12acee7cc8f3747d815b4ab3e445a28d2e5373a7f4a6e76ba1 \
30 | --hash=sha256:4ee3ee247f795a69e53cd91d927146fb16c4e803c7ac86c84104940c7d2cabf0 \
31 | --hash=sha256:54d43960d809c4c12508a60b66cb936e7ed57d51fb5e30b513934a4a23874fae \
32 | --hash=sha256:57119b0116e3f408acbdccf9eb6ef19d7fe7baf0d1e9aaa5381489bc1aa56556 \
33 | --hash=sha256:58569c491e7f7e874f11519ef46737cea1d6eda1b514e4eb5ac7dab6aa864d02 \
34 | --hash=sha256:5a011cf354107b47c58ea932d13b04d93c6d1d69b8b6dce885e642531f847566 \
35 | --hash=sha256:5caeacc68642e5f19d707471890f037a13007feba8427eb7f2a60811a1fc1350 \
36 | --hash=sha256:5dd34c1ae752515318224cba7fc62b53130c45ac6a1040c8b7c1a223c46e8967 \
37 | --hash=sha256:60835badb5ed5f4e194a6f21c09283dd6e007664a86101431bf870d9e86266c4 \
38 | --hash=sha256:62398c80ef57589bdbe1eb8537127321c1abcfdf8c5f14f479dbbe27d0322e66 \
39 | --hash=sha256:6381fa66866b0ea35e15d197fc06ac3840a9b2643a6475c8fff267db8b9f1e69 \
40 | --hash=sha256:64757f6460fc55d7e16ed4f1de193f362104285c667c112b50a804d482777edd \
41 | --hash=sha256:69f8ff4db108815addd900a74df665e135dbbd6547a8a69333a68e1f6e368ac2 \
42 | --hash=sha256:6c180d89a28787e4b73b07e9b0e2dac7741261dbdca95f2b489c4f8f887dd810 \
43 | --hash=sha256:71b0bf0c30d432278793d2141362ac853859e87de0a7dee24a1cea35231f0d50 \
44 | --hash=sha256:769eef00437edf115e24d87f8926955f00f7704bede656ce605097584f9966dc \
45 | --hash=sha256:7f6979d20ee5693a1057ab53e043adffa1e7418d734c1532e2d9e915b08d8ec2 \
46 | --hash=sha256:87f4d8941a9564cda3f7fa6a6cd9b32ec575830780677932abdec7bcb61717b0 \
47 | --hash=sha256:89ba9bb365446a22411f0673abf6ee1fea3b2cf47b37533b970904880ceb72f3 \
48 | --hash=sha256:8acf74b5d383414401926c1598ed77825cd530ac7b463ebc2e4f46638f56cce6 \
49 | --hash=sha256:9056c5310eb1daa33fc234ef39ebfb8c8e2533f088bbf0bc7350f70a29bde1ac \
50 | --hash=sha256:95c3acddf921944f241b6773b767f1cbce71d03307270e2d769fd584d5d1092d \
51 | --hash=sha256:9e20e5a1908e18aaa60d9077a6d8753090e3f85ca25da6e25d30dc0a9e84c2c6 \
52 | --hash=sha256:a1e97b86f73715e8670ef45292d7cc033548266f07d54e2183ecb3c87598888f \
53 | --hash=sha256:a877ada905f7d69b2a31796c4b66e31a8068b37aa9b78832d41c82fc3e056ddd \
54 | --hash=sha256:a9d7587d2fdc820cc9177139b56795c39fb8560f540bba9ceea215f1f66e1566 \
55 | --hash=sha256:abf298af1e7ad44eeb93501e40eb5a67abbf93b5d90e468d01fc0c4451971afa \
56 | --hash=sha256:ae90d5a8590e5310c32a7630b4b8618cef7563cebf649011da80874d0aa8f414 \
57 | --hash=sha256:b6d0f9e1d39dbfb3977f9dd79f156c86eb03e57a7face96f199e02b18e58d32a \
58 | --hash=sha256:b8d587cc39057d0afd4166083d289bdeff221ac6d3ee5046aef2d480dc4b503c \
59 | --hash=sha256:c5210e5d5117e9aec8c47d9156d1d3835570dd909a899171b9535cb4a3f32693 \
60 | --hash=sha256:cc331c13902d0f50845099434cd936d49d7a2ca76cb654b39691974cb1e4812d \
61 | --hash=sha256:ce41676b3d0dd16dbcfabcc1dc46090aaf4688fd6e819ef343dbda5a57ef0161 \
62 | --hash=sha256:d8165a088d31798b59e91117d1f5fc3df8168d8b48c4acc10fc0df0d0bdbcc5e \
63 | --hash=sha256:e7281244c99fd7c6f27c1c6bfafba878517b0b62925a09b586d88ce750a016d2 \
64 | --hash=sha256:e96a08b62bb8de960d3a6afbc5ed8421bf1a2d9c85cc4ea73f4bc81b4910500f \
65 | --hash=sha256:ed33433fc3820263a6368e532f19ddb4c5990855e4886088ad84fd7c4e561c71 \
66 | --hash=sha256:efb8f6d08ca7998cf59eaf50c9d60717f29a1a0a09caa46460d33b2924839dbd \
67 | --hash=sha256:efe99298ba37e37787f6a2ea868265465410822f7bea163edcc1bd3903354ea9 \
68 | --hash=sha256:f99e9486bf1bb979d95d5cffed40689cb595abb2b841f2991fc894b3452290e8 \
69 | --hash=sha256:fc1464c97579da9f3ab16763c32e5c5d5bb5fa1ec7ce509a4ca6108b61b84fab \
70 | --hash=sha256:fd7dc0e6812b799a34f6d12fcb1000539098c249c8da54f3566c6a6461d0dbad
71 | # via matplotlib
72 | cycler==0.11.0 \
73 | --hash=sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3 \
74 | --hash=sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f
75 | # via matplotlib
76 | django==4.1.7 \
77 | --hash=sha256:44f714b81c5f190d9d2ddad01a532fe502fa01c4cb8faf1d081f4264ed15dcd8 \
78 | --hash=sha256:f2f431e75adc40039ace496ad3b9f17227022e8b11566f4b363da44c7e44761e
79 | # via -r requirements.in
80 | faker==17.6.0 \
81 | --hash=sha256:51f37ff9df710159d6d736d0ba1c75e063430a8c806b91334d7794305b5a6114 \
82 | --hash=sha256:5aaa16fa9cfde7d117eef70b6b293a705021e57158f3fa6b44ed1b70202d2065
83 | # via -r requirements.in
84 | fonttools==4.39.0 \
85 | --hash=sha256:909c104558835eac27faeb56be5a4c32694192dca123d073bf746ce9254054af \
86 | --hash=sha256:f5e764e1fd6ad54dfc201ff32af0ba111bcfbe0d05b24540af74c63db4ed6390
87 | # via matplotlib
88 | iniconfig==2.0.0 \
89 | --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \
90 | --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374
91 | # via pytest
92 | kiwisolver==1.4.4 \
93 | --hash=sha256:02f79693ec433cb4b5f51694e8477ae83b3205768a6fb48ffba60549080e295b \
94 | --hash=sha256:03baab2d6b4a54ddbb43bba1a3a2d1627e82d205c5cf8f4c924dc49284b87166 \
95 | --hash=sha256:1041feb4cda8708ce73bb4dcb9ce1ccf49d553bf87c3954bdfa46f0c3f77252c \
96 | --hash=sha256:10ee06759482c78bdb864f4109886dff7b8a56529bc1609d4f1112b93fe6423c \
97 | --hash=sha256:1d1573129aa0fd901076e2bfb4275a35f5b7aa60fbfb984499d661ec950320b0 \
98 | --hash=sha256:283dffbf061a4ec60391d51e6155e372a1f7a4f5b15d59c8505339454f8989e4 \
99 | --hash=sha256:28bc5b299f48150b5f822ce68624e445040595a4ac3d59251703779836eceff9 \
100 | --hash=sha256:2a66fdfb34e05b705620dd567f5a03f239a088d5a3f321e7b6ac3239d22aa286 \
101 | --hash=sha256:2e307eb9bd99801f82789b44bb45e9f541961831c7311521b13a6c85afc09767 \
102 | --hash=sha256:2e407cb4bd5a13984a6c2c0fe1845e4e41e96f183e5e5cd4d77a857d9693494c \
103 | --hash=sha256:2f5e60fabb7343a836360c4f0919b8cd0d6dbf08ad2ca6b9cf90bf0c76a3c4f6 \
104 | --hash=sha256:36dafec3d6d6088d34e2de6b85f9d8e2324eb734162fba59d2ba9ed7a2043d5b \
105 | --hash=sha256:3fe20f63c9ecee44560d0e7f116b3a747a5d7203376abeea292ab3152334d004 \
106 | --hash=sha256:41dae968a94b1ef1897cb322b39360a0812661dba7c682aa45098eb8e193dbdf \
107 | --hash=sha256:4bd472dbe5e136f96a4b18f295d159d7f26fd399136f5b17b08c4e5f498cd494 \
108 | --hash=sha256:4ea39b0ccc4f5d803e3337dd46bcce60b702be4d86fd0b3d7531ef10fd99a1ac \
109 | --hash=sha256:5853eb494c71e267912275e5586fe281444eb5e722de4e131cddf9d442615626 \
110 | --hash=sha256:5bce61af018b0cb2055e0e72e7d65290d822d3feee430b7b8203d8a855e78766 \
111 | --hash=sha256:6295ecd49304dcf3bfbfa45d9a081c96509e95f4b9d0eb7ee4ec0530c4a96514 \
112 | --hash=sha256:62ac9cc684da4cf1778d07a89bf5f81b35834cb96ca523d3a7fb32509380cbf6 \
113 | --hash=sha256:70e7c2e7b750585569564e2e5ca9845acfaa5da56ac46df68414f29fea97be9f \
114 | --hash=sha256:7577c1987baa3adc4b3c62c33bd1118c3ef5c8ddef36f0f2c950ae0b199e100d \
115 | --hash=sha256:75facbe9606748f43428fc91a43edb46c7ff68889b91fa31f53b58894503a191 \
116 | --hash=sha256:787518a6789009c159453da4d6b683f468ef7a65bbde796bcea803ccf191058d \
117 | --hash=sha256:78d6601aed50c74e0ef02f4204da1816147a6d3fbdc8b3872d263338a9052c51 \
118 | --hash=sha256:7c43e1e1206cd421cd92e6b3280d4385d41d7166b3ed577ac20444b6995a445f \
119 | --hash=sha256:81e38381b782cc7e1e46c4e14cd997ee6040768101aefc8fa3c24a4cc58e98f8 \
120 | --hash=sha256:841293b17ad704d70c578f1f0013c890e219952169ce8a24ebc063eecf775454 \
121 | --hash=sha256:872b8ca05c40d309ed13eb2e582cab0c5a05e81e987ab9c521bf05ad1d5cf5cb \
122 | --hash=sha256:877272cf6b4b7e94c9614f9b10140e198d2186363728ed0f701c6eee1baec1da \
123 | --hash=sha256:8c808594c88a025d4e322d5bb549282c93c8e1ba71b790f539567932722d7bd8 \
124 | --hash=sha256:8ed58b8acf29798b036d347791141767ccf65eee7f26bde03a71c944449e53de \
125 | --hash=sha256:91672bacaa030f92fc2f43b620d7b337fd9a5af28b0d6ed3f77afc43c4a64b5a \
126 | --hash=sha256:968f44fdbf6dd757d12920d63b566eeb4d5b395fd2d00d29d7ef00a00582aac9 \
127 | --hash=sha256:9f85003f5dfa867e86d53fac6f7e6f30c045673fa27b603c397753bebadc3008 \
128 | --hash=sha256:a553dadda40fef6bfa1456dc4be49b113aa92c2a9a9e8711e955618cd69622e3 \
129 | --hash=sha256:a68b62a02953b9841730db7797422f983935aeefceb1679f0fc85cbfbd311c32 \
130 | --hash=sha256:abbe9fa13da955feb8202e215c4018f4bb57469b1b78c7a4c5c7b93001699938 \
131 | --hash=sha256:ad881edc7ccb9d65b0224f4e4d05a1e85cf62d73aab798943df6d48ab0cd79a1 \
132 | --hash=sha256:b1792d939ec70abe76f5054d3f36ed5656021dcad1322d1cc996d4e54165cef9 \
133 | --hash=sha256:b428ef021242344340460fa4c9185d0b1f66fbdbfecc6c63eff4b7c29fad429d \
134 | --hash=sha256:b533558eae785e33e8c148a8d9921692a9fe5aa516efbdff8606e7d87b9d5824 \
135 | --hash=sha256:ba59c92039ec0a66103b1d5fe588fa546373587a7d68f5c96f743c3396afc04b \
136 | --hash=sha256:bc8d3bd6c72b2dd9decf16ce70e20abcb3274ba01b4e1c96031e0c4067d1e7cd \
137 | --hash=sha256:bc9db8a3efb3e403e4ecc6cd9489ea2bac94244f80c78e27c31dcc00d2790ac2 \
138 | --hash=sha256:bf7d9fce9bcc4752ca4a1b80aabd38f6d19009ea5cbda0e0856983cf6d0023f5 \
139 | --hash=sha256:c2dbb44c3f7e6c4d3487b31037b1bdbf424d97687c1747ce4ff2895795c9bf69 \
140 | --hash=sha256:c79ebe8f3676a4c6630fd3f777f3cfecf9289666c84e775a67d1d358578dc2e3 \
141 | --hash=sha256:c97528e64cb9ebeff9701e7938653a9951922f2a38bd847787d4a8e498cc83ae \
142 | --hash=sha256:d0611a0a2a518464c05ddd5a3a1a0e856ccc10e67079bb17f265ad19ab3c7597 \
143 | --hash=sha256:d06adcfa62a4431d404c31216f0f8ac97397d799cd53800e9d3efc2fbb3cf14e \
144 | --hash=sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955 \
145 | --hash=sha256:d5b61785a9ce44e5a4b880272baa7cf6c8f48a5180c3e81c59553ba0cb0821ca \
146 | --hash=sha256:da152d8cdcab0e56e4f45eb08b9aea6455845ec83172092f09b0e077ece2cf7a \
147 | --hash=sha256:da7e547706e69e45d95e116e6939488d62174e033b763ab1496b4c29b76fabea \
148 | --hash=sha256:db5283d90da4174865d520e7366801a93777201e91e79bacbac6e6927cbceede \
149 | --hash=sha256:db608a6757adabb32f1cfe6066e39b3706d8c3aa69bbc353a5b61edad36a5cb4 \
150 | --hash=sha256:e0ea21f66820452a3f5d1655f8704a60d66ba1191359b96541eaf457710a5fc6 \
151 | --hash=sha256:e7da3fec7408813a7cebc9e4ec55afed2d0fd65c4754bc376bf03498d4e92686 \
152 | --hash=sha256:e92a513161077b53447160b9bd8f522edfbed4bd9759e4c18ab05d7ef7e49408 \
153 | --hash=sha256:ecb1fa0db7bf4cff9dac752abb19505a233c7f16684c5826d1f11ebd9472b871 \
154 | --hash=sha256:efda5fc8cc1c61e4f639b8067d118e742b812c930f708e6667a5ce0d13499e29 \
155 | --hash=sha256:f0a1dbdb5ecbef0d34eb77e56fcb3e95bbd7e50835d9782a45df81cc46949750 \
156 | --hash=sha256:f0a71d85ecdd570ded8ac3d1c0f480842f49a40beb423bb8014539a9f32a5897 \
157 | --hash=sha256:f4f270de01dd3e129a72efad823da90cc4d6aafb64c410c9033aba70db9f1ff0 \
158 | --hash=sha256:f6cb459eea32a4e2cf18ba5fcece2dbdf496384413bc1bae15583f19e567f3b2 \
159 | --hash=sha256:f8ad8285b01b0d4695102546b342b493b3ccc6781fc28c8c6a1bb63e95d22f09 \
160 | --hash=sha256:f9f39e2f049db33a908319cf46624a569b36983c7c78318e9726a4cb8923b26c
161 | # via matplotlib
162 | matplotlib==3.7.1 \
163 | --hash=sha256:08308bae9e91aca1ec6fd6dda66237eef9f6294ddb17f0d0b3c863169bf82353 \
164 | --hash=sha256:14645aad967684e92fc349493fa10c08a6da514b3d03a5931a1bac26e6792bd1 \
165 | --hash=sha256:21e9cff1a58d42e74d01153360de92b326708fb205250150018a52c70f43c290 \
166 | --hash=sha256:28506a03bd7f3fe59cd3cd4ceb2a8d8a2b1db41afede01f66c42561b9be7b4b7 \
167 | --hash=sha256:2bf092f9210e105f414a043b92af583c98f50050559616930d884387d0772aba \
168 | --hash=sha256:3032884084f541163f295db8a6536e0abb0db464008fadca6c98aaf84ccf4717 \
169 | --hash=sha256:3a2cb34336110e0ed8bb4f650e817eed61fa064acbefeb3591f1b33e3a84fd96 \
170 | --hash=sha256:3ba2af245e36990facf67fde840a760128ddd71210b2ab6406e640188d69d136 \
171 | --hash=sha256:3d7bc90727351fb841e4d8ae620d2d86d8ed92b50473cd2b42ce9186104ecbba \
172 | --hash=sha256:438196cdf5dc8d39b50a45cb6e3f6274edbcf2254f85fa9b895bf85851c3a613 \
173 | --hash=sha256:46a561d23b91f30bccfd25429c3c706afe7d73a5cc64ef2dfaf2b2ac47c1a5dc \
174 | --hash=sha256:4cf327e98ecf08fcbb82685acaf1939d3338548620ab8dfa02828706402c34de \
175 | --hash=sha256:4f99e1b234c30c1e9714610eb0c6d2f11809c9c78c984a613ae539ea2ad2eb4b \
176 | --hash=sha256:544764ba51900da4639c0f983b323d288f94f65f4024dc40ecb1542d74dc0500 \
177 | --hash=sha256:56d94989191de3fcc4e002f93f7f1be5da476385dde410ddafbb70686acf00ea \
178 | --hash=sha256:57bfb8c8ea253be947ccb2bc2d1bb3862c2bccc662ad1b4626e1f5e004557042 \
179 | --hash=sha256:617f14ae9d53292ece33f45cba8503494ee199a75b44de7717964f70637a36aa \
180 | --hash=sha256:6eb88d87cb2c49af00d3bbc33a003f89fd9f78d318848da029383bfc08ecfbfb \
181 | --hash=sha256:75d4725d70b7c03e082bbb8a34639ede17f333d7247f56caceb3801cb6ff703d \
182 | --hash=sha256:770a205966d641627fd5cf9d3cb4b6280a716522cd36b8b284a8eb1581310f61 \
183 | --hash=sha256:7b73305f25eab4541bd7ee0b96d87e53ae9c9f1823be5659b806cd85786fe882 \
184 | --hash=sha256:7c9a4b2da6fac77bcc41b1ea95fadb314e92508bf5493ceff058e727e7ecf5b0 \
185 | --hash=sha256:81a6b377ea444336538638d31fdb39af6be1a043ca5e343fe18d0f17e098770b \
186 | --hash=sha256:83111e6388dec67822e2534e13b243cc644c7494a4bb60584edbff91585a83c6 \
187 | --hash=sha256:8704726d33e9aa8a6d5215044b8d00804561971163563e6e6591f9dcf64340cc \
188 | --hash=sha256:89768d84187f31717349c6bfadc0e0d8c321e8eb34522acec8a67b1236a66332 \
189 | --hash=sha256:8bf26ade3ff0f27668989d98c8435ce9327d24cffb7f07d24ef609e33d582439 \
190 | --hash=sha256:8c587963b85ce41e0a8af53b9b2de8dddbf5ece4c34553f7bd9d066148dc719c \
191 | --hash=sha256:95cbc13c1fc6844ab8812a525bbc237fa1470863ff3dace7352e910519e194b1 \
192 | --hash=sha256:97cc368a7268141afb5690760921765ed34867ffb9655dd325ed207af85c7529 \
193 | --hash=sha256:a867bf73a7eb808ef2afbca03bcdb785dae09595fbe550e1bab0cd023eba3de0 \
194 | --hash=sha256:b867e2f952ed592237a1828f027d332d8ee219ad722345b79a001f49df0936eb \
195 | --hash=sha256:c0bd19c72ae53e6ab979f0ac6a3fafceb02d2ecafa023c5cca47acd934d10be7 \
196 | --hash=sha256:ce463ce590f3825b52e9fe5c19a3c6a69fd7675a39d589e8b5fbe772272b3a24 \
197 | --hash=sha256:cf0e4f727534b7b1457898c4f4ae838af1ef87c359b76dcd5330fa31893a3ac7 \
198 | --hash=sha256:def58098f96a05f90af7e92fd127d21a287068202aa43b2a93476170ebd99e87 \
199 | --hash=sha256:e99bc9e65901bb9a7ce5e7bb24af03675cbd7c70b30ac670aa263240635999a4 \
200 | --hash=sha256:eb7d248c34a341cd4c31a06fd34d64306624c8cd8d0def7abb08792a5abfd556 \
201 | --hash=sha256:f67bfdb83a8232cb7a92b869f9355d677bce24485c460b19d01970b64b2ed476 \
202 | --hash=sha256:f883a22a56a84dba3b588696a2b8a1ab0d2c3d41be53264115c71b0a942d8fdb \
203 | --hash=sha256:fbdeeb58c0cf0595efe89c05c224e0a502d1aa6a8696e68a73c3efc6bc354304
204 | # via -r requirements.in
205 | numpy==1.24.2 \
206 | --hash=sha256:003a9f530e880cb2cd177cba1af7220b9aa42def9c4afc2a2fc3ee6be7eb2b22 \
207 | --hash=sha256:150947adbdfeceec4e5926d956a06865c1c690f2fd902efede4ca6fe2e657c3f \
208 | --hash=sha256:2620e8592136e073bd12ee4536149380695fbe9ebeae845b81237f986479ffc9 \
209 | --hash=sha256:2eabd64ddb96a1239791da78fa5f4e1693ae2dadc82a76bc76a14cbb2b966e96 \
210 | --hash=sha256:4173bde9fa2a005c2c6e2ea8ac1618e2ed2c1c6ec8a7657237854d42094123a0 \
211 | --hash=sha256:4199e7cfc307a778f72d293372736223e39ec9ac096ff0a2e64853b866a8e18a \
212 | --hash=sha256:4cecaed30dc14123020f77b03601559fff3e6cd0c048f8b5289f4eeabb0eb281 \
213 | --hash=sha256:557d42778a6869c2162deb40ad82612645e21d79e11c1dc62c6e82a2220ffb04 \
214 | --hash=sha256:63e45511ee4d9d976637d11e6c9864eae50e12dc9598f531c035265991910468 \
215 | --hash=sha256:6524630f71631be2dabe0c541e7675db82651eb998496bbe16bc4f77f0772253 \
216 | --hash=sha256:76807b4063f0002c8532cfeac47a3068a69561e9c8715efdad3c642eb27c0756 \
217 | --hash=sha256:7de8fdde0003f4294655aa5d5f0a89c26b9f22c0a58790c38fae1ed392d44a5a \
218 | --hash=sha256:889b2cc88b837d86eda1b17008ebeb679d82875022200c6e8e4ce6cf549b7acb \
219 | --hash=sha256:92011118955724465fb6853def593cf397b4a1367495e0b59a7e69d40c4eb71d \
220 | --hash=sha256:97cf27e51fa078078c649a51d7ade3c92d9e709ba2bfb97493007103c741f1d0 \
221 | --hash=sha256:9a23f8440561a633204a67fb44617ce2a299beecf3295f0d13c495518908e910 \
222 | --hash=sha256:a51725a815a6188c662fb66fb32077709a9ca38053f0274640293a14fdd22978 \
223 | --hash=sha256:a77d3e1163a7770164404607b7ba3967fb49b24782a6ef85d9b5f54126cc39e5 \
224 | --hash=sha256:adbdce121896fd3a17a77ab0b0b5eedf05a9834a18699db6829a64e1dfccca7f \
225 | --hash=sha256:c29e6bd0ec49a44d7690ecb623a8eac5ab8a923bce0bea6293953992edf3a76a \
226 | --hash=sha256:c72a6b2f4af1adfe193f7beb91ddf708ff867a3f977ef2ec53c0ffb8283ab9f5 \
227 | --hash=sha256:d0a2db9d20117bf523dde15858398e7c0858aadca7c0f088ac0d6edd360e9ad2 \
228 | --hash=sha256:e3ab5d32784e843fc0dd3ab6dcafc67ef806e6b6828dc6af2f689be0eb4d781d \
229 | --hash=sha256:e428c4fbfa085f947b536706a2fc349245d7baa8334f0c5723c56a10595f9b95 \
230 | --hash=sha256:e8d2859428712785e8a8b7d2b3ef0a1d1565892367b32f915c4a4df44d0e64f5 \
231 | --hash=sha256:eef70b4fc1e872ebddc38cddacc87c19a3709c0e3e5d20bf3954c147b1dd941d \
232 | --hash=sha256:f64bb98ac59b3ea3bf74b02f13836eb2e24e48e0ab0145bbda646295769bd780 \
233 | --hash=sha256:f9006288bcf4895917d02583cf3411f98631275bc67cce355a7f39f8c14338fa
234 | # via
235 | # contourpy
236 | # matplotlib
237 | packaging==23.0 \
238 | --hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
239 | --hash=sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97
240 | # via
241 | # matplotlib
242 | # pytest
243 | pillow==9.4.0 \
244 | --hash=sha256:013016af6b3a12a2f40b704677f8b51f72cb007dac785a9933d5c86a72a7fe33 \
245 | --hash=sha256:0845adc64fe9886db00f5ab68c4a8cd933ab749a87747555cec1c95acea64b0b \
246 | --hash=sha256:0884ba7b515163a1a05440a138adeb722b8a6ae2c2b33aea93ea3118dd3a899e \
247 | --hash=sha256:09b89ddc95c248ee788328528e6a2996e09eaccddeeb82a5356e92645733be35 \
248 | --hash=sha256:0dd4c681b82214b36273c18ca7ee87065a50e013112eea7d78c7a1b89a739153 \
249 | --hash=sha256:0e51f608da093e5d9038c592b5b575cadc12fd748af1479b5e858045fff955a9 \
250 | --hash=sha256:0f3269304c1a7ce82f1759c12ce731ef9b6e95b6df829dccd9fe42912cc48569 \
251 | --hash=sha256:16a8df99701f9095bea8a6c4b3197da105df6f74e6176c5b410bc2df2fd29a57 \
252 | --hash=sha256:19005a8e58b7c1796bc0167862b1f54a64d3b44ee5d48152b06bb861458bc0f8 \
253 | --hash=sha256:1b4b4e9dda4f4e4c4e6896f93e84a8f0bcca3b059de9ddf67dac3c334b1195e1 \
254 | --hash=sha256:28676836c7796805914b76b1837a40f76827ee0d5398f72f7dcc634bae7c6264 \
255 | --hash=sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157 \
256 | --hash=sha256:3f4cc516e0b264c8d4ccd6b6cbc69a07c6d582d8337df79be1e15a5056b258c9 \
257 | --hash=sha256:3fa1284762aacca6dc97474ee9c16f83990b8eeb6697f2ba17140d54b453e133 \
258 | --hash=sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9 \
259 | --hash=sha256:451f10ef963918e65b8869e17d67db5e2f4ab40e716ee6ce7129b0cde2876eab \
260 | --hash=sha256:46c259e87199041583658457372a183636ae8cd56dbf3f0755e0f376a7f9d0e6 \
261 | --hash=sha256:46f39cab8bbf4a384ba7cb0bc8bae7b7062b6a11cfac1ca4bc144dea90d4a9f5 \
262 | --hash=sha256:519e14e2c49fcf7616d6d2cfc5c70adae95682ae20f0395e9280db85e8d6c4df \
263 | --hash=sha256:53dcb50fbdc3fb2c55431a9b30caeb2f7027fcd2aeb501459464f0214200a503 \
264 | --hash=sha256:54614444887e0d3043557d9dbc697dbb16cfb5a35d672b7a0fcc1ed0cf1c600b \
265 | --hash=sha256:575d8912dca808edd9acd6f7795199332696d3469665ef26163cd090fa1f8bfa \
266 | --hash=sha256:5dd5a9c3091a0f414a963d427f920368e2b6a4c2f7527fdd82cde8ef0bc7a327 \
267 | --hash=sha256:5f532a2ad4d174eb73494e7397988e22bf427f91acc8e6ebf5bb10597b49c493 \
268 | --hash=sha256:60e7da3a3ad1812c128750fc1bc14a7ceeb8d29f77e0a2356a8fb2aa8925287d \
269 | --hash=sha256:653d7fb2df65efefbcbf81ef5fe5e5be931f1ee4332c2893ca638c9b11a409c4 \
270 | --hash=sha256:6663977496d616b618b6cfa43ec86e479ee62b942e1da76a2c3daa1c75933ef4 \
271 | --hash=sha256:6abfb51a82e919e3933eb137e17c4ae9c0475a25508ea88993bb59faf82f3b35 \
272 | --hash=sha256:6c6b1389ed66cdd174d040105123a5a1bc91d0aa7059c7261d20e583b6d8cbd2 \
273 | --hash=sha256:6d9dfb9959a3b0039ee06c1a1a90dc23bac3b430842dcb97908ddde05870601c \
274 | --hash=sha256:765cb54c0b8724a7c12c55146ae4647e0274a839fb6de7bcba841e04298e1011 \
275 | --hash=sha256:7a21222644ab69ddd9967cfe6f2bb420b460dae4289c9d40ff9a4896e7c35c9a \
276 | --hash=sha256:7ac7594397698f77bce84382929747130765f66406dc2cd8b4ab4da68ade4c6e \
277 | --hash=sha256:7cfc287da09f9d2a7ec146ee4d72d6ea1342e770d975e49a8621bf54eaa8f30f \
278 | --hash=sha256:83125753a60cfc8c412de5896d10a0a405e0bd88d0470ad82e0869ddf0cb3848 \
279 | --hash=sha256:847b114580c5cc9ebaf216dd8c8dbc6b00a3b7ab0131e173d7120e6deade1f57 \
280 | --hash=sha256:87708d78a14d56a990fbf4f9cb350b7d89ee8988705e58e39bdf4d82c149210f \
281 | --hash=sha256:8a2b5874d17e72dfb80d917213abd55d7e1ed2479f38f001f264f7ce7bae757c \
282 | --hash=sha256:8f127e7b028900421cad64f51f75c051b628db17fb00e099eb148761eed598c9 \
283 | --hash=sha256:94cdff45173b1919350601f82d61365e792895e3c3a3443cf99819e6fbf717a5 \
284 | --hash=sha256:99d92d148dd03fd19d16175b6d355cc1b01faf80dae93c6c3eb4163709edc0a9 \
285 | --hash=sha256:9a3049a10261d7f2b6514d35bbb7a4dfc3ece4c4de14ef5876c4b7a23a0e566d \
286 | --hash=sha256:9d9a62576b68cd90f7075876f4e8444487db5eeea0e4df3ba298ee38a8d067b0 \
287 | --hash=sha256:9e5f94742033898bfe84c93c831a6f552bb629448d4072dd312306bab3bd96f1 \
288 | --hash=sha256:a1c2d7780448eb93fbcc3789bf3916aa5720d942e37945f4056680317f1cd23e \
289 | --hash=sha256:a2e0f87144fcbbe54297cae708c5e7f9da21a4646523456b00cc956bd4c65815 \
290 | --hash=sha256:a4dfdae195335abb4e89cc9762b2edc524f3c6e80d647a9a81bf81e17e3fb6f0 \
291 | --hash=sha256:a96e6e23f2b79433390273eaf8cc94fec9c6370842e577ab10dabdcc7ea0a66b \
292 | --hash=sha256:aabdab8ec1e7ca7f1434d042bf8b1e92056245fb179790dc97ed040361f16bfd \
293 | --hash=sha256:b222090c455d6d1a64e6b7bb5f4035c4dff479e22455c9eaa1bdd4c75b52c80c \
294 | --hash=sha256:b52ff4f4e002f828ea6483faf4c4e8deea8d743cf801b74910243c58acc6eda3 \
295 | --hash=sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab \
296 | --hash=sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858 \
297 | --hash=sha256:b9b752ab91e78234941e44abdecc07f1f0d8f51fb62941d32995b8161f68cfe5 \
298 | --hash=sha256:ba6612b6548220ff5e9df85261bddc811a057b0b465a1226b39bfb8550616aee \
299 | --hash=sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343 \
300 | --hash=sha256:c3c4ed2ff6760e98d262e0cc9c9a7f7b8a9f61aa4d47c58835cdaf7b0b8811bb \
301 | --hash=sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47 \
302 | --hash=sha256:cb362e3b0976dc994857391b776ddaa8c13c28a16f80ac6522c23d5257156bed \
303 | --hash=sha256:d197df5489004db87d90b918033edbeee0bd6df3848a204bca3ff0a903bef837 \
304 | --hash=sha256:d3b56206244dc8711f7e8b7d6cad4663917cd5b2d950799425076681e8766286 \
305 | --hash=sha256:d5b2f8a31bd43e0f18172d8ac82347c8f37ef3e0b414431157718aa234991b28 \
306 | --hash=sha256:d7081c084ceb58278dd3cf81f836bc818978c0ccc770cbbb202125ddabec6628 \
307 | --hash=sha256:db74f5562c09953b2c5f8ec4b7dfd3f5421f31811e97d1dbc0a7c93d6e3a24df \
308 | --hash=sha256:df41112ccce5d47770a0c13651479fbcd8793f34232a2dd9faeccb75eb5d0d0d \
309 | --hash=sha256:e1339790c083c5a4de48f688b4841f18df839eb3c9584a770cbd818b33e26d5d \
310 | --hash=sha256:e621b0246192d3b9cb1dc62c78cfa4c6f6d2ddc0ec207d43c0dedecb914f152a \
311 | --hash=sha256:e8c5cf126889a4de385c02a2c3d3aba4b00f70234bfddae82a5eaa3ee6d5e3e6 \
312 | --hash=sha256:e9d7747847c53a16a729b6ee5e737cf170f7a16611c143d95aa60a109a59c336 \
313 | --hash=sha256:eaef5d2de3c7e9b21f1e762f289d17b726c2239a42b11e25446abf82b26ac132 \
314 | --hash=sha256:ed3e4b4e1e6de75fdc16d3259098de7c6571b1a6cc863b1a49e7d3d53e036070 \
315 | --hash=sha256:ef21af928e807f10bf4141cad4746eee692a0dd3ff56cfb25fce076ec3cc8abe \
316 | --hash=sha256:f09598b416ba39a8f489c124447b007fe865f786a89dbfa48bb5cf395693132a \
317 | --hash=sha256:f0caf4a5dcf610d96c3bd32932bfac8aee61c96e60481c2a0ea58da435e25acd \
318 | --hash=sha256:f6e78171be3fb7941f9910ea15b4b14ec27725865a73c15277bc39f5ca4f8391 \
319 | --hash=sha256:f715c32e774a60a337b2bb8ad9839b4abf75b267a0f18806f6f4f5f1688c4b5a \
320 | --hash=sha256:fb5c1ad6bad98c57482236a21bf985ab0ef42bd51f7ad4e4538e89a997624e12
321 | # via matplotlib
322 | pluggy==1.0.0 \
323 | --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \
324 | --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3
325 | # via pytest
326 | pyparsing==3.0.9 \
327 | --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \
328 | --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc
329 | # via matplotlib
330 | pytest==7.2.1 \
331 | --hash=sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5 \
332 | --hash=sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42
333 | # via
334 | # -r requirements.in
335 | # pytest-django
336 | pytest-django==4.5.2 \
337 | --hash=sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e \
338 | --hash=sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2
339 | # via -r requirements.in
340 | python-dateutil==2.8.2 \
341 | --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \
342 | --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9
343 | # via
344 | # faker
345 | # matplotlib
346 | six==1.16.0 \
347 | --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
348 | --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
349 | # via python-dateutil
350 | sqlparse==0.4.3 \
351 | --hash=sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34 \
352 | --hash=sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268
353 | # via django
354 | tqdm==4.64.1 \
355 | --hash=sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4 \
356 | --hash=sha256:6fee160d6ffcd1b1c68c65f14c829c22832bc401726335ce92c52d395944a6a1
357 | # via -r requirements.in
358 |
--------------------------------------------------------------------------------
/requirements/py311-django42.txt:
--------------------------------------------------------------------------------
1 | #
2 | # This file is autogenerated by pip-compile with Python 3.11
3 | # by the following command:
4 | #
5 | # requirements/compile.py
6 | #
7 | asgiref==3.6.0 \
8 | --hash=sha256:71e68008da809b957b7ee4b43dbccff33d1b23519fb8344e33f049897077afac \
9 | --hash=sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506
10 | # via django
11 | attrs==22.2.0 \
12 | --hash=sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836 \
13 | --hash=sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99
14 | # via pytest
15 | contourpy==1.0.7 \
16 | --hash=sha256:031154ed61f7328ad7f97662e48660a150ef84ee1bc8876b6472af88bf5a9b98 \
17 | --hash=sha256:0f9d350b639db6c2c233d92c7f213d94d2e444d8e8fc5ca44c9706cf72193772 \
18 | --hash=sha256:130230b7e49825c98edf0b428b7aa1125503d91732735ef897786fe5452b1ec2 \
19 | --hash=sha256:152fd8f730c31fd67fe0ffebe1df38ab6a669403da93df218801a893645c6ccc \
20 | --hash=sha256:1c71fdd8f1c0f84ffd58fca37d00ca4ebaa9e502fb49825484da075ac0b0b803 \
21 | --hash=sha256:24847601071f740837aefb730e01bd169fbcaa610209779a78db7ebb6e6a7051 \
22 | --hash=sha256:2e9ebb4425fc1b658e13bace354c48a933b842d53c458f02c86f371cecbedecc \
23 | --hash=sha256:30676ca45084ee61e9c3da589042c24a57592e375d4b138bd84d8709893a1ba4 \
24 | --hash=sha256:31a55dccc8426e71817e3fe09b37d6d48ae40aae4ecbc8c7ad59d6893569c436 \
25 | --hash=sha256:366a0cf0fc079af5204801786ad7a1c007714ee3909e364dbac1729f5b0849e5 \
26 | --hash=sha256:38e2e577f0f092b8e6774459317c05a69935a1755ecfb621c0a98f0e3c09c9a5 \
27 | --hash=sha256:3c184ad2433635f216645fdf0493011a4667e8d46b34082f5a3de702b6ec42e3 \
28 | --hash=sha256:3caea6365b13119626ee996711ab63e0c9d7496f65641f4459c60a009a1f3e80 \
29 | --hash=sha256:3e927b3868bd1e12acee7cc8f3747d815b4ab3e445a28d2e5373a7f4a6e76ba1 \
30 | --hash=sha256:4ee3ee247f795a69e53cd91d927146fb16c4e803c7ac86c84104940c7d2cabf0 \
31 | --hash=sha256:54d43960d809c4c12508a60b66cb936e7ed57d51fb5e30b513934a4a23874fae \
32 | --hash=sha256:57119b0116e3f408acbdccf9eb6ef19d7fe7baf0d1e9aaa5381489bc1aa56556 \
33 | --hash=sha256:58569c491e7f7e874f11519ef46737cea1d6eda1b514e4eb5ac7dab6aa864d02 \
34 | --hash=sha256:5a011cf354107b47c58ea932d13b04d93c6d1d69b8b6dce885e642531f847566 \
35 | --hash=sha256:5caeacc68642e5f19d707471890f037a13007feba8427eb7f2a60811a1fc1350 \
36 | --hash=sha256:5dd34c1ae752515318224cba7fc62b53130c45ac6a1040c8b7c1a223c46e8967 \
37 | --hash=sha256:60835badb5ed5f4e194a6f21c09283dd6e007664a86101431bf870d9e86266c4 \
38 | --hash=sha256:62398c80ef57589bdbe1eb8537127321c1abcfdf8c5f14f479dbbe27d0322e66 \
39 | --hash=sha256:6381fa66866b0ea35e15d197fc06ac3840a9b2643a6475c8fff267db8b9f1e69 \
40 | --hash=sha256:64757f6460fc55d7e16ed4f1de193f362104285c667c112b50a804d482777edd \
41 | --hash=sha256:69f8ff4db108815addd900a74df665e135dbbd6547a8a69333a68e1f6e368ac2 \
42 | --hash=sha256:6c180d89a28787e4b73b07e9b0e2dac7741261dbdca95f2b489c4f8f887dd810 \
43 | --hash=sha256:71b0bf0c30d432278793d2141362ac853859e87de0a7dee24a1cea35231f0d50 \
44 | --hash=sha256:769eef00437edf115e24d87f8926955f00f7704bede656ce605097584f9966dc \
45 | --hash=sha256:7f6979d20ee5693a1057ab53e043adffa1e7418d734c1532e2d9e915b08d8ec2 \
46 | --hash=sha256:87f4d8941a9564cda3f7fa6a6cd9b32ec575830780677932abdec7bcb61717b0 \
47 | --hash=sha256:89ba9bb365446a22411f0673abf6ee1fea3b2cf47b37533b970904880ceb72f3 \
48 | --hash=sha256:8acf74b5d383414401926c1598ed77825cd530ac7b463ebc2e4f46638f56cce6 \
49 | --hash=sha256:9056c5310eb1daa33fc234ef39ebfb8c8e2533f088bbf0bc7350f70a29bde1ac \
50 | --hash=sha256:95c3acddf921944f241b6773b767f1cbce71d03307270e2d769fd584d5d1092d \
51 | --hash=sha256:9e20e5a1908e18aaa60d9077a6d8753090e3f85ca25da6e25d30dc0a9e84c2c6 \
52 | --hash=sha256:a1e97b86f73715e8670ef45292d7cc033548266f07d54e2183ecb3c87598888f \
53 | --hash=sha256:a877ada905f7d69b2a31796c4b66e31a8068b37aa9b78832d41c82fc3e056ddd \
54 | --hash=sha256:a9d7587d2fdc820cc9177139b56795c39fb8560f540bba9ceea215f1f66e1566 \
55 | --hash=sha256:abf298af1e7ad44eeb93501e40eb5a67abbf93b5d90e468d01fc0c4451971afa \
56 | --hash=sha256:ae90d5a8590e5310c32a7630b4b8618cef7563cebf649011da80874d0aa8f414 \
57 | --hash=sha256:b6d0f9e1d39dbfb3977f9dd79f156c86eb03e57a7face96f199e02b18e58d32a \
58 | --hash=sha256:b8d587cc39057d0afd4166083d289bdeff221ac6d3ee5046aef2d480dc4b503c \
59 | --hash=sha256:c5210e5d5117e9aec8c47d9156d1d3835570dd909a899171b9535cb4a3f32693 \
60 | --hash=sha256:cc331c13902d0f50845099434cd936d49d7a2ca76cb654b39691974cb1e4812d \
61 | --hash=sha256:ce41676b3d0dd16dbcfabcc1dc46090aaf4688fd6e819ef343dbda5a57ef0161 \
62 | --hash=sha256:d8165a088d31798b59e91117d1f5fc3df8168d8b48c4acc10fc0df0d0bdbcc5e \
63 | --hash=sha256:e7281244c99fd7c6f27c1c6bfafba878517b0b62925a09b586d88ce750a016d2 \
64 | --hash=sha256:e96a08b62bb8de960d3a6afbc5ed8421bf1a2d9c85cc4ea73f4bc81b4910500f \
65 | --hash=sha256:ed33433fc3820263a6368e532f19ddb4c5990855e4886088ad84fd7c4e561c71 \
66 | --hash=sha256:efb8f6d08ca7998cf59eaf50c9d60717f29a1a0a09caa46460d33b2924839dbd \
67 | --hash=sha256:efe99298ba37e37787f6a2ea868265465410822f7bea163edcc1bd3903354ea9 \
68 | --hash=sha256:f99e9486bf1bb979d95d5cffed40689cb595abb2b841f2991fc894b3452290e8 \
69 | --hash=sha256:fc1464c97579da9f3ab16763c32e5c5d5bb5fa1ec7ce509a4ca6108b61b84fab \
70 | --hash=sha256:fd7dc0e6812b799a34f6d12fcb1000539098c249c8da54f3566c6a6461d0dbad
71 | # via matplotlib
72 | cycler==0.11.0 \
73 | --hash=sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3 \
74 | --hash=sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f
75 | # via matplotlib
76 | django==4.2b1 \
77 | --hash=sha256:33e3b3b80924dae3e6d4b5e697eaee724d5a35c1a430df44b1d72c802657992f \
78 | --hash=sha256:9bf13063a882a9b0f7028c4cdc32ea36fe104491cd7720859117990933f9c589
79 | # via -r requirements.in
80 | faker==17.6.0 \
81 | --hash=sha256:51f37ff9df710159d6d736d0ba1c75e063430a8c806b91334d7794305b5a6114 \
82 | --hash=sha256:5aaa16fa9cfde7d117eef70b6b293a705021e57158f3fa6b44ed1b70202d2065
83 | # via -r requirements.in
84 | fonttools==4.39.0 \
85 | --hash=sha256:909c104558835eac27faeb56be5a4c32694192dca123d073bf746ce9254054af \
86 | --hash=sha256:f5e764e1fd6ad54dfc201ff32af0ba111bcfbe0d05b24540af74c63db4ed6390
87 | # via matplotlib
88 | iniconfig==2.0.0 \
89 | --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \
90 | --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374
91 | # via pytest
92 | kiwisolver==1.4.4 \
93 | --hash=sha256:02f79693ec433cb4b5f51694e8477ae83b3205768a6fb48ffba60549080e295b \
94 | --hash=sha256:03baab2d6b4a54ddbb43bba1a3a2d1627e82d205c5cf8f4c924dc49284b87166 \
95 | --hash=sha256:1041feb4cda8708ce73bb4dcb9ce1ccf49d553bf87c3954bdfa46f0c3f77252c \
96 | --hash=sha256:10ee06759482c78bdb864f4109886dff7b8a56529bc1609d4f1112b93fe6423c \
97 | --hash=sha256:1d1573129aa0fd901076e2bfb4275a35f5b7aa60fbfb984499d661ec950320b0 \
98 | --hash=sha256:283dffbf061a4ec60391d51e6155e372a1f7a4f5b15d59c8505339454f8989e4 \
99 | --hash=sha256:28bc5b299f48150b5f822ce68624e445040595a4ac3d59251703779836eceff9 \
100 | --hash=sha256:2a66fdfb34e05b705620dd567f5a03f239a088d5a3f321e7b6ac3239d22aa286 \
101 | --hash=sha256:2e307eb9bd99801f82789b44bb45e9f541961831c7311521b13a6c85afc09767 \
102 | --hash=sha256:2e407cb4bd5a13984a6c2c0fe1845e4e41e96f183e5e5cd4d77a857d9693494c \
103 | --hash=sha256:2f5e60fabb7343a836360c4f0919b8cd0d6dbf08ad2ca6b9cf90bf0c76a3c4f6 \
104 | --hash=sha256:36dafec3d6d6088d34e2de6b85f9d8e2324eb734162fba59d2ba9ed7a2043d5b \
105 | --hash=sha256:3fe20f63c9ecee44560d0e7f116b3a747a5d7203376abeea292ab3152334d004 \
106 | --hash=sha256:41dae968a94b1ef1897cb322b39360a0812661dba7c682aa45098eb8e193dbdf \
107 | --hash=sha256:4bd472dbe5e136f96a4b18f295d159d7f26fd399136f5b17b08c4e5f498cd494 \
108 | --hash=sha256:4ea39b0ccc4f5d803e3337dd46bcce60b702be4d86fd0b3d7531ef10fd99a1ac \
109 | --hash=sha256:5853eb494c71e267912275e5586fe281444eb5e722de4e131cddf9d442615626 \
110 | --hash=sha256:5bce61af018b0cb2055e0e72e7d65290d822d3feee430b7b8203d8a855e78766 \
111 | --hash=sha256:6295ecd49304dcf3bfbfa45d9a081c96509e95f4b9d0eb7ee4ec0530c4a96514 \
112 | --hash=sha256:62ac9cc684da4cf1778d07a89bf5f81b35834cb96ca523d3a7fb32509380cbf6 \
113 | --hash=sha256:70e7c2e7b750585569564e2e5ca9845acfaa5da56ac46df68414f29fea97be9f \
114 | --hash=sha256:7577c1987baa3adc4b3c62c33bd1118c3ef5c8ddef36f0f2c950ae0b199e100d \
115 | --hash=sha256:75facbe9606748f43428fc91a43edb46c7ff68889b91fa31f53b58894503a191 \
116 | --hash=sha256:787518a6789009c159453da4d6b683f468ef7a65bbde796bcea803ccf191058d \
117 | --hash=sha256:78d6601aed50c74e0ef02f4204da1816147a6d3fbdc8b3872d263338a9052c51 \
118 | --hash=sha256:7c43e1e1206cd421cd92e6b3280d4385d41d7166b3ed577ac20444b6995a445f \
119 | --hash=sha256:81e38381b782cc7e1e46c4e14cd997ee6040768101aefc8fa3c24a4cc58e98f8 \
120 | --hash=sha256:841293b17ad704d70c578f1f0013c890e219952169ce8a24ebc063eecf775454 \
121 | --hash=sha256:872b8ca05c40d309ed13eb2e582cab0c5a05e81e987ab9c521bf05ad1d5cf5cb \
122 | --hash=sha256:877272cf6b4b7e94c9614f9b10140e198d2186363728ed0f701c6eee1baec1da \
123 | --hash=sha256:8c808594c88a025d4e322d5bb549282c93c8e1ba71b790f539567932722d7bd8 \
124 | --hash=sha256:8ed58b8acf29798b036d347791141767ccf65eee7f26bde03a71c944449e53de \
125 | --hash=sha256:91672bacaa030f92fc2f43b620d7b337fd9a5af28b0d6ed3f77afc43c4a64b5a \
126 | --hash=sha256:968f44fdbf6dd757d12920d63b566eeb4d5b395fd2d00d29d7ef00a00582aac9 \
127 | --hash=sha256:9f85003f5dfa867e86d53fac6f7e6f30c045673fa27b603c397753bebadc3008 \
128 | --hash=sha256:a553dadda40fef6bfa1456dc4be49b113aa92c2a9a9e8711e955618cd69622e3 \
129 | --hash=sha256:a68b62a02953b9841730db7797422f983935aeefceb1679f0fc85cbfbd311c32 \
130 | --hash=sha256:abbe9fa13da955feb8202e215c4018f4bb57469b1b78c7a4c5c7b93001699938 \
131 | --hash=sha256:ad881edc7ccb9d65b0224f4e4d05a1e85cf62d73aab798943df6d48ab0cd79a1 \
132 | --hash=sha256:b1792d939ec70abe76f5054d3f36ed5656021dcad1322d1cc996d4e54165cef9 \
133 | --hash=sha256:b428ef021242344340460fa4c9185d0b1f66fbdbfecc6c63eff4b7c29fad429d \
134 | --hash=sha256:b533558eae785e33e8c148a8d9921692a9fe5aa516efbdff8606e7d87b9d5824 \
135 | --hash=sha256:ba59c92039ec0a66103b1d5fe588fa546373587a7d68f5c96f743c3396afc04b \
136 | --hash=sha256:bc8d3bd6c72b2dd9decf16ce70e20abcb3274ba01b4e1c96031e0c4067d1e7cd \
137 | --hash=sha256:bc9db8a3efb3e403e4ecc6cd9489ea2bac94244f80c78e27c31dcc00d2790ac2 \
138 | --hash=sha256:bf7d9fce9bcc4752ca4a1b80aabd38f6d19009ea5cbda0e0856983cf6d0023f5 \
139 | --hash=sha256:c2dbb44c3f7e6c4d3487b31037b1bdbf424d97687c1747ce4ff2895795c9bf69 \
140 | --hash=sha256:c79ebe8f3676a4c6630fd3f777f3cfecf9289666c84e775a67d1d358578dc2e3 \
141 | --hash=sha256:c97528e64cb9ebeff9701e7938653a9951922f2a38bd847787d4a8e498cc83ae \
142 | --hash=sha256:d0611a0a2a518464c05ddd5a3a1a0e856ccc10e67079bb17f265ad19ab3c7597 \
143 | --hash=sha256:d06adcfa62a4431d404c31216f0f8ac97397d799cd53800e9d3efc2fbb3cf14e \
144 | --hash=sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955 \
145 | --hash=sha256:d5b61785a9ce44e5a4b880272baa7cf6c8f48a5180c3e81c59553ba0cb0821ca \
146 | --hash=sha256:da152d8cdcab0e56e4f45eb08b9aea6455845ec83172092f09b0e077ece2cf7a \
147 | --hash=sha256:da7e547706e69e45d95e116e6939488d62174e033b763ab1496b4c29b76fabea \
148 | --hash=sha256:db5283d90da4174865d520e7366801a93777201e91e79bacbac6e6927cbceede \
149 | --hash=sha256:db608a6757adabb32f1cfe6066e39b3706d8c3aa69bbc353a5b61edad36a5cb4 \
150 | --hash=sha256:e0ea21f66820452a3f5d1655f8704a60d66ba1191359b96541eaf457710a5fc6 \
151 | --hash=sha256:e7da3fec7408813a7cebc9e4ec55afed2d0fd65c4754bc376bf03498d4e92686 \
152 | --hash=sha256:e92a513161077b53447160b9bd8f522edfbed4bd9759e4c18ab05d7ef7e49408 \
153 | --hash=sha256:ecb1fa0db7bf4cff9dac752abb19505a233c7f16684c5826d1f11ebd9472b871 \
154 | --hash=sha256:efda5fc8cc1c61e4f639b8067d118e742b812c930f708e6667a5ce0d13499e29 \
155 | --hash=sha256:f0a1dbdb5ecbef0d34eb77e56fcb3e95bbd7e50835d9782a45df81cc46949750 \
156 | --hash=sha256:f0a71d85ecdd570ded8ac3d1c0f480842f49a40beb423bb8014539a9f32a5897 \
157 | --hash=sha256:f4f270de01dd3e129a72efad823da90cc4d6aafb64c410c9033aba70db9f1ff0 \
158 | --hash=sha256:f6cb459eea32a4e2cf18ba5fcece2dbdf496384413bc1bae15583f19e567f3b2 \
159 | --hash=sha256:f8ad8285b01b0d4695102546b342b493b3ccc6781fc28c8c6a1bb63e95d22f09 \
160 | --hash=sha256:f9f39e2f049db33a908319cf46624a569b36983c7c78318e9726a4cb8923b26c
161 | # via matplotlib
162 | matplotlib==3.7.1 \
163 | --hash=sha256:08308bae9e91aca1ec6fd6dda66237eef9f6294ddb17f0d0b3c863169bf82353 \
164 | --hash=sha256:14645aad967684e92fc349493fa10c08a6da514b3d03a5931a1bac26e6792bd1 \
165 | --hash=sha256:21e9cff1a58d42e74d01153360de92b326708fb205250150018a52c70f43c290 \
166 | --hash=sha256:28506a03bd7f3fe59cd3cd4ceb2a8d8a2b1db41afede01f66c42561b9be7b4b7 \
167 | --hash=sha256:2bf092f9210e105f414a043b92af583c98f50050559616930d884387d0772aba \
168 | --hash=sha256:3032884084f541163f295db8a6536e0abb0db464008fadca6c98aaf84ccf4717 \
169 | --hash=sha256:3a2cb34336110e0ed8bb4f650e817eed61fa064acbefeb3591f1b33e3a84fd96 \
170 | --hash=sha256:3ba2af245e36990facf67fde840a760128ddd71210b2ab6406e640188d69d136 \
171 | --hash=sha256:3d7bc90727351fb841e4d8ae620d2d86d8ed92b50473cd2b42ce9186104ecbba \
172 | --hash=sha256:438196cdf5dc8d39b50a45cb6e3f6274edbcf2254f85fa9b895bf85851c3a613 \
173 | --hash=sha256:46a561d23b91f30bccfd25429c3c706afe7d73a5cc64ef2dfaf2b2ac47c1a5dc \
174 | --hash=sha256:4cf327e98ecf08fcbb82685acaf1939d3338548620ab8dfa02828706402c34de \
175 | --hash=sha256:4f99e1b234c30c1e9714610eb0c6d2f11809c9c78c984a613ae539ea2ad2eb4b \
176 | --hash=sha256:544764ba51900da4639c0f983b323d288f94f65f4024dc40ecb1542d74dc0500 \
177 | --hash=sha256:56d94989191de3fcc4e002f93f7f1be5da476385dde410ddafbb70686acf00ea \
178 | --hash=sha256:57bfb8c8ea253be947ccb2bc2d1bb3862c2bccc662ad1b4626e1f5e004557042 \
179 | --hash=sha256:617f14ae9d53292ece33f45cba8503494ee199a75b44de7717964f70637a36aa \
180 | --hash=sha256:6eb88d87cb2c49af00d3bbc33a003f89fd9f78d318848da029383bfc08ecfbfb \
181 | --hash=sha256:75d4725d70b7c03e082bbb8a34639ede17f333d7247f56caceb3801cb6ff703d \
182 | --hash=sha256:770a205966d641627fd5cf9d3cb4b6280a716522cd36b8b284a8eb1581310f61 \
183 | --hash=sha256:7b73305f25eab4541bd7ee0b96d87e53ae9c9f1823be5659b806cd85786fe882 \
184 | --hash=sha256:7c9a4b2da6fac77bcc41b1ea95fadb314e92508bf5493ceff058e727e7ecf5b0 \
185 | --hash=sha256:81a6b377ea444336538638d31fdb39af6be1a043ca5e343fe18d0f17e098770b \
186 | --hash=sha256:83111e6388dec67822e2534e13b243cc644c7494a4bb60584edbff91585a83c6 \
187 | --hash=sha256:8704726d33e9aa8a6d5215044b8d00804561971163563e6e6591f9dcf64340cc \
188 | --hash=sha256:89768d84187f31717349c6bfadc0e0d8c321e8eb34522acec8a67b1236a66332 \
189 | --hash=sha256:8bf26ade3ff0f27668989d98c8435ce9327d24cffb7f07d24ef609e33d582439 \
190 | --hash=sha256:8c587963b85ce41e0a8af53b9b2de8dddbf5ece4c34553f7bd9d066148dc719c \
191 | --hash=sha256:95cbc13c1fc6844ab8812a525bbc237fa1470863ff3dace7352e910519e194b1 \
192 | --hash=sha256:97cc368a7268141afb5690760921765ed34867ffb9655dd325ed207af85c7529 \
193 | --hash=sha256:a867bf73a7eb808ef2afbca03bcdb785dae09595fbe550e1bab0cd023eba3de0 \
194 | --hash=sha256:b867e2f952ed592237a1828f027d332d8ee219ad722345b79a001f49df0936eb \
195 | --hash=sha256:c0bd19c72ae53e6ab979f0ac6a3fafceb02d2ecafa023c5cca47acd934d10be7 \
196 | --hash=sha256:ce463ce590f3825b52e9fe5c19a3c6a69fd7675a39d589e8b5fbe772272b3a24 \
197 | --hash=sha256:cf0e4f727534b7b1457898c4f4ae838af1ef87c359b76dcd5330fa31893a3ac7 \
198 | --hash=sha256:def58098f96a05f90af7e92fd127d21a287068202aa43b2a93476170ebd99e87 \
199 | --hash=sha256:e99bc9e65901bb9a7ce5e7bb24af03675cbd7c70b30ac670aa263240635999a4 \
200 | --hash=sha256:eb7d248c34a341cd4c31a06fd34d64306624c8cd8d0def7abb08792a5abfd556 \
201 | --hash=sha256:f67bfdb83a8232cb7a92b869f9355d677bce24485c460b19d01970b64b2ed476 \
202 | --hash=sha256:f883a22a56a84dba3b588696a2b8a1ab0d2c3d41be53264115c71b0a942d8fdb \
203 | --hash=sha256:fbdeeb58c0cf0595efe89c05c224e0a502d1aa6a8696e68a73c3efc6bc354304
204 | # via -r requirements.in
205 | numpy==1.24.2 \
206 | --hash=sha256:003a9f530e880cb2cd177cba1af7220b9aa42def9c4afc2a2fc3ee6be7eb2b22 \
207 | --hash=sha256:150947adbdfeceec4e5926d956a06865c1c690f2fd902efede4ca6fe2e657c3f \
208 | --hash=sha256:2620e8592136e073bd12ee4536149380695fbe9ebeae845b81237f986479ffc9 \
209 | --hash=sha256:2eabd64ddb96a1239791da78fa5f4e1693ae2dadc82a76bc76a14cbb2b966e96 \
210 | --hash=sha256:4173bde9fa2a005c2c6e2ea8ac1618e2ed2c1c6ec8a7657237854d42094123a0 \
211 | --hash=sha256:4199e7cfc307a778f72d293372736223e39ec9ac096ff0a2e64853b866a8e18a \
212 | --hash=sha256:4cecaed30dc14123020f77b03601559fff3e6cd0c048f8b5289f4eeabb0eb281 \
213 | --hash=sha256:557d42778a6869c2162deb40ad82612645e21d79e11c1dc62c6e82a2220ffb04 \
214 | --hash=sha256:63e45511ee4d9d976637d11e6c9864eae50e12dc9598f531c035265991910468 \
215 | --hash=sha256:6524630f71631be2dabe0c541e7675db82651eb998496bbe16bc4f77f0772253 \
216 | --hash=sha256:76807b4063f0002c8532cfeac47a3068a69561e9c8715efdad3c642eb27c0756 \
217 | --hash=sha256:7de8fdde0003f4294655aa5d5f0a89c26b9f22c0a58790c38fae1ed392d44a5a \
218 | --hash=sha256:889b2cc88b837d86eda1b17008ebeb679d82875022200c6e8e4ce6cf549b7acb \
219 | --hash=sha256:92011118955724465fb6853def593cf397b4a1367495e0b59a7e69d40c4eb71d \
220 | --hash=sha256:97cf27e51fa078078c649a51d7ade3c92d9e709ba2bfb97493007103c741f1d0 \
221 | --hash=sha256:9a23f8440561a633204a67fb44617ce2a299beecf3295f0d13c495518908e910 \
222 | --hash=sha256:a51725a815a6188c662fb66fb32077709a9ca38053f0274640293a14fdd22978 \
223 | --hash=sha256:a77d3e1163a7770164404607b7ba3967fb49b24782a6ef85d9b5f54126cc39e5 \
224 | --hash=sha256:adbdce121896fd3a17a77ab0b0b5eedf05a9834a18699db6829a64e1dfccca7f \
225 | --hash=sha256:c29e6bd0ec49a44d7690ecb623a8eac5ab8a923bce0bea6293953992edf3a76a \
226 | --hash=sha256:c72a6b2f4af1adfe193f7beb91ddf708ff867a3f977ef2ec53c0ffb8283ab9f5 \
227 | --hash=sha256:d0a2db9d20117bf523dde15858398e7c0858aadca7c0f088ac0d6edd360e9ad2 \
228 | --hash=sha256:e3ab5d32784e843fc0dd3ab6dcafc67ef806e6b6828dc6af2f689be0eb4d781d \
229 | --hash=sha256:e428c4fbfa085f947b536706a2fc349245d7baa8334f0c5723c56a10595f9b95 \
230 | --hash=sha256:e8d2859428712785e8a8b7d2b3ef0a1d1565892367b32f915c4a4df44d0e64f5 \
231 | --hash=sha256:eef70b4fc1e872ebddc38cddacc87c19a3709c0e3e5d20bf3954c147b1dd941d \
232 | --hash=sha256:f64bb98ac59b3ea3bf74b02f13836eb2e24e48e0ab0145bbda646295769bd780 \
233 | --hash=sha256:f9006288bcf4895917d02583cf3411f98631275bc67cce355a7f39f8c14338fa
234 | # via
235 | # contourpy
236 | # matplotlib
237 | packaging==23.0 \
238 | --hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
239 | --hash=sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97
240 | # via
241 | # matplotlib
242 | # pytest
243 | pillow==9.4.0 \
244 | --hash=sha256:013016af6b3a12a2f40b704677f8b51f72cb007dac785a9933d5c86a72a7fe33 \
245 | --hash=sha256:0845adc64fe9886db00f5ab68c4a8cd933ab749a87747555cec1c95acea64b0b \
246 | --hash=sha256:0884ba7b515163a1a05440a138adeb722b8a6ae2c2b33aea93ea3118dd3a899e \
247 | --hash=sha256:09b89ddc95c248ee788328528e6a2996e09eaccddeeb82a5356e92645733be35 \
248 | --hash=sha256:0dd4c681b82214b36273c18ca7ee87065a50e013112eea7d78c7a1b89a739153 \
249 | --hash=sha256:0e51f608da093e5d9038c592b5b575cadc12fd748af1479b5e858045fff955a9 \
250 | --hash=sha256:0f3269304c1a7ce82f1759c12ce731ef9b6e95b6df829dccd9fe42912cc48569 \
251 | --hash=sha256:16a8df99701f9095bea8a6c4b3197da105df6f74e6176c5b410bc2df2fd29a57 \
252 | --hash=sha256:19005a8e58b7c1796bc0167862b1f54a64d3b44ee5d48152b06bb861458bc0f8 \
253 | --hash=sha256:1b4b4e9dda4f4e4c4e6896f93e84a8f0bcca3b059de9ddf67dac3c334b1195e1 \
254 | --hash=sha256:28676836c7796805914b76b1837a40f76827ee0d5398f72f7dcc634bae7c6264 \
255 | --hash=sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157 \
256 | --hash=sha256:3f4cc516e0b264c8d4ccd6b6cbc69a07c6d582d8337df79be1e15a5056b258c9 \
257 | --hash=sha256:3fa1284762aacca6dc97474ee9c16f83990b8eeb6697f2ba17140d54b453e133 \
258 | --hash=sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9 \
259 | --hash=sha256:451f10ef963918e65b8869e17d67db5e2f4ab40e716ee6ce7129b0cde2876eab \
260 | --hash=sha256:46c259e87199041583658457372a183636ae8cd56dbf3f0755e0f376a7f9d0e6 \
261 | --hash=sha256:46f39cab8bbf4a384ba7cb0bc8bae7b7062b6a11cfac1ca4bc144dea90d4a9f5 \
262 | --hash=sha256:519e14e2c49fcf7616d6d2cfc5c70adae95682ae20f0395e9280db85e8d6c4df \
263 | --hash=sha256:53dcb50fbdc3fb2c55431a9b30caeb2f7027fcd2aeb501459464f0214200a503 \
264 | --hash=sha256:54614444887e0d3043557d9dbc697dbb16cfb5a35d672b7a0fcc1ed0cf1c600b \
265 | --hash=sha256:575d8912dca808edd9acd6f7795199332696d3469665ef26163cd090fa1f8bfa \
266 | --hash=sha256:5dd5a9c3091a0f414a963d427f920368e2b6a4c2f7527fdd82cde8ef0bc7a327 \
267 | --hash=sha256:5f532a2ad4d174eb73494e7397988e22bf427f91acc8e6ebf5bb10597b49c493 \
268 | --hash=sha256:60e7da3a3ad1812c128750fc1bc14a7ceeb8d29f77e0a2356a8fb2aa8925287d \
269 | --hash=sha256:653d7fb2df65efefbcbf81ef5fe5e5be931f1ee4332c2893ca638c9b11a409c4 \
270 | --hash=sha256:6663977496d616b618b6cfa43ec86e479ee62b942e1da76a2c3daa1c75933ef4 \
271 | --hash=sha256:6abfb51a82e919e3933eb137e17c4ae9c0475a25508ea88993bb59faf82f3b35 \
272 | --hash=sha256:6c6b1389ed66cdd174d040105123a5a1bc91d0aa7059c7261d20e583b6d8cbd2 \
273 | --hash=sha256:6d9dfb9959a3b0039ee06c1a1a90dc23bac3b430842dcb97908ddde05870601c \
274 | --hash=sha256:765cb54c0b8724a7c12c55146ae4647e0274a839fb6de7bcba841e04298e1011 \
275 | --hash=sha256:7a21222644ab69ddd9967cfe6f2bb420b460dae4289c9d40ff9a4896e7c35c9a \
276 | --hash=sha256:7ac7594397698f77bce84382929747130765f66406dc2cd8b4ab4da68ade4c6e \
277 | --hash=sha256:7cfc287da09f9d2a7ec146ee4d72d6ea1342e770d975e49a8621bf54eaa8f30f \
278 | --hash=sha256:83125753a60cfc8c412de5896d10a0a405e0bd88d0470ad82e0869ddf0cb3848 \
279 | --hash=sha256:847b114580c5cc9ebaf216dd8c8dbc6b00a3b7ab0131e173d7120e6deade1f57 \
280 | --hash=sha256:87708d78a14d56a990fbf4f9cb350b7d89ee8988705e58e39bdf4d82c149210f \
281 | --hash=sha256:8a2b5874d17e72dfb80d917213abd55d7e1ed2479f38f001f264f7ce7bae757c \
282 | --hash=sha256:8f127e7b028900421cad64f51f75c051b628db17fb00e099eb148761eed598c9 \
283 | --hash=sha256:94cdff45173b1919350601f82d61365e792895e3c3a3443cf99819e6fbf717a5 \
284 | --hash=sha256:99d92d148dd03fd19d16175b6d355cc1b01faf80dae93c6c3eb4163709edc0a9 \
285 | --hash=sha256:9a3049a10261d7f2b6514d35bbb7a4dfc3ece4c4de14ef5876c4b7a23a0e566d \
286 | --hash=sha256:9d9a62576b68cd90f7075876f4e8444487db5eeea0e4df3ba298ee38a8d067b0 \
287 | --hash=sha256:9e5f94742033898bfe84c93c831a6f552bb629448d4072dd312306bab3bd96f1 \
288 | --hash=sha256:a1c2d7780448eb93fbcc3789bf3916aa5720d942e37945f4056680317f1cd23e \
289 | --hash=sha256:a2e0f87144fcbbe54297cae708c5e7f9da21a4646523456b00cc956bd4c65815 \
290 | --hash=sha256:a4dfdae195335abb4e89cc9762b2edc524f3c6e80d647a9a81bf81e17e3fb6f0 \
291 | --hash=sha256:a96e6e23f2b79433390273eaf8cc94fec9c6370842e577ab10dabdcc7ea0a66b \
292 | --hash=sha256:aabdab8ec1e7ca7f1434d042bf8b1e92056245fb179790dc97ed040361f16bfd \
293 | --hash=sha256:b222090c455d6d1a64e6b7bb5f4035c4dff479e22455c9eaa1bdd4c75b52c80c \
294 | --hash=sha256:b52ff4f4e002f828ea6483faf4c4e8deea8d743cf801b74910243c58acc6eda3 \
295 | --hash=sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab \
296 | --hash=sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858 \
297 | --hash=sha256:b9b752ab91e78234941e44abdecc07f1f0d8f51fb62941d32995b8161f68cfe5 \
298 | --hash=sha256:ba6612b6548220ff5e9df85261bddc811a057b0b465a1226b39bfb8550616aee \
299 | --hash=sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343 \
300 | --hash=sha256:c3c4ed2ff6760e98d262e0cc9c9a7f7b8a9f61aa4d47c58835cdaf7b0b8811bb \
301 | --hash=sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47 \
302 | --hash=sha256:cb362e3b0976dc994857391b776ddaa8c13c28a16f80ac6522c23d5257156bed \
303 | --hash=sha256:d197df5489004db87d90b918033edbeee0bd6df3848a204bca3ff0a903bef837 \
304 | --hash=sha256:d3b56206244dc8711f7e8b7d6cad4663917cd5b2d950799425076681e8766286 \
305 | --hash=sha256:d5b2f8a31bd43e0f18172d8ac82347c8f37ef3e0b414431157718aa234991b28 \
306 | --hash=sha256:d7081c084ceb58278dd3cf81f836bc818978c0ccc770cbbb202125ddabec6628 \
307 | --hash=sha256:db74f5562c09953b2c5f8ec4b7dfd3f5421f31811e97d1dbc0a7c93d6e3a24df \
308 | --hash=sha256:df41112ccce5d47770a0c13651479fbcd8793f34232a2dd9faeccb75eb5d0d0d \
309 | --hash=sha256:e1339790c083c5a4de48f688b4841f18df839eb3c9584a770cbd818b33e26d5d \
310 | --hash=sha256:e621b0246192d3b9cb1dc62c78cfa4c6f6d2ddc0ec207d43c0dedecb914f152a \
311 | --hash=sha256:e8c5cf126889a4de385c02a2c3d3aba4b00f70234bfddae82a5eaa3ee6d5e3e6 \
312 | --hash=sha256:e9d7747847c53a16a729b6ee5e737cf170f7a16611c143d95aa60a109a59c336 \
313 | --hash=sha256:eaef5d2de3c7e9b21f1e762f289d17b726c2239a42b11e25446abf82b26ac132 \
314 | --hash=sha256:ed3e4b4e1e6de75fdc16d3259098de7c6571b1a6cc863b1a49e7d3d53e036070 \
315 | --hash=sha256:ef21af928e807f10bf4141cad4746eee692a0dd3ff56cfb25fce076ec3cc8abe \
316 | --hash=sha256:f09598b416ba39a8f489c124447b007fe865f786a89dbfa48bb5cf395693132a \
317 | --hash=sha256:f0caf4a5dcf610d96c3bd32932bfac8aee61c96e60481c2a0ea58da435e25acd \
318 | --hash=sha256:f6e78171be3fb7941f9910ea15b4b14ec27725865a73c15277bc39f5ca4f8391 \
319 | --hash=sha256:f715c32e774a60a337b2bb8ad9839b4abf75b267a0f18806f6f4f5f1688c4b5a \
320 | --hash=sha256:fb5c1ad6bad98c57482236a21bf985ab0ef42bd51f7ad4e4538e89a997624e12
321 | # via matplotlib
322 | pluggy==1.0.0 \
323 | --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \
324 | --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3
325 | # via pytest
326 | pyparsing==3.0.9 \
327 | --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \
328 | --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc
329 | # via matplotlib
330 | pytest==7.2.1 \
331 | --hash=sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5 \
332 | --hash=sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42
333 | # via
334 | # -r requirements.in
335 | # pytest-django
336 | pytest-django==4.5.2 \
337 | --hash=sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e \
338 | --hash=sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2
339 | # via -r requirements.in
340 | python-dateutil==2.8.2 \
341 | --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \
342 | --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9
343 | # via
344 | # faker
345 | # matplotlib
346 | six==1.16.0 \
347 | --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
348 | --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
349 | # via python-dateutil
350 | sqlparse==0.4.3 \
351 | --hash=sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34 \
352 | --hash=sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268
353 | # via django
354 | tqdm==4.64.1 \
355 | --hash=sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4 \
356 | --hash=sha256:6fee160d6ffcd1b1c68c65f14c829c22832bc401726335ce92c52d395944a6a1
357 | # via -r requirements.in
358 |
--------------------------------------------------------------------------------
/requirements/requirements.in:
--------------------------------------------------------------------------------
1 | django
2 | matplotlib
3 | pytest
4 | pytest-django
5 | faker
6 | tqdm
7 | weasyprint==52.5
8 | django-weasyprint==1.1.0.post2
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/terminalkitten/django-insights/054cbd8e50c1c4ce21166d875251631583f679b9/tests/__init__.py
--------------------------------------------------------------------------------
/tests/test_api.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from django.core import management
3 | from django.core.exceptions import ImproperlyConfigured
4 | from django.test import TestCase, override_settings
5 |
6 | from django_insights.database import check_settings
7 | from django_insights.models import Counter
8 | from django_insights.registry import registry
9 | from project.settings.test import DATABASES
10 | from project.testapp.users.models import AppUser
11 | from tests.utils import collect_insights
12 |
13 | COPY_DATABASES = DATABASES.copy()
14 | COPY_DATABASES.pop('insights')
15 |
16 |
17 | class ApiTests(TestCase):
18 | databases = ['default', 'insights']
19 |
20 | @classmethod
21 | def setUpClass(cls) -> None:
22 | super().setUpClass()
23 | management.call_command('seed_db')
24 | collect_insights()
25 |
26 | @override_settings(DATABASES=COPY_DATABASES)
27 | def test_throw_error_no_insights_db_configured(self):
28 | with pytest.raises(ImproperlyConfigured):
29 | check_settings()
30 |
31 | def test_autodiscover_registry(self):
32 | assert registry.registered_insights
33 |
34 | def test_one_app_user_found(self):
35 | assert AppUser.objects.count() == 1
36 |
37 | def test_count_authors(self):
38 | counter = Counter.objects.get(label="count_authors")
39 | assert counter.value == 2000
40 |
41 | def test_count_books(self):
42 | counter = Counter.objects.get(label="count_books")
43 | assert counter.value == 5000
44 |
--------------------------------------------------------------------------------
/tests/utils.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from django_insights.registry import registry
4 |
5 |
6 | @pytest.mark.django_db(True)
7 | def collect_insights() -> None:
8 | from django_insights.metrics import metrics
9 |
10 | registry.autodiscover_insights()
11 | metrics.collect()
12 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist =
3 | py38-django{32,40,41,42}
4 | py39-django{32,40,41,42}
5 | py310-django{32,40,41,42}
6 | py311-django{41,42}
7 |
8 | [testenv]
9 | commands =
10 | python \
11 | -W error::ResourceWarning \
12 | -W error::DeprecationWarning \
13 | -W error::PendingDeprecationWarning \
14 | -m pytest {posargs:tests}
15 | deps = -r requirements/{envname}.txt
16 | setenv =
17 | PYTHONDEVMODE=1
--------------------------------------------------------------------------------