Initial proof of concept
This commit is contained in:
parent
bec3f79798
commit
d96eed1cae
|
@ -0,0 +1,22 @@
|
|||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v2.2.3
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: flake8
|
||||
|
||||
- repo: https://github.com/asottile/seed-isort-config
|
||||
rev: v1.9.2
|
||||
hooks:
|
||||
- id: seed-isort-config
|
||||
- repo: https://github.com/pre-commit/mirrors-isort
|
||||
rev: v4.3.20
|
||||
hooks:
|
||||
- id: isort
|
||||
|
||||
- repo: https://github.com/ambv/black
|
||||
rev: stable
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3.7
|
|
@ -0,0 +1,9 @@
|
|||
quicksetup:
|
||||
poetry install
|
||||
poetry run pre-commit install
|
||||
cp .env.example .env
|
||||
poetry run python manage.py migrate
|
||||
poetry run python manage.py loaddata local_data
|
||||
|
||||
clean:
|
||||
if [ -f "local.db" ]; then rm local.db; fi;
|
19
README.md
19
README.md
|
@ -8,9 +8,20 @@ Jeeves will take care of your boring tasks so you don't have to worry.
|
|||
|
||||
You need [poetry](https://poetry.eustace.io/) to manage this project dependencies. Go to the provided link to get more information.
|
||||
|
||||
Running this will use _poetry_ to generate a virtualenv, install all required dependencies and initialize a database with an admin user.
|
||||
|
||||
``` bash
|
||||
poetry install # This install al requirements in an environment
|
||||
poetry shell # Opens a shell with the virtualenv enabled
|
||||
python manage.py migrate
|
||||
python manage.py runserver
|
||||
make quicksetup
|
||||
```
|
||||
|
||||
To run the webserver run this and then access [http://localhost:8000/admin](http://localhost:8000/admin) with credentials `admin`/`Qwer1234`
|
||||
|
||||
``` bash
|
||||
poetry run python manage.py runserver
|
||||
```
|
||||
|
||||
To run the test suite:
|
||||
|
||||
``` bash
|
||||
DJANGO_SETTINGS_MODULE=jeeves.settings pytest .
|
||||
```
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
from .models import Run, Flow, Task
|
||||
|
||||
admin.site.register(Flow)
|
||||
admin.site.register(Run)
|
||||
admin.site.register(Task)
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
name = "core"
|
|
@ -0,0 +1,20 @@
|
|||
[
|
||||
{
|
||||
"model": "auth.user",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"password": "pbkdf2_sha256$150000$8GIlObHdvhdv$phdSFNeTHkkN6MJnzBCcuC+qo5RI0gAQdLhAmWyFSR0=",
|
||||
"last_login": "2019-09-23T18:48:28.585Z",
|
||||
"is_superuser": true,
|
||||
"username": "admin",
|
||||
"first_name": "",
|
||||
"last_name": "",
|
||||
"email": "admin@admin.com",
|
||||
"is_staff": true,
|
||||
"is_active": true,
|
||||
"date_joined": "2019-09-23T18:48:20.713Z",
|
||||
"groups": [],
|
||||
"user_permissions": []
|
||||
}
|
||||
}
|
||||
]
|
|
@ -0,0 +1,93 @@
|
|||
# Generated by Django 2.2.5 on 2019-09-23 20:56
|
||||
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import models, migrations
|
||||
|
||||
import jeeves.core.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Flow",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("modified_at", models.DateTimeField(auto_now=True)),
|
||||
("name", models.CharField(max_length=64)),
|
||||
("_definition", models.TextField()),
|
||||
],
|
||||
options={"abstract": False},
|
||||
bases=(models.Model, jeeves.core.models.DefinitionMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Task",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("modified_at", models.DateTimeField(auto_now=True)),
|
||||
("type", models.CharField(max_length=32)),
|
||||
("_definition", models.TextField()),
|
||||
],
|
||||
options={"abstract": False},
|
||||
bases=(jeeves.core.models.DefinitionMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Run",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("modified_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
choices=[("pending", "Pending"), ("finished", "Finished")],
|
||||
default="pending",
|
||||
max_length=32,
|
||||
),
|
||||
),
|
||||
("success", models.BooleanField(default=True)),
|
||||
("_definition", models.TextField()),
|
||||
("_result", models.TextField(default="{}")),
|
||||
(
|
||||
"flow",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="core.Flow"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={"abstract": False},
|
||||
bases=(jeeves.core.models.DefinitionMixin, models.Model),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,110 @@
|
|||
import copy
|
||||
import json
|
||||
|
||||
from django.db import models
|
||||
|
||||
from jeeves.base.models import BaseModel
|
||||
|
||||
|
||||
class DefinitionMixin:
|
||||
@property
|
||||
def definition(self):
|
||||
return json.loads(self._definition)
|
||||
|
||||
@definition.setter
|
||||
def definition(self, value: dict):
|
||||
self._definition = json.dumps(value)
|
||||
|
||||
|
||||
class Flow(BaseModel, DefinitionMixin):
|
||||
name = models.CharField(max_length=64)
|
||||
_definition = models.TextField()
|
||||
|
||||
def serialize(self):
|
||||
definition = copy.deepcopy(self.definition)
|
||||
definition["tasks"] = [task.serialize() for task in self.tasks]
|
||||
return {"name": self.name, "definition": definition}
|
||||
|
||||
@property
|
||||
def tasks(self):
|
||||
for task_ref in self.definition["tasks"]:
|
||||
yield Task.objects.from_reference(task_ref)
|
||||
|
||||
|
||||
class TaskManager(models.Manager):
|
||||
def from_reference(self, reference):
|
||||
return self.get_queryset().get(pk=reference)
|
||||
|
||||
|
||||
class Task(DefinitionMixin, BaseModel):
|
||||
type = models.CharField(max_length=32)
|
||||
_definition = models.TextField()
|
||||
|
||||
objects = TaskManager()
|
||||
|
||||
def serialize(self):
|
||||
return {"type": self.type, "definition": self.definition}
|
||||
|
||||
def run(self, message=None):
|
||||
from jeeves.tasks.shell import ShellTask
|
||||
|
||||
runner = ShellTask(**self.definition)
|
||||
return Message(error=runner.execute(message))
|
||||
|
||||
|
||||
class Run(DefinitionMixin, BaseModel):
|
||||
PENDING = "pending"
|
||||
FINISHED = "finished"
|
||||
RUN_STATUS = ((PENDING, PENDING.capitalize()), (FINISHED, FINISHED.capitalize()))
|
||||
status = models.CharField(max_length=32, choices=RUN_STATUS, default=PENDING)
|
||||
success = models.BooleanField(default=True)
|
||||
flow = models.ForeignKey(Flow, on_delete=models.CASCADE)
|
||||
_definition = models.TextField()
|
||||
_result = models.TextField(default="{}")
|
||||
|
||||
@classmethod
|
||||
def execute_flow(cls, flow: Flow):
|
||||
run = cls()
|
||||
run.definition = flow.serialize()
|
||||
run.flow = flow
|
||||
message = None
|
||||
for task in run.flow.tasks:
|
||||
message = task.run(message=message)
|
||||
if message.error:
|
||||
run.success = False
|
||||
run._result = json.dumps(message.serialize())
|
||||
break
|
||||
|
||||
run.status = Run.FINISHED
|
||||
run.save()
|
||||
return run
|
||||
|
||||
|
||||
class Message:
|
||||
error: bool = False
|
||||
|
||||
def __init__(self, error):
|
||||
self.error = error
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Message error={self.error}>"
|
||||
|
||||
def serialize(self):
|
||||
return {"error": self.error}
|
||||
|
||||
|
||||
class Result:
|
||||
success: bool = True
|
||||
last_message: Message
|
||||
|
||||
def __init__(self):
|
||||
self.steps = []
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<Result
|
||||
success={self.success}
|
||||
last_message={self.last_message}>
|
||||
"""
|
||||
|
||||
def to_dict(self):
|
||||
return {"success": self.success, "last_message": self.last_message.to_dict()}
|
|
@ -0,0 +1,46 @@
|
|||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from jeeves.core.models import Run, Flow, Task
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def task_model():
|
||||
TASK_DEFINITION = {"script": "#!/bin/bash\n\necho HELLO"}
|
||||
TASK = {
|
||||
"type": "jeeves.tasks.shell.ShellTask",
|
||||
"_definition": json.dumps(TASK_DEFINITION),
|
||||
}
|
||||
task = Task.objects.create(**TASK)
|
||||
yield task
|
||||
task.delete()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def flow_model(task_model):
|
||||
FLOW_DEFINITION = {"tasks": [str(task_model.id)]}
|
||||
FLOW = {"name": "Test", "_definition": json.dumps(FLOW_DEFINITION)}
|
||||
flow = Flow.objects.create(**FLOW)
|
||||
yield flow, task_model
|
||||
flow.delete()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_task_from_reference_ok(task_model):
|
||||
assert Task.objects.from_reference(str(task_model.id)) == task_model
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_flow_definition_is_expanded_on_serialize_ok(flow_model):
|
||||
flow, task = flow_model
|
||||
flow_dict = flow.serialize()
|
||||
flow_dict["definition"]["tasks"][0] == task.definition
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_run_execute_flow_ok(flow_model):
|
||||
flow, task = flow_model
|
||||
run = Run.execute_flow(flow)
|
||||
assert run.status == Run.FINISHED
|
||||
assert run.success is True
|
|
@ -0,0 +1 @@
|
|||
urlpatterns = []
|
|
@ -24,55 +24,52 @@ ALLOWED_HOSTS = env.list("JEEVES_ALLOWED_HOSTS", default=["*"])
|
|||
# Application definition
|
||||
INSTALLED_APPS = [
|
||||
# Django
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
# Third party
|
||||
|
||||
# Own
|
||||
"jeeves.core",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'jeeves.urls'
|
||||
ROOT_URLCONF = "jeeves.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'jeeves.wsgi.application'
|
||||
WSGI_APPLICATION = "jeeves.wsgi.application"
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': env.db()
|
||||
}
|
||||
DATABASES = {"default": env.db()}
|
||||
|
||||
|
||||
# Password validation
|
||||
|
@ -80,26 +77,20 @@ DATABASES = {
|
|||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
|
||||
},
|
||||
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
|
@ -111,4 +102,4 @@ USE_TZ = True
|
|||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_URL = "/static/"
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import subprocess
|
||||
from typing import Text
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ShellTask:
|
||||
script: Text
|
||||
|
||||
def execute(self, message=None):
|
||||
# error = None
|
||||
process = subprocess.run(self.script, shell=True, capture_output=True)
|
||||
# if process.returncode > 0:
|
||||
# error = {"error": process.stderr}
|
||||
return process.returncode > 0
|
|
@ -13,9 +13,10 @@ Including another URLconf
|
|||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.urls import path, include
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path("admin/", admin.site.urls),
|
||||
path("tests/", include("jeeves.core.urls")),
|
||||
]
|
||||
|
|
|
@ -26,6 +26,14 @@ version = "1.3.0"
|
|||
[package.dependencies]
|
||||
pyyaml = "*"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Atomic file writes."
|
||||
name = "atomicwrites"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "1.3.0"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Classes Without Boilerplate"
|
||||
|
@ -79,7 +87,7 @@ version = "7.0"
|
|||
[[package]]
|
||||
category = "dev"
|
||||
description = "Cross-platform colored terminal text."
|
||||
marker = "python_version >= \"3.4\" and sys_platform == \"win32\""
|
||||
marker = "python_version >= \"3.4\" and sys_platform == \"win32\" or sys_platform == \"win32\""
|
||||
name = "colorama"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
@ -245,6 +253,18 @@ optional = false
|
|||
python-versions = "*"
|
||||
version = "1.3.3"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Core utilities for Python packages"
|
||||
name = "packaging"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "19.2"
|
||||
|
||||
[package.dependencies]
|
||||
pyparsing = ">=2.0.2"
|
||||
six = "*"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "A Python Parser"
|
||||
|
@ -275,6 +295,19 @@ optional = false
|
|||
python-versions = "*"
|
||||
version = "0.7.5"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "plugin and hook calling mechanisms for python"
|
||||
name = "pluggy"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "0.13.0"
|
||||
|
||||
[package.dependencies]
|
||||
[package.dependencies.importlib-metadata]
|
||||
python = "<3.8"
|
||||
version = ">=0.12"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
||||
|
@ -316,6 +349,14 @@ optional = false
|
|||
python-versions = "*"
|
||||
version = "0.6.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "library with cross-python path, ini-parsing, io, code, log facilities"
|
||||
name = "py"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "1.8.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Python style guide checker"
|
||||
|
@ -341,6 +382,58 @@ optional = false
|
|||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "2.4.2"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Python parsing module"
|
||||
name = "pyparsing"
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
version = "2.4.2"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
name = "pytest"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
version = "5.1.3"
|
||||
|
||||
[package.dependencies]
|
||||
atomicwrites = ">=1.0"
|
||||
attrs = ">=17.4.0"
|
||||
colorama = "*"
|
||||
more-itertools = ">=4.0.0"
|
||||
packaging = "*"
|
||||
pluggy = ">=0.12,<1.0"
|
||||
py = ">=1.5.0"
|
||||
wcwidth = "*"
|
||||
|
||||
[package.dependencies.importlib-metadata]
|
||||
python = "<3.8"
|
||||
version = ">=0.12"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "A Django plugin for pytest."
|
||||
name = "pytest-django"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "3.5.1"
|
||||
|
||||
[package.dependencies]
|
||||
pytest = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "A pytest plugin that limits the output to just the things you need."
|
||||
name = "pytest-tldr"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "0.2.1"
|
||||
|
||||
[package.dependencies]
|
||||
pytest = ">=3.5.0"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "World timezone definitions, modern and historical"
|
||||
|
@ -414,7 +507,6 @@ version = "16.7.5"
|
|||
[[package]]
|
||||
category = "dev"
|
||||
description = "Measures number of Terminal column cells of wide-character codes"
|
||||
marker = "python_version >= \"3.4\""
|
||||
name = "wcwidth"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
@ -432,13 +524,14 @@ version = "0.6.0"
|
|||
more-itertools = "*"
|
||||
|
||||
[metadata]
|
||||
content-hash = "07b2fd4f428f3a57c3c230ca6f45b555af1b09dd87af6202a739e7bab1b16a9e"
|
||||
content-hash = "40f0c0a3f3e7900fd92973ba513b9d4449452e66cf462d9cba9c5299f961c3d5"
|
||||
python-versions = "^3.7"
|
||||
|
||||
[metadata.hashes]
|
||||
appdirs = ["9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", "d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"]
|
||||
appnope = ["5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", "8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"]
|
||||
"aspy.yaml" = ["463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc", "e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45"]
|
||||
atomicwrites = ["03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", "75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"]
|
||||
attrs = ["69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", "f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"]
|
||||
backcall = ["38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", "bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2"]
|
||||
black = ["817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739", "e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"]
|
||||
|
@ -460,15 +553,22 @@ jedi = ["786b6c3d80e2f06fd77162a07fed81b8baa22dde5d62896a790a331d6ac21a27", "ba8
|
|||
mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"]
|
||||
more-itertools = ["409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", "92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"]
|
||||
nodeenv = ["ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a"]
|
||||
packaging = ["28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", "d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"]
|
||||
parso = ["63854233e1fadb5da97f2744b6b24346d2750b85965e7e399bec1620232797dc", "666b0ee4a7a1220f65d367617f2cd3ffddff3e205f3f16a0284df30e774c2a9c"]
|
||||
pexpect = ["2094eefdfcf37a1fdbfb9aa090862c1a4878e5c7e0e7e7088bdb511c558e5cd1", "9e2c1fd0e6ee3a49b28f95d4b33bc389c89b20af6a1255906e90ff1262ce62eb"]
|
||||
pickleshare = ["87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", "9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"]
|
||||
pluggy = ["0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6", "fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34"]
|
||||
pre-commit = ["1d3c0587bda7c4e537a46c27f2c84aa006acc18facf9970bf947df596ce91f3f", "fa78ff96e8e9ac94c748388597693f18b041a181c94a4f039ad20f45287ba44a"]
|
||||
prompt-toolkit = ["11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463780", "2519ad1d8038fd5fc8e770362237ad0364d16a7650fb5724af6997ed5515e3c1", "977c6583ae813a37dc1c2e1b715892461fcbdaa57f6fc62f33a528c4886c8f55"]
|
||||
ptyprocess = ["923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", "d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"]
|
||||
py = ["64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", "dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"]
|
||||
pycodestyle = ["95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", "e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"]
|
||||
pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", "d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"]
|
||||
pygments = ["71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", "881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"]
|
||||
pyparsing = ["6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", "d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"]
|
||||
pytest = ["813b99704b22c7d377bbd756ebe56c35252bb710937b46f207100e843440b3c2", "cc6620b96bc667a0c8d4fa592a8c9c94178a1bd6cc799dbb057dfd9286d31a31"]
|
||||
pytest-django = ["264fb4c506db5d48a6364c311a0b00b7b48a52715bad8839b2d8bee9b99ed6bb", "4adfe5fb3ed47f0ba55506dd3daf688b1f74d5e69148c10ad2dd2f79f40c0d62"]
|
||||
pytest-tldr = ["008b7d53cabbce9d49ee7a92754ed4adafc056bc49ae01b257c2ffb1f5ce2408", "dca4a464a002f389677f4c42f5b9c815aae43219d73ecfe6b7fffe2d190e38eb"]
|
||||
pytz = ["26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32", "c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7"]
|
||||
pyyaml = ["0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", "01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", "5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", "5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", "7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", "7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", "87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", "9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", "a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", "b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", "b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", "bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", "f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"]
|
||||
rope = ["6b728fdc3e98a83446c27a91fc5d56808a004f8beab7a31ab1d7224cecc7d969", "c5c5a6a87f7b1a2095fb311135e2a3d1f194f5ecb96900fdd0a9100881f48aaf", "f0dcf719b63200d492b85535ebe5ea9b29e0d0b8aebeb87fe03fc1a65924fdaf"]
|
||||
|
|
|
@ -15,6 +15,9 @@ isort = "^4.3"
|
|||
pre-commit = "^1.18"
|
||||
rope = "^0.14.0"
|
||||
ipdb = "^0.12.2"
|
||||
pytest = "^5.1"
|
||||
pytest-django = "^3.5"
|
||||
pytest-tldr = "^0.2.1"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry>=0.12"]
|
||||
|
|
|
@ -11,6 +11,6 @@ include_trailing_comma = True
|
|||
length_sort = 1
|
||||
lines_between_types = 0
|
||||
line_length = 88
|
||||
known_third_party = django,environ,jsoneditor,pydantic,pytest,requests,rest_framework,sentry_sdk
|
||||
known_third_party = django,environ,pytest
|
||||
sections = FUTURE, STDLIB, DJANGO, THIRDPARTY, FIRSTPARTY, LOCALFOLDER
|
||||
no_lines_before = LOCALFOLDER
|
||||
|
|
Reference in New Issue