diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b6a1b77..5c93940 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,32 +1,43 @@ image: python:3 +stages: +- test +- deploy + # Change pip's cache directory to be inside the project directory since we can # only cache local items. variables: + POETRY_CACHE_DIR: "$CI_PROJECT_DIR/.cache/poetry" PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" -# Pip's cache doesn't store the python packages -# https://pip.pypa.io/en/stable/reference/pip_install/#caching -# -# If you want to also cache the installed packages, you have to install -# them in a virtualenv and cache it as well. cache: + key: ${CI_COMMIT_REF_SLUG} paths: - - .cache/pypoetry + - .cache/pip + - .cache/poetry before_script: - python -V - pip install poetry + - "poetry config settings.virtualenvs.path ${POETRY_CACHE_DIR}" - poetry install test: + stage: test script: - - poetry run pytest . + - poetry run pytest . --cov=jeeves.core --cov-report xml --cov-report term + artifacts: + paths: + - coverage.xml pages: + stage: deploy + dependencies: + - test script: - poetry run docs/build.sh - mv docs/_build/html/ public + - coverage html --directory public/htmlcov artifacts: paths: - public diff --git a/docs/Makefile b/docs/Makefile index f8e9408..d4bb2cb 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -12,9 +12,6 @@ BUILDDIR = _build help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -apidoc: - sphinx-apidoc -o api ../jeeves/core - .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new diff --git a/docs/build.sh b/docs/build.sh index f0fea6a..a2300b7 100755 --- a/docs/build.sh +++ b/docs/build.sh @@ -1,5 +1,5 @@ #!/bin/bash cd docs || exit 1 -make apidoc +sphinx-apidoc -o api ../jeeves/core make html diff --git a/docs/usage.rst b/docs/usage.rst index 6a03fc0..b18ba59 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -15,7 +15,7 @@ for example. given a test flow: "tasks": [ { "name": "Say hello", - "type": "jeeves.core.tasks.shell:ShellTask", + "type": "jeeves.core.actions.shell:ScriptAction", "parameters": { "script": "#!/bin/bash\necho HELLO WORLD!" } diff --git a/jeeves/core/actions/__init__.py b/jeeves/core/actions/__init__.py new file mode 100644 index 0000000..fbb127d --- /dev/null +++ b/jeeves/core/actions/__init__.py @@ -0,0 +1 @@ +PROVIDED_ACTIONS = ["jeeves.core.actions.shell:ScriptAction"] diff --git a/jeeves/core/tasks/base.py b/jeeves/core/actions/base.py similarity index 95% rename from jeeves/core/tasks/base.py rename to jeeves/core/actions/base.py index 6904d23..940bc14 100644 --- a/jeeves/core/tasks/base.py +++ b/jeeves/core/actions/base.py @@ -3,7 +3,7 @@ import logging import pydantic -class Task: +class Action: id = "" class Parameters(pydantic.BaseModel): diff --git a/jeeves/core/actions/shell.py b/jeeves/core/actions/shell.py new file mode 100644 index 0000000..379ad9a --- /dev/null +++ b/jeeves/core/actions/shell.py @@ -0,0 +1,69 @@ +import os +import tempfile +import subprocess +from typing import Text + +import pydantic + +from jeeves.core.objects import Result +from .base import Action + + +class ScriptAction(Action): + """ + Executes the provided script direcly on the system. + + The script is written into a temporary file that is then executed. + + If no shebang is provided, a default of ``#!/bin/bash -e`` will be used, if + the provided shebang interpreter is not found on the system the action will fail. + + .. automethod:: _get_script + """ + + DEFAULT_SHEBANG = "#!/bin/bash" + + id = "contrib/script" + verbose_name = "Execute script" + + class Parameters(pydantic.BaseModel): + """ + +----------------+------+-----------+---------------------------+ + | Parameter name | Type | Mandatory | Description | + +================+======+===========+===========================+ + | ``script`` | text | yes | The script to be executed | + +----------------+------+-----------+---------------------------+ + """ + + script: Text + + def _get_script(self): + """ + Returns the script defined in the parameters, checking for a shebang. + + If no shebang is defined, :any:`ScriptAction.DEFAULT_SHEBANG` with be used. + """ + if not self.parameters.script.startswith("#!"): + return f"{self.DEFAULT_SHEBANG}{os.linesep}{self.parameters.script}" + return self.parameters.script + + def execute(self): + script = self._get_script() + + # Write the script to a temporary file + script_file = tempfile.NamedTemporaryFile(mode="w", delete=False) + with script_file as handler: + handler.write(script) + + # Read/Execute only for owner + os.chmod(script_file.name, mode=0o500) + + process = subprocess.run( + script_file.name, stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) + + os.unlink(script_file.name) + + return Result( + success=process.returncode == 0, output=process.stdout.decode("utf-8") + ) diff --git a/jeeves/core/tasks/stub.py b/jeeves/core/actions/stub.py similarity index 54% rename from jeeves/core/tasks/stub.py rename to jeeves/core/actions/stub.py index 43cc2bd..5783a61 100644 --- a/jeeves/core/tasks/stub.py +++ b/jeeves/core/actions/stub.py @@ -1,48 +1,48 @@ """ -This is a collection of Tasks provided for testing purposes only. +This is a collection of Actions provided for testing purposes only. """ from typing import Text, Optional from jeeves.core.objects import Result -from .base import Task +from .base import Action -class StubSuccessTask(Task): +class StubSuccessAction(Action): id = "stub/success" def execute(self, message=None): return Result(success=True) -class StubNonSuccessTask(Task): +class StubNonSuccessAction(Action): id = "stub/non-success" def execute(self): return Result(output="error!", success=False) -class StubUncaughtExceptionTask(Task): +class StubUncaughtExceptionAction(Action): id = "stub/uncaught-exception" def execute(self): raise Exception("Oh god...") -class StubNoParametersTask(StubSuccessTask): +class StubNoParametersAction(StubSuccessAction): """ - An empty task that provides no configurable parameters. + An empty Action that provides no configurable parameters. """ id = "stub/no-parameters" -class StubParametersTask(StubSuccessTask): +class StubParametersAction(StubSuccessAction): """ - An empty task that provide two configurable parameters. + An empty Action that provide two configurable parameters. """ id = "sub/parameters" - class Parameters(Task.Parameters): + class Parameters(Action.Parameters): mandatory: Text non_mandatory: Optional[Text] = None diff --git a/jeeves/core/executor.py b/jeeves/core/executor.py index 228ac8b..131b6f3 100644 --- a/jeeves/core/executor.py +++ b/jeeves/core/executor.py @@ -14,13 +14,13 @@ class Executor: for step in self._execution.steps: yield step - def _get_steps(self, flow): + def _get_steps(self, flow: Flow): for task in flow.tasks: yield ExecutionStep(task=task, result=Result()) - def execute_step(self, step): + def execute_step(self, step: ExecutionStep): try: - step.result = step.task.runner.execute() + step.result = step.task.action.execute() except Exception as error: # Catch unhandled exceptions, mark the result as unsuccessful # and append the error as output. diff --git a/jeeves/core/objects.py b/jeeves/core/objects.py index b044713..16e160d 100644 --- a/jeeves/core/objects.py +++ b/jeeves/core/objects.py @@ -3,7 +3,7 @@ from dataclasses import field from pydantic import BaseModel -from jeeves.core.registry import TaskRegistry +from jeeves.core.registry import ActionRegistry class BaseObject(BaseModel): @@ -21,8 +21,11 @@ class Task(BaseObject): parameters: Dict[Any, Any] @property - def runner(self): - return TaskRegistry.get_task_cls(self.type)(parameters=self.parameters) + def action(self): + """ + Returns the instanced :any:`jeeves.core.actions.base.Action` defined in ``type`` for this ``Task``. + """ + return ActionRegistry.get_action_cls(self.type)(parameters=self.parameters) class Flow(BaseObject): diff --git a/jeeves/core/parsers.py b/jeeves/core/parsers.py index 55a4050..7ba75c6 100644 --- a/jeeves/core/parsers.py +++ b/jeeves/core/parsers.py @@ -1,4 +1,4 @@ -from typing import Dict, Text +from typing import Any, Text, MutableMapping import toml @@ -6,14 +6,14 @@ from jeeves.core.objects import Flow, BaseObject class ObjectParser: - object = None + object: BaseObject = None @classmethod def from_json(cls, serialized: Text) -> BaseObject: return cls.object.parse_raw(serialized) @classmethod - def from_dict(cls, serialized: Dict) -> BaseObject: + def from_dict(cls, serialized: MutableMapping[str, Any]) -> BaseObject: return cls.object.parse_obj(serialized) @classmethod diff --git a/jeeves/core/registry.py b/jeeves/core/registry.py index 2841c0d..17c522e 100644 --- a/jeeves/core/registry.py +++ b/jeeves/core/registry.py @@ -1,6 +1,8 @@ import logging +from typing import Any, Dict, Text, Type, Optional -from jeeves.core.tasks import PROVIDED_TASKS +from jeeves.core.actions import PROVIDED_ACTIONS +from jeeves.core.actions.base import Action class Singleton(type): @@ -14,53 +16,62 @@ class Singleton(type): return cls._instance -class TaskRegistry(metaclass=Singleton): - tasks = {} +class ActionRegistry(metaclass=Singleton): + actions: Dict[str, str] = {} def __init__(self): self.logger = logging.getLogger(self.__class__.__name__) @classmethod def autodiscover(cls): - """Loads all provided tasks.""" + """Loads all provided actions.""" # TODO: Third party plugins registry = cls() - for task_namespace in PROVIDED_TASKS: - registry.register_task(cls.get_task_cls(task_namespace)) + for action_namespace in PROVIDED_ACTIONS: + registry.register_action(cls.get_action_cls(action_namespace)) @classmethod - def register_task(cls, task_cls): + def register_action(cls, action_cls): registry = cls() - # namespace = task_cls.id - namespace = f"{task_cls.__module__}:{task_cls.__name__}" + # namespace = action_cls.id + namespace = f"{action_cls.__module__}:{action_cls.__name__}" - if namespace in registry.tasks: - raise cls.TaskNamespaceConflict( + if namespace in registry.actions: + raise cls.ActionNamespaceConflict( f"Namespace {namespace} is already registered" ) - registry.tasks[namespace] = task_cls + registry.actions[namespace] = action_cls @classmethod - def get_task_cls(cls, namespace, **kwargs): - # Right now tasks are being imported and returned dinamically because it's easier, - # but we will need a way of autodiscover all tasks (or register them manually) and - # referencing them on a list so the user knows which tasks are available. + def get_action_cls(cls, namespace) -> Type[Action]: + """Returns the class for the provided action namespace""" + # Right now actions are being imported and returned dinamically because it's easier, + # but we will need a way of autodiscover all (or register them manually) and + # referencing them on a list so the user knows which actions are available. modulename, clsname = namespace.split(":") try: module = __import__(f"{modulename}", fromlist=(clsname,), level=0) - task_cls = getattr(module, clsname) - return task_cls + action_cls = getattr(module, clsname) + return action_cls except ModuleNotFoundError as error: - raise cls.TaskDoesNotExist(f"Error importing task {namespace}: {error.msg}") + raise cls.ActionDoesNotExist(f"Error importing action {namespace}: {error}") - class TaskNamespaceConflict(Exception): - pass + @classmethod + def get_action( + cls, namespace: Text, parameters: Optional[Dict[Any, Any]] = None + ) -> Action: + """Returns the instanced action for the provided namespace""" + action_cls: Type[Action] = cls.get_action_cls(namespace) + return action_cls(parameters=parameters or {}) - class TaskDoesNotExist(Exception): - """ - Used when there's a problem retrieving a task. More info will be available on the message. - """ + class ActionNamespaceConflict(Exception): + """Raised when an action is defined with an already registered namespace""" + + pass + + class ActionDoesNotExist(Exception): + """Raised when there's a problem retrieving an action. More info will be available on the message.""" pass diff --git a/jeeves/core/tasks/__init__.py b/jeeves/core/tasks/__init__.py deleted file mode 100644 index ad878ef..0000000 --- a/jeeves/core/tasks/__init__.py +++ /dev/null @@ -1 +0,0 @@ -PROVIDED_TASKS = ["jeeves.core.tasks.shell:ShellTask"] diff --git a/jeeves/core/tasks/shell.py b/jeeves/core/tasks/shell.py deleted file mode 100644 index 7e276a8..0000000 --- a/jeeves/core/tasks/shell.py +++ /dev/null @@ -1,26 +0,0 @@ -import subprocess -from typing import Text - -import pydantic - -from jeeves.core.objects import Result -from .base import Task - - -class ShellTask(Task): - id = "library/shell" - verbose_name = "Execute Shell script" - - class Parameters(pydantic.BaseModel): - script: Text - - def execute(self): - process = subprocess.run( - self.parameters.script, - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) - return Result( - success=process.returncode == 0, output=process.stdout.decode("utf-8") - ) diff --git a/jeeves/core/tests/conftest.py b/jeeves/core/tests/conftest.py index fd3e16f..18e60c9 100644 --- a/jeeves/core/tests/conftest.py +++ b/jeeves/core/tests/conftest.py @@ -1,8 +1,8 @@ import pytest -from jeeves.core.registry import TaskRegistry +from jeeves.core.registry import ActionRegistry @pytest.fixture(scope="session", autouse=True) -def autoregister_tasks(): - TaskRegistry.autodiscover() +def autoregister_actions(): + ActionRegistry.autodiscover() diff --git a/jeeves/core/tests/factories.py b/jeeves/core/tests/factories.py index c734d8c..d266062 100644 --- a/jeeves/core/tests/factories.py +++ b/jeeves/core/tests/factories.py @@ -1,15 +1,12 @@ import factory -from faker import Faker from jeeves.core.objects import Flow, Task -from jeeves.core.registry import TaskRegistry - -fake = Faker() +from jeeves.core.registry import ActionRegistry class TaskFactory(factory.Factory): - name = fake.sentence() - type = factory.Iterator(TaskRegistry.tasks) + name = factory.Faker("name") + type = factory.Iterator(ActionRegistry.actions) parameters = {"script": "#!/bin/bash\necho test"} class Meta: @@ -17,7 +14,7 @@ class TaskFactory(factory.Factory): class FlowFactory(factory.Factory): - name = fake.name() + name = factory.Faker("name") tasks = factory.LazyFunction(lambda: [TaskFactory() for _ in range(0, 2)]) class Meta: diff --git a/jeeves/core/tests/tasks/__init__.py b/jeeves/core/tests/tasks/__init__.py new file mode 100644 index 0000000..76878c5 --- /dev/null +++ b/jeeves/core/tests/tasks/__init__.py @@ -0,0 +1,3 @@ +""" +This module contains tests excluve to the tasks provided bundled with Jeeves +""" diff --git a/jeeves/core/tests/tasks/test_shell.py b/jeeves/core/tests/tasks/test_shell.py new file mode 100644 index 0000000..d0271ad --- /dev/null +++ b/jeeves/core/tests/tasks/test_shell.py @@ -0,0 +1,110 @@ +import sys +import os.path +import tempfile +from unittest import mock +from subprocess import CompletedProcess + +from jeeves.core.objects import Task + +MOCK_DEFINITION = { + "type": f"jeeves.core.actions.shell:ScriptAction", + "name": "Say hello world in bash", + "parameters": {"script": "#!/bin/bash\necho Hello World"}, +} + + +def get_completed_process(returncode=0, stdout=b"", **kwargs): + """Mocks a :class:`subprocess.CompletedProcess` object""" + return CompletedProcess("", returncode=returncode, stdout=stdout, **kwargs) + + +@mock.patch("subprocess.run", mock.MagicMock(return_value=get_completed_process())) +def test_script_bash_task_ok(): + task = Task.parse_obj(MOCK_DEFINITION).action + result = task.execute() + assert result.success + + +@mock.patch( + "subprocess.run", mock.MagicMock(return_value=get_completed_process(returncode=1)) +) +def test_script_bash_task_ko(): + task = Task.parse_obj(MOCK_DEFINITION).action + result = task.execute() + assert not result.success + + +@mock.patch("subprocess.run", mock.MagicMock(return_value=get_completed_process())) +def test_script_no_shebang_defaults_to_bash_ok(): + definition = MOCK_DEFINITION.copy() + definition["parameters"]["script"] = definition["parameters"]["script"].strip( + "#!/bin/bash" + ) + task = Task.parse_obj(definition).action + assert task._get_script().startswith(task.DEFAULT_SHEBANG) + result = task.execute() + assert result.success + + +@mock.patch("subprocess.run", mock.MagicMock(return_value=get_completed_process())) +def test_script_with_other_shebang_ok(): + py_interpreter = sys.executable + expected_output = "Hello world! (from python)" + definition = MOCK_DEFINITION.copy() + py_script = f"#!{py_interpreter}\nprint('{expected_output}')" + definition["parameters"]["script"] = py_script + task = Task.parse_obj(definition).action + assert task._get_script().startswith(f"#!{py_interpreter}") + result = task.execute() + assert result.success + + +def test_script_stdout_and_stderr_is_sent_to_result_ok(): + """ + ..warning:: This test actually calls ``subprocess.run``. + + Not 100% sure this one is needed since we are just testing that subprocess.STDOUT works. + I'm leaving it for now since it's important to ensure we get the entire stdout/err in the + :any:`Result` object. + """ + script = "\n".join( + [ + f"#!{sys.executable}", + "import sys", + "sys.stdout.write('Hello')", + "sys.stderr.write('World')", + ] + ) + definition = MOCK_DEFINITION.copy() + definition["parameters"]["script"] = script + task = Task.parse_obj(definition).action + result = task.execute() + assert "Hello" in result.output + assert "World" in result.output + + +@mock.patch("subprocess.run", mock.MagicMock(return_value=get_completed_process())) +def test_script_task_cleans_tempfile_ok(): + """Make sure that the script is removed from the system after execution""" + task = Task.parse_obj(MOCK_DEFINITION).action + temp = tempfile.NamedTemporaryFile(mode="w", delete=False) + with mock.patch( + "tempfile.NamedTemporaryFile", mock.MagicMock(return_value=temp) + ) as mocked: + task.execute() + assert not os.path.isfile(mocked.return_value.name) + + +@mock.patch("subprocess.run", mock.MagicMock(return_value=get_completed_process())) +def test_script_task_sets_permissions_for_owner_only_ok(): + """Make sure that the script have only read and execution permissions for owner""" + task = Task.parse_obj(MOCK_DEFINITION).action + temp = tempfile.NamedTemporaryFile(mode="w", delete=False) + with mock.patch( + "tempfile.NamedTemporaryFile", mock.MagicMock(return_value=temp) + ) as mocked: + with mock.patch("os.unlink"): + task.execute() + stat = os.stat(mocked.return_value.name) + assert oct(stat.st_mode).endswith("500") + os.unlink(mocked.return_value.name) diff --git a/jeeves/core/tests/test_executor.py b/jeeves/core/tests/test_executor.py index 2823079..6d96c4d 100644 --- a/jeeves/core/tests/test_executor.py +++ b/jeeves/core/tests/test_executor.py @@ -3,7 +3,7 @@ from .factories import FlowFactory, TaskFactory def test_executor_success_task_ok(): - task = TaskFactory(type="jeeves.core.tasks.stub:StubSuccessTask") + task = TaskFactory(type="jeeves.core.actions.stub:StubSuccessAction") flow = FlowFactory(tasks=[task]) runner = Executor(flow) runner.start() @@ -13,7 +13,7 @@ def test_executor_success_task_ok(): def test_executor_non_success_task_ok(): - task = TaskFactory(type="jeeves.core.tasks.stub:StubNonSuccessTask") + task = TaskFactory(type="jeeves.core.actions.stub:StubNonSuccessAction") flow = FlowFactory(tasks=[task]) runner = Executor(flow) runner.start() @@ -23,7 +23,7 @@ def test_executor_non_success_task_ok(): def test_executor_uncaught_exception_in_task_ok(): - task = TaskFactory(type="jeeves.core.tasks.stub:StubUncaughtExceptionTask") + task = TaskFactory(type="jeeves.core.actions.stub:StubUncaughtExceptionAction") flow = FlowFactory(tasks=[task]) runner = Executor(flow) runner.start() diff --git a/jeeves/core/tests/test_parsers.py b/jeeves/core/tests/test_parsers.py index 405484b..87a4c16 100644 --- a/jeeves/core/tests/test_parsers.py +++ b/jeeves/core/tests/test_parsers.py @@ -6,8 +6,8 @@ EXPORTED_FLOW = { "name": "Test", "tasks": [ { - "type": "jeeves.core.tasks.stub:StubSuccessfulTask", - "name": "Test task", + "type": "jeeves.core.actions.stub:StubSuccessfulAction", + "name": "Test Action", "parameters": {}, } ], diff --git a/jeeves/core/tests/test_registry.py b/jeeves/core/tests/test_registry.py index 0df9fed..844696e 100644 --- a/jeeves/core/tests/test_registry.py +++ b/jeeves/core/tests/test_registry.py @@ -1,14 +1,27 @@ import pytest -from jeeves.core.registry import TaskRegistry -from jeeves.core.tasks.base import Task +from jeeves.core.registry import ActionRegistry +from jeeves.core.actions.base import Action +from jeeves.core.actions.stub import StubSuccessAction -def test_registry_get_task_cls_ok(): - task = TaskRegistry.get_task_cls("jeeves.core.tasks.stub:StubSuccessTask") - assert issubclass(task, Task) and not isinstance(task, Task) +def test_registry_get_action_cls_ok(): + action = ActionRegistry.get_action_cls("jeeves.core.actions.stub:StubSuccessAction") + assert issubclass(action, Action) and not isinstance(action, Action) -def test_registry_get_task_ko(): - with pytest.raises(TaskRegistry.TaskDoesNotExist): - TaskRegistry.get_task_cls("non.existant:task") +def test_registry_get_action_cls_ko(): + with pytest.raises(ActionRegistry.ActionDoesNotExist): + ActionRegistry.get_action_cls("non.existant:action") + + +def test_registry_get_action_ok(): + action = ActionRegistry.get_action("jeeves.core.actions.stub:StubSuccessAction") + assert issubclass(action.__class__, Action) and isinstance(action, Action) + + +def test_registry_namespace_conflict_ok(): + + ActionRegistry.register_action(StubSuccessAction) + with pytest.raises(ActionRegistry.ActionNamespaceConflict): + ActionRegistry.register_action(StubSuccessAction) diff --git a/jeeves/core/tests/test_tasks.py b/jeeves/core/tests/test_tasks.py index 67a3f1e..b7ad5c0 100644 --- a/jeeves/core/tests/test_tasks.py +++ b/jeeves/core/tests/test_tasks.py @@ -1,22 +1,28 @@ import pytest import pydantic -from jeeves.core.registry import TaskRegistry +from jeeves.core.registry import ActionRegistry -def test_task_with_empty_parameters_ok(): - task = TaskRegistry.get_task_cls("jeeves.core.tasks.stub:StubNoParametersTask") - task() - task(parameters=None) - task(parameters={}) +def test_action_with_empty_parameters_ok(): + action = ActionRegistry.get_action_cls( + "jeeves.core.actions.stub:StubNoParametersAction" + ) + action() + action(parameters=None) + action(parameters={}) -def test_task_with_parameters_ok(): - task = TaskRegistry.get_task_cls("jeeves.core.tasks.stub:StubParametersTask") - task(parameters=dict(mandatory="text", non_mandatory="text")) +def test_action_with_parameters_ok(): + action = ActionRegistry.get_action_cls( + "jeeves.core.actions.stub:StubParametersAction" + ) + action(parameters=dict(mandatory="text", non_mandatory="text")) -def test_task_with_parameters_ko(): - task = TaskRegistry.get_task_cls("jeeves.core.tasks.stub:StubParametersTask") +def test_action_with_parameters_ko(): + action = ActionRegistry.get_action_cls( + "jeeves.core.actions.stub:StubParametersAction" + ) with pytest.raises(pydantic.ValidationError): - task(parameters=dict(thisshould="fail")) + action(parameters=dict(thisshould="fail")) diff --git a/jeeves/frontend/templates/flows/task-add.j2 b/jeeves/frontend/templates/flows/task-add.j2 index f87b591..f5d9614 100644 --- a/jeeves/frontend/templates/flows/task-add.j2 +++ b/jeeves/frontend/templates/flows/task-add.j2 @@ -27,7 +27,7 @@
{% if form.type.errors %} diff --git a/poetry.lock b/poetry.lock index a0f87f6..22d4fa1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -34,6 +34,23 @@ version = "1.3.0" [package.dependencies] pyyaml = "*" +[[package]] +category = "dev" +description = "An abstract syntax tree for Python with inference support." +name = "astroid" +optional = false +python-versions = ">=3.5.*" +version = "2.3.1" + +[package.dependencies] +lazy-object-proxy = ">=1.4.0,<1.5.0" +six = "1.12" +wrapt = ">=1.11.0,<1.12.0" + +[package.dependencies.typed-ast] +python = "<3.8" +version = ">=1.4.0,<1.5" + [[package]] category = "dev" description = "Atomic file writes." @@ -128,6 +145,14 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "0.4.1" +[[package]] +category = "dev" +description = "Code coverage measurement for Python" +name = "coverage" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4" +version = "4.5.4" + [[package]] category = "dev" description = "Better living through Python with decorators" @@ -362,6 +387,14 @@ version = "2.10.1" [package.dependencies] MarkupSafe = ">=0.23" +[[package]] +category = "dev" +description = "A fast and thorough lazy object proxy." +name = "lazy-object-proxy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.4.2" + [[package]] category = "main" description = "Safely add untrusted strings to HTML/XML markup." @@ -386,6 +419,27 @@ optional = false python-versions = ">=3.4" version = "7.2.0" +[[package]] +category = "dev" +description = "Optional static typing for Python" +name = "mypy" +optional = false +python-versions = ">=3.5" +version = "0.730" + +[package.dependencies] +mypy-extensions = ">=0.4.0,<0.5.0" +typed-ast = ">=1.4.0,<1.5.0" +typing-extensions = ">=3.7.4" + +[[package]] +category = "dev" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +name = "mypy-extensions" +optional = false +python-versions = "*" +version = "0.4.2" + [[package]] category = "dev" description = "Node.js virtual environment builder" @@ -546,6 +600,20 @@ 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 code static checker" +name = "pylint" +optional = false +python-versions = ">=3.5.*" +version = "2.4.2" + +[package.dependencies] +astroid = ">=2.3.0,<2.4" +colorama = "*" +isort = ">=4.2.5,<5" +mccabe = ">=0.6,<0.7" + [[package]] category = "dev" description = "Python parsing module" @@ -576,6 +644,18 @@ wcwidth = "*" python = "<3.8" version = ">=0.12" +[[package]] +category = "dev" +description = "Pytest plugin for measuring coverage." +name = "pytest-cov" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.7.1" + +[package.dependencies] +coverage = ">=4.4" +pytest = ">=3.6" + [[package]] category = "dev" description = "A Django plugin for pytest." @@ -710,14 +790,6 @@ sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" sphinxcontrib-serializinghtml = "*" -[[package]] -category = "dev" -description = "GLPI theme for Sphinx" -name = "sphinx-glpi-theme" -optional = false -python-versions = "*" -version = "0.3" - [[package]] category = "dev" description = "Read the Docs theme for Sphinx" @@ -827,6 +899,33 @@ decorator = "*" ipython-genutils = "*" six = "*" +[[package]] +category = "dev" +description = "a fork of Python 2 and 3 ast modules with type comment support" +name = "typed-ast" +optional = false +python-versions = "*" +version = "1.4.0" + +[[package]] +category = "dev" +description = "Type Hints for Python" +name = "typing" +optional = false +python-versions = "*" +version = "3.7.4.1" + +[[package]] +category = "dev" +description = "Backported and Experimental Type Hints for Python 3.5+" +name = "typing-extensions" +optional = false +python-versions = "*" +version = "3.7.4" + +[package.dependencies] +typing = ">=3.7.4" + [[package]] category = "dev" description = "HTTP library with thread-safe connection pooling, file post, and more." @@ -851,6 +950,14 @@ optional = false python-versions = "*" version = "0.1.7" +[[package]] +category = "dev" +description = "Module for decorators, wrappers and monkey patching." +name = "wrapt" +optional = false +python-versions = "*" +version = "1.11.2" + [[package]] category = "dev" description = "Backport of pathlib-compatible object wrapper for zip files" @@ -863,7 +970,7 @@ version = "0.6.0" more-itertools = "*" [metadata] -content-hash = "50511f4767d8112f485d30bc7a85814ec7ae997416532bcf90ef5e3cc73503c0" +content-hash = "f4d87d4fe9e9f6dd2357037fca409b0942685ba69ab5f7e8674f02d462dc21d9" python-versions = "^3.7" [metadata.hashes] @@ -871,6 +978,7 @@ alabaster = ["446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", appdirs = ["9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", "d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"] appnope = ["5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", "8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"] "aspy.yaml" = ["463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc", "e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45"] +astroid = ["98c665ad84d10b18318c5ab7c3d203fe11714cbad2a4aef4f44651f415392754", "b7546ffdedbf7abcfbff93cd1de9e9980b1ef744852689decc5aeada324238c6"] atomicwrites = ["03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", "75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"] attrs = ["69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", "f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"] babel = ["af92e6106cb7c55286b25b38ad7695f8b4efb36a90ba483d7f7a6628c46158ab", "e86135ae101e31e2c8ec20a4e0c5220f4eed12487d5cf3f78be7e98d3a57fc28"] @@ -881,6 +989,7 @@ cfgv = ["edb387943b665bf9c434f717bf630fa78aecd53d5900d2e05da6ad6048553144", "fbd chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"] colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"] +coverage = ["08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", "0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", "141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", "19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", "23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", "245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", "331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", "386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", "3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", "60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", "63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", "6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", "6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", "7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", "826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", "93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", "9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", "af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", "bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", "bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", "c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", "dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", "df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", "e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", "e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", "e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", "eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", "eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", "ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", "efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", "fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", "ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025"] decorator = ["86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de", "f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6"] django = ["148a4a2d1a85b23883b0a4e99ab7718f518a83675e4485e44dc0c1d36988c5fa", "deb70aa038e59b58593673b15e9a711d1e5ccd941b5973b30750d5d026abfd56"] django-dramatiq = ["492ef6e216ca6ce1c5b09447b2be554ad299207d625649f2e934d28f96af4db9", "5f4f11027413cb3043254a5fa91f0e79db7482a3a1e4b80e55fcdb3f7206689e"] @@ -902,9 +1011,12 @@ ipython-genutils = ["72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c6 isort = ["54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", "6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"] jedi = ["786b6c3d80e2f06fd77162a07fed81b8baa22dde5d62896a790a331d6ac21a27", "ba859c74fa3c966a22f2aeebe1b74ee27e2a462f56d3f5f7ca4a59af61bfe42e"] jinja2 = ["065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", "14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"] +lazy-object-proxy = ["02b260c8deb80db09325b99edf62ae344ce9bc64d68b7a634410b8e9a568edbf", "18f9c401083a4ba6e162355873f906315332ea7035803d0fd8166051e3d402e3", "1f2c6209a8917c525c1e2b55a716135ca4658a3042b5122d4e3413a4030c26ce", "2f06d97f0ca0f414f6b707c974aaf8829c2292c1c497642f63824119d770226f", "616c94f8176808f4018b39f9638080ed86f96b55370b5a9463b2ee5c926f6c5f", "63b91e30ef47ef68a30f0c3c278fbfe9822319c15f34b7538a829515b84ca2a0", "77b454f03860b844f758c5d5c6e5f18d27de899a3db367f4af06bec2e6013a8e", "83fe27ba321e4cfac466178606147d3c0aa18e8087507caec78ed5a966a64905", "84742532d39f72df959d237912344d8a1764c2d03fe58beba96a87bfa11a76d8", "874ebf3caaf55a020aeb08acead813baf5a305927a71ce88c9377970fe7ad3c2", "9f5caf2c7436d44f3cec97c2fa7791f8a675170badbfa86e1992ca1b84c37009", "a0c8758d01fcdfe7ae8e4b4017b13552efa7f1197dd7358dc9da0576f9d0328a", "a4def978d9d28cda2d960c279318d46b327632686d82b4917516c36d4c274512", "ad4f4be843dace866af5fc142509e9b9817ca0c59342fdb176ab6ad552c927f5", "ae33dd198f772f714420c5ab698ff05ff900150486c648d29951e9c70694338e", "b4a2b782b8a8c5522ad35c93e04d60e2ba7f7dcb9271ec8e8c3e08239be6c7b4", "c462eb33f6abca3b34cdedbe84d761f31a60b814e173b98ede3c81bb48967c4f", "fd135b8d35dfdcdb984828c84d695937e58cc5f49e1c854eb311c4d6aa03f4f1"] markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"] mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"] more-itertools = ["409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", "92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"] +mypy = ["1d98fd818ad3128a5408148c9e4a5edce6ed6b58cc314283e631dd5d9216527b", "22ee018e8fc212fe601aba65d3699689dd29a26410ef0d2cc1943de7bec7e3ac", "3a24f80776edc706ec8d05329e854d5b9e464cd332e25cde10c8da2da0a0db6c", "42a78944e80770f21609f504ca6c8173f7768043205b5ac51c9144e057dcf879", "4b2b20106973548975f0c0b1112eceb4d77ed0cafe0a231a1318f3b3a22fc795", "591a9625b4d285f3ba69f541c84c0ad9e7bffa7794da3fa0585ef13cf95cb021", "5b4b70da3d8bae73b908a90bb2c387b977e59d484d22c604a2131f6f4397c1a3", "84edda1ffeda0941b2ab38ecf49302326df79947fa33d98cdcfbf8ca9cf0bb23", "b2b83d29babd61b876ae375786960a5374bba0e4aba3c293328ca6ca5dc448dd", "cc4502f84c37223a1a5ab700649b5ab1b5e4d2bf2d426907161f20672a21930b", "e29e24dd6e7f39f200a5bb55dcaa645d38a397dd5a6674f6042ef02df5795046"] +mypy-extensions = ["a161e3b917053de87dbe469987e173e49fb454eca10ef28b48b384538cc11458"] nodeenv = ["ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a"] packaging = ["28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", "d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"] parso = ["63854233e1fadb5da97f2744b6b24346d2750b85965e7e399bec1620232797dc", "666b0ee4a7a1220f65d367617f2cd3ffddff3e205f3f16a0284df30e774c2a9c"] @@ -921,8 +1033,10 @@ pycodestyle = ["95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56 pydantic = ["18598557f0d9ab46173045910ed50458c4fb4d16153c23346b504d7a5b679f77", "6a9335c968e13295430a208487e74d69fef40168b72dea8d975765d14e2da660", "6f5eb88fe4c21380aa064b7d249763fc6306f0b001d7e7d52d80866d1afc9ed3", "bc6c6a78647d7a65a493e1107572d993f26a652c49183201e3c7d23924bf7311", "e1a63b4e6bf8820833cb6fa239ffbe8eec57ccdd7d66359eff20e68a83c1deeb", "ede2d65ae33788d4e26e12b330b4a32c53cb14131c65bca3a59f037c73f6ee7a"] pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", "d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"] pygments = ["71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", "881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"] +pylint = ["7edbae11476c2182708063ac387a8f97c760d9cfe36a5ede0ca996f90cf346c8", "844ce067788028c1a35086a5c66bc5e599ddd851841c41d6ee1623b36774d9f2"] pyparsing = ["6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", "d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"] pytest = ["13c1c9b22127a77fc684eee24791efafcef343335d855e3573791c68588fe1a5", "d8ba7be9466f55ef96ba203fc0f90d0cf212f2f927e69186e1353e30bc7f62e5"] +pytest-cov = ["2b097cde81a302e1047331b48cadacf23577e431b61e9c6f49a1170bbe3d3da6", "e00ea4fdde970725482f1f35630d12f074e121a23801aabf2ae154ec6bdd343a"] pytest-django = ["264fb4c506db5d48a6364c311a0b00b7b48a52715bad8839b2d8bee9b99ed6bb", "4adfe5fb3ed47f0ba55506dd3daf688b1f74d5e69148c10ad2dd2f79f40c0d62"] pytest-tldr = ["008b7d53cabbce9d49ee7a92754ed4adafc056bc49ae01b257c2ffb1f5ce2408", "dca4a464a002f389677f4c42f5b9c815aae43219d73ecfe6b7fffe2d190e38eb"] python-dateutil = ["7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", "c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"] @@ -935,7 +1049,6 @@ rope = ["6b728fdc3e98a83446c27a91fc5d56808a004f8beab7a31ab1d7224cecc7d969", "c5c six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"] snowballstemmer = ["209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0", "df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"] sphinx = ["0d586b0f8c2fc3cc6559c5e8fd6124628110514fda0e5d7c82e682d749d2e845", "839a3ed6f6b092bb60f492024489cc9e6991360fb9f52ed6361acd510d261069"] -sphinx-glpi-theme = ["5acf9a8bd942e8478d987c6da5caccc1e9459d7cbaafbf95eca4f2178b1de29c", "896c8630af2f2995f3b384ea598e891b7dea092b592a8708e6db7a179be26060"] sphinx-rtd-theme = ["00cf895504a7895ee433807c62094cf1e95f065843bf3acd17037c3e9a2becd4", "728607e34d60456d736cc7991fd236afb828b21b82f956c5ea75f94c8414040a"] sphinxcontrib-applehelp = ["edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897", "fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d"] sphinxcontrib-devhelp = ["6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34", "9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981"] @@ -948,7 +1061,11 @@ stevedore = ["01d9f4beecf0fbd070ddb18e5efb10567801ba7ef3ddab0074f54e3cd4e91730", text-unidecode = ["1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", "bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"] toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"] traitlets = ["9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", "c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9"] +typed-ast = ["18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", "262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", "2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", "354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", "4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", "630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", "66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", "71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", "95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", "bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", "cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", "d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", "d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", "d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", "ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"] +typing = ["91dfe6f3f706ee8cc32d38edbbf304e9b7583fb37108fef38229617f8b3eba23", "c8cabb5ab8945cd2f54917be357d134db9cc1eb039e59d1606dc1e60cb1d9d36", "f38d83c5a7a7086543a0f649564d661859c5146a85775ab90c0d2f93ffaa9714"] +typing-extensions = ["2ed632b30bb54fc3941c382decfd0ee4148f5c591651c9272473fea2c6397d95", "b1edbbf0652660e32ae780ac9433f4231e7339c7f9a8057d0f042fcbcea49b87", "d8179012ec2c620d3791ca6fe2bf7979d979acdbef1fca0bc56b37411db682ed"] urllib3 = ["3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", "9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"] virtualenv = ["680af46846662bb38c5504b78bad9ed9e4f3ba2d54f54ba42494fdf94337fe30", "f78d81b62d3147396ac33fc9d77579ddc42cc2a98dd9ea38886f616b33bc7fb2"] wcwidth = ["3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", "f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"] +wrapt = ["565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"] zipp = ["3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", "f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"] diff --git a/pyproject.toml b/pyproject.toml index 3a5adb8..e4fa791 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,10 @@ factory-boy = "^2.12" sphinx = "^2.2" doc8 = "^0.8.0" sphinx-rtd-theme = "^0.4.3" +coverage = "^4.5" +pytest-cov = "^2.7" +pylint = "^2.4" +mypy = "^0.730.0" [build-system] requires = ["poetry>=0.12"] diff --git a/setup.cfg b/setup.cfg index 623fbd1..419de85 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,6 +11,6 @@ include_trailing_comma = True length_sort = 1 lines_between_types = 0 line_length = 88 -known_third_party = click,django,dramatiq,environ,factory,faker,pydantic,pytest,toml +known_third_party = click,django,dramatiq,environ,factory,pydantic,pytest,toml sections = FUTURE, STDLIB, DJANGO, THIRDPARTY, FIRSTPARTY, LOCALFOLDER no_lines_before = LOCALFOLDER