Template for professional data science and python applications made by deepsense.ai
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html

# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information

project = 'Project Template'
author = 'deepsense.ai'
copyright = f'2023, {author}'

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

extensions = [
    "sphinx.ext.napoleon",  # support for google style docstrings
    "sphinx.ext.autosummary",
    "sphinx.ext.intersphinx",
    "sphinx.ext.autosectionlabel",
    "myst_parser",
    "sphinxcontrib.mermaid",
]

templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']

# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output

html_theme = "sphinx_material"
html_static_path = ["_static"]

# Mapping to link other documentations
intersphinx_mapping = {
    'python': ('https://docs.python.org/3', None),
}


# Configuration for Napoleon extension
napoleon_google_docstring = True
napoleon_numpy_docstring = True
napoleon_include_init_with_doc = False
napoleon_include_private_with_doc = False
napoleon_include_special_with_doc = True
napoleon_use_admonition_for_examples = False
napoleon_use_admonition_for_notes = False
napoleon_use_admonition_for_references = False
napoleon_use_ivar = False
napoleon_use_param = True
napoleon_use_rtype = True
napoleon_preprocess_types = False
napoleon_type_aliases = None
napoleon_attr_annotations = True

add_module_names = False
autodoc_typehints = 'both'

html_theme_options = {
    #'nav_title': project,

    # Set the color and the accent color
    'color_primary': 'blue',
    'color_accent': 'light-blue',

    # Set the repo location to get a badge with stats
    'repo_url': 'https://github.com/deepsense-ai/ds-template/',
    'repo_name': project,

    # Visible levels of the global TOC; -1 means unlimited
    'globaltoc_depth': 3,
    # If False, expand all TOC entries
    'globaltoc_collapse': True,
    # If True, show hidden TOC entries
    'globaltoc_includehidden': False,
}

# workaround for sphinx material issue with empty left sidebar
# See: https://github.com/bashtage/sphinx-material/issues/30
html_sidebars = {
    "**": ["logo-text.html", "globaltoc.html", "localtoc.html", "searchbox.html"]
}

source_suffix = {
    '.rst': 'restructuredtext',
    '.txt': 'markdown',
    '.md': 'markdown',
}
docs/conf.py
deepsense-ai/ds-template
from pathlib import Path
import stat
print("Running post generation...")

ci = "{{ cookiecutter.ci }}"

REMOVE_PATHS = []

gitlab_files = [
    ".gitlab-ci.yml"
]

{% if cookiecutter.ci != "GitLab" %}
REMOVE_PATHS.extend(gitlab_files)
{% endif %}

print("Cleaning files... 🌀")
for path in REMOVE_PATHS:
    path = Path(path)
    if path.exists() and path.is_file():
        print(f"Clean up file: '{path}'")
        path.unlink()

#Solves problems when template fails to keep linux permissions. (e.g. after zipping template)
print("Updating permissions... 🚀")
for path in Path("").rglob("*.sh"):
    path.chmod(path.stat().st_mode | stat.S_IXUSR)
    
print("DONE 🎆")
hooks/post_gen_project.py
deepsense-ai/ds-template
import re
import sys

logo = """
██████  ███████ ███████ ██████  ███████ ███████ ███    ██ ███████ ███████     █████  ██ 
██   ██ ██      ██      ██   ██ ██      ██      ████   ██ ██      ██         ██   ██ ██ 
██   ██ █████   █████   ██████  ███████ █████   ██ ██  ██ ███████ █████      ███████ ██ 
██   ██ ██      ██      ██           ██ ██      ██  ██ ██      ██ ██         ██   ██ ██ 
██████  ███████ ███████ ██      ███████ ███████ ██   ████ ███████ ███████ ██ ██   ██ ██ 
"""
print(logo)
print("Validation of template values...")

package_name = '{{ cookiecutter.python_package_name }}'
valid_python_name = r'^[_a-zA-Z][_a-zA-Z0-9]+$'
if not re.match(valid_python_name, package_name):
    print(f"ERROR: '{package_name}' is not a valid Python module name.")
    sys.exit(1)

print("Template looks ok.")
hooks/pre_gen_project.py
deepsense-ai/ds-template
import os
from pathlib import Path
from contextlib import contextmanager
import subprocess
import shlex
import re
from typing import Sequence

class JinjaSolvedValidator:
    """Tests if all jinja templates are matched."""
    def __init__(self):
        self.brackets_matcher = re.compile("(?:\{\{|\{\%).*cookiecutter.*(?:\}\}|\%\})")

    def has_unresolved(self, content: str) -> bool:
        return self.brackets_matcher.search(content) is not None


def test_regex():
    val = JinjaSolvedValidator()
    assert val.has_unresolved('asd {{cookiecutter.foo }}{{ cookie.foo }} {{ % {{ }} }}are a') == True
    assert val.has_unresolved('{{ % {{ }} }}') == False
    assert val.has_unresolved('''maa aal
                              asd dd {%- cookiecutter.zlo -%}
                              ''') == True
    assert val.has_unresolved('}} {{') == False

@contextmanager
def change_dir(dir : Path):
    assert dir.is_dir()
    assert dir.exists()
    cwd = Path.cwd()
    try:
        os.chdir(dir)
        yield
    finally:
        os.chdir(cwd)


def run_command(command: str, dir: Path):
    with change_dir(dir):
        try:
            return subprocess.check_call(shlex.split(command))
        except subprocess.CalledProcessError as ex:
            print(ex)
            return ex.returncode


