1
0
mirror of https://github.com/enpaul/kodak.git synced 2024-11-13 10:07:04 +00:00

Rename project to dehance

This commit is contained in:
Ethan Paul 2021-05-02 18:12:04 -04:00
parent 3b05fb2053
commit 6a7662ae0f
No known key found for this signature in database
GPG Key ID: D0E2CBF1245E92BF
32 changed files with 784 additions and 668 deletions

115
.gitignore vendored
View File

@ -1,112 +1,27 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
requirements.txt
*.pyc
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
*.swp
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
docs/imagemonk*.rst
docs/modules.rst
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# dotenv
.env
# virtualenv
.venv
venv/
ENV/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
*_cache/
.tox/
.cache/
.venv/
.mypy_cache/
# Pycharm
.idea/
.idea/vcs.xml
docs/_build/
docs/modules.rst
docs/dehance*.rst
.vscode/
.idea/
# Project specific
*.db
*.db-shm
*.db-wal

View File

@ -1,48 +1,59 @@
---
# All of the pre-commit hooks here actually use the `pytyhon` pre-commit language
# setting. However, for the python language setting, pre-commit will create and manage
# a cached virtual environment for each hook ID and do a bare `pip install <repo>` into
# the venv to setup the hook. This can result in conflicting dependency versions between
# the version installed to the pre-commit venv and the version installed to the Poetry
# venv specified in the lockfile.
#
# The solution is to specify `language: system` for all hooks and then install the
# required dependencies to the Poetry venv. The `system` language skips the isolated
# venv creation and looks for the entrypoint specified by the hook in the global
# environment which, if running in the Poetry venv, will find the entrypoint provided
# by the Poetry-managed dependency.
#
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.3.0
- repo: local
hooks:
- id: end-of-file-fixer
name: end-of-file-fixer
entry: end-of-file-fixer
language: system
types: [text]
- id: fix-encoding-pragma
name: fix-encoding-pragma
entry: fix-encoding-pragma
args:
- "--remove"
language: system
- id: trailing-whitespace
types: [python]
- id: trailing-whitespace-fixer
name: trailing-whitespace-fixer
entry: trailing-whitespace-fixer
language: system
types: [text]
- id: check-merge-conflict
name: check-merge-conflict
entry: check-merge-conflict
language: system
types: [text]
- repo: https://github.com/psf/black
rev: master
hooks:
- id: black
language: system
- repo: https://github.com/asottile/blacken-docs
rev: v1.8.0
hooks:
- id: blacken-docs
language: system
- repo: https://github.com/asottile/reorder_python_imports
rev: v2.3.6
hooks:
- id: reorder-python-imports
name: reorder-python-imports
entry: reorder-python-imports
args:
- "--unclassifiable-application-module=imagemonk"
- "--unclassifiable-application-module=dehance"
language: system
types: [python]
- id: black
name: black
entry: black
language: system
types: [python]
- id: blacken-docs
name: blacken-docs
entry: blacken-docs
language: system
types: [text]
- id: mdformat
name: mdformat
entry: mdformat
language: system
args:
- "--number"
- "--wrap=90"
types:
- markdown

View File

@ -1,11 +1,12 @@
## Copyright 2020 Ethan Paul
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of
the Software.
**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.**
## Copyright 2020 Ethan Paul
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.
**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.**

View File

