working on adding linting and isort on CI

This commit is contained in:
David Burke committed 2021-10-28 20:22:09 -04:00
1 parent 4cbd8f86d8
commit 055e638728
39 files changed
+255 -110

No files matched your search

+9
View File
@@ -31,6 +31,15 @@ test:
- poetry install --no-interaction --no-ansi
- ./manage.py test
lint:
image: python:3.9-slim
script:
- apt-get update && apt-get install -y gcc
- pip install poetry
- poetry install --no-interaction --no-ansi
- isort --check glitchtip
- pylint --load-plugins=pylint_django --django-settings-module=glitchtip.settings --disable=R glitchtip
build:
image: docker:20
rules:
+4
View File
@@ -1,3 +1,7 @@
[MASTER]
disable=
C0114, # missing-module-docstring
missing-class-docstring,
missing-function-docstring,
abstract-method,
unused-argument
+3 -1
View File
@@ -2,10 +2,12 @@
"python.formatting.provider": "black",
"python.linting.pylintArgs": [
"--enable=W0614",
"--django-settings-module=glitchtip.settings",
"--load-plugins",
"pylint_django"
],
"python.linting.enabled": true,
"python.linting.pylintEnabled": true,
"editor.formatOnSave": true
"editor.formatOnSave": true,
"files.trimTrailingWhitespace": true
}
+2 -1
View File
@@ -1,6 +1,7 @@
from django.utils.translation import gettext_lazy as _
from rest_framework.authentication import TokenAuthentication
from rest_framework import exceptions
from rest_framework.authentication import TokenAuthentication
from api_tokens.models import APIToken
+1
View File
@@ -19,4 +19,5 @@ app.autodiscover_tasks()
@app.task(bind=True)
def debug_task(self):
# pylint: disable=consider-using-f-string
print("Request: {0!r}".format(self.request))
+2 -3
View File
@@ -1,5 +1,6 @@
# Credit https://gist.github.com/pardo/19d672235bbef6fa793a
import functools
from django.core.cache import cache
@@ -32,9 +33,7 @@ def debounced_task(key_generator):
def wrapper(**kwargs):
func_args = kwargs.get("args", [])
func_kwargs = kwargs.get("kwargs", {})
key = "{}.{}.{}".format(
func.__module__, func.__name__, key_generator(*func_args, **func_kwargs)
)
key = f"{func.__module__}.{func.__name__}.{key_generator(*func_args, **func_kwargs)}"
cache.add(key, 0)
call_count = cache.incr(key)
func_kwargs.update({"key": key, "call_count": call_count})
+1 -1
View File
@@ -1,9 +1,9 @@
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.views.generic.base import ContextMixin
from django.views.generic.detail import SingleObjectMixin
from django.template.loader import render_to_string
User = get_user_model()
-1
View File
@@ -6,4 +6,3 @@ class ConflictException(exceptions.APIException):
status_code = status.HTTP_409_CONFLICT
default_detail = _("Already present!")
default_code = "already_present"
+1
View File
@@ -1,5 +1,6 @@
import urllib.parse as urlparse
from urllib.parse import parse_qs
from rest_framework.pagination import CursorPagination
from rest_framework.response import Response
+3 -1
View File
@@ -8,6 +8,8 @@ class ScopedPermission(BasePermission):
Fall back to checking for user authentication
"""
scope_map = {}
def get_allowed_scopes(self, request, view):
try:
return self.scope_map[request.method]
@@ -22,7 +24,7 @@ class ScopedPermission(BasePermission):
return bool(request.user and request.user.is_authenticated)
def get_user_scopes(self, obj, user):
pass
return set()
def has_object_permission(self, request, view, obj):
allowed_scopes = self.get_allowed_scopes(request, view)
+1
View File
@@ -1,4 +1,5 @@
from datetime import datetime
from django.utils.timezone import make_aware
from rest_framework import serializers
+4 -3
View File
@@ -14,12 +14,13 @@ import logging
import os
import sys
import warnings
from datetime import timedelta
import environ
import sentry_sdk
from datetime import timedelta
from celery.schedules import crontab
from django.core.exceptions import ImproperlyConfigured
from sentry_sdk.integrations.django import DjangoIntegration
from celery.schedules import crontab
env = environ.Env(
ALLOWED_HOSTS=(list, ["*"]),
@@ -46,7 +47,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env("SECRET_KEY")
SECRET_KEY = env.str("SECRET_KEY", "change_me")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env("DEBUG")
+11 -12
View File
@@ -1,20 +1,19 @@
from django.conf import settings
from django.contrib.auth import get_backends
from rest_framework import serializers
from rest_framework.response import Response
from dj_rest_auth.registration.views import SocialConnectView, SocialLoginView
from dj_rest_auth.registration.serializers import (
SocialLoginSerializer as BaseSocialLoginSerializer,
)
from django_rest_mfa.helpers import has_mfa
from allauth.account.adapter import DefaultAccountAdapter
from allauth.account.auth_backends import AuthenticationBackend
from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter
from allauth.socialaccount.providers.gitlab.views import GitLabOAuth2Adapter
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
from allauth.socialaccount.providers.microsoft.views import MicrosoftGraphOAuth2Adapter
from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
from dj_rest_auth.registration.serializers import (
SocialLoginSerializer as BaseSocialLoginSerializer,
)
from dj_rest_auth.registration.views import SocialConnectView, SocialLoginView
from django.conf import settings
from django.contrib.auth import get_backends
from django_rest_mfa.helpers import has_mfa
from rest_framework import serializers
from rest_framework.response import Response
DOMAIN = settings.GLITCHTIP_URL.geturl()
@@ -29,7 +28,7 @@ class MFAAccountAdapter(DefaultAccountAdapter):
if not hasattr(user, "backend"):
backends = get_backends()
backend = None
for b in backends:
for b in backends: # pylint: disable=invalid-name
if isinstance(b, AuthenticationBackend):
# prefer our own backend
backend = b
+1 -1
View File
@@ -1,5 +1,5 @@
from model_bakery import baker
from model_bakery.random_gen import gen_slug, gen_datetime, gen_integer, gen_related
from model_bakery.random_gen import gen_datetime, gen_integer, gen_related, gen_slug
def currency_code():
+3 -1
View File
@@ -1,5 +1,7 @@
from rest_framework.test import APITestCase
# pylint: disable=attribute-defined-outside-init,invalid-name
from model_bakery import baker
from rest_framework.test import APITestCase
from organizations_ext.models import OrganizationUserRole
+1 -1
View File
@@ -1,5 +1,5 @@
from django.test import TestCase
from django.shortcuts import reverse
from django.test import TestCase
class DocsTestCase(TestCase):
+3 -1
View File
@@ -3,13 +3,15 @@ from django.contrib import admin
from django.forms.models import BaseInlineFormSet
from django.urls import reverse
from django.utils import timezone
from .models import Monitor, MonitorCheck
class MonitorCheckInlineFormSet(BaseInlineFormSet):
def get_queryset(self):
if not hasattr(self, "_queryset"):
self._queryset = super().get_queryset()[:50]
# pylint: disable=attribute-defined-outside-init
self._queryset = super().get_queryset()[:50] # noqa
return self._queryset
-1
View File
@@ -17,4 +17,3 @@ class MonitorCheckReason(models.IntegerChoices):
BODY = 3, _("Expected response not found")
SSL = 4, _("SSL error")
NETWORK = 5, _("Network error")
+4 -4
View File
@@ -2,10 +2,11 @@ from django.contrib.auth import get_user_model
from django.db.models import Q
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from users.models import ProjectAlertStatus, User
from glitchtip.email import DetailEmail
from .models import MonitorCheck
from glitchtip.email import DetailEmail
from users.models import ProjectAlertStatus, User
from .models import MonitorCheck
User = get_user_model()
@@ -39,4 +40,3 @@ class MonitorEmail(DetailEmail):
)
| Q(subscribe_by_default=False, userprojectalert=None),
)
+113 -34
View File
@@ -1,8 +1,9 @@
# Generated by Django 3.2.5 on 2021-08-04 17:27
import datetime
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
@@ -10,50 +11,128 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('organizations_ext', '0001_squashed_0009_organization_scrub_ip_addresses'),
('environments', '0002_auto_20201229_1643'),
('projects', '0008_alter_projectkey_created'),
("organizations_ext", "0001_squashed_0009_organization_scrub_ip_addresses"),
("environments", "0002_auto_20201229_1643"),
("projects", "0008_alter_projectkey_created"),
]
operations = [
migrations.CreateModel(
name='Monitor',
name="Monitor",
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, db_index=True)),
('monitor_type', models.CharField(choices=[('ping', 'Ping'), ('get', 'GET'), ('post', 'POST'), ('ssl', 'SSL'), ('heartbeat', 'Heartbeat')], default='ping', max_length=12)),
('name', models.CharField(max_length=200)),
('url', models.URLField(blank=True)),
('expected_status', models.PositiveSmallIntegerField(default=200)),
('expected_body', models.CharField(blank=True, max_length=2000)),
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
('interval', models.DurationField(default=datetime.timedelta(seconds=60))),
('environment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='environments.environment')),
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='organizations_ext.organization')),
('project', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='projects.project')),
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(auto_now_add=True, db_index=True)),
(
"monitor_type",
models.CharField(
choices=[
("ping", "Ping"),
("get", "GET"),
("post", "POST"),
("ssl", "SSL"),
("heartbeat", "Heartbeat"),
],
default="ping",
max_length=12,
),
),
("name", models.CharField(max_length=200)),
("url", models.URLField(blank=True)),
("expected_status", models.PositiveSmallIntegerField(default=200)),
("expected_body", models.CharField(blank=True, max_length=2000)),
("ip_address", models.GenericIPAddressField(blank=True, null=True)),
(
"interval",
models.DurationField(default=datetime.timedelta(seconds=60)),
),
(
"environment",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="environments.environment",
),
),
(
"organization",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="organizations_ext.organization",
),
),
(
"project",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="projects.project",
),
),
],
options={
'abstract': False,
},
options={"abstract": False,},
),
migrations.CreateModel(
name='MonitorCheck',
name="MonitorCheck",
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, db_index=True)),
('is_up', models.BooleanField()),
('start_check', models.DateTimeField(help_text='Time when the start of this check was performed')),
('reason', models.PositiveSmallIntegerField(blank=True, choices=[(0, 'Unknown'), (1, 'Timeout'), (2, 'Wrong status code'), (3, 'Expected response not found'), (4, 'SSL Error')], default=0, null=True)),
('response_time', models.DurationField(blank=True, null=True)),
('data', models.JSONField(blank=True, null=True)),
('monitor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checks', to='uptime.monitor')),
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(auto_now_add=True, db_index=True)),
("is_up", models.BooleanField()),
(
"start_check",
models.DateTimeField(
help_text="Time when the start of this check was performed"
),
),
(
"reason",
models.PositiveSmallIntegerField(
blank=True,
choices=[
(0, "Unknown"),
(1, "Timeout"),
(2, "Wrong status code"),
(3, "Expected response not found"),
(4, "SSL Error"),
],
default=0,
null=True,
),
),
("response_time", models.DurationField(blank=True, null=True)),
("data", models.JSONField(blank=True, null=True)),
(
"monitor",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="checks",
to="uptime.monitor",
),
),
],
options={
'ordering': ('-created',),
},
options={"ordering": ("-created",),},
),
migrations.AddIndex(
model_name='monitorcheck',
index=models.Index(fields=['start_check', 'monitor'], name='uptime_moni_start_c_6434f7_idx'),
model_name="monitorcheck",
index=models.Index(
fields=["start_check", "monitor"], name="uptime_moni_start_c_6434f7_idx"
),
),
]
@@ -6,18 +6,35 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('uptime', '0001_initial'),
("uptime", "0001_initial"),
]
operations = [
migrations.AddField(
model_name='monitor',
name='endpoint_id',
field=models.UUIDField(blank=True, editable=False, help_text='Used for referencing heartbeat endpoint', null=True),
model_name="monitor",
name="endpoint_id",
field=models.UUIDField(
blank=True,
editable=False,
help_text="Used for referencing heartbeat endpoint",
null=True,
),
),
migrations.AlterField(
model_name='monitorcheck',
name='reason',
field=models.PositiveSmallIntegerField(blank=True, choices=[(0, 'Unknown'), (1, 'Timeout'), (2, 'Wrong status code'), (3, 'Expected response not found'), (4, 'SSL error'), (5, 'Network error')], default=0, null=True),
model_name="monitorcheck",
name="reason",
field=models.PositiveSmallIntegerField(
blank=True,
choices=[
(0, "Unknown"),
(1, "Timeout"),
(2, "Wrong status code"),
(3, "Expected response not found"),
(4, "SSL error"),
(5, "Network error"),
],
default=0,
null=True,
),
),
]
+6 -2
View File
@@ -1,9 +1,12 @@
import uuid
from datetime import timedelta
from django.db import models
from django.db.models import Subquery, OuterRef
from django.db.models import OuterRef, Subquery
from glitchtip.base_models import CreatedModel
from .constants import MonitorType, MonitorCheckReason
from .constants import MonitorCheckReason, MonitorType
class MonitorManager(models.Manager):
@@ -64,6 +67,7 @@ class Monitor(CreatedModel):
if self.monitor_type == MonitorType.HEARTBEAT and not self.endpoint_id:
self.endpoint_id = uuid.uuid4()
super().save(*args, **kwargs)
# pylint: disable=import-outside-toplevel
from glitchtip.uptime.tasks import perform_checks
if self.monitor_type != MonitorType.HEARTBEAT:
+1
View File
@@ -1,5 +1,6 @@
from django.utils import timezone
from rest_framework import serializers
from .models import Monitor, MonitorCheck
+7 -5
View File
@@ -1,16 +1,17 @@
import asyncio
from datetime import timedelta
from typing import List
from django.db import transaction
from django.db.models import F, Q, ExpressionWrapper, DateTimeField, Subquery, OuterRef
from celery import shared_task
from django.conf import settings
from django.db import transaction
from django.db.models import DateTimeField, ExpressionWrapper, F, OuterRef, Q, Subquery
from django.utils import timezone
from django.utils.dateparse import parse_datetime
from celery import shared_task
from .email import MonitorEmail
from .models import Monitor, MonitorCheck
from .utils import fetch_all
from .email import MonitorEmail
@shared_task
@@ -103,4 +104,5 @@ def cleanup_old_monitor_checks():
""" Delete older checks and associated data """
days = settings.GLITCHTIP_MAX_EVENT_LIFE_DAYS
qs = MonitorCheck.objects.filter(created__lt=timezone.now() - timedelta(days=days))
qs._raw_delete(qs.db)
# pylint: disable=protected-access
qs._raw_delete(qs.db) # noqa
+1
View File
@@ -1,5 +1,6 @@
from django.shortcuts import reverse
from model_bakery import baker
from glitchtip.test_utils.test_case import GlitchTipTestCase
+5 -2
View File
@@ -1,17 +1,20 @@
from datetime import timedelta
from unittest import mock
from django.conf import settings
from django.utils.timezone import now
from model_bakery import baker
from freezegun import freeze_time
from model_bakery import baker
from glitchtip.test_utils.test_case import GlitchTipTestCase
from ..models import MonitorCheck
from ..tasks import cleanup_old_monitor_checks
class TasksTestCase(GlitchTipTestCase):
@mock.patch("glitchtip.uptime.tasks.perform_checks.run")
def test_cleanup_old_monitor_checks(self, mock):
def test_cleanup_old_monitor_checks(self, _):
baker.make(MonitorCheck, _quantity=2)
cleanup_old_monitor_checks()
self.assertEqual(MonitorCheck.objects.count(), 2)
+5 -2
View File
@@ -1,15 +1,18 @@
import asyncio
from unittest import mock
from aioresponses import aioresponses
from django.core import mail
from django.urls import reverse
from freezegun import freeze_time
from model_bakery import baker
from glitchtip.test_utils.test_case import GlitchTipTestCase
from ..constants import MonitorType
from ..models import Monitor, MonitorCheck
from ..tasks import dispatch_checks
from ..utils import fetch_all
from ..models import Monitor, MonitorCheck
from ..constants import MonitorType
class UptimeTestCase(GlitchTipTestCase):
+1 -2
View File
@@ -1,6 +1,6 @@
from django.urls import path
from .views import HeartBeatCheckView
from .views import HeartBeatCheckView
urlpatterns = [
path(
@@ -9,4 +9,3 @@ urlpatterns = [
name="heartbeat-check",
),
]
+1 -1
View File
@@ -5,8 +5,8 @@ from ssl import SSLError
import aiohttp
from aiohttp.client_exceptions import ClientConnectorError
from .constants import MonitorType, MonitorCheckReason
from .constants import MonitorCheckReason, MonitorType
DEFAULT_TIMEOUT = 10
DEFAULT_PING_TIMEOUT = 1
+1
View File
@@ -1,6 +1,7 @@
from django.shortcuts import get_object_or_404
from rest_framework import permissions, viewsets
from rest_framework.generics import CreateAPIView
from .models import Monitor
from .serializers import HeartBeatCheckSerializer, MonitorSerializer
from .tasks import send_monitor_notification
+11 -10
View File
@@ -1,27 +1,28 @@
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include, re_path
from django.urls import include, path, re_path
from django.views.generic import TemplateView
from django.views.generic.base import RedirectView
from django_rest_mfa.rest_auth_helpers.views import MFALoginView
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from organizations.backends import invitation_backend
from rest_framework import permissions
from rest_framework_nested import routers
from django_rest_mfa.rest_auth_helpers.views import MFALoginView
from organizations.backends import invitation_backend
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
from api_tokens.urls import router as apiTokensRouter
from issues.urls import router as issuesRouter
from issues.views import EventJsonView
from organizations_ext.urls import router as organizationsRouter
from projects.urls import router as projectsRouter
from teams.urls import router as teamsRouter
from organizations_ext.urls import router as organizationsRouter
from users.urls import router as usersRouter
from api_tokens.urls import router as apiTokensRouter
from users.views import SocialAccountDisconnectView
from . import social
from .yasg import CustomOpenAPISchemaGenerator
from .views import SettingsView, health, APIRootView
from . import social
from .views import APIRootView, SettingsView, health
from .yasg import CustomOpenAPISchemaGenerator
router = routers.DefaultRouter()
router.registry.extend(projectsRouter.registry)
+7 -6
View File
@@ -1,12 +1,13 @@
from allauth.socialaccount.models import SocialApp
from django.conf import settings
from django.http import HttpResponse
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.permissions import AllowAny
from allauth.socialaccount.models import SocialApp
from users.utils import is_user_registration_open
from users.serializers import SocialAppSerializer, UserSerializer
from api_tokens.serializers import APITokenAuthScopesSerializer
from users.serializers import SocialAppSerializer, UserSerializer
from users.utils import is_user_registration_open
try:
from djstripe.settings import STRIPE_PUBLIC_KEY
@@ -19,7 +20,7 @@ class SettingsView(APIView):
permission_classes = [AllowAny]
def get(self, request, format=None):
def get(self, request, *args, **kwargs):
billing_enabled = settings.BILLING_ENABLED
enable_user_registration = is_user_registration_open()
stripe_public_key = None
@@ -49,7 +50,7 @@ class SettingsView(APIView):
class APIRootView(APIView):
""" /api/0/ gives information about the server and current user """
def get(self, request, format=None):
def get(self, request, *args, **kwargs):
user_data = None
auth_data = None
if request.user.is_authenticated:
+1
View File
@@ -1,4 +1,5 @@
from rest_framework import serializers
from api_tokens.serializers import APITokenSerializer
from projects.serializers.serializers import ProjectWithKeysSerializer
+4 -2
View File
@@ -1,7 +1,9 @@
from django.urls import reverse
from django.core.cache import cache
from django.urls import reverse
from glitchtip.test_utils.test_case import GlitchTipTestCase
from .views import SETUP_WIZARD_CACHE_KEY, SETUP_WIZARD_CACHE_EMPTY
from .views import SETUP_WIZARD_CACHE_EMPTY, SETUP_WIZARD_CACHE_KEY
class WizardTestCase(GlitchTipTestCase):
+2 -1
View File
@@ -1,5 +1,6 @@
from django.urls import path, re_path
from .views import SetupWizardView, SetupWizardSetTokenView
from .views import SetupWizardSetTokenView, SetupWizardView
urlpatterns = [
path("wizard/", SetupWizardView.as_view(), name="setup-wizard"),
+5 -2
View File
@@ -1,13 +1,16 @@
import string
from collections import namedtuple
from django.core.cache import cache
from django.utils.crypto import get_random_string
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.throttling import AnonRateThrottle
from rest_framework.views import APIView
from api_tokens.models import APIToken
from projects.models import Project
from .serializers import SetupWizardSerializer, SetupWizardResultSerializer
from .serializers import SetupWizardResultSerializer, SetupWizardSerializer
SETUP_WIZARD_CACHE_KEY = "setup-wizard-keys:v1:"
SETUP_WIZARD_CACHE_TIMEOUT = 600
+1 -1
View File
@@ -1,5 +1,5 @@
from django.conf import settings
from drf_yasg.generators import OpenAPISchemaGenerator, EndpointEnumerator
from drf_yasg.generators import EndpointEnumerator, OpenAPISchemaGenerator
from drf_yasg.inspectors import SwaggerAutoSchema
Generated
+1 -1
View File
@@ -1990,7 +1990,7 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "8aa85cb1a478cfd174566ba7ab0d300449757a23fd049186b9bb05391c69c561"
content-hash = "77d2b89d318bc2ee71d285daa4a8016094dd25b2c71eb0139aee0d7f4701af7b"
[metadata.files]
aiohttp = [
+4
View File
@@ -57,6 +57,10 @@ mypy = "~0.910"
django-stubs = "^1.9.0"
django-sslserver = "^0.22"
aioresponses = "^0.7.2"
isort = "^5.9.3"
[tool.isort]
profile = "black"
[build-system]
requires = ["poetry>=0.12"]