def assert_jinja_resolved(files: Sequence[Path]) -> None:
    """Asserts to make sure no curly braces appear in a file name nor in it's content.
    """
    text_files = ['.txt', '.py', '.rst', '.md', '.cfg', '.toml', '.json', '.yaml', '.yml', '.ini', '.sh']
    validator = JinjaSolvedValidator()
    for file in files:
        assert validator.has_unresolved(file.name) == False
        if file.is_file() and file.suffix in text_files:
            content = file.read_text(encoding="utf-8")
            assert validator.has_unresolved(content) == False


def test_template_project(cookies):
    result = cookies.bake(extra_context={
        "client_name": "ds.ai",
        "project_name": "hello",
        "ci": "GitLab"
        })

    assert result.exit_code == 0
    assert result.exception is None

    assert result.project_path.name == "ds-ai-hello"
    assert result.project_path.is_dir()

    rpath: Path = result.project_path
    assert_jinja_resolved(rpath.rglob("*"))


def test_template_creates_package(cookies):
    result = cookies.bake(extra_context={
        "client_name": "test",
        "project_name": "test",
        "ci": "None"
        })

    assert result.exit_code == 0
    assert result.exception is None
    rpath: Path = result.project_path
    assert run_command("./setup.py sdist", rpath) == 0, "Package creation failed!"


def test_template_project_no_gitlab(cookies):
    result = cookies.bake(extra_context={
        "client_name": "no",
        "project_name": "gitlab",
        "ci": "None"
        })
    assert result.exit_code == 0
    assert result.exception is None

    rpath: Path = result.project_path
    assert (rpath / ".gitlab-ci.yml").exists() is False


def test_template_project_with_gitlab(cookies):
    result = cookies.bake(extra_context={
        "client_name": "with",
        "project_name": "gitlab",
        "ci": "GitLab"
        })
    assert result.exit_code == 0
    assert result.exception is None

    rpath: Path = result.project_path
    assert (rpath / ".gitlab-ci.yml").exists() is True

tests/test_template.py
deepsense-ai/ds-template
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html

# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information

project = '{{ cookiecutter.project_name }}'
copyright = '2023, {{ cookiecutter.client_name }}'
author = 'deepsense.ai'

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

extensions = [
    "sphinx.ext.napoleon",# support for google style docstrings
    "sphinx.ext.autodoc", # auto* are for automatic code docs generation
    "sphinx.ext.autosummary", # as above
    "sphinx.ext.intersphinx", # allows to cross reference other sphinx documentations
    "sphinx.ext.autosectionlabel", # each doc section gets automatic reference generated
    "myst_parser", # adds support for Markdown
    "sphinxcontrib.mermaid", # allows to use Mermaid diagrams
]

templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']

# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output

html_theme = "sphinx_rtd_theme"
html_static_path = ["_static"]

# Mapping to link other documentations
intersphinx_mapping = {
    'python': ('https://docs.python.org/3', None),
    "scipy": ("https://docs.scipy.org/doc/scipy/reference/", None),
    "numpy": ("https://numpy.org/doc/stable", None),
}


# Configuration for Napoleon extension
napoleon_google_docstring = True
napoleon_numpy_docstring = True
napoleon_include_init_with_doc = False
napoleon_include_private_with_doc = False
napoleon_include_special_with_doc = True
napoleon_use_admonition_for_examples = False
napoleon_use_admonition_for_notes = False
napoleon_use_admonition_for_references = False
napoleon_use_ivar = False
napoleon_use_param = True
napoleon_use_rtype = True
napoleon_preprocess_types = False
napoleon_type_aliases = None
napoleon_attr_annotations = True

add_module_names = False
autodoc_typehints = 'both'

source_suffix = {
    '.rst': 'restructuredtext',
    '.txt': 'markdown',
    '.md': 'markdown',
}

# workaround for sphinx material issue with empty left sidebar
# See: https://github.com/bashtage/sphinx-material/issues/30
# uncomment below lines if you use: html_theme = "sphinx_material"
# html_sidebars = {
#    "**": ["logo-text.html", "globaltoc.html", "localtoc.html", "searchbox.html"]
# }

{{ cookiecutter.repo_name }}/docs/conf.py
deepsense-ai/ds-template
#!/usr/bin/env python3
"""Packaging code - boilerplate required by pip <= 21.1 for development install (-e)."""
from setuptools import setup

setup()

{{ cookiecutter.repo_name }}/setup.py
deepsense-ai/ds-template
""" {{ cookiecutter.__package_name }} """
from .__version__ import __version__

__all__ = ["__version__"]

{{ cookiecutter.repo_name }}/src/{{ cookiecutter.__package_name }}/__init__.py
deepsense-ai/ds-template
"""Version information."""
from pathlib import Path

version_file = Path(__file__).absolute().parents[0] / "VERSION"
__version__ = version_file.read_text(encoding="utf-8").strip()

{{ cookiecutter.repo_name }}/src/{{ cookiecutter.__package_name }}/__version__.py
deepsense-ai/ds-template
"""Remove this file. Required only to run coverage on empty project."""


def hello_world() -> None:
    """Says hello world."""
    print("hello world")

{{ cookiecutter.repo_name }}/src/{{ cookiecutter.__package_name }}/hello.py
deepsense-ai/ds-template
import {{ cookiecutter.__package_name }}.hello


def test_basic_test() -> None:
    {{ cookiecutter.__package_name }}.hello.hello_world()
    assert True

{{ cookiecutter.repo_name }}/tests/test_{{ cookiecutter.__package_name }}.py
deepsense-ai/ds-template