@ -1,7 +1,7 @@
# ImageMonk makefile
# Dehance makefile
# You can set these variables from the command line
PROJECT = imagemonk
PROJECT = dehance
.PHONY: help
# Put it first so that "make" without argument is like "make help"
@ -28,7 +28,8 @@ clean-docs:
rm --force docs/$(PROJECT)*.rst
rm --force docs/modules.rst
clean: clean-tox clean-py clean-docs; ## Clean temp build/cache files and directories
clean: clean-tox clean-py clean-docs ## Clean temp build/cache files and directories
rm --force ./*db*
wheel: ## Build Python binary distribution wheel package
poetry build --format wheel
@ -37,10 +38,10 @@ source: ## Build Python source distribution package
poetry build --format sdist
test: clean-tox ## Run the project testsuite(s)
tox
poetry run tox --parallelize-locked-install=10
publish: clean test wheel source ## Build and upload to pypi (requires $PYPI_API_KEY be set)
@poetry publish --username __token__ --password $(PYPI_API_KEY)
docs: clean-docs ## Build the documentation using Sphinx
tox -e docs
poetry run tox -e docs

View File

@ -1,5 +1,42 @@
# imagemonk
# dehance
HTTP server for handling image uploads and thumbnail generation.
This project requires [Poetry 1.0+](https://python-poetry.org/)
## Implementation goals
Support token based authentication:
```
POST /auth/token
GET /img/abcdefg.jpg?token=XYZ
```
Support dynamic resolution generation:
```
GET /img/abcdefg/100x50.jpg
```
Support server-side aliasing of resolutions to names:
```
GET /img/abcdefg/foobar.jpg # translates to something like 120x90
```
Support parameter-based selection of scaling method:
```
# "absolute scale horizontal", "relative scale vertical"
GET /img/abcdefg/200x100.jpg?h=abs&v=rel
```
Support both sqlite and maria storage backend
Support redis caching to relieve file system strain
Support autocleaning of cached file system files to reduce directory size
Support

View File

@ -1,9 +1,9 @@
"""Programatically accessible project metadata"""
__title__ = "imagemonk"
__title__ = "dehance"
__version__ = "0.1.0"
__authors__ = ["Ethan Paul <24588726+enpaul@users.noreply.github.com>"]
__license__ = "MIT"
__summary__ = "HTTP server for uploading images and generating thumbnails"
__url__ = "https://github.com/mocproject/imagemonk/"
__url__ = "https://github.com/mocproject/dehance/"

View File

@ -1,12 +1,12 @@
"""Development server stub entrypoint
Flask comes with a built-in development server. This entrypoint allows ``imagemonk``
Flask comes with a built-in development server. This entrypoint allows ``dehance``
to be run directly to run the development server and expose some simple config options for ease of
access. Run the below command to start the server:
::
python -m imagemonk
python -m dehance
In addition to the helpful CLI flags, the Flask development server run by this module will also
load any ``.env`` files in the current working directory when running the application.
@ -17,7 +17,7 @@ load any ``.env`` files in the current working directory when running the applic
import argparse
import sys
from imagemonk.application import APPLICATION
from dehance.application import APPLICATION
# pylint: disable=invalid-name

View File

@ -1,11 +1,11 @@
import flask
from imagemonk import __about__
from imagemonk import configuration
from imagemonk import constants
from imagemonk import database
from imagemonk import exceptions
from imagemonk.resources import ResponseHeaders
from dehance import __about__
from dehance import configuration
from dehance import constants
from dehance import database
from dehance import exceptions
from dehance.resources import ResponseHeaders
def make_the_tea() -> None:
@ -23,7 +23,7 @@ def initialize_database() -> None:
database.initialize(flask.current_app.appconfig)
class ImageMonkRequest(flask.Request):
class DehanceRequest(flask.Request):
"""Extend the default Flask request object to add custom application state settings"""
def make_response_headers(self) -> ResponseHeaders:
@ -40,14 +40,14 @@ class ImageMonkRequest(flask.Request):
}
class ImageMonkFlask(flask.Flask):
class DehanceFlask(flask.Flask):
"""Extend the default Flask object to add the custom application config
There's probably an easier/more kosher way to do this, but ¯\\_(ツ)_/¯
"""
request_class = ImageMonkRequest
request_class = DehanceRequest
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.appconfig: configuration.ImageMonkConfig = configuration.load()
self.appconfig: configuration.DehanceConfig = configuration.load()

View File

@ -1,12 +1,12 @@
import flask_restful
from imagemonk import resources
from imagemonk._server import ImageMonkFlask
from imagemonk._server import initialize_database
from imagemonk._server import make_the_tea
from dehance import resources
from dehance._server import DehanceFlask
from dehance._server import initialize_database
from dehance._server import make_the_tea
APPLICATION = ImageMonkFlask(__name__)
APPLICATION = DehanceFlask(__name__)
API = flask_restful.Api(APPLICATION, catch_all_404s=True)

View File

@ -8,7 +8,7 @@ from typing import Dict
from typing import Optional
from typing import Tuple
from imagemonk import constants
from dehance import constants
def _default_sqlite_pragmas() -> Dict[str, Any]:
@ -31,7 +31,7 @@ def _default_sqlite_pragmas() -> Dict[str, Any]:
@dataclass
class _DBSqliteConfig:
path: Path = Path.cwd() / "imagemonk.db"
path: Path = Path.cwd() / "dehance.db"
pragmas: Dict[str, Any] = field(default_factory=_default_sqlite_pragmas)
@classmethod
@ -51,7 +51,7 @@ class _DBMariaConfig:
username: str = "root"
password: Optional[str] = None
port: int = 3306
schema: str = "imagemonk"
schema: str = "dehance"
@classmethod
def build(cls):
@ -106,7 +106,7 @@ class _UploadConfig:
@dataclass
class ImageMonkConfig:
class DehanceConfig:
database: _DBConfig = field(default_factory=_DBConfig.build)
upload: _UploadConfig = field(default_factory=_UploadConfig.build)
storage_path: Path = Path.cwd()
@ -120,6 +120,6 @@ class ImageMonkConfig:
)
def load() -> ImageMonkConfig:
def load() -> DehanceConfig:
return ImageMonkConfig.build()
return DehanceConfig.build()

View File

@ -7,7 +7,7 @@ class SupportedDatabaseBackend(enum.Enum):
SQLITE = enum.auto()
HTTP_HEADER_RESPONSE_VERSION = "x-imagemonk-version"
HTTP_HEADER_RESPONSE_VERSION = "x-dehance-version"
HTTP_HEADER_RESPONSE_DIGEST = "Digest"

View File

@ -3,18 +3,18 @@ from typing import Tuple
import peewee
from imagemonk import constants
from imagemonk.configuration import ImageMonkConfig
from imagemonk.database._shared import ImageMonkModel
from imagemonk.database._shared import INTERFACE as interface
from imagemonk.database.image import ImageRecord
from imagemonk.database.thumbnail import ThumbnailRecord
from dehance import constants
from dehance.configuration import DehanceConfig
from dehance.database._shared import DehanceModel
from dehance.database._shared import INTERFACE as interface
from dehance.database.image import ImageRecord
from dehance.database.thumbnail import ThumbnailRecord
MODELS: Tuple[ImageMonkModel, ...] = (ImageRecord, ThumbnailRecord)
MODELS: Tuple[DehanceModel, ...] = (ImageRecord, ThumbnailRecord)
def initialize(config: ImageMonkConfig):
def initialize(config: DehanceConfig):
"""Initialize the database interface
Defining the database as an

View File

@ -7,7 +7,7 @@ import peewee
INTERFACE = peewee.DatabaseProxy()
class ImageMonkModel(peewee.Model):
class DehanceModel(peewee.Model):
class Meta: # pylint: disable=too-few-public-methods,missing-class-docstring
database = INTERFACE

View File

@ -4,10 +4,10 @@ from typing import List
import peewee
from imagemonk.database._shared import ImageMonkModel
from dehance.database._shared import DehanceModel
class ImageRecord(ImageMonkModel):
class ImageRecord(DehanceModel):
"""Database record for"""
width = peewee.IntegerField(null=False)

View File

@ -1,10 +1,10 @@
import peewee
from imagemonk.database._shared import ImageMonkModel
from imagemonk.database.image import ImageRecord
from dehance.database._shared import DehanceModel
from dehance.database.image import ImageRecord
class ThumbnailRecord(ImageMonkModel):
class ThumbnailRecord(DehanceModel):
parent = peewee.ForeignKeyField(ImageRecord)
width = peewee.IntegerField(null=False)

View File

@ -2,13 +2,13 @@
::
ImageMonkException
DehanceException
+-- ClientError
+-- ServerError
"""
class ImageMonkException(Exception):
class DehanceException(Exception):
"""Whomp whomp, something went wrong
But seriously, don't ever raise this exception
@ -17,7 +17,7 @@ class ImageMonkException(Exception):
status: int
class ClientError(ImageMonkException):
class ClientError(DehanceException):
"""Error while processing client side input"""
status = 400
@ -29,7 +29,7 @@ class ImageResourceDeletedError(ClientError):
status = 410
class ServerError(ImageMonkException):
class ServerError(DehanceException):
"""Error while processing server side data"""
status = 500

View File

@ -0,0 +1,19 @@
from typing import Tuple
from dehance.resources._shared import DehanceResource
from dehance.resources._shared import ResponseBody
from dehance.resources._shared import ResponseHeaders
from dehance.resources.image import Image
from dehance.resources.image import ImageUpload
from dehance.resources.openapi import OpenAPI
from dehance.resources.thumbnail import ThumbnailResize
from dehance.resources.thumbnail import ThumbnailScale
RESOURCES: Tuple[DehanceResource, ...] = (
ImageUpload,
Image,
OpenAPI,
ThumbnailScale,
ThumbnailResize,
)

View File

@ -31,7 +31,7 @@ class ResponseTuple(NamedTuple):
headers: ResponseHeaders
class ImageMonkResource(flask_restful.Resource):
class DehanceResource(flask_restful.Resource):
"""Extension of the default :class:`flask_restful.Resource` class
Add a couple of useful things to the default resource class:

View File

@ -4,13 +4,13 @@ import uuid
import flask
from imagemonk import constants
from imagemonk import database
from imagemonk import exceptions
from imagemonk.resources._shared import ImageMonkResource
from dehance import constants
from dehance import database
from dehance import exceptions
from dehance.resources._shared import DehanceResource
class ImageUpload(ImageMonkResource):
class ImageUpload(DehanceResource):
routes = ("/image/",)
@ -20,8 +20,6 @@ class ImageUpload(ImageMonkResource):
uploaded = flask.request.files["image"]
breakpoint()
if not uploaded.filename:
raise
@ -47,16 +45,14 @@ class ImageUpload(ImageMonkResource):
return None, 201
class Image(ImageMonkResource):
class Image(DehanceResource):
routes = ("/image/<string:image_id>.jpeg",)
def get(self, image_id: str):
image = database.ImageRecord.get(
database.ImageRecord.uuid
== uuid.UUID(image_id) & database.ImageRecord.format
== format
database.ImageRecord.uuid == uuid.UUID(image_id)
)
if image.deleted:

View File

@ -2,12 +2,12 @@ from pathlib import Path
from ruamel.yaml import YAML
from imagemonk.resources._shared import ImageMonkResource
from dehance.resources._shared import DehanceResource
yaml = YAML(typ="safe")
class OpenAPI(ImageMonkResource):
class OpenAPI(DehanceResource):
routes = ("/openapi.json",)

View File

@ -1,7 +1,7 @@
from imagemonk.resources._shared import ImageMonkResource
from dehance.resources._shared import DehanceResource
class ThumbnailScale(ImageMonkResource):
class ThumbnailScale(DehanceResource):
routes = ("/thumb/<string:image_id>/scale/<int:scale_width>.jpg",)
@ -9,7 +9,7 @@ class ThumbnailScale(ImageMonkResource):
raise NotImplementedError
class ThumbnailResize(ImageMonkResource):
class ThumbnailResize(DehanceResource):
routes = ("/thumb/<string:image_id>/size/<int:width>x<int:height>.jpg",)

View File

@ -7,18 +7,19 @@
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
import datetime
from pathlib import Path
BASE_DIR = Path(__file__).parent.resolve()
ABOUT = {}
with open(Path(BASE_DIR, "..", "imagemonk", "__about__.py")) as infile:
with open(Path(BASE_DIR, "..", "dehance", "__about__.py")) as infile:
exec(infile.read(), ABOUT)
# -- Project information -----------------------------------------------------
project = ABOUT["__title__"]
copyright = "2020"
copyright = str(datetime.date.today().year)
author = ", ".join(ABOUT["__authors__"])
# The full version, including alpha/beta/rc tags

View File

@ -1,11 +1,11 @@
ImageMonk
Dehance
=========
.. toctree::
:maxdepth: 2
:caption: Contents:
API Reference <imagemonk>
API Reference <dehance>

View File

@ -1,19 +0,0 @@
from typing import Tuple
from imagemonk.resources._shared import ImageMonkResource
from imagemonk.resources._shared import ResponseBody
from imagemonk.resources._shared import ResponseHeaders
from imagemonk.resources.image import Image
from imagemonk.resources.image import ImageUpload
from imagemonk.resources.openapi import OpenAPI
from imagemonk.resources.thumbnail import ThumbnailResize
from imagemonk.resources.thumbnail import ThumbnailScale
RESOURCES: Tuple[ImageMonkResource, ...] = (
ImageUpload,
Image,
OpenAPI,
ThumbnailScale,
ThumbnailResize,
)

View File

@ -2,9 +2,9 @@
openapi: "3.0.2"
info:
version: 0.1.0
title: ImageMonk
title: Dehance
description: >-
ImageMonk is a simple HTTP server that allows users to upload
Dehance is a simple HTTP server that allows users to upload
images and retrieve them at a later time. In addition, it
supports generating (and caching) scaled versions of the
uploaded images for use as thumbnails.
@ -13,7 +13,7 @@ info:
url: https://mit-license.org/
x-anchors:
DefaultHeaders: &headers-default
x-imagemonk-version:
x-dehance-version:
$ref: "#/components/headers/Version"
OptionsResponses: &responses-options
'204':

984
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,17 @@
[tool.poetry]
name = "imagemonk"
name = "dehance"
version = "0.1.0"
license = "MIT"
authors = ["Ethan Paul <24588726+enpaul@users.noreply.github.com>"]
description = "HTTP server for uploading images and generating thumbnails"
repository = "https://github.com/mocproject/imagemonk/"
repository = "https://github.com/mocproject/dehance/"
packages = [
{include = "imagemonk"},
{include = "dehance"},
{include = "tests", format = "sdist"}
]
include = [
"imagemonk/py.typed",
"imagemonk/resources/openapi.yaml"
"dehance/py.typed",
"dehance/resources/openapi.yaml"
]
keywords = ["flask", "image", "thumbnail", "hosting"]
readme = "README.md"
@ -45,10 +45,10 @@ peewee = "^3.13.3"
[tool.poetry.dev-dependencies]
bandit = "^1.6.2"
black = { git = "https://github.com/psf/black.git" }
black = "^21.4b2"
blacken-docs = "^1.9.1"
ipython = "^7.10.2"
mypy = "^0.790"
mypy = "^0.800"
pre-commit = "^2.1.1"
pre-commit-hooks = "^3.4.0"
pylint = "^2.6.0"
@ -60,7 +60,9 @@ sphinx = "^3.3.0"
sphinx-autodoc-typehints = "^1.11.1"
toml = "^0.10.1"
tox = "^3.20.0"
tox-poetry-installer = {extras = ["poetry"], version = "^0.6.1"}
tox-poetry-installer = {extras = ["poetry"], version = "^0.7.0"}
mdformat = "^0.6"
mdformat-gfm = "^0.2"
[build-system]
requires = ["poetry-core>=1.0.0"]

View File

@ -3,7 +3,7 @@ from pathlib import Path
import toml
from imagemonk import __about__
from dehance import __about__
def test_about():

12
tox.ini
View File

@ -16,7 +16,7 @@ locked_deps =
pytest-cov
toml
commands =
pytest --cov={envsitepackagesdir}/imagemonk --cov-config {toxinidir}/.coveragerc --cov-report term-missing {toxinidir}/tests/
pytest --cov={envsitepackagesdir}/dehance --cov-config {toxinidir}/.coveragerc --cov-report term-missing {toxinidir}/tests/
[testenv:static]
description = Static formatting and quality enforcement
@ -26,6 +26,8 @@ skip_install = true
locked_deps =
black
blacken-docs
mdformat
mdformat-gfm
mypy
reorder-python-imports
pre-commit
@ -33,8 +35,8 @@ locked_deps =
pylint
commands =
pre-commit run --all-files
pylint --rcfile {toxinidir}/.pylintrc {toxinidir}/imagemonk/
mypy --ignore-missing-imports --no-strict-optional {toxinidir}/imagemonk/
pylint --rcfile {toxinidir}/.pylintrc {toxinidir}/dehance/
mypy --ignore-missing-imports --no-strict-optional {toxinidir}/dehance/
[testenv:static-tests]
description = Static formatting and quality enforcement for the tests
@ -58,7 +60,7 @@ locked_deps =
safety
poetry
commands =
bandit --recursive --quiet {toxinidir}/imagemonk/
bandit --recursive --quiet {toxinidir}/dehance/
bandit --recursive --quiet --skip B101 {toxinidir}/tests/
poetry export --format requirements.txt --output {envtmpdir}/requirements.txt --without-hashes --dev
safety check --bare --file {envtmpdir}/requirements.txt
@ -70,5 +72,5 @@ locked_deps =
sphinx
sphinx-autodoc-typehints
commands =
sphinx-apidoc --no-toc --output-dir {toxinidir}/docs/ {toxinidir}/imagemonk/
sphinx-apidoc --no-toc --output-dir {toxinidir}/docs/ {toxinidir}/dehance/
sphinx-build -W -b html {toxinidir}/docs/ {toxinidir}/docs/_build