From 711c6aff3e23e0507cd8cdfb7291cd2e0f9f9235 Mon Sep 17 00:00:00 2001 From: William Chu Date: Tue, 16 Jan 2024 11:12:50 +1100 Subject: [PATCH 01/10] chore: initial commit v2 --- django_pev/admin.py | 3 + django_pev/apps.py | 6 + django_pev/migrations/__init__.py | 0 django_pev/models.py | 3 + django_pev/templates/django_pev/base.html | 87 ++++ django_pev/templates/django_pev/index.html | 72 +++ .../templates/django_pev/indexes_view.html | 165 ++++++ .../templates/django_pev/maintenance.html | 49 ++ django_pev/templates/django_pev/space.html | 83 +++ django_pev/tests.py | 3 + django_pev/urls.py | 11 + django_pev/utils/__init__.py | 130 +++++ django_pev/utils/index_lookup.sql | 79 +++ django_pev/utils/indexes.py | 105 ++++ django_pev/utils/maintenance.py | 71 +++ django_pev/utils/space.py | 52 ++ django_pev/views.py | 52 ++ poetry.lock | 474 +++++++++--------- pyproject.toml | 1 + 19 files changed, 1207 insertions(+), 239 deletions(-) create mode 100644 django_pev/admin.py create mode 100644 django_pev/apps.py create mode 100644 django_pev/migrations/__init__.py create mode 100644 django_pev/models.py create mode 100644 django_pev/templates/django_pev/base.html create mode 100644 django_pev/templates/django_pev/index.html create mode 100644 django_pev/templates/django_pev/indexes_view.html create mode 100644 django_pev/templates/django_pev/maintenance.html create mode 100644 django_pev/templates/django_pev/space.html create mode 100644 django_pev/tests.py create mode 100755 django_pev/urls.py create mode 100644 django_pev/utils/__init__.py create mode 100644 django_pev/utils/index_lookup.sql create mode 100644 django_pev/utils/indexes.py create mode 100644 django_pev/utils/maintenance.py create mode 100644 django_pev/utils/space.py create mode 100644 django_pev/views.py diff --git a/django_pev/admin.py b/django_pev/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/django_pev/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/django_pev/apps.py b/django_pev/apps.py new file mode 100644 index 0000000..e67f559 --- /dev/null +++ b/django_pev/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DjangoPevConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'django_pev' diff --git a/django_pev/migrations/__init__.py b/django_pev/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_pev/models.py b/django_pev/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/django_pev/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/django_pev/templates/django_pev/base.html b/django_pev/templates/django_pev/base.html new file mode 100644 index 0000000..a4054f1 --- /dev/null +++ b/django_pev/templates/django_pev/base.html @@ -0,0 +1,87 @@ + +{% load static %} + + + + + + CoreUI Free Bootstrap Admin Template + + + + + + + + +
+ + Django PEV + +
+ +
+ + +
+
+ {% block content %} +
+
+ {% block inner_content %} + {% endblock %} +
+
+ {% endblock %} +
+ +
+ {% comment %} End app-body {% endcomment %} +
+ + + + \ No newline at end of file diff --git a/django_pev/templates/django_pev/index.html b/django_pev/templates/django_pev/index.html new file mode 100644 index 0000000..b8c5858 --- /dev/null +++ b/django_pev/templates/django_pev/index.html @@ -0,0 +1,72 @@ +{% extends "django_pev/base.html" %} +{% block content %} +
+
+
+
+
+

Traffic

+
January - July 2022
+
+ +
+
+ +
+
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/django_pev/templates/django_pev/indexes_view.html b/django_pev/templates/django_pev/indexes_view.html new file mode 100644 index 0000000..2eba302 --- /dev/null +++ b/django_pev/templates/django_pev/indexes_view.html @@ -0,0 +1,165 @@ +{% extends "django_pev/base.html" %} +{% block content %} +
+
+
+
+
+

Index Stats

+
+
+ +
+
+
+
Index Hit Rate
+
How often we are hitting index vs full table scan.
+
{{ total_index_hitrate |floatformat:2 }}%
+
+
+
+
+
+
+
Duplicated Indexes
+
{{ count_duplicated_indexes }} Indexes covered by other indexes
+
{{ total_duplicated_indexes_size | filesizeformat}} ({{percent_duplicated_indexes |floatformat:2}}%)
+
+
+
+
+
+
Unused Indexes
+
{{count_unused_indexes}} indexes not used enough to justify
+
{{total_unused_indexes_size |filesizeformat}} ({{percent_unused_indexes |floatformat:2}}%)
+
+
+
+
+
+
+ + + + + +
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/django_pev/templates/django_pev/maintenance.html b/django_pev/templates/django_pev/maintenance.html new file mode 100644 index 0000000..9f012ac --- /dev/null +++ b/django_pev/templates/django_pev/maintenance.html @@ -0,0 +1,49 @@ +{% extends "django_pev/base.html" %} +{% block content %} +
+
+
+
+
+

Maintenance Stats

+
+
+ + + + + + + + + + + + + + + + {% for row in tables %} + + + + + + + + + + + + {% endfor%} + +
NameSchemaLast VacuumLast Auto VacuumLast AnalyzeLast Auto AnalyzeDead RowsLive RowsIndex Hit Rate
{{ row.name }} + {% if row.requires_index %}Requires Index {% endif %} + {{ row.schema }}{{ row.last_vacuum | timesince }}{{ row.last_autovacuum | timesince }}{{ row.last_analyze | timesince }}{{ row.last_autoanalyze | timesince }}{{ row.dead_rows }}{{ row.live_rows }}{{ row.index_hit_rate_formatted }}
+ +
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/django_pev/templates/django_pev/space.html b/django_pev/templates/django_pev/space.html new file mode 100644 index 0000000..50be380 --- /dev/null +++ b/django_pev/templates/django_pev/space.html @@ -0,0 +1,83 @@ +{% extends "django_pev/base.html" %} +{% block content %} +
+
+
+
+
+

Space Stats

+
    +
  • + Total database size: {{ database_size | filesizeformat}} +
  • +
  • + Total table size: {{ table_size | filesizeformat}} +
  • +
  • + Total indexes size: {{ indexes_size | filesizeformat}} +
  • +
+
+
+ + + +
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/django_pev/tests.py b/django_pev/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/django_pev/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/django_pev/urls.py b/django_pev/urls.py new file mode 100755 index 0000000..9d5c519 --- /dev/null +++ b/django_pev/urls.py @@ -0,0 +1,11 @@ +from django.contrib import admin +from django.urls import path + +from . import views + +urlpatterns = [ + path('', views.index, name="index"), + path('space', views.SpaceView.as_view(), name="space"), + path('maintenance', views.MaintenanceView.as_view(), name="maintenance"), + path('indexes', views.IndexesView.as_view(), name="indexes") +] diff --git a/django_pev/utils/__init__.py b/django_pev/utils/__init__.py new file mode 100644 index 0000000..5724866 --- /dev/null +++ b/django_pev/utils/__init__.py @@ -0,0 +1,130 @@ +import functools +import logging +import traceback +import webbrowser +from contextlib import contextmanager +from dataclasses import dataclass, field + +import sqlparse # type:ignore[import] +from django.core.signals import request_started +from django.db import connections, reset_queries + +from django_pev.dalibo import PevResponse, upload_sql_plan +from django_pev.exceptions import PevException + +logger = logging.Logger("django_pev") + + +@dataclass(frozen=True) +class Explain: + index: int + duration: float + sql: str + stack_trace: str + db_alias: str + + def __repr__(self) -> str: + return f"Explain(duration={self.duration} sql={self.sql[:20]})" + + def __str__(self) -> str: + return f"Explain(duration={self.duration} sql={self.sql[:20]})" + + @functools.cache # noqa + def visualize(self, upload_query: bool = False, analyze: bool = True, title: str = "") -> PevResponse: + """Uploads the query and plan to explain.dalibo + + By default we do not embed the SQL query unless `upload_query` is set to True. + + The PevResponse object can be used to delete the uploaded plan. + """ + with connections[self.db_alias].cursor() as cursor: + if analyze: + sql = f"EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON) {self.sql}" + else: + sql = f"EXPLAIN (VERBOSE, FORMAT JSON) {self.sql}" + cursor.execute(sql) + plan = cursor.fetchone()[0] + response = upload_sql_plan(query=self.sql if upload_query else "", plan=plan, title=title) + logging.info(f"View Postgresql Explain @ {response.url}") + return response + + def visualize_in_browser(self, upload_query: bool = False, analyze: bool = True, title: str = "") -> PevResponse: + """Uploads the query and plan t oexplain.dalibo and then open in the browser""" + response = self.visualize(upload_query, analyze, title) + webbrowser.open(response.url) + return response + + def explain(self, analyze: bool = True) -> str: + with connections[self.db_alias].cursor() as cursor: + if analyze: + sql = f"EXPLAIN ANALYZE {self.sql}" + else: + sql = f"EXPLAIN {self.sql}" + cursor.execute(sql) + explain = cursor.fetchone()[0] + return explain + + +@dataclass +class ExplainSet: + queries: list[Explain] = field(default_factory=list) + + @property + def n_queries(self) -> int: + return len(self.queries) + + def __getitem__(self, index: int) -> Explain: + return self.queries[index] + + @property + def slowest(self) -> Explain: + if not self.queries: + raise PevException("Can not visualize results when there are no results.") + + return sorted(self.queries, key=lambda q: q.duration, reverse=True)[0] + + +@contextmanager +def explain( + db_alias: str = "default", + trace_limit: int = 10, +): + """Capture all queries within this context and returns an ExplainSet container. + + Captured queries contain timing information, a stack trace and allows for visualization + with explain.dalibo.com + + Usage: + >>> with explain() as queries: + >>> User.objects.count() + >>> result = queries.slowest.visualize(upload_query=True) + >>> result.delete() + >>> queries.slowest.explain() + """ + result = ExplainSet() + conn = connections[db_alias] + + force_debug_cursor = conn.force_debug_cursor + conn.force_debug_cursor = True + request_started.disconnect(reset_queries) + + logger.debug("Reseting queries") + reset_queries() + try: + yield result + finally: + conn.force_debug_cursor = force_debug_cursor + request_started.connect(reset_queries) + queries_after = conn.queries[:] + logger.debug(f"Captured {len(queries_after)} queries") + + for index, q in enumerate(queries_after): + result.queries.append( + Explain( + index=index, + duration=float(q["time"]), + sql=sqlparse.format(q["sql"], reindent=True, keyword_case="upper"), + stack_trace="".join(traceback.format_stack()[-trace_limit:-2]), + db_alias=db_alias, + ) + ) diff --git a/django_pev/utils/index_lookup.sql b/django_pev/utils/index_lookup.sql new file mode 100644 index 0000000..63b3cef --- /dev/null +++ b/django_pev/utils/index_lookup.sql @@ -0,0 +1,79 @@ +with index_columns as ( + select + n.nspname as schema, + t.relname as table, + i.relname as name, + array_agg(a.attname order by array_position(ix.indkey, a.attnum) ) as column_names + from + pg_catalog.pg_class t + join pg_catalog.pg_attribute a on t.oid = a.attrelid + join pg_catalog.pg_index ix on t.oid = ix.indrelid + join pg_catalog.pg_class i on a.attnum = any(ix.indkey) + and i.oid = ix.indexrelid + join pg_catalog.pg_namespace n on n.oid = t.relnamespace + where + t.relkind = 'r' + group by + 1,2,3 + order by + 1,2 +), +index_stats as ( + SELECT + ui.schemaname AS schema, + t.relname AS table, + ix.relname AS name, + regexp_replace(pg_get_indexdef(i.indexrelid), '.* USING ([^ ]*) \(.*', '\1') AS using, + indisunique AS unique, + indisprimary AS primary, + indisvalid AS valid, + indexprs::text, + indpred::text, + pg_get_indexdef(i.indexrelid) AS definition, + CASE WHEN pg_statio_user_indexes.idx_blks_hit + pg_statio_user_indexes.idx_blks_read = 0 THEN + 0 + ELSE + ROUND(1.0 * pg_statio_user_indexes.idx_blks_hit / (pg_statio_user_indexes.idx_blks_hit + pg_statio_user_indexes.idx_blks_read), 2) + END AS hit_rate, + idx_scan as index_scans, + pg_table_size(ix.oid) AS size_bytes + FROM + pg_index i + INNER JOIN + pg_class t ON t.oid = i.indrelid + INNER JOIN + pg_class ix ON ix.oid = i.indexrelid + LEFT JOIN + pg_stat_user_indexes ui ON ui.indexrelid = i.indexrelid + LEFT JOIN + pg_statio_user_indexes ON pg_statio_user_indexes.indexrelid = i.indexrelid + WHERE + ui.schemaname IS NOT NULL +) +select + index_columns.schema, + index_columns.table, + index_columns.name, + column_names, + index_stats.using, + index_stats.unique, + index_stats.primary, + index_stats.valid, + index_stats.indexprs, + index_stats.indpred, + index_stats.definition, + index_stats.hit_rate, + index_stats.index_scans, + index_stats.size_bytes, + null as covered_by +from + index_columns +join + index_stats on + ( index_columns.schema = index_stats.schema + and + index_columns.table = index_stats.table + and + index_columns.name = index_stats.name + ) + ; \ No newline at end of file diff --git a/django_pev/utils/indexes.py b/django_pev/utils/indexes.py new file mode 100644 index 0000000..e40686f --- /dev/null +++ b/django_pev/utils/indexes.py @@ -0,0 +1,105 @@ +import dataclasses +from decimal import Decimal +from itertools import groupby +from pathlib import Path + +from django.db import connection + + +@dataclasses.dataclass +class IndexInfo: + schema: str + table: str + name: str + columns: list[str] + using: str + unique: bool + primary: bool + valid: bool + indexprs: str | None + indpred: str | None + definition: str + hit_rate: Decimal + index_scans: int + size_bytes: int + covered_by: "IndexInfo | None" + + @property + def is_unused(self) -> bool: + return self.index_scans < 50 + + @property + def is_duplicated(self) -> bool: + return bool(self.covered_by) + + @property + def columns_formatted(self) -> str: + return ", ".join(self.columns) + + @property + def table_key(self) -> str: + return f"{self.schema}.{self.table}" + + def index_covers(self, columns: list[str]) -> bool: + """Returns true if this index covers the given columns""" + return self.columns[:len(columns)] == columns + +def get_indexes() -> list[IndexInfo]: + with connection.cursor() as cursor: + sql = (Path(__file__).parent / "index_lookup.sql").read_text() + cursor.execute(sql) + indexes = list(IndexInfo(*c) for c in cursor.fetchall()) + _update_indexes_with_duplicated_indexes(indexes) + return indexes + + +def get_total_index_hitrate() -> float: + """ Total hit rate with all indexes """ + with connection.cursor() as cursor: + cursor.execute(""" + SELECT + (sum(idx_blks_hit)) / nullif(sum(idx_blks_hit + idx_blks_read), 0) AS rate + FROM + pg_statio_user_indexes + """) + return cursor.fetchone()[0] + + +def _update_indexes_with_duplicated_indexes(indexes: list[IndexInfo] | None = None) -> None: + """Modifies a list of indexes a reference to the other index that covers it""" + indexes = indexes or get_indexes() + indexes_by_table = { + key: list(group) for key,group in groupby(indexes, lambda i: i.table_key) + } + for index in indexes: + if index.valid and not index.primary and not index.unique: + for other_index in indexes_by_table[index.table_key]: + if ( + other_index.valid \ + and other_index.name != index.name \ + and other_index.index_covers(index.columns) \ + and other_index.using == index.using \ + and other_index.indexprs == index.indexprs \ + and other_index.indpred == index.indpred + ): + index.covered_by = other_index + + +def get_index_stats(indexes: list[IndexInfo] | None = None) -> dict: + indexes = indexes or get_indexes() + + ret = { + "total_index_hitrate": get_total_index_hitrate() * 100, + "total_indexes_size": sum(i.size_bytes for i in indexes), + "total_duplicated_indexes_size": sum(i.size_bytes for i in indexes if i.is_duplicated), + "total_unused_indexes_size": sum(i.size_bytes for i in indexes if i.is_unused), + "count_indexes": len(indexes), + "count_duplicated_indexes": sum(1 for i in indexes if i.is_duplicated), + "count_unused_indexes": sum(1 for i in indexes if i.is_unused), + } + + ret["percent_duplicated_indexes"] = float(ret["total_duplicated_indexes_size"]) / ret["total_indexes_size"] * 100 # type:ignore + ret["percent_unused_indexes"] = float(ret["total_unused_indexes_size"]) / ret["total_indexes_size"] * 100 # type:ignore + + return ret + diff --git a/django_pev/utils/maintenance.py b/django_pev/utils/maintenance.py new file mode 100644 index 0000000..efbc9d4 --- /dev/null +++ b/django_pev/utils/maintenance.py @@ -0,0 +1,71 @@ +import dataclasses +from datetime import datetime + +from django.db import connection + + +def get_tables_missing_indexes() -> list[tuple[str, str, str, int]]: + with connection.cursor() as cursor: + sql = """ + SELECT + schemaname AS schema, + relname AS table, + FROM + pg_stat_user_tables + WHERE + idx_scan > 0 + AND (100 * idx_scan / (seq_scan + idx_scan)) < 95 + AND n_live_tup >= 10000 + ORDER BY + n_live_tup DESC, + relname ASC + """ + cursor.execute(sql) + return cursor.fetchall() + +@dataclasses.dataclass +class TableInfo: + schema: str + name: str + last_vacuum: datetime | None + last_autovacuum: datetime | None + last_analyze: datetime | None + last_autoanalyze: datetime | None + dead_rows: int + live_rows: int + idx_scan: int + seq_scan: int + + @property + def requires_index(self) -> bool: + return self.idx_scan > 0 and (100 * self.idx_scan / (self.seq_scan + self.idx_scan)) < 95 and self.live_rows >= 10000 + + @property + def index_hit_rate(self) -> float: + return self.idx_scan / (self.seq_scan + self.idx_scan) if self.idx_scan > 0 else 0 + + @property + def index_hit_rate_formatted(self) -> str: + return f"{self.index_hit_rate * 100:0.2f}%" if self.index_hit_rate else "" + +def get_maintenance_info() -> list[TableInfo]: + sql = """ + SELECT + schemaname AS schema, + relname AS name, + last_vacuum, + last_autovacuum, + last_analyze, + last_autoanalyze, + n_dead_tup AS dead_rows, + n_live_tup AS live_rows, + idx_scan, + seq_scan + FROM + pg_stat_user_tables + ORDER BY + 1, 2 + """ + with connection.cursor() as cursor: + cursor.execute(sql) + return [TableInfo(*row) for row in cursor.fetchall()] diff --git a/django_pev/utils/space.py b/django_pev/utils/space.py new file mode 100644 index 0000000..687b744 --- /dev/null +++ b/django_pev/utils/space.py @@ -0,0 +1,52 @@ +import dataclasses +from typing import Literal + +from django.db import connection + + +@dataclasses.dataclass +class RelationSpaceStat: + schema: str + name: str + type: Literal['table', 'index'] + size_bytes: int + row_estimate: int + +def get_database_size() -> int: + """Returns the size of the current database in bytes""" + with connection.cursor() as cursor: + cursor.execute(""" + SELECT pg_database_size(current_database()) as database_size + """) + return cursor.fetchone()[0] + + +def get_relation_sizes() -> list[RelationSpaceStat]: + with connection.cursor() as cursor: + cursor.execute(""" +SELECT + n.nspname AS schema, + c.relname AS name, + CASE WHEN c.relkind = 'r' THEN 'table' ELSE 'index' END AS type, + pg_table_size(c.oid) AS size_bytes, + (case WHEN reltuples < 0 THEN 0 else reltuples END)::int as row_estimate +FROM + pg_class c +LEFT JOIN + pg_namespace n ON n.oid = c.relnamespace +WHERE + n.nspname NOT IN ('pg_catalog', 'information_schema') + AND n.nspname !~ '^pg_toast' + AND c.relkind IN ('r', 'i') +ORDER BY + pg_table_size(c.oid) DESC, + 2 ASC +""") + return [RelationSpaceStat(*row) for row in cursor.fetchall()] + +def get_table_sizes() -> list[RelationSpaceStat]: + return [r for r in get_relation_sizes() if r.type == 'table'] + +def get_index_sizes() -> list[RelationSpaceStat]: + return [r for r in get_relation_sizes() if r.type == 'index'] + diff --git a/django_pev/views.py b/django_pev/views.py new file mode 100644 index 0000000..f0c5102 --- /dev/null +++ b/django_pev/views.py @@ -0,0 +1,52 @@ +from typing import Any + +from django.http import HttpResponse +from django.shortcuts import render +from django.views.generic import RedirectView, TemplateView + +from .utils import indexes, maintenance, space + +# Create your views here. + + +def index(request): + return render(request, "django_pev/index.html") + + +class SpaceView(TemplateView): + template_name = "django_pev/space.html" + + def get_context_data(self, **kwargs: Any) -> dict[str, Any]: + ctx = super().get_context_data(**kwargs) + + ctx['database_size'] = space.get_database_size() + ctx['tables'] = space.get_table_sizes() + # ctx['indexes'] = indexes.get_indexes() + ctx['indexes'] = space.get_index_sizes() + + ctx['indexes_size'] = sum(c.size_bytes for c in ctx['indexes']) + ctx['table_size'] = sum(c.size_bytes for c in ctx['tables']) + + return ctx + +class MaintenanceView(TemplateView): + template_name = "django_pev/maintenance.html" + + def get_context_data(self, **kwargs: Any) -> dict[str, Any]: + ctx = super().get_context_data(**kwargs) + + ctx['tables'] = maintenance.get_maintenance_info() + return ctx + +class IndexesView(TemplateView): + template_name = "django_pev/indexes_view.html" + + def get_context_data(self, **kwargs: Any) -> dict[str, Any]: + ctx = super().get_context_data(**kwargs) + all_indexes = indexes.get_indexes() + ctx['all_indexes'] = all_indexes + ctx['unused_indexes'] = sorted([i for i in all_indexes if i.is_unused], key=lambda i: i.size_bytes, reverse=True) + ctx['duplicated_indexes'] = sorted([i for i in all_indexes if i.is_duplicated], key=lambda i: i.size_bytes, reverse=True) + + ctx.update(indexes.get_index_stats()) + return ctx \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 50149b5..11c62cc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,15 @@ +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + [[package]] name = "asgiref" version = "3.5.2" description = "ASGI specs, helper code, and adapters" -category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"}, + {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"}, +] [package.dependencies] typing-extensions = {version = "*", markers = "python_version < \"3.8\""} @@ -16,9 +21,31 @@ tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] name = "black" version = "22.10.0" description = "The uncompromising code formatter." -category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"}, + {file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"}, + {file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"}, + {file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"}, + {file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"}, + {file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"}, + {file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"}, + {file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"}, + {file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"}, + {file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"}, + {file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"}, + {file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"}, + {file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"}, + {file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"}, + {file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"}, + {file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"}, + {file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"}, + {file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"}, + {file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"}, + {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"}, + {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"}, +] [package.dependencies] click = ">=8.0.0" @@ -39,9 +66,12 @@ uvloop = ["uvloop (>=0.15.2)"] name = "click" version = "8.1.3" description = "Composable command line interface toolkit" -category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} @@ -51,17 +81,23 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] [[package]] name = "django" version = "3.2.16" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." -category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "Django-3.2.16-py3-none-any.whl", hash = "sha256:18ba8efa36b69cfcd4b670d0fa187c6fe7506596f0ababe580e16909bcdec121"}, + {file = "Django-3.2.16.tar.gz", hash = "sha256:3adc285124244724a394fa9b9839cc8cd116faf7d159554c43ecdaa8cdf0b94d"}, +] [package.dependencies] asgiref = ">=3.3.2,<4" @@ -76,9 +112,12 @@ bcrypt = ["bcrypt"] name = "django-stubs" version = "1.13.0" description = "Mypy stubs for Django" -category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "django-stubs-1.13.0.tar.gz", hash = "sha256:424fdd1935f859a802365056f9ccf4db12d1d93a5ab3de6d5633dddba0c5fc76"}, + {file = "django_stubs-1.13.0-py3-none-any.whl", hash = "sha256:eaecc1fc71532c1148f0c9687556651d880165476d7629bf318ff86a903a150c"}, +] [package.dependencies] django = "*" @@ -96,9 +135,12 @@ compatible-mypy = ["mypy (>=0.980,<0.990)"] name = "django-stubs-ext" version = "0.7.0" description = "Monkey-patching and extensions for django-stubs" -category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "django-stubs-ext-0.7.0.tar.gz", hash = "sha256:4fd8cdbc68d1a421f21bb7e0d9e76d50f6a4b504d350ba786405daf536e90c21"}, + {file = "django_stubs_ext-0.7.0-py3-none-any.whl", hash = "sha256:d729fbc7fe8970a7e26b35956c35b48502516f011d523c0577bdfb02ed956284"}, +] [package.dependencies] django = "*" @@ -108,9 +150,12 @@ typing-extensions = "*" name = "fancycompleter" version = "0.9.1" description = "colorful TAB completion for Python prompt" -category = "dev" optional = false python-versions = "*" +files = [ + {file = "fancycompleter-0.9.1-py3-none-any.whl", hash = "sha256:dd076bca7d9d524cc7f25ec8f35ef95388ffef9ef46def4d3d25e9b044ad7080"}, + {file = "fancycompleter-0.9.1.tar.gz", hash = "sha256:09e0feb8ae242abdfd7ef2ba55069a46f011814a80fe5476be48f51b00247272"}, +] [package.dependencies] pyreadline = {version = "*", markers = "platform_system == \"Windows\""} @@ -120,9 +165,12 @@ pyrepl = ">=0.8.2" name = "flake8" version = "5.0.4" description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" optional = false python-versions = ">=3.6.1" +files = [ + {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, + {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, +] [package.dependencies] importlib-metadata = {version = ">=1.1.0,<4.3", markers = "python_version < \"3.8\""} @@ -134,9 +182,12 @@ pyflakes = ">=2.5.0,<2.6.0" name = "importlib-metadata" version = "4.2.0" description = "Read metadata from Python packages" -category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, + {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, +] [package.dependencies] typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} @@ -150,9 +201,12 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", name = "isort" version = "5.10.1" description = "A Python utility / library to sort Python imports." -category = "dev" optional = false python-versions = ">=3.6.1,<4.0" +files = [ + {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, + {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, +] [package.extras] colors = ["colorama (>=0.4.3,<0.5.0)"] @@ -164,17 +218,51 @@ requirements-deprecated-finder = ["pip-api", "pipreqs"] name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" -category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] [[package]] name = "mypy" version = "0.991" description = "Optional static typing for Python" -category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "mypy-0.991-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab"}, + {file = "mypy-0.991-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d"}, + {file = "mypy-0.991-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6"}, + {file = "mypy-0.991-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb"}, + {file = "mypy-0.991-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305"}, + {file = "mypy-0.991-cp310-cp310-win_amd64.whl", hash = "sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c"}, + {file = "mypy-0.991-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372"}, + {file = "mypy-0.991-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f"}, + {file = "mypy-0.991-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33"}, + {file = "mypy-0.991-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05"}, + {file = "mypy-0.991-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad"}, + {file = "mypy-0.991-cp311-cp311-win_amd64.whl", hash = "sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297"}, + {file = "mypy-0.991-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813"}, + {file = "mypy-0.991-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711"}, + {file = "mypy-0.991-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd"}, + {file = "mypy-0.991-cp37-cp37m-win_amd64.whl", hash = "sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef"}, + {file = "mypy-0.991-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a"}, + {file = "mypy-0.991-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93"}, + {file = "mypy-0.991-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf"}, + {file = "mypy-0.991-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135"}, + {file = "mypy-0.991-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70"}, + {file = "mypy-0.991-cp38-cp38-win_amd64.whl", hash = "sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243"}, + {file = "mypy-0.991-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d"}, + {file = "mypy-0.991-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5"}, + {file = "mypy-0.991-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3"}, + {file = "mypy-0.991-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648"}, + {file = "mypy-0.991-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476"}, + {file = "mypy-0.991-cp39-cp39-win_amd64.whl", hash = "sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461"}, + {file = "mypy-0.991-py3-none-any.whl", hash = "sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb"}, + {file = "mypy-0.991.tar.gz", hash = "sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06"}, +] [package.dependencies] mypy-extensions = ">=0.4.3" @@ -192,25 +280,34 @@ reports = ["lxml"] name = "mypy-extensions" version = "0.4.3" description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" optional = false python-versions = "*" +files = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] [[package]] name = "pathspec" version = "0.10.2" description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "pathspec-0.10.2-py3-none-any.whl", hash = "sha256:88c2606f2c1e818b978540f73ecc908e13999c6c3a383daf3705652ae79807a5"}, + {file = "pathspec-0.10.2.tar.gz", hash = "sha256:8f6bf73e5758fd365ef5d58ce09ac7c27d2833a8d7da51712eac6e27e35141b0"}, +] [[package]] name = "pdbpp" version = "0.10.3" description = "pdb++, a drop-in replacement for pdb" -category = "dev" optional = false python-versions = "*" +files = [ + {file = "pdbpp-0.10.3-py2.py3-none-any.whl", hash = "sha256:79580568e33eb3d6f6b462b1187f53e10cd8e4538f7d31495c9181e2cf9665d1"}, + {file = "pdbpp-0.10.3.tar.gz", hash = "sha256:d9e43f4fda388eeb365f2887f4e7b66ac09dce9b6236b76f63616530e2f669f5"}, +] [package.dependencies] fancycompleter = ">=0.8" @@ -225,9 +322,12 @@ testing = ["funcsigs", "pytest"] name = "platformdirs" version = "2.5.4" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "platformdirs-2.5.4-py3-none-any.whl", hash = "sha256:af0276409f9a02373d540bf8480021a048711d572745aef4b7842dad245eba10"}, + {file = "platformdirs-2.5.4.tar.gz", hash = "sha256:1006647646d80f16130f052404c6b901e80ee4ed6bef6792e1f238a8969106f7"}, +] [package.extras] docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"] @@ -237,33 +337,56 @@ test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock name = "psycopg2" version = "2.9.5" description = "psycopg2 - Python-PostgreSQL Database Adapter" -category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "psycopg2-2.9.5-cp310-cp310-win32.whl", hash = "sha256:d3ef67e630b0de0779c42912fe2cbae3805ebaba30cda27fea2a3de650a9414f"}, + {file = "psycopg2-2.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:4cb9936316d88bfab614666eb9e32995e794ed0f8f6b3b718666c22819c1d7ee"}, + {file = "psycopg2-2.9.5-cp311-cp311-win32.whl", hash = "sha256:093e3894d2d3c592ab0945d9eba9d139c139664dcf83a1c440b8a7aa9bb21955"}, + {file = "psycopg2-2.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:920bf418000dd17669d2904472efeab2b20546efd0548139618f8fa305d1d7ad"}, + {file = "psycopg2-2.9.5-cp36-cp36m-win32.whl", hash = "sha256:b9ac1b0d8ecc49e05e4e182694f418d27f3aedcfca854ebd6c05bb1cffa10d6d"}, + {file = "psycopg2-2.9.5-cp36-cp36m-win_amd64.whl", hash = "sha256:fc04dd5189b90d825509caa510f20d1d504761e78b8dfb95a0ede180f71d50e5"}, + {file = "psycopg2-2.9.5-cp37-cp37m-win32.whl", hash = "sha256:922cc5f0b98a5f2b1ff481f5551b95cd04580fd6f0c72d9b22e6c0145a4840e0"}, + {file = "psycopg2-2.9.5-cp37-cp37m-win_amd64.whl", hash = "sha256:1e5a38aa85bd660c53947bd28aeaafb6a97d70423606f1ccb044a03a1203fe4a"}, + {file = "psycopg2-2.9.5-cp38-cp38-win32.whl", hash = "sha256:f5b6320dbc3cf6cfb9f25308286f9f7ab464e65cfb105b64cc9c52831748ced2"}, + {file = "psycopg2-2.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:1a5c7d7d577e0eabfcf15eb87d1e19314c8c4f0e722a301f98e0e3a65e238b4e"}, + {file = "psycopg2-2.9.5-cp39-cp39-win32.whl", hash = "sha256:322fd5fca0b1113677089d4ebd5222c964b1760e361f151cbb2706c4912112c5"}, + {file = "psycopg2-2.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:190d51e8c1b25a47484e52a79638a8182451d6f6dff99f26ad9bd81e5359a0fa"}, + {file = "psycopg2-2.9.5.tar.gz", hash = "sha256:a5246d2e683a972e2187a8714b5c2cf8156c064629f9a9b1a873c1730d9e245a"}, +] [[package]] name = "pycodestyle" version = "2.9.1" description = "Python style guide checker" -category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, + {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, +] [[package]] name = "pyflakes" version = "2.5.0" description = "passive checker of Python programs" -category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, + {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, +] [[package]] name = "pygments" version = "2.13.0" description = "Pygments is a syntax highlighting package written in Python." -category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, + {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, +] [package.extras] plugins = ["importlib-metadata"] @@ -272,300 +395,173 @@ plugins = ["importlib-metadata"] name = "pyreadline" version = "2.1" description = "A python implmementation of GNU readline." -category = "dev" optional = false python-versions = "*" +files = [ + {file = "pyreadline-2.1.zip", hash = "sha256:4530592fc2e85b25b1a9f79664433da09237c1a270e4d78ea5aa3a2c7229e2d1"}, +] [[package]] name = "pyrepl" version = "0.9.0" description = "A library for building flexible command line interfaces" -category = "dev" optional = false python-versions = "*" +files = [ + {file = "pyrepl-0.9.0.tar.gz", hash = "sha256:292570f34b5502e871bbb966d639474f2b57fbfcd3373c2d6a2f3d56e681a775"}, +] [[package]] name = "pytz" version = "2022.6" description = "World timezone definitions, modern and historical" -category = "main" optional = false python-versions = "*" +files = [ + {file = "pytz-2022.6-py2.py3-none-any.whl", hash = "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427"}, + {file = "pytz-2022.6.tar.gz", hash = "sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2"}, +] + +[[package]] +name = "ruff" +version = "0.1.13" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.1.13-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e3fd36e0d48aeac672aa850045e784673449ce619afc12823ea7868fcc41d8ba"}, + {file = "ruff-0.1.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9fb6b3b86450d4ec6a6732f9f60c4406061b6851c4b29f944f8c9d91c3611c7a"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b13ba5d7156daaf3fd08b6b993360a96060500aca7e307d95ecbc5bb47a69296"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9ebb40442f7b531e136d334ef0851412410061e65d61ca8ce90d894a094feb22"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226b517f42d59a543d6383cfe03cccf0091e3e0ed1b856c6824be03d2a75d3b6"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5f0312ba1061e9b8c724e9a702d3c8621e3c6e6c2c9bd862550ab2951ac75c16"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2f59bcf5217c661254bd6bc42d65a6fd1a8b80c48763cb5c2293295babd945dd"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6894b00495e00c27b6ba61af1fc666f17de6140345e5ef27dd6e08fb987259d"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1600942485c6e66119da294c6294856b5c86fd6df591ce293e4a4cc8e72989"}, + {file = "ruff-0.1.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ee3febce7863e231a467f90e681d3d89210b900d49ce88723ce052c8761be8c7"}, + {file = "ruff-0.1.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dcaab50e278ff497ee4d1fe69b29ca0a9a47cd954bb17963628fa417933c6eb1"}, + {file = "ruff-0.1.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f57de973de4edef3ad3044d6a50c02ad9fc2dff0d88587f25f1a48e3f72edf5e"}, + {file = "ruff-0.1.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a36fa90eb12208272a858475ec43ac811ac37e91ef868759770b71bdabe27b6"}, + {file = "ruff-0.1.13-py3-none-win32.whl", hash = "sha256:a623349a505ff768dad6bd57087e2461be8db58305ebd5577bd0e98631f9ae69"}, + {file = "ruff-0.1.13-py3-none-win_amd64.whl", hash = "sha256:f988746e3c3982bea7f824c8fa318ce7f538c4dfefec99cd09c8770bd33e6539"}, + {file = "ruff-0.1.13-py3-none-win_arm64.whl", hash = "sha256:6bbbc3042075871ec17f28864808540a26f0f79a4478c357d3e3d2284e832998"}, + {file = "ruff-0.1.13.tar.gz", hash = "sha256:e261f1baed6291f434ffb1d5c6bd8051d1c2a26958072d38dfbec39b3dda7352"}, +] [[package]] name = "sqlparse" version = "0.4.3" description = "A non-validating SQL parser." -category = "main" optional = false python-versions = ">=3.5" +files = [ + {file = "sqlparse-0.4.3-py3-none-any.whl", hash = "sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34"}, + {file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"}, +] [[package]] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] [[package]] name = "typed-ast" version = "1.5.4" description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, + {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, + {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, + {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, + {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, + {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, + {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, + {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, + {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, + {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, +] [[package]] name = "types-pytz" version = "2022.6.0.1" description = "Typing stubs for pytz" -category = "dev" optional = false python-versions = "*" +files = [ + {file = "types-pytz-2022.6.0.1.tar.gz", hash = "sha256:d078196374d1277e9f9984d49373ea043cf2c64d5d5c491fbc86c258557bd46f"}, + {file = "types_pytz-2022.6.0.1-py3-none-any.whl", hash = "sha256:bea605ce5d5a5d52a8e1afd7656c9b42476e18a0f888de6be91587355313ddf4"}, +] [[package]] name = "types-pyyaml" version = "6.0.12.2" description = "Typing stubs for PyYAML" -category = "dev" optional = false python-versions = "*" +files = [ + {file = "types-PyYAML-6.0.12.2.tar.gz", hash = "sha256:6840819871c92deebe6a2067fb800c11b8a063632eb4e3e755914e7ab3604e83"}, + {file = "types_PyYAML-6.0.12.2-py3-none-any.whl", hash = "sha256:1e94e80aafee07a7e798addb2a320e32956a373f376655128ae20637adb2655b"}, +] [[package]] name = "typing-extensions" version = "4.4.0" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, + {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, +] [[package]] name = "wmctrl" version = "0.4" description = "A tool to programmatically control windows inside X" -category = "dev" optional = false python-versions = "*" +files = [ + {file = "wmctrl-0.4.tar.gz", hash = "sha256:66cbff72b0ca06a22ec3883ac3a4d7c41078bdae4fb7310f52951769b10e14e0"}, +] [[package]] name = "zipp" version = "3.10.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "zipp-3.10.0-py3-none-any.whl", hash = "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1"}, + {file = "zipp-3.10.0.tar.gz", hash = "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8"}, +] [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [metadata] -lock-version = "1.1" +lock-version = "2.0" python-versions = "^3.7" -content-hash = "cf759bfba2b66c5079d8f1ec46aed0d1f9755f683f03c93e4f5ea776573bc909" - -[metadata.files] -asgiref = [ - {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"}, - {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"}, -] -black = [ - {file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"}, - {file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"}, - {file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"}, - {file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"}, - {file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"}, - {file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"}, - {file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"}, - {file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"}, - {file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"}, - {file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"}, - {file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"}, - {file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"}, - {file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"}, - {file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"}, - {file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"}, - {file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"}, - {file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"}, - {file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"}, - {file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"}, - {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"}, - {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"}, -] -click = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, -] -colorama = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] -django = [ - {file = "Django-3.2.16-py3-none-any.whl", hash = "sha256:18ba8efa36b69cfcd4b670d0fa187c6fe7506596f0ababe580e16909bcdec121"}, - {file = "Django-3.2.16.tar.gz", hash = "sha256:3adc285124244724a394fa9b9839cc8cd116faf7d159554c43ecdaa8cdf0b94d"}, -] -django-stubs = [ - {file = "django-stubs-1.13.0.tar.gz", hash = "sha256:424fdd1935f859a802365056f9ccf4db12d1d93a5ab3de6d5633dddba0c5fc76"}, - {file = "django_stubs-1.13.0-py3-none-any.whl", hash = "sha256:eaecc1fc71532c1148f0c9687556651d880165476d7629bf318ff86a903a150c"}, -] -django-stubs-ext = [ - {file = "django-stubs-ext-0.7.0.tar.gz", hash = "sha256:4fd8cdbc68d1a421f21bb7e0d9e76d50f6a4b504d350ba786405daf536e90c21"}, - {file = "django_stubs_ext-0.7.0-py3-none-any.whl", hash = "sha256:d729fbc7fe8970a7e26b35956c35b48502516f011d523c0577bdfb02ed956284"}, -] -fancycompleter = [ - {file = "fancycompleter-0.9.1-py3-none-any.whl", hash = "sha256:dd076bca7d9d524cc7f25ec8f35ef95388ffef9ef46def4d3d25e9b044ad7080"}, - {file = "fancycompleter-0.9.1.tar.gz", hash = "sha256:09e0feb8ae242abdfd7ef2ba55069a46f011814a80fe5476be48f51b00247272"}, -] -flake8 = [ - {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, - {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, -] -importlib-metadata = [ - {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, - {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, -] -isort = [ - {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, - {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, -] -mccabe = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] -mypy = [ - {file = "mypy-0.991-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab"}, - {file = "mypy-0.991-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d"}, - {file = "mypy-0.991-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6"}, - {file = "mypy-0.991-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb"}, - {file = "mypy-0.991-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305"}, - {file = "mypy-0.991-cp310-cp310-win_amd64.whl", hash = "sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c"}, - {file = "mypy-0.991-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372"}, - {file = "mypy-0.991-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f"}, - {file = "mypy-0.991-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33"}, - {file = "mypy-0.991-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05"}, - {file = "mypy-0.991-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad"}, - {file = "mypy-0.991-cp311-cp311-win_amd64.whl", hash = "sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297"}, - {file = "mypy-0.991-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813"}, - {file = "mypy-0.991-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711"}, - {file = "mypy-0.991-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd"}, - {file = "mypy-0.991-cp37-cp37m-win_amd64.whl", hash = "sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef"}, - {file = "mypy-0.991-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a"}, - {file = "mypy-0.991-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93"}, - {file = "mypy-0.991-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf"}, - {file = "mypy-0.991-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135"}, - {file = "mypy-0.991-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70"}, - {file = "mypy-0.991-cp38-cp38-win_amd64.whl", hash = "sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243"}, - {file = "mypy-0.991-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d"}, - {file = "mypy-0.991-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5"}, - {file = "mypy-0.991-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3"}, - {file = "mypy-0.991-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648"}, - {file = "mypy-0.991-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476"}, - {file = "mypy-0.991-cp39-cp39-win_amd64.whl", hash = "sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461"}, - {file = "mypy-0.991-py3-none-any.whl", hash = "sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb"}, - {file = "mypy-0.991.tar.gz", hash = "sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06"}, -] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, -] -pathspec = [ - {file = "pathspec-0.10.2-py3-none-any.whl", hash = "sha256:88c2606f2c1e818b978540f73ecc908e13999c6c3a383daf3705652ae79807a5"}, - {file = "pathspec-0.10.2.tar.gz", hash = "sha256:8f6bf73e5758fd365ef5d58ce09ac7c27d2833a8d7da51712eac6e27e35141b0"}, -] -pdbpp = [ - {file = "pdbpp-0.10.3-py2.py3-none-any.whl", hash = "sha256:79580568e33eb3d6f6b462b1187f53e10cd8e4538f7d31495c9181e2cf9665d1"}, - {file = "pdbpp-0.10.3.tar.gz", hash = "sha256:d9e43f4fda388eeb365f2887f4e7b66ac09dce9b6236b76f63616530e2f669f5"}, -] -platformdirs = [ - {file = "platformdirs-2.5.4-py3-none-any.whl", hash = "sha256:af0276409f9a02373d540bf8480021a048711d572745aef4b7842dad245eba10"}, - {file = "platformdirs-2.5.4.tar.gz", hash = "sha256:1006647646d80f16130f052404c6b901e80ee4ed6bef6792e1f238a8969106f7"}, -] -psycopg2 = [ - {file = "psycopg2-2.9.5-cp310-cp310-win32.whl", hash = "sha256:d3ef67e630b0de0779c42912fe2cbae3805ebaba30cda27fea2a3de650a9414f"}, - {file = "psycopg2-2.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:4cb9936316d88bfab614666eb9e32995e794ed0f8f6b3b718666c22819c1d7ee"}, - {file = "psycopg2-2.9.5-cp36-cp36m-win32.whl", hash = "sha256:b9ac1b0d8ecc49e05e4e182694f418d27f3aedcfca854ebd6c05bb1cffa10d6d"}, - {file = "psycopg2-2.9.5-cp36-cp36m-win_amd64.whl", hash = "sha256:fc04dd5189b90d825509caa510f20d1d504761e78b8dfb95a0ede180f71d50e5"}, - {file = "psycopg2-2.9.5-cp37-cp37m-win32.whl", hash = "sha256:922cc5f0b98a5f2b1ff481f5551b95cd04580fd6f0c72d9b22e6c0145a4840e0"}, - {file = "psycopg2-2.9.5-cp37-cp37m-win_amd64.whl", hash = "sha256:1e5a38aa85bd660c53947bd28aeaafb6a97d70423606f1ccb044a03a1203fe4a"}, - {file = "psycopg2-2.9.5-cp38-cp38-win32.whl", hash = "sha256:f5b6320dbc3cf6cfb9f25308286f9f7ab464e65cfb105b64cc9c52831748ced2"}, - {file = "psycopg2-2.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:1a5c7d7d577e0eabfcf15eb87d1e19314c8c4f0e722a301f98e0e3a65e238b4e"}, - {file = "psycopg2-2.9.5-cp39-cp39-win32.whl", hash = "sha256:322fd5fca0b1113677089d4ebd5222c964b1760e361f151cbb2706c4912112c5"}, - {file = "psycopg2-2.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:190d51e8c1b25a47484e52a79638a8182451d6f6dff99f26ad9bd81e5359a0fa"}, - {file = "psycopg2-2.9.5.tar.gz", hash = "sha256:a5246d2e683a972e2187a8714b5c2cf8156c064629f9a9b1a873c1730d9e245a"}, -] -pycodestyle = [ - {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, - {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, -] -pyflakes = [ - {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, - {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, -] -pygments = [ - {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, - {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, -] -pyreadline = [ - {file = "pyreadline-2.1.zip", hash = "sha256:4530592fc2e85b25b1a9f79664433da09237c1a270e4d78ea5aa3a2c7229e2d1"}, -] -pyrepl = [ - {file = "pyrepl-0.9.0.tar.gz", hash = "sha256:292570f34b5502e871bbb966d639474f2b57fbfcd3373c2d6a2f3d56e681a775"}, -] -pytz = [ - {file = "pytz-2022.6-py2.py3-none-any.whl", hash = "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427"}, - {file = "pytz-2022.6.tar.gz", hash = "sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2"}, -] -sqlparse = [ - {file = "sqlparse-0.4.3-py3-none-any.whl", hash = "sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34"}, - {file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"}, -] -tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] -typed-ast = [ - {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, - {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, - {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, - {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, - {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, - {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, - {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, - {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, - {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, - {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, -] -types-pytz = [ - {file = "types-pytz-2022.6.0.1.tar.gz", hash = "sha256:d078196374d1277e9f9984d49373ea043cf2c64d5d5c491fbc86c258557bd46f"}, - {file = "types_pytz-2022.6.0.1-py3-none-any.whl", hash = "sha256:bea605ce5d5a5d52a8e1afd7656c9b42476e18a0f888de6be91587355313ddf4"}, -] -types-pyyaml = [ - {file = "types-PyYAML-6.0.12.2.tar.gz", hash = "sha256:6840819871c92deebe6a2067fb800c11b8a063632eb4e3e755914e7ab3604e83"}, - {file = "types_PyYAML-6.0.12.2-py3-none-any.whl", hash = "sha256:1e94e80aafee07a7e798addb2a320e32956a373f376655128ae20637adb2655b"}, -] -typing-extensions = [ - {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, - {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, -] -wmctrl = [ - {file = "wmctrl-0.4.tar.gz", hash = "sha256:66cbff72b0ca06a22ec3883ac3a4d7c41078bdae4fb7310f52951769b10e14e0"}, -] -zipp = [ - {file = "zipp-3.10.0-py3-none-any.whl", hash = "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1"}, - {file = "zipp-3.10.0.tar.gz", hash = "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8"}, -] +content-hash = "76c803c2daa00ae20e2b97deaee32e45baa6260ed095f262a817aa76a7c9930d" diff --git a/pyproject.toml b/pyproject.toml index f725ac4..1314ff7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ black = "^22.10.0" isort = "^5.10.1" flake8 = "^5.0.4" django-stubs = "^1.13.0" +ruff = "^0.1.13" [tool.nitpick] # Generated by the 'nitpick init' command From 9536745ca053e1e4e6363793adb4d2a9ef0e2d74 Mon Sep 17 00:00:00 2001 From: William Chu Date: Tue, 16 Jan 2024 11:17:14 +1100 Subject: [PATCH 02/10] chore: lint fixes --- django_pev/admin.py | 2 -- django_pev/apps.py | 4 ++-- django_pev/models.py | 2 -- django_pev/tests.py | 3 --- django_pev/urls.py | 1 - django_pev/utils/indexes.py | 38 ++++++++++++++++++--------------- django_pev/utils/maintenance.py | 10 +++++++-- django_pev/utils/space.py | 22 ++++++++++++------- django_pev/views.py | 29 ++++++++++++++----------- 9 files changed, 62 insertions(+), 49 deletions(-) diff --git a/django_pev/admin.py b/django_pev/admin.py index 8c38f3f..846f6b4 100644 --- a/django_pev/admin.py +++ b/django_pev/admin.py @@ -1,3 +1 @@ -from django.contrib import admin - # Register your models here. diff --git a/django_pev/apps.py b/django_pev/apps.py index e67f559..1312d0c 100644 --- a/django_pev/apps.py +++ b/django_pev/apps.py @@ -2,5 +2,5 @@ class DjangoPevConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'django_pev' + default_auto_field = "django.db.models.BigAutoField" + name = "django_pev" diff --git a/django_pev/models.py b/django_pev/models.py index 71a8362..6b20219 100644 --- a/django_pev/models.py +++ b/django_pev/models.py @@ -1,3 +1 @@ -from django.db import models - # Create your models here. diff --git a/django_pev/tests.py b/django_pev/tests.py index 7ce503c..e69de29 100644 --- a/django_pev/tests.py +++ b/django_pev/tests.py @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/django_pev/urls.py b/django_pev/urls.py index 9d5c519..b74e5db 100755 --- a/django_pev/urls.py +++ b/django_pev/urls.py @@ -1,4 +1,3 @@ -from django.contrib import admin from django.urls import path from . import views diff --git a/django_pev/utils/indexes.py b/django_pev/utils/indexes.py index e40686f..79a20a4 100644 --- a/django_pev/utils/indexes.py +++ b/django_pev/utils/indexes.py @@ -42,44 +42,45 @@ def table_key(self) -> str: def index_covers(self, columns: list[str]) -> bool: """Returns true if this index covers the given columns""" - return self.columns[:len(columns)] == columns + return self.columns[: len(columns)] == columns + def get_indexes() -> list[IndexInfo]: with connection.cursor() as cursor: sql = (Path(__file__).parent / "index_lookup.sql").read_text() cursor.execute(sql) - indexes = list(IndexInfo(*c) for c in cursor.fetchall()) + indexes = list(IndexInfo(*c) for c in cursor.fetchall()) _update_indexes_with_duplicated_indexes(indexes) return indexes def get_total_index_hitrate() -> float: - """ Total hit rate with all indexes """ + """Total hit rate with all indexes""" with connection.cursor() as cursor: - cursor.execute(""" + cursor.execute( + """ SELECT (sum(idx_blks_hit)) / nullif(sum(idx_blks_hit + idx_blks_read), 0) AS rate FROM pg_statio_user_indexes - """) + """ + ) return cursor.fetchone()[0] def _update_indexes_with_duplicated_indexes(indexes: list[IndexInfo] | None = None) -> None: """Modifies a list of indexes a reference to the other index that covers it""" indexes = indexes or get_indexes() - indexes_by_table = { - key: list(group) for key,group in groupby(indexes, lambda i: i.table_key) - } + indexes_by_table = {key: list(group) for key, group in groupby(indexes, lambda i: i.table_key)} for index in indexes: if index.valid and not index.primary and not index.unique: for other_index in indexes_by_table[index.table_key]: if ( - other_index.valid \ - and other_index.name != index.name \ - and other_index.index_covers(index.columns) \ - and other_index.using == index.using \ - and other_index.indexprs == index.indexprs \ + other_index.valid + and other_index.name != index.name + and other_index.index_covers(index.columns) + and other_index.using == index.using + and other_index.indexprs == index.indexprs and other_index.indpred == index.indpred ): index.covered_by = other_index @@ -91,15 +92,18 @@ def get_index_stats(indexes: list[IndexInfo] | None = None) -> dict: ret = { "total_index_hitrate": get_total_index_hitrate() * 100, "total_indexes_size": sum(i.size_bytes for i in indexes), - "total_duplicated_indexes_size": sum(i.size_bytes for i in indexes if i.is_duplicated), + "total_duplicated_indexes_size": sum(i.size_bytes for i in indexes if i.is_duplicated), "total_unused_indexes_size": sum(i.size_bytes for i in indexes if i.is_unused), "count_indexes": len(indexes), "count_duplicated_indexes": sum(1 for i in indexes if i.is_duplicated), "count_unused_indexes": sum(1 for i in indexes if i.is_unused), } - ret["percent_duplicated_indexes"] = float(ret["total_duplicated_indexes_size"]) / ret["total_indexes_size"] * 100 # type:ignore - ret["percent_unused_indexes"] = float(ret["total_unused_indexes_size"]) / ret["total_indexes_size"] * 100 # type:ignore + ret["percent_duplicated_indexes"] = ( + float(ret["total_duplicated_indexes_size"]) / ret["total_indexes_size"] * 100 + ) # type:ignore + ret["percent_unused_indexes"] = ( + float(ret["total_unused_indexes_size"]) / ret["total_indexes_size"] * 100 + ) # type:ignore return ret - diff --git a/django_pev/utils/maintenance.py b/django_pev/utils/maintenance.py index efbc9d4..65cdbf6 100644 --- a/django_pev/utils/maintenance.py +++ b/django_pev/utils/maintenance.py @@ -23,6 +23,7 @@ def get_tables_missing_indexes() -> list[tuple[str, str, str, int]]: cursor.execute(sql) return cursor.fetchall() + @dataclasses.dataclass class TableInfo: schema: str @@ -38,7 +39,11 @@ class TableInfo: @property def requires_index(self) -> bool: - return self.idx_scan > 0 and (100 * self.idx_scan / (self.seq_scan + self.idx_scan)) < 95 and self.live_rows >= 10000 + return ( + self.idx_scan > 0 + and (100 * self.idx_scan / (self.seq_scan + self.idx_scan)) < 95 + and self.live_rows >= 10000 + ) @property def index_hit_rate(self) -> float: @@ -46,7 +51,8 @@ def index_hit_rate(self) -> float: @property def index_hit_rate_formatted(self) -> str: - return f"{self.index_hit_rate * 100:0.2f}%" if self.index_hit_rate else "" + return f"{self.index_hit_rate * 100: 0.2f}%" if self.index_hit_rate else "" + def get_maintenance_info() -> list[TableInfo]: sql = """ diff --git a/django_pev/utils/space.py b/django_pev/utils/space.py index 687b744..d97b971 100644 --- a/django_pev/utils/space.py +++ b/django_pev/utils/space.py @@ -8,22 +8,26 @@ class RelationSpaceStat: schema: str name: str - type: Literal['table', 'index'] + type: Literal["table", "index"] size_bytes: int row_estimate: int + def get_database_size() -> int: """Returns the size of the current database in bytes""" with connection.cursor() as cursor: - cursor.execute(""" + cursor.execute( + """ SELECT pg_database_size(current_database()) as database_size - """) + """ + ) return cursor.fetchone()[0] def get_relation_sizes() -> list[RelationSpaceStat]: with connection.cursor() as cursor: - cursor.execute(""" + cursor.execute( + """ SELECT n.nspname AS schema, c.relname AS name, @@ -41,12 +45,14 @@ def get_relation_sizes() -> list[RelationSpaceStat]: ORDER BY pg_table_size(c.oid) DESC, 2 ASC -""") +""" + ) return [RelationSpaceStat(*row) for row in cursor.fetchall()] + def get_table_sizes() -> list[RelationSpaceStat]: - return [r for r in get_relation_sizes() if r.type == 'table'] + return [r for r in get_relation_sizes() if r.type == "table"] -def get_index_sizes() -> list[RelationSpaceStat]: - return [r for r in get_relation_sizes() if r.type == 'index'] +def get_index_sizes() -> list[RelationSpaceStat]: + return [r for r in get_relation_sizes() if r.type == "index"] diff --git a/django_pev/views.py b/django_pev/views.py index f0c5102..f4704c6 100644 --- a/django_pev/views.py +++ b/django_pev/views.py @@ -1,8 +1,7 @@ from typing import Any -from django.http import HttpResponse from django.shortcuts import render -from django.views.generic import RedirectView, TemplateView +from django.views.generic import TemplateView from .utils import indexes, maintenance, space @@ -19,34 +18,40 @@ class SpaceView(TemplateView): def get_context_data(self, **kwargs: Any) -> dict[str, Any]: ctx = super().get_context_data(**kwargs) - ctx['database_size'] = space.get_database_size() - ctx['tables'] = space.get_table_sizes() + ctx["database_size"] = space.get_database_size() + ctx["tables"] = space.get_table_sizes() # ctx['indexes'] = indexes.get_indexes() - ctx['indexes'] = space.get_index_sizes() + ctx["indexes"] = space.get_index_sizes() - ctx['indexes_size'] = sum(c.size_bytes for c in ctx['indexes']) - ctx['table_size'] = sum(c.size_bytes for c in ctx['tables']) + ctx["indexes_size"] = sum(c.size_bytes for c in ctx["indexes"]) + ctx["table_size"] = sum(c.size_bytes for c in ctx["tables"]) return ctx + class MaintenanceView(TemplateView): template_name = "django_pev/maintenance.html" def get_context_data(self, **kwargs: Any) -> dict[str, Any]: ctx = super().get_context_data(**kwargs) - ctx['tables'] = maintenance.get_maintenance_info() + ctx["tables"] = maintenance.get_maintenance_info() return ctx + class IndexesView(TemplateView): template_name = "django_pev/indexes_view.html" def get_context_data(self, **kwargs: Any) -> dict[str, Any]: ctx = super().get_context_data(**kwargs) all_indexes = indexes.get_indexes() - ctx['all_indexes'] = all_indexes - ctx['unused_indexes'] = sorted([i for i in all_indexes if i.is_unused], key=lambda i: i.size_bytes, reverse=True) - ctx['duplicated_indexes'] = sorted([i for i in all_indexes if i.is_duplicated], key=lambda i: i.size_bytes, reverse=True) + ctx["all_indexes"] = all_indexes + ctx["unused_indexes"] = sorted( + [i for i in all_indexes if i.is_unused], key=lambda i: i.size_bytes, reverse=True + ) + ctx["duplicated_indexes"] = sorted( + [i for i in all_indexes if i.is_duplicated], key=lambda i: i.size_bytes, reverse=True + ) ctx.update(indexes.get_index_stats()) - return ctx \ No newline at end of file + return ctx From 625ba2131ae7edcedac225598e1aaa04ed4c7867 Mon Sep 17 00:00:00 2001 From: William Chu Date: Tue, 16 Jan 2024 14:30:44 +1100 Subject: [PATCH 03/10] feat: add live-connections --- django_pev/templates/django_pev/base.html | 18 +++--- .../templates/django_pev/connections.html | 64 +++++++++++++++++++ django_pev/urls.py | 5 +- django_pev/utils/live_connections.py | 49 ++++++++++++++ django_pev/views.py | 20 ++++-- 5 files changed, 142 insertions(+), 14 deletions(-) create mode 100644 django_pev/templates/django_pev/connections.html create mode 100644 django_pev/utils/live_connections.py diff --git a/django_pev/templates/django_pev/base.html b/django_pev/templates/django_pev/base.html index a4054f1..c611a4b 100644 --- a/django_pev/templates/django_pev/base.html +++ b/django_pev/templates/django_pev/base.html @@ -5,7 +5,7 @@ - CoreUI Free Bootstrap Admin Template + Django Postgres Explain Visualiser Dashboard @@ -29,37 +29,37 @@

Django PEV

diff --git a/django_pev/templates/django_pev/connections.html b/django_pev/templates/django_pev/connections.html new file mode 100644 index 0000000..fcbbb51 --- /dev/null +++ b/django_pev/templates/django_pev/connections.html @@ -0,0 +1,64 @@ +{% extends "django_pev/base.html" %} +{% block content %} +
+
+
+
+
+

Live Connections

+
+
+ +
+
+
    +
  • + Total connections: {{ connections| length}} +
  • +
+
+
+ + + + + + + + + + + + + + + + + + {% for row in connections%} + + + + + + + + + + + + + {% endfor%} + +
PIDUserSourceIPOpen SinceQuery begin timeWait EventStateQuerySSL?
{{ row.pid }}{{ row.user }}{{ row.source }}{{ row.ip }}{{ row.backend_start | timesince }}{{ row.query_start | timesince }}{{ row.wait_event }}{{ row.start }}{{ row.query }} + {% if row.ssl %} + + {% endif%} +
+ +
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/django_pev/urls.py b/django_pev/urls.py index b74e5db..78077cb 100755 --- a/django_pev/urls.py +++ b/django_pev/urls.py @@ -6,5 +6,8 @@ path('', views.index, name="index"), path('space', views.SpaceView.as_view(), name="space"), path('maintenance', views.MaintenanceView.as_view(), name="maintenance"), - path('indexes', views.IndexesView.as_view(), name="indexes") + path('connections', views.ConnectionsView.as_view(), name="connections"), + path('indexes', views.IndexesView.as_view(), name="indexes"), + path('queries', views.IndexesView.as_view(), name="queries"), + path('live-queries', views.IndexesView.as_view(), name="live-queries"), ] diff --git a/django_pev/utils/live_connections.py b/django_pev/utils/live_connections.py new file mode 100644 index 0000000..c23d8f3 --- /dev/null +++ b/django_pev/utils/live_connections.py @@ -0,0 +1,49 @@ +import dataclasses +from datetime import datetime + +from django.db import connection + + +@dataclasses.dataclass +class ConnectionInfo: + pid: int + database: str + user: str + source: str + ip: str + backend_start: datetime + query_start: datetime | None + wait_event: str + state: str + query: str + ssl: bool + + +def get_connections_current_database() -> list[ConnectionInfo]: + """Returns list of postgres connections""" + sql = """ +SELECT + pg_stat_activity.pid, + datname AS database, + usename AS user, + application_name AS source, + client_addr AS ip, + backend_start, + query_start, + wait_event, + state, + query, + ssl +FROM + pg_stat_activity +LEFT JOIN + pg_stat_ssl ON pg_stat_activity.pid = pg_stat_ssl.pid +WHERE + datname = %s +ORDER BY + pg_stat_activity.pid + """ + with connection.cursor() as cursor: + database_name = connection.get_connection_params()['database'] + cursor.execute(sql, [database_name]) + return list(ConnectionInfo(*c) for c in cursor.fetchall()) \ No newline at end of file diff --git a/django_pev/views.py b/django_pev/views.py index f4704c6..c137c0f 100644 --- a/django_pev/views.py +++ b/django_pev/views.py @@ -3,7 +3,7 @@ from django.shortcuts import render from django.views.generic import TemplateView -from .utils import indexes, maintenance, space +from .utils import indexes, live_connections, maintenance, space # Create your views here. @@ -12,7 +12,11 @@ def index(request): return render(request, "django_pev/index.html") -class SpaceView(TemplateView): +class BaseView(TemplateView): + template_name = "django_pev/indexes_view.html" + pass + +class SpaceView(BaseView): template_name = "django_pev/space.html" def get_context_data(self, **kwargs: Any) -> dict[str, Any]: @@ -29,7 +33,7 @@ def get_context_data(self, **kwargs: Any) -> dict[str, Any]: return ctx -class MaintenanceView(TemplateView): +class MaintenanceView(BaseView): template_name = "django_pev/maintenance.html" def get_context_data(self, **kwargs: Any) -> dict[str, Any]: @@ -39,7 +43,7 @@ def get_context_data(self, **kwargs: Any) -> dict[str, Any]: return ctx -class IndexesView(TemplateView): +class IndexesView(BaseView): template_name = "django_pev/indexes_view.html" def get_context_data(self, **kwargs: Any) -> dict[str, Any]: @@ -55,3 +59,11 @@ def get_context_data(self, **kwargs: Any) -> dict[str, Any]: ctx.update(indexes.get_index_stats()) return ctx + +class ConnectionsView(BaseView): + template_name = "django_pev/connections.html" + + def get_context_data(self, **kwargs: Any) -> dict[str, Any]: + ctx = super().get_context_data(**kwargs) + ctx['connections'] = live_connections.get_connections_current_database() + return ctx From e392dfdd5962351908eebbda6a0b85a0b32f6071 Mon Sep 17 00:00:00 2001 From: William Chu Date: Tue, 16 Jan 2024 17:58:52 +1100 Subject: [PATCH 04/10] feat: add queries views --- django_pev/templates/django_pev/base.html | 11 +- .../templates/django_pev/live_queries.html | 66 ++++++ django_pev/templates/django_pev/queries.html | 164 ++++++++++++++ django_pev/urls.py | 6 +- django_pev/utils.py | 24 +- django_pev/utils/queries.py | 208 ++++++++++++++++++ django_pev/views.py | 21 +- 7 files changed, 486 insertions(+), 14 deletions(-) create mode 100644 django_pev/templates/django_pev/live_queries.html create mode 100644 django_pev/templates/django_pev/queries.html create mode 100644 django_pev/utils/queries.py diff --git a/django_pev/templates/django_pev/base.html b/django_pev/templates/django_pev/base.html index c611a4b..a39231b 100644 --- a/django_pev/templates/django_pev/base.html +++ b/django_pev/templates/django_pev/base.html @@ -29,12 +29,7 @@

Django PEV

diff --git a/django_pev/views.py b/django_pev/views.py index 1717a4a..4a91c82 100644 --- a/django_pev/views.py +++ b/django_pev/views.py @@ -1,5 +1,6 @@ from typing import Any +from django.contrib.auth.mixins import UserPassesTestMixin from django.shortcuts import render from django.views.generic import TemplateView @@ -12,9 +13,10 @@ def index(request): return render(request, "django_pev/index.html") -class BaseView(TemplateView): +class BaseView(UserPassesTestMixin, TemplateView): template_name = "django_pev/indexes_view.html" - pass + def test_func(self): + return self.request.user.is_superuser class SpaceView(BaseView): @@ -25,7 +27,6 @@ def get_context_data(self, **kwargs: Any) -> dict[str, Any]: ctx["database_size"] = space.get_database_size() ctx["tables"] = space.get_table_sizes() - # ctx['indexes'] = indexes.get_indexes() ctx["indexes"] = space.get_index_sizes() ctx["indexes_size"] = sum(c.size_bytes for c in ctx["indexes"]) From f922a09f5bdbed2a9281c462339a38fd93c4b1c1 Mon Sep 17 00:00:00 2001 From: William Chu Date: Wed, 17 Jan 2024 08:29:04 +1100 Subject: [PATCH 07/10] chore: add queries to nav --- django_pev/templates/django_pev/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_pev/templates/django_pev/base.html b/django_pev/templates/django_pev/base.html index 356fa5b..6dd8f40 100644 --- a/django_pev/templates/django_pev/base.html +++ b/django_pev/templates/django_pev/base.html @@ -28,7 +28,7 @@

Django PEV