diff --git a/.gitignore b/.gitignore index 4be797b..5dae2ff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,13 @@ __pycache__ +*.egg-info +/.coverage +/.mxmake/ +/bin/ /build -*.egg-info \ No newline at end of file +/docs/html/ +/requirements-mxdev.txt +/utils/3.6/ +/utils/engine.license/ +/utils/lib/ +/utils/run +/utils/run.blend1 \ No newline at end of file diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000..3757f91 --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,5 @@ +[settings] +force_grid_wrap = 2 +multi_line_output = 3 +include_trailing_comma = false +force_alphabetical_sort = true diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..c6a3a0f --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +include *.md +recursive-include uplogic * +recursive-exclude src *.pyc *.pyo +prune tests +prune docs diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..24d22a8 --- /dev/null +++ b/Makefile @@ -0,0 +1,633 @@ +############################################################################## +# THIS FILE IS GENERATED BY MXMAKE +# +# DOMAINS: +#: core.base +#: core.mxenv +#: core.mxfiles +#: core.packages +#: docs.sphinx +#: qa.coverage +#: qa.isort +#: qa.mypy +#: qa.ruff +#: qa.test +# +# SETTINGS (ALL CHANGES MADE BELOW SETTINGS WILL BE LOST) +############################################################################## + +## core.base + +# `deploy` target dependencies. +# No default value. +DEPLOY_TARGETS?= + +# target to be executed when calling `make run` +# No default value. +RUN_TARGET?= + +# Additional files and folders to remove when running clean target +# No default value. +CLEAN_FS?=coverage_html + +# Optional makefile to include before default targets. This can +# be used to provide custom targets or hook up to existing targets. +# Default: include.mk +INCLUDE_MAKEFILE?=include.mk + +# Optional additional directories to be added to PATH in format +# `/path/to/dir/:/path/to/other/dir`. Gets inserted first, thus gets searched +# first. +# No default value. +ifeq ("$(OS)", "Windows_NT") +EXTRA_PATH?=./bin/3.6/python/bin:./bin/3.6/python/Scripts +else +EXTRA_PATH?=./bin/3.6/python/bin +endif + +## core.mxenv + +# Primary Python interpreter to use. It is used to create the +# virtual environment if `VENV_ENABLED` and `VENV_CREATE` are set to `true`. +# Default: python3 +ifeq ("$(OS)", "Windows_NT") +PRIMARY_PYTHON?=python +else +PRIMARY_PYTHON?=python3.10 +endif + +# Minimum required Python version. +# Default: 3.7 +PYTHON_MIN_VERSION?=3.10 + +# Install packages using the given package installer method. +# Supported are `pip` and `uv`. If uv is used, its global availability is +# checked. Otherwise, it is installed, either in the virtual environment or +# using the `PRIMARY_PYTHON`, dependent on the `VENV_ENABLED` setting. If +# `VENV_ENABLED` and uv is selected, uv is used to create the virtual +# environment. +# Default: pip +PYTHON_PACKAGE_INSTALLER?=pip + +# Flag whether to use a global installed 'uv' or install +# it in the virtual environment. +# Default: false +MXENV_UV_GLOBAL?=false + +# Flag whether to use virtual environment. If `false`, the +# interpreter according to `PRIMARY_PYTHON` found in `PATH` is used. +# Default: true +VENV_ENABLED?=false + +# Flag whether to create a virtual environment. If set to `false` +# and `VENV_ENABLED` is `true`, `VENV_FOLDER` is expected to point to an +# existing virtual environment. +# Default: true +VENV_CREATE?=false + +# The folder of the virtual environment. +# If `VENV_ENABLED` is `true` and `VENV_CREATE` is true it is used as the +# target folder for the virtual environment. If `VENV_ENABLED` is `true` and +# `VENV_CREATE` is false it is expected to point to an existing virtual +# environment. If `VENV_ENABLED` is `false` it is ignored. +# Default: .venv +VENV_FOLDER?=.venv + +# mxdev to install in virtual environment. +# Default: mxdev +MXDEV?=mxdev + +# mxmake to install in virtual environment. +# Default: mxmake +MXMAKE?=mxmake + +## qa.ruff + +# Source folder to scan for Python files to run ruff on. +# Default: src +RUFF_SRC?=uplogic + +## qa.isort + +# Source folder to scan for Python files to run isort on. +# Default: src +ISORT_SRC?=uplogic + +## docs.sphinx + +# Documentation source folder. +# Default: docs/source +DOCS_SOURCE_FOLDER?=docs/source + +# Documentation generation target folder. +# Default: docs/html +DOCS_TARGET_FOLDER?=docs/html + +# Documentation Python requirements to be installed (via pip). +# No default value. +DOCS_REQUIREMENTS?= + +## core.mxfiles + +# The config file to use. +# Default: mx.ini +PROJECT_CONFIG?=mx.ini + +## core.packages + +# Allow prerelease and development versions. +# By default, the package installer only finds stable versions. +# Default: false +PACKAGES_ALLOW_PRERELEASES?=false + +## qa.test + +# The command which gets executed. Defaults to the location the +# :ref:`run-tests` template gets rendered to if configured. +# Default: .mxmake/files/run-tests.sh +TEST_COMMAND?= ./utils/run tests + +# Additional Python requirements for running tests to be +# installed (via pip). +# Default: pytest +TEST_REQUIREMENTS?=pytest + +# Additional make targets the test target depends on. +# No default value. +TEST_DEPENDENCY_TARGETS?= + +## qa.coverage + +# The command which gets executed. Defaults to the location the +# :ref:`run-coverage` template gets rendered to if configured. +# Default: .mxmake/files/run-coverage.sh +COVERAGE_COMMAND?=\ + COVERAGE_PROCESS_START=setup.cfg \ + ./utils/run tests && \ + coverage report && \ + coverage html + +## qa.mypy + +# Source folder for code analysis. +# Default: src +MYPY_SRC?=src + +# Mypy Python requirements to be installed (via pip). +# Default: types-setuptools +MYPY_REQUIREMENTS?=types-setuptools + +############################################################################## +# END SETTINGS - DO NOT EDIT BELOW THIS LINE +############################################################################## + +INSTALL_TARGETS?= +DIRTY_TARGETS?= +CLEAN_TARGETS?= +PURGE_TARGETS?= +CHECK_TARGETS?= +TYPECHECK_TARGETS?= +FORMAT_TARGETS?= + +export PATH:=$(if $(EXTRA_PATH),$(EXTRA_PATH):,)$(PATH) + +# Defensive settings for make: https://tech.davis-hansson.com/p/make/ +SHELL:=bash +.ONESHELL: +# for Makefile debugging purposes add -x to the .SHELLFLAGS +.SHELLFLAGS:=-eu -o pipefail -O inherit_errexit -c +.SILENT: +.DELETE_ON_ERROR: +MAKEFLAGS+=--warn-undefined-variables +MAKEFLAGS+=--no-builtin-rules + +# mxmake folder +MXMAKE_FOLDER?=.mxmake + +# Sentinel files +SENTINEL_FOLDER?=$(MXMAKE_FOLDER)/sentinels +SENTINEL?=$(SENTINEL_FOLDER)/about.txt +$(SENTINEL): $(firstword $(MAKEFILE_LIST)) + @mkdir -p $(SENTINEL_FOLDER) + @echo "Sentinels for the Makefile process." > $(SENTINEL) + +############################################################################## +# mxenv +############################################################################## + +# Determine the executable path +ifeq ("$(VENV_ENABLED)", "true") +export VIRTUAL_ENV=$(abspath $(VENV_FOLDER)) +ifeq ("$(OS)", "Windows_NT") +VENV_EXECUTABLE_FOLDER=$(VIRTUAL_ENV)/Scripts +else +VENV_EXECUTABLE_FOLDER=$(VIRTUAL_ENV)/bin +endif +export PATH:=$(VENV_EXECUTABLE_FOLDER):$(PATH) +MXENV_PYTHON=python +else +MXENV_PYTHON=$(PRIMARY_PYTHON) +endif + +# Determine the package installer +ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv") +PYTHON_PACKAGE_COMMAND=uv pip +else +PYTHON_PACKAGE_COMMAND=$(MXENV_PYTHON) -m pip +endif + +MXENV_TARGET:=$(SENTINEL_FOLDER)/mxenv.sentinel +$(MXENV_TARGET): $(SENTINEL) + @$(PRIMARY_PYTHON) -c "import sys; vi = sys.version_info; sys.exit(1 if (int(vi[0]), int(vi[1])) >= tuple(map(int, '$(PYTHON_MIN_VERSION)'.split('.'))) else 0)" \ + && echo "Need Python >= $(PYTHON_MIN_VERSION)" && exit 1 || : + @[[ "$(VENV_ENABLED)" == "true" && "$(VENV_FOLDER)" == "" ]] \ + && echo "VENV_FOLDER must be configured if VENV_ENABLED is true" && exit 1 || : + @[[ "$(VENV_ENABLED)$(PYTHON_PACKAGE_INSTALLER)" == "falseuv" ]] \ + && echo "Package installer uv does not work with a global Python interpreter." && exit 1 || : +ifeq ("$(VENV_ENABLED)", "true") +ifeq ("$(VENV_CREATE)", "true") +ifeq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvtrue") + @echo "Setup Python Virtual Environment using package 'uv' at '$(VENV_FOLDER)'" + @uv venv -p $(PRIMARY_PYTHON) --seed $(VENV_FOLDER) +else + @echo "Setup Python Virtual Environment using module 'venv' at '$(VENV_FOLDER)'" + @$(PRIMARY_PYTHON) -m venv $(VENV_FOLDER) + @$(MXENV_PYTHON) -m ensurepip -U +endif +endif +else + @echo "Using system Python interpreter" +endif +ifeq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvfalse") + @echo "Install uv" + @$(MXENV_PYTHON) -m pip install uv +endif + @$(PYTHON_PACKAGE_COMMAND) install -U pip setuptools wheel + @echo "Install/Update MXStack Python packages" + @$(PYTHON_PACKAGE_COMMAND) install -U $(MXDEV) $(MXMAKE) + @touch $(MXENV_TARGET) + +.PHONY: mxenv +mxenv: $(MXENV_TARGET) + +.PHONY: mxenv-dirty +mxenv-dirty: + @rm -f $(MXENV_TARGET) + +.PHONY: mxenv-clean +mxenv-clean: mxenv-dirty +ifeq ("$(VENV_ENABLED)", "true") +ifeq ("$(VENV_CREATE)", "true") + @rm -rf $(VENV_FOLDER) +endif +else + @test -e $(MXENV_PYTHON) && $(PYTHON_PACKAGE_COMMAND) uninstall -y $(MXDEV) || : + @test -e $(MXENV_PYTHON) && $(PYTHON_PACKAGE_COMMAND) uninstall -y $(MXMAKE) || : +endif + +INSTALL_TARGETS+=mxenv +DIRTY_TARGETS+=mxenv-dirty +CLEAN_TARGETS+=mxenv-clean + +############################################################################## +# ruff +############################################################################## + +RUFF_TARGET:=$(SENTINEL_FOLDER)/ruff.sentinel +$(RUFF_TARGET): $(MXENV_TARGET) + @echo "Install Ruff" + @$(PYTHON_PACKAGE_COMMAND) install ruff + @touch $(RUFF_TARGET) + +.PHONY: ruff-check +ruff-check: $(RUFF_TARGET) + @echo "Run ruff check" + @ruff check $(RUFF_SRC) + +.PHONY: ruff-format +ruff-format: $(RUFF_TARGET) + @echo "Run ruff format" + @ruff format $(RUFF_SRC) + +.PHONY: ruff-dirty +ruff-dirty: + @rm -f $(RUFF_TARGET) + +.PHONY: ruff-clean +ruff-clean: ruff-dirty + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y ruff || : + @rm -rf .ruff_cache + +INSTALL_TARGETS+=$(RUFF_TARGET) +CHECK_TARGETS+=ruff-check +FORMAT_TARGETS+=ruff-format +DIRTY_TARGETS+=ruff-dirty +CLEAN_TARGETS+=ruff-clean + +############################################################################## +# isort +############################################################################## + +ISORT_TARGET:=$(SENTINEL_FOLDER)/isort.sentinel +$(ISORT_TARGET): $(MXENV_TARGET) + @echo "Install isort" + @$(PYTHON_PACKAGE_COMMAND) install isort + @touch $(ISORT_TARGET) + +.PHONY: isort-check +isort-check: $(ISORT_TARGET) + @echo "Run isort check" + @isort --check $(ISORT_SRC) + +.PHONY: isort-format +isort-format: $(ISORT_TARGET) + @echo "Run isort format" + @isort $(ISORT_SRC) + +.PHONY: isort-dirty +isort-dirty: + @rm -f $(ISORT_TARGET) + +.PHONY: isort-clean +isort-clean: isort-dirty + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y isort || : + +INSTALL_TARGETS+=$(ISORT_TARGET) +CHECK_TARGETS+=isort-check +FORMAT_TARGETS+=isort-format +DIRTY_TARGETS+=isort-dirty +CLEAN_TARGETS+=isort-clean + +############################################################################## +# sphinx +############################################################################## + +# additional targets required for building docs. +DOCS_TARGETS+= + +SPHINX_BIN=sphinx-build +SPHINX_AUTOBUILD_BIN=sphinx-autobuild + +DOCS_TARGET:=$(SENTINEL_FOLDER)/sphinx.sentinel +$(DOCS_TARGET): $(MXENV_TARGET) + @echo "Install Sphinx" + @$(PYTHON_PACKAGE_COMMAND) install -U sphinx sphinx-autobuild $(DOCS_REQUIREMENTS) + @touch $(DOCS_TARGET) + +.PHONY: docs +docs: $(DOCS_TARGET) $(DOCS_TARGETS) + @echo "Build sphinx docs" + @$(SPHINX_BIN) $(DOCS_SOURCE_FOLDER) $(DOCS_TARGET_FOLDER) + +.PHONY: docs-live +docs-live: $(DOCS_TARGET) $(DOCS_TARGETS) + @echo "Rebuild Sphinx documentation on changes, with live-reload in the browser" + @$(SPHINX_AUTOBUILD_BIN) $(DOCS_SOURCE_FOLDER) $(DOCS_TARGET_FOLDER) + +.PHONY: docs-dirty +docs-dirty: + @rm -f $(DOCS_TARGET) + +.PHONY: docs-clean +docs-clean: docs-dirty + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y \ + sphinx sphinx-autobuild $(DOCS_REQUIREMENTS) || : + @rm -rf $(DOCS_TARGET_FOLDER) + +INSTALL_TARGETS+=$(DOCS_TARGET) +DIRTY_TARGETS+=docs-dirty +CLEAN_TARGETS+=docs-clean + +############################################################################## +# mxfiles +############################################################################## + +# case `core.sources` domain not included +SOURCES_TARGET?= + +# File generation target +MXMAKE_FILES?=$(MXMAKE_FOLDER)/files + +# set environment variables for mxmake +define set_mxfiles_env + @export MXMAKE_FILES=$(1) +endef + +# unset environment variables for mxmake +define unset_mxfiles_env + @unset MXMAKE_FILES +endef + +$(PROJECT_CONFIG): +ifneq ("$(wildcard $(PROJECT_CONFIG))","") + @touch $(PROJECT_CONFIG) +else + @echo "[settings]" > $(PROJECT_CONFIG) +endif + +LOCAL_PACKAGE_FILES:=$(wildcard pyproject.toml setup.cfg setup.py requirements.txt constraints.txt) + +FILES_TARGET:=requirements-mxdev.txt +$(FILES_TARGET): $(PROJECT_CONFIG) $(MXENV_TARGET) $(SOURCES_TARGET) $(LOCAL_PACKAGE_FILES) + @echo "Create project files" + @mkdir -p $(MXMAKE_FILES) + $(call set_mxfiles_env,$(MXMAKE_FILES)) + @mxdev -n -c $(PROJECT_CONFIG) + $(call unset_mxfiles_env) + @test -e $(MXMAKE_FILES)/pip.conf && cp $(MXMAKE_FILES)/pip.conf $(VENV_FOLDER)/pip.conf || : + @touch $(FILES_TARGET) + +.PHONY: mxfiles +mxfiles: $(FILES_TARGET) + +.PHONY: mxfiles-dirty +mxfiles-dirty: + @touch $(PROJECT_CONFIG) + +.PHONY: mxfiles-clean +mxfiles-clean: mxfiles-dirty + @rm -rf constraints-mxdev.txt requirements-mxdev.txt $(MXMAKE_FILES) + +INSTALL_TARGETS+=mxfiles +DIRTY_TARGETS+=mxfiles-dirty +CLEAN_TARGETS+=mxfiles-clean + +############################################################################## +# packages +############################################################################## + +# additional sources targets which requires package re-install on change +-include $(MXMAKE_FILES)/additional_sources_targets.mk +ADDITIONAL_SOURCES_TARGETS?= + +INSTALLED_PACKAGES=$(MXMAKE_FILES)/installed.txt + +ifeq ("$(PACKAGES_ALLOW_PRERELEASES)","true") +ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv") +PACKAGES_PRERELEASES=--prerelease=allow +else +PACKAGES_PRERELEASES=--pre +endif +else +PACKAGES_PRERELEASES= +endif + +PACKAGES_TARGET:=$(INSTALLED_PACKAGES) +$(PACKAGES_TARGET): $(FILES_TARGET) $(ADDITIONAL_SOURCES_TARGETS) + @echo "Install python packages" + @$(PYTHON_PACKAGE_COMMAND) install $(PACKAGES_PRERELEASES) -r $(FILES_TARGET) + @$(PYTHON_PACKAGE_COMMAND) freeze > $(INSTALLED_PACKAGES) + @touch $(PACKAGES_TARGET) + +.PHONY: packages +packages: $(PACKAGES_TARGET) + +.PHONY: packages-dirty +packages-dirty: + @rm -f $(PACKAGES_TARGET) + +.PHONY: packages-clean +packages-clean: + @test -e $(FILES_TARGET) \ + && test -e $(MXENV_PYTHON) \ + && $(MXENV_PYTHON) -m pip uninstall -y -r $(FILES_TARGET) \ + || : + @rm -f $(PACKAGES_TARGET) + +INSTALL_TARGETS+=packages +DIRTY_TARGETS+=packages-dirty +CLEAN_TARGETS+=packages-clean + +############################################################################## +# test +############################################################################## + +TEST_TARGET:=$(SENTINEL_FOLDER)/test.sentinel +$(TEST_TARGET): $(MXENV_TARGET) + @echo "Install $(TEST_REQUIREMENTS)" + @$(PYTHON_PACKAGE_COMMAND) install $(TEST_REQUIREMENTS) + @touch $(TEST_TARGET) + +.PHONY: test +test: $(FILES_TARGET) $(SOURCES_TARGET) $(PACKAGES_TARGET) $(TEST_TARGET) $(TEST_DEPENDENCY_TARGETS) + @test -z "$(TEST_COMMAND)" && echo "No test command defined" && exit 1 || : + @echo "Run tests using $(TEST_COMMAND)" + @/usr/bin/env bash -c "$(TEST_COMMAND)" + +.PHONY: test-dirty +test-dirty: + @rm -f $(TEST_TARGET) + +.PHONY: test-clean +test-clean: test-dirty + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y $(TEST_REQUIREMENTS) || : + @rm -rf .pytest_cache + +INSTALL_TARGETS+=$(TEST_TARGET) +CLEAN_TARGETS+=test-clean +DIRTY_TARGETS+=test-dirty + +############################################################################## +# coverage +############################################################################## + +COVERAGE_TARGET:=$(SENTINEL_FOLDER)/coverage.sentinel +$(COVERAGE_TARGET): $(TEST_TARGET) + @echo "Install Coverage" + @$(PYTHON_PACKAGE_COMMAND) install -U coverage + @touch $(COVERAGE_TARGET) + +.PHONY: coverage +coverage: $(FILES_TARGET) $(SOURCES_TARGET) $(PACKAGES_TARGET) $(COVERAGE_TARGET) + @test -z "$(COVERAGE_COMMAND)" && echo "No coverage command defined" && exit 1 || : + @echo "Run coverage using $(COVERAGE_COMMAND)" + @/usr/bin/env bash -c "$(COVERAGE_COMMAND)" + +.PHONY: coverage-dirty +coverage-dirty: + @rm -f $(COVERAGE_TARGET) + +.PHONY: coverage-clean +coverage-clean: coverage-dirty + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y coverage || : + @rm -rf .coverage htmlcov + +INSTALL_TARGETS+=$(COVERAGE_TARGET) +DIRTY_TARGETS+=coverage-dirty +CLEAN_TARGETS+=coverage-clean + +############################################################################## +# mypy +############################################################################## + +MYPY_TARGET:=$(SENTINEL_FOLDER)/mypy.sentinel +$(MYPY_TARGET): $(MXENV_TARGET) + @echo "Install mypy" + @$(PYTHON_PACKAGE_COMMAND) install mypy $(MYPY_REQUIREMENTS) + @touch $(MYPY_TARGET) + +.PHONY: mypy +mypy: $(PACKAGES_TARGET) $(MYPY_TARGET) + @echo "Run mypy" + @mypy $(MYPY_SRC) + +.PHONY: mypy-dirty +mypy-dirty: + @rm -f $(MYPY_TARGET) + +.PHONY: mypy-clean +mypy-clean: mypy-dirty + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y mypy || : + @rm -rf .mypy_cache + +INSTALL_TARGETS+=$(MYPY_TARGET) +TYPECHECK_TARGETS+=mypy +CLEAN_TARGETS+=mypy-clean +DIRTY_TARGETS+=mypy-dirty + +-include $(INCLUDE_MAKEFILE) + +############################################################################## +# Default targets +############################################################################## + +INSTALL_TARGET:=$(SENTINEL_FOLDER)/install.sentinel +$(INSTALL_TARGET): $(INSTALL_TARGETS) + @touch $(INSTALL_TARGET) + +.PHONY: install +install: $(INSTALL_TARGET) + @touch $(INSTALL_TARGET) + +.PHONY: run +run: $(RUN_TARGET) + +.PHONY: deploy +deploy: $(DEPLOY_TARGETS) + +.PHONY: dirty +dirty: $(DIRTY_TARGETS) + @rm -f $(INSTALL_TARGET) + +.PHONY: clean +clean: dirty $(CLEAN_TARGETS) + @rm -rf $(CLEAN_TARGETS) $(MXMAKE_FOLDER) $(CLEAN_FS) + +.PHONY: purge +purge: clean $(PURGE_TARGETS) + +.PHONY: runtime-clean +runtime-clean: + @echo "Remove runtime artifacts, like byte-code and caches." + @find . -name '*.py[c|o]' -delete + @find . -name '*~' -exec rm -f {} + + @find . -name '__pycache__' -exec rm -fr {} + + +.PHONY: check +check: $(CHECK_TARGETS) + +.PHONY: typecheck +typecheck: $(TYPECHECK_TARGETS) + +.PHONY: format +format: $(FORMAT_TARGETS) diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..747ffb7 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/_static/rtd_dark.css b/docs/source/_static/rtd_dark.css new file mode 100644 index 0000000..0b401a7 --- /dev/null +++ b/docs/source/_static/rtd_dark.css @@ -0,0 +1,190 @@ +/*! + * @name Readthedocs + * @namespace http://userstyles.org + * @description Styles the documentation pages hosted on Readthedocs.io + * @author Anthony Post + * @homepage https://userstyles.org/styles/142968 + * @version 0.20170529055029 + * + * Modified by Aloïs Dreyfus: 20200527-1037 + * Modified by Erik Kalkoken: 20220615 + */ + +/* aum added */ +@media (prefers-color-scheme: dark) { + + .caption-text { + color: rgb(200, 200, 200); + } + + a.reference.internal { + color: rgb(240, 240, 240) !important; + background-color: rgb(30, 30, 30) !important; + } + + a.reference.internal.current { + color: rgb(200, 200, 200) !important; + background-color: rgb(50, 50, 50) !important; + } + + .admonition { + background-color: #2d2d2d !important; + } + + .admonition-title { + color: rgb(40, 40, 40) !important; + } + + .important .admonition-title { + background-color: #FF5A00 !important; + } + + .warning .admonition-title { + background-color: #FF302A !important; + } + + .seealso .admonition-title { + background-color: rgb(100, 100, 100) !important; + } + + .nt, .nf { + color: lightblue !important; + } + +/* Draws a box around menuselection role. */ +/* Temporary fix for "theme(0.2.5b2)" bug: no box around 'kbd' role. */ + .menuselection, .kbd.docutils.literal { + font-size: 90%; + font-weight: normal; + background-color: rgba(50, 50, 50, 0.65) !important; + color: white !important; + border: solid #E1E4E5 1px; + white-space: nowrap; + padding: 2px 5px; + } + + /* 'kbd' role. */ + .kbd.docutils.literal { + font-family: "Lato", "proxima-nova", + "Helvetica Neue", Arial, sans-serif; + font-size: 85%; + font-weight: normal; + color: white !important; + } + + figcaption { + margin-top: 10px !important; + } + + figure { + margin-bottom: 5px !important; + } + + .wy-body-for-nav { + text-align: justify; + background-color: rgb(40, 40, 40) !important; + } + + h3 { + margin-top: 5px !important; + } + + /* aum added end*/ + + /* .wy-menu-vertical li code { */ + /* color: rgba(200, 200, 200, 1.0) !important; */ + /* } */ + + /* .wy-menu-vertical .xref { */ + /* color: #2980B9 !important; */ + /* } */ + + a:visited { + color: #bf84d8; + } + + .rst-content .refbox .admonition-title{ + background-color: rgb(255, 90, 00) !important; + color: yellow; + } + + pre, .gh, .docutils.literal.notranslate { + background-color: rgb(40, 40, 40) !important; + color: white !important; + } + + .wy-nav-content { + background-color: rgb(30, 30, 30) !important; + color: rgb(240, 240, 240) !important; + } + + .method dt, .class dt, .data dt, .attribute dt, .function dt, + .descclassname, .descname { + background-color: #525252 !important; + color: white !important; + } + + /* .toc-backref { */ + /* background-color: blue !important; */ + /* /* color: grey !important; */ + /* } */ + + code.literal { + background-color: #2d2d2d !important; + border: 1px solid #6d6d6d !important; + } + + /* .wy-nav-content-wrap { */ + /* background-color: rgba(0, 0, 0, 1.0) !important; */ + /* } */ + + /* .sidebar { */ + /* background-color: #191919 !important; */ + /* } */ + + /* .sidebar-title { */ + /* background-color: #2b2b2b !important; */ + /* } */ + + .xref, .py-meth { + color: #7ec3e6 !important; + } + + /* .wy-side-nav-search { */ + /* background-color: rgb(80, 80, 80) !important; */ + /* /* background-color: inherit; */ + /* /* border-bottom: 1px solid #fcfcfc; */ + /* } */ + + .wy-table thead, .rst-content table.docutils thead, .rst-content table.field-list thead { + background-color: #b9b9b9; + } + + .wy-table thead th, .rst-content table.docutils thead th, .rst-content table.field-list thead th { + color: rgb(200, 200, 200); + border: solid 2px #e1e4e5; + } + + .wy-table thead p, .rst-content table.docutils thead p, .rst-content table.field-list thead p { + margin: 0; + } + + .wy-table-odd td, .wy-table-striped tr:nth-child(2n-1) td, .rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td { + background-color: #343131; + } + + .highlight .m { + color: inherit + } + + /* Literal.Number */ + .highlight .nv { + color: #3a7ca8 + } + + /* Name.Variable */ + .rst-content .section .admonition ul { + margin-bottom: 0; + } + +} diff --git a/docs/source/_static/theme_overrides.css b/docs/source/_static/theme_overrides.css new file mode 100644 index 0000000..85015c0 --- /dev/null +++ b/docs/source/_static/theme_overrides.css @@ -0,0 +1,497 @@ +/* + * This stylesheet is applied after the theme's default one, + * and thus any overrides or additions can be added here. + * + * More info: + * https://www.sphinx-doc.org/en/master/extdev/appapi.html#sphinx.application.Sphinx.add_css_file + */ +/* aum added */ +/* --- aum dark ------------------------------------------------------------- */ +@media (prefers-color-scheme: dark) { +/* Draws a box around menuselection & kbd & code & logic node role. */ + .socket { + line-height: 4em !important; + } + .center { text-align: center; } + .menuselection, + .kbd.docutils.literal.notranslate, + code.docutils.literal.notranslate, + .ln, + .py { + font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif !important; + font-size: 90% !important; + font-weight: normal !important; + background-color: #101010 !important; + color: white !important; + white-space: normal !important; + /* white-space: nowrap !important; */ + padding: 2px 4px !important; + border: 1px solid #40a6e5 !important; + border-radius: 4px !important; + } + .kbd.docutils.literal.notranslate { + border: 1px solid #529952 !important; + box-shadow: none !important; + font-size: 90% !important; + } + .kbd.compound .docutils.notranslate { + border: none !important; + margin: -2px !important; + font-size: 100% !important; + } + code.docutils.literal.notranslate { + border: 1px solid #b97e4d !important; + font-size: 90% !important; + } +/* --- aum : custom '.. role:: ln' --------------------------------------- */ + .ln { + border: 1px solid #83314a !important; + background-color: transparent !important; + font-size: 90% !important; + } +/* --- aum : custom '.. role:: py' --------------------------------------- */ + .py { + border: 1px solid #ffcf3f !important; + background-color: transparent !important; + font-size: 90% !important; + } +/* --- aum : custom '.. role:: py' end ----------------------------------- */ + a.reference.internal:visited { + color: #999999 !important; + } + a.reference.internal:hover { + color: #40a6e5 !important; + } + code.xref.docutils.literal.notranslate:hover { + color: #40a6e5 !important; + } + a.reference.internal { + color: #9f61b9 !important; + background-color: #1e1e1e !important; + } + a.reference.internal.current { + color: rgb(200, 200, 200) !important; + background-color: rgb(50, 50, 50) !important; + } + .caption-text { + color: rgb(200, 200, 200); + } + .admonition { + background-color: #2d2d2d !important; + } + .admonition a.reference.internal { + background-color: transparent !important; + } + .admonition-title { + color: rgb(40, 40, 40) !important; + } + .important .admonition-title { + background-color: #FF5A00 !important; + } + .warning .admonition-title { + background-color: #FF302A !important; + } + .seealso .admonition-title { + background-color: rgb(100, 100, 100) !important; + } + .nt, .nf { + color: lightblue !important; + } + figcaption { + margin-bottom: -10px !important; + margin-top: 7px !important; + padding-left: 10px !important; + padding-right: 10px !important; + } + figure { + margin-bottom: -10px !important; + margin-top: 10px !important; + /*border: 1px solid #606060;*/ + /*padding: 5px;*/ + } + .wy-body-for-nav { + text-align: justify; + background-color: rgb(40, 40, 40) !important; + } + h3 { + margin-top: 5px !important; + } + .rst-content .refbox .admonition-title{ + background-color: rgb(255, 90, 00) !important; + color: yellow; + } + pre, .gh, .docutils.literal.notranslate { + background-color: rgb(40, 40, 40) !important; + color: white !important; + } + .wy-nav-content { + background-color: rgb(30, 30, 30) !important; + color: rgb(240, 240, 240) !important; + } + .method dt, .class dt, .data dt, .attribute dt, .function dt, + .descclassname, .descname { + background-color: #525252 !important; + color: white !important; + } + .xref, .py-meth { + color: #7ec3e6 !important; + } + .wy-table thead, .rst-content table.docutils thead, .rst-content table.field-list thead { + background-color: #b9b9b9; + } + .wy-table thead th, .rst-content table.docutils thead th, .rst-content table.field-list thead th { + color: #404040; + border: solid 2px #e1e4e5; + } + .wy-table thead p, .rst-content table.docutils thead p, .rst-content table.field-list thead p { + margin: 0; + } + .wy-table-odd td, .wy-table-striped tr:nth-child(2n-1) td, .rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td { + background-color: #343131; + } + td a.reference.internal { + background-color: transparent !important; + } + .highlight .m { + color: inherit + } + /* Literal.Number */ + .highlight .nv { + color: #3a7ca8 + } + /* Name.Variable */ + .rst-content .section .admonition ul { + margin-bottom: 0; + } +} +/* --- aum dark mode end --------------------------------------------------------------------------------- */ +h5 {margin-bottom: 5px} +/* Sidebar menu links. */ +.wy-menu-vertical li.toctree-l2.current li.toctree-l3 > a:hover {background: #c0c0c0} +.wy-menu-vertical li.toctree-l3.current li.toctree-l4 > a:hover {background: #b5b5b5} +.wy-menu-vertical li.toctree-l4 {font-size: 1em} +.wy-menu-vertical li.current a {border: 0} +.wy-side-nav-search > a:hover {background: none; opacity: 0.9} +.wy-side-nav-search > a.icon::before {content: none} +/* 'kbd' role. */ +.kbd.docutils.literal { + font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; + font-size: 85%; + font-weight: normal; + color: #404040; +} +/* Ensure background of input fields is light color even when a local theme wants it to be dark. */ +input[type="text"], +input[type="search"], +input[type="password"], +input[type="email"], +input[type="url"], +input[type="date"], +input[type="month"], +input[type="week"], +input[type="time"], +input[type="datetime"], +input[type="datetime-local"], +input[type="number"], +input[type="tel"], +input[type="color"] { + background-color:#FCFCFC; +} +/* Fixes overlay of "align-right" images on block. */ +/* aum : does not work as expected ; workaround - move admonitions to the bottom of text */ +.rst-content .admonition {overflow: auto} +/* Boxed paragraphs. */ +.rst-content .refbox .admonition-title {background-color: #bbb} +.rst-content .refbox { + -webkit-font-smoothing: antialiased; + background-color: #e3e3e3; + line-height: 24px; + margin-bottom: 24px; padding: 12px; +} +.rst-content .seealso .admonition-title {background-color: #7a87e6} +.rst-content .seealso {background-color: #e7ebfa} +.rst-content .important .admonition-title {background-color: #ddca3b} +.rst-content .important {background-color: #f6f3a5} +/* refbox =, seealso ( > ), note ( i ), tip i , hint (+), warn / ! \ */ +.refbox .admonition-title::before {content:"\f00b"} +.seealso .admonition-title::before{content:"\f138"} +.note .admonition-title::before{content:"\f05a"} +.tip .admonition-title::before{content:"\f129"; width: 0.75em; text-align: center} +.hint .admonition-title::before{content:"\f055"} +.warning .admonition-title::before{content:"\f071"} +/* 'refbox' field. */ +.refbox .field-list .field-name, .refbox .field-list .field-body { + padding: 0px; +} +.refbox dl dt {font-weight: normal} +/* Ugly 'red' literals. */ +.rst-content tt.literal, .rst-content tt.literal, .rst-content code.literal { + color:#606060; +} +/* Literal blocks that use too much padding, make them look like regular literals. */ +/* +.rst-content pre.literal-block { + font-size: 75%; + margin:0; + padding:2px 4px; + color:#404040; + background: #ffffff; + white-space: normal; + display: inline; +} */ +/* Fix definitions with different ids. */ +.rst-content dl.simple { + margin-bottom: 0px; +} +/* Fix nested block spacing. */ +.rst-content .document dl dt, +.rst-content dd dl, +.rst-content dl.field-list dd > p { + margin: 0; +} +/* Dont indent field lists */ +.rst-content dl.field-list dt { + padding-left: 0 !important; +} +/* Fix padding for normal definitions nested in field lists */ +.rst-content dl.field-list dd > dl.simple { + padding-top: 12px; +} +/* Without this, paragraphs in bullet points within definition lists have too much vertical padding. */ +.rst-content li > p { + margin-bottom: 0px !important; +} +/* TABLE & FIGURE */ +/* Moves captions to bottom. */ +table {caption-side: bottom} +/* captions text style */ +.rst-content .figure .caption, +.rst-content table.docutils caption, +.rst-content table.field-list caption { + font: italic 90%/18px Lato, proxima-nova, 'Helvetica Neue', Arial, sans-serif; + color: #808080; +} +/* Captions top padding. */ +.rst-content .figure .caption { margin: 4px; } +.rst-content table.docutils caption { padding: 0.5em; } +/* Text word wrap. */ +.wy-table-responsive table td, +.wy-table-responsive table th { white-space: normal; } +/* Cell's vertical align. */ +/* use "valign" class for middle align. */ +.rst-content table.docutils:not(.valign) td { vertical-align: baseline; } +/* Field list align. */ +.rst-content table.field-list td { padding-top: 8px; } +/* Perma-link to table, hide until hover. */ +.rst-content table.docutils caption .headerlink { visibility: hidden; } +.rst-content table.docutils caption:hover .headerlink { visibility: visible; } +/* Table header cells border color. */ +.rst-content table.docutils th { border-color: #e1e4e5; } +/* Figure in table margin. */ +.rst-content td div.figure { + margin-top: 4px; margin-bottom: 0; +} +/* Figure legend. */ +.legend { + font-size: 90%; + color: #484848; + margin-top: -20px; +} +.rst-content dl .legend { margin-top: -10px; } +@media screen and (max-width: 768px){ + .wy-table-responsive table:not(.footnote) { min-width: 520px; } + .rst-content table.docutils caption { text-align: left; } + img { width: auto; } +} +/* End TABLE & FIGURE. */ +/* Video center. */ +iframe { + display: block; + margin: 0 auto 24px auto; +} +/* Copyright font scale down. */ +footer p{ font-size: smaller} +.footer-contribute { + display: block; + font-size: smaller; + margin-top: -12px +} +.footer-contribute li { + display: inline; + list-style-type: none; + padding-right: 20px; +} +/* Breadcrumbs "Docs" to icon. */ +.wy-breadcrumbs-aside a { + position: absolute; + top: 2px; + right: 0; +} +.wy-breadcrumbs li:first-child a { + line-height: 0; + font-size: 0; +} +.wy-breadcrumbs li:first-child a::before { + content: "\f015"; + font: 16px/1 FontAwesome; +} +/* Spacing bugfix. */ +.wy-breadcrumbs li a:first-child{padding: 5px} +.wy-breadcrumbs li:first-child a{padding-left: 0} +.wy-breadcrumbs li:nth-last-child(2){padding-left: 5px} +/* Block-quote > dl bugfix. */ +.rst-content dl{line-height:normal} +/* multi-paragraph dl bugfix */ +.rst-content dl p{line-height:inherit} +/* Quotes for Fig. "link". */ +a[href^="#fig-"]::before {content: "\201c";} +a[href^="#fig-"]::after {content: "\201d";} +/* Intermediate headline. */ +.rubric {font-family: "Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif} +/* ".. container::" lead, block text float around image. */ +.lead { + clear: both; width: 100%; +} +/* Mark external links. */ +a.external {color:#656AE0;} +/* List blender.org as internal. */ +.external[href^="https://www.blender.org"], .external[href^="https://docs.blender.org"], .external[href^="https://wiki.blender.org"] { + color:#2980B9; +} +/* Draws a box around menuselection role. */ +/* Temporary fix for "theme(0.2.5b2)" bug: no box around 'kbd' role. */ +.menuselection, .kbd.docutils.literal { + font-size: 90%; + font-weight: normal; + background-color: rgba(255, 255, 255, 0.65); + border: solid #E1E4E5 1px; + white-space: nowrap; + padding: 2px 5px; +} +@media screen and (max-width: 420px) { + .menuselection {white-space: normal} +} +.caption .menuselection { + background-color: transparent; + border: none; +} +.caption .kbd {background-color: transparent} +/* Remove indent on line break. */ +.rst-content .line-block {margin-left: 0px} +/* Applied on main index:sections. */ +/* Start section description. */ +@media screen and (min-width: 450px){ + .tocdescr { + display: flex; display: -webkit-flex; + flex-flow: row wrap; -webkit-flex-flow: row wrap; + justify-content: space-between; -webkit-justify-content: space-between; + align-items: flex-start; -webkit-align-items: flex-start; + align-content: flex-start; -webkit-align-content: flex-start; + list-style-type: none; + margin-bottom: 10px; + } + /* Dan Andreasson on Stack Overflow. */ + .tocdescr:after { + content: ""; + flex: 1 0 33.3%; -webkit-flex: 1 0 33.3%; + margin-left: 45px; + align-self: stretch; -webkit-align-self: stretch; + } +} +@media screen and (max-width: 450px) { + .tocdescr { + display: flex; display: -webkit-flex; + flex-flow: column wrap; -webkit-flex-flow: column wrap; + justify-content: space-between; -webkit-justify-content: space-between; + align-items: flex-start; -webkit-align-items: flex-start; + align-content: flex-start; -webkit-align-content: flex-start; + list-style-type: none; + margin-bottom: 10px; + } + .tocdescr:after { + content: ""; + flex: none; -webkit-flex: none; + } +} +.descr { + flex: 2 0 33.3%; -webkit-flex: 2 0 33.3%; + margin: 10px 15px; + border-radius: .3em; + user-select: none; +} +.descr div.figure { + margin-bottom: 0px; + display: block; +} +.descr img { + border-top-left-radius: .3em; + border-top-right-radius: .3em; +} +.descr dl {margin-bottom: 10px} +.descr dl dt > a { + display: block; + width: 100%; + margin-bottom: 10px; +} +.descr dl dt a em, .descr dl dt a span{ + font-weight: bold; + font-style: normal; + font-size: 1.3em; +} +.descr dl dt{padding: 18px 15px 0px!important} +.descr dl dd{ + padding: 0px 15px; + font-style: normal; + margin: 0px; + color: #808080; + font-size: 90%; +} +.descr { + box-shadow: + rgba(0,0,0,0.05) 0px 1px 4px 0px, + rgba(211,216,223,0.33) 0px 15px 20px -1px; +} +#getting-started .descr { + box-shadow: none; +} +/* End section description. */ +/* Start custom toctree. */ +.toctree-wrapper .toctree-l1 > a {margin-bottom: 0.15em} +/* Indent all lines following the first. */ +.toctree-wrapper * a { + display: block; + width: 90%; + text-indent: -1em; + margin-left: 1em;/*invert indent*/ + padding-top: 0.25em; + line-height: 1.25em; +} +/* Underline provided by nested ul (not li). */ +.toctree-wrapper * ul { + padding-left: 2em; + border-top: solid #ececec 1px; +} +.toctree-wrapper > ul {margin-left: 1em} +.rst-content .toctree-wrapper ul li ul { + margin-bottom: 0.75em; + padding-top: 0.5em; +} +.rst-content .toctree-wrapper ul li a:hover {color: #25afef} +.rst-content .toctree-wrapper ul li a:visited:hover {color: #C961DA} +.toctree-wrapper .toctree-l1 > a{font-size: 104%} +.toctree-wrapper .toctree-l2 > a{font-size: 102%} +.toctree-wrapper .toctree-l3 > a{font-size: 100%} +.toctree-wrapper .toctree-l1 > ul{border-color: #bfbfbf} +.toctree-wrapper .toctree-l2 > ul{border-color: #e1e0e0} +.toctree-wrapper .toctree-l3 > ul{border-color: #ececec} +/* Remove list styling, css rule hierarchy. */ +.rst-content .toctree-wrapper ul li, .rst-content .toctree-wrapper ul li li , .rst-content .toctree-wrapper ul li li li { + list-style-type: none; + margin-left: 0px; +} +/* End custom toctree. */ +/* Start genindex consistency. */ +.genindextable * strong {font-weight: normal} +.genindex-jumpbox {margin-bottom: 1.245em} +.indextable {margin-bottom: 1.245em} +/* End genindex consistency. */ diff --git a/docs/source/_static/theme_overrides_blender.css b/docs/source/_static/theme_overrides_blender.css new file mode 100644 index 0000000..563732e --- /dev/null +++ b/docs/source/_static/theme_overrides_blender.css @@ -0,0 +1,508 @@ +/* + * This stylesheet is applied after the theme's default one, + * and thus any overrides or additions can be added here. + * + * More info: + * https://www.sphinx-doc.org/en/master/extdev/appapi.html#sphinx.application.Sphinx.add_css_file + */ + +h5 { + margin-bottom: 5px +} + +/* Sidebar menu links. */ +.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a:hover { + background: #c0c0c0 +} + +.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a:hover { + background: #b5b5b5 +} + +.wy-menu-vertical li.toctree-l4 { + font-size: 1em +} + +.wy-menu-vertical li.current a { + border: 0 +} + +.wy-side-nav-search>a:hover { + background: none; + opacity: 0.9 +} + +.wy-side-nav-search>a.icon::before { + content: none +} + +.wy-menu-vertical p.caption { + color: #e6e6e6; +} + +/* Ensure background of input fields is light color even when a local theme wants it to be dark. */ +input[type="text"], +input[type="search"], +input[type="password"], +input[type="email"], +input[type="url"], +input[type="date"], +input[type="month"], +input[type="week"], +input[type="time"], +input[type="datetime"], +input[type="datetime-local"], +input[type="number"], +input[type="tel"], +input[type="color"] { + background-color: #FCFCFC; +} + +/* Boxed paragraphs. */ +.rst-content .refbox .admonition-title { + background-color: #bbb +} + +.rst-content .refbox { + background-color: #e3e3e3 +} + +.rst-content .seealso .admonition-title { + background-color: #7a87e6 +} + +.rst-content .seealso { + background-color: #e7ebfa +} + +.rst-content .important .admonition-title { + background-color: #ddca3b +} + +.rst-content .important { + background-color: #f6f3a5 +} + +/* refbox =, seealso ( > ), note ( i ), tip i , hint (+), warn / ! \ */ +.refbox .admonition-title::before { + content: "\f00b" +} + +.seealso .admonition-title::before { + content: "\f138" +} + +.note .admonition-title::before { + content: "\f05a" +} + +.tip .admonition-title::before { + content: "\f129"; + width: 0.75em; + text-align: center +} + +.hint .admonition-title::before { + content: "\f055" +} + +.warning .admonition-title::before { + content: "\f071" +} + +/* 'refbox' field. */ +.refbox .field-list .field-name, +.refbox .field-list .field-body { + padding: 0px; +} + +.refbox dl dt { + font-weight: normal +} + +/* Ugly 'red' literals. */ +.rst-content tt.literal, +.rst-content tt.literal, +.rst-content code.literal { + color: #404040; + border-radius: .4em; +} + +/* Literal blocks that use too much padding, make them look like regular literals. */ +.rst-content pre.literal-block { + font-size: 75%; + margin: 0; + padding: 2px 4px; + color: #404040; + background: #ffffff; + white-space: normal; + display: inline; + border-radius: .4em; +} + +/* Fix definisions with different ids. */ +.rst-content dl.simple { + margin-bottom: 0px; +} + +/* Fix nested block spacing. */ +.rst-content .document dl dt, +.rst-content dd dl, +.rst-content dl.field-list dd>p { + margin: 0; +} + +/* Dont indent field lists */ +.rst-content dl.field-list dt { + padding-left: 0 !important; +} + +/* Fix padding for normal definitions nested in field lists */ +.rst-content dl.field-list dd>dl.simple { + padding-top: 12px; +} + +/* Without this, paragraphs in bullet points within definition lists have too much vertical padding. */ +.rst-content li>p { + margin-bottom: 0px !important; +} + +/* TABLE & FIGURE */ + +/* captions text style */ +.rst-content .figure .caption, +.rst-content figure figcaption>p, +.rst-content table.docutils caption, +.rst-content table.field-list caption { + font: italic 90%/18px Lato, proxima-nova, 'Helvetica Neue', Arial, sans-serif; + color: #808080; +} + +/* Captions top padding. */ +.rst-content .figure .caption, +.rst-content figure figcaption { + margin-top: 4px; +} + +.rst-content table.docutils caption { + padding: 0.5em; +} + +/* Text word wrap. */ +.wy-table-responsive table td, +.wy-table-responsive table th { + white-space: normal; +} + +/* Cell's vertical align. */ +/* use "valign" class for middle align. */ +.rst-content table.docutils:not(.valign) td { + vertical-align: baseline; +} + +/* Field list align. */ +.rst-content table.field-list td { + padding-top: 8px; +} + +/* Table header cells border color. */ +.rst-content table.docutils th { + border-color: #e1e4e5; +} + +/* Figure in table margin. */ +.rst-content td div.figure, +.rst-content td figure { + margin-top: 4px; + margin-bottom: 0; +} + +/* Figure legend. */ +.legend { + font-size: 90%; + color: #484848; + margin-top: -20px; +} + +.rst-content dl .legend { + margin-top: -10px; +} + +@media screen and (max-width: 768px) { + + .wy-table-responsive table:not(.footnote) { + min-width: 520px; + } + + .rst-content table.docutils caption { + text-align: left; + } + + img { + width: auto; + } + +} + +/* End TABLE & FIGURE. */ + +/* Video center. */ +iframe { + display: block; + margin: 0 auto 24px auto; + border: 0; + max-width: 100%; +} + +/* Copyright font scale down. */ +footer p { + font-size: smaller +} + +.footer-contribute { + display: block; + font-size: smaller; + margin-top: -12px +} + +.footer-contribute li { + display: inline; + list-style-type: none; + padding-right: 20px; +} + +/* Quotes for Fig. "link". */ +a[href^="#fig-"]::before { + content: "\201c"; +} + +a[href^="#fig-"]::after { + content: "\201d"; +} + +/* Intermediate headline. */ +.rubric { + font-family: "Roboto Slab", "ff-tisa-web-pro", "Georgia", Arial, sans-serif +} + +/* ".. container::" lead, block text float around image. */ +.lead { + clear: both; + width: 100%; +} + +/* Mark external links. */ +a.external { + color: #656AE0; +} + +/* List blender.org as internal. */ +.external[href^="https://www.blender.org"], +.external[href^="https://docs.blender.org"], +.external[href^="https://wiki.blender.org"] { + color: #2980B9; +} + +/* Draws a box around menuselection role. */ +.menuselection { + font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; + font-size: 90%; + font-weight: normal; + background-color: rgba(255, 255, 255, 0.65); + border: solid #E1E4E5 1px; + padding: 2px 5px; + border-radius: .4em; +} + +@media screen and (max-width: 420px) { + .menuselection { + white-space: normal + } +} + +.caption .menuselection { + background-color: transparent; + border: none; +} + +/* Remove indent on line break. */ +.rst-content .line-block { + margin-left: 0px +} + +/* Applied on main index:sections. */ + +.global-index-toc { + display: none; +} + +/* Start section cards. */ +.toc-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(225px, 1fr)); + grid-gap: 20px; + list-style-type: none; + margin-bottom: 24px; +} + +.card { + border-radius: .3em; + user-select: none; +} + +.card div.figure, +.card figure { + margin-bottom: 0px; + display: block; +} + +.card img { + border-top-left-radius: .3em; + border-top-right-radius: .3em; +} + +.card dl { + margin-bottom: 10px +} + +.card dl dt>a { + display: block; + width: 100%; + margin-bottom: 10px; +} + +.card dl dt a em, +.card dl dt a span { + font-weight: bold; + font-style: normal; + font-size: 1.3em; +} + +.card dl dt { + padding: 18px 15px 0px !important +} + +.card dl dd { + padding: 0px 15px 5px 15px; + font-style: normal; + margin: 0px; + color: #808080; + font-size: 90%; +} + +.card { + box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 4px 0px, + rgba(211, 216, 223, 0.33) 0px 15px 20px -1px; +} + +#getting-started .card { + box-shadow: none; +} + +/* End section cards. */ + +/* Start custom toctree. */ +.toctree-wrapper .toctree-l1>a { + margin-bottom: 0.15em +} + +/* Indent all lines following the first. */ +.toctree-wrapper * a { + display: block; + width: 90%; + text-indent: -1em; + margin-left: 1em; + /*invert indent*/ + padding-top: 0.25em; + line-height: 1.25em; +} + +/* Underline provided by nested ul (not li). */ +.toctree-wrapper * ul { + padding-left: 2em; + border-top: solid #ececec 1px; +} + +.toctree-wrapper>ul { + margin-left: 1em +} + +.rst-content .toctree-wrapper ul li ul { + margin-bottom: 0.75em; + padding-top: 0.5em; +} + +.rst-content .toctree-wrapper ul li a:hover { + color: #25afef +} + +.rst-content .toctree-wrapper ul li a:visited:hover { + color: #C961DA +} + +.toctree-wrapper .toctree-l1>a { + font-size: 104% +} + +.toctree-wrapper .toctree-l2>a { + font-size: 102% +} + +.toctree-wrapper .toctree-l3>a { + font-size: 100% +} + +.toctree-wrapper .toctree-l1>ul { + border-color: #bfbfbf +} + +.toctree-wrapper .toctree-l2>ul { + border-color: #e1e0e0 +} + +.toctree-wrapper .toctree-l3>ul { + border-color: #ececec +} + +/* Remove list styling, css rule hierarchy. */ +.rst-content .toctree-wrapper ul li, +.rst-content .toctree-wrapper ul li li, +.rst-content .toctree-wrapper ul li li li { + list-style-type: none; + margin-left: 0px; +} + +/* End custom toctree. */ + +/* Start genindex consistency. */ +.genindextable * strong { + font-weight: normal +} + +.genindex-jumpbox { + margin-bottom: 1.245em +} + +.indextable { + margin-bottom: 1.245em +} + +/* End genindex consistency. */ + +/* Correctly display keyboard shortcuts inside definition lists (theme.css explicitly excludes this case for some reason) */ +dt>kbd { + font-weight: normal; + font-size: 80%; + background-color: #fff; + border: 1px solid #a6a6a6; + border-radius: 4px; + box-shadow: 0 2px grey; + padding: 2.4px 6px; + margin: auto 0; +} + +/* Fixes to field list, see #104636 */ + +.field-list { + grid-template-columns: fit-content(24px) auto !important; +} \ No newline at end of file diff --git a/docs/source/_static/upbge_logo.png b/docs/source/_static/upbge_logo.png new file mode 100644 index 0000000..5558962 Binary files /dev/null and b/docs/source/_static/upbge_logo.png differ diff --git a/docs/source/animation.rst b/docs/source/animation.rst new file mode 100644 index 0000000..967da35 --- /dev/null +++ b/docs/source/animation.rst @@ -0,0 +1,6 @@ +uplogic.animation +================= + +.. autoclass:: uplogic.animation.ULAction + +.. autoclass:: uplogic.animation.Action diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..f0621f5 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,40 @@ +# 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 = 'uplogic' +copyright = '2024, Leopold Auersperg-Castell' +author = 'Leopold Auersperg-Castell' +release = '2.1' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx_rtd_theme' +] + +templates_path = ['_templates'] +exclude_patterns = [] + + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_logo = '_static/upbge_logo.png' +html_title = 'UPLOGIC Manual' +html_theme = 'sphinx_rtd_theme' +html_static_path = ['_static'] +html_theme_options = { + "navigation_with_keys": True, + # included in the title + "display_version": False, + "collapse_navigation": True, # slows build down; useful + "navigation_depth": 3, +} diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..3536502 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,20 @@ +.. uplogic documentation master file, created by + sphinx-quickstart on Wed Apr 10 16:04:15 2024. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to uplogic's documentation! +=================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + Animation + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/include.mk b/include.mk new file mode 100644 index 0000000..38909cf --- /dev/null +++ b/include.mk @@ -0,0 +1,62 @@ +############################################################################## +# upbge +############################################################################## + +ifeq ("$(OS)", "Windows_NT") +UPBGE_VERSION=upbge-0.36.1-windows-x86_64 +UPBGE_TARBALL=$(UPBGE_VERSION).7z +else +UPBGE_VERSION=upbge-0.36.1-linux-x86_64 +UPBGE_TARBALL=$(UPBGE_VERSION).tar.xz +endif +UPBGE_URL=https://github.com/UPBGE/upbge/releases/download/v0.36.1/$(UPBGE_TARBALL) +UPBGE_DONWLOAD_FOLDER?=$(MXMAKE_FOLDER)/downloads + +UPBGE_DOWNLOAD_TARGET:=$(SENTINEL_FOLDER)/upbge-download.sentinel +$(UPBGE_DOWNLOAD_TARGET): $(SENTINEL) + @echo "Download upbge tarball" + @mkdir -p $(UPBGE_DONWLOAD_FOLDER) + @wget -P $(UPBGE_DONWLOAD_FOLDER) $(UPBGE_URL) + @touch $(UPBGE_DOWNLOAD_TARGET) + +UPBGE_EXTRACT_TARGET:=$(SENTINEL_FOLDER)/upbge-extract.sentinel +$(UPBGE_EXTRACT_TARGET): $(UPBGE_DOWNLOAD_TARGET) +ifeq ("$(OS)", "Windows_NT") + @7z x $(UPBGE_DONWLOAD_FOLDER)/$(UPBGE_TARBALL) +else + @tar xf $(UPBGE_DONWLOAD_FOLDER)/$(UPBGE_TARBALL) -C . +endif + @mv $(UPBGE_VERSION) bin + @touch $(UPBGE_EXTRACT_TARGET) + +UPBGE_RUN_TARGET:=$(SENTINEL_FOLDER)/upbge-run.sentinel +$(UPBGE_RUN_TARGET): + @./bin/blender -b ./utils/run.blend -P ./utils/build.py + @touch $(UPBGE_RUN_TARGET) + +.PHONY: upbge-install +upbge-install: $(UPBGE_EXTRACT_TARGET) + +.PHONY: upbge-run-clean +upbge-run-clean: + @rm -rf ./utils/3.6 + @rm -rf ./utils/engine.license + @rm -f ./utils/run + @rm -f ./utils/run.blend1 + @rm -rf ./utils/lib/ + @rm -f $(UPBGE_RUN_TARGET) + +.PHONY: upbge-clean +upbge-clean: upbge-run-clean + @rm -rf $(UPBGE_DONWLOAD_FOLDER)/$(UPBGE_TARBALL) + @rm -f $(UPBGE_DOWNLOAD_TARGET) + @rm -rf bin + @rm -f $(UPBGE_EXTRACT_TARGET) + +############################################################################## +# default targets +############################################################################## + +SPHINX_BIN:=./utils/run sphinx +INSTALL_TARGETS:=upbge-install $(INSTALL_TARGETS) $(UPBGE_RUN_TARGET) +CLEAN_TARGETS+=upbge-clean diff --git a/mx.ini b/mx.ini new file mode 100644 index 0000000..82a4ab4 --- /dev/null +++ b/mx.ini @@ -0,0 +1,2 @@ +[settings] +main-package = -e .[test,docs] diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..5979882 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,4 @@ +target-version = "py310" + +[format] +quote-style = "single" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..97d1b7c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,9 @@ +[coverage:run] +branch = True +source = uplogic + +[coverage:report] +ignore_errors = True + +[coverage:html] +directory = coverage_html diff --git a/setup.py b/setup.py index 728fa16..4421183 100644 --- a/setup.py +++ b/setup.py @@ -59,5 +59,9 @@ def read_file(name): # 'uplogic\\nodes\\logictree.pyx' # ]), zip_safe=True, - install_requires=['setuptools'] + install_requires=['setuptools'], + extras_require=dict( + test=['pytest', 'coverage'], + docs=['sphinx', 'sphinx_rtd_theme'] + ) ) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..458323c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,115 @@ +import bpy +import doctest +import pytest +import shutil +import tempfile +import bge +import time + + +class Example(object): + + def __init__(self, want): + self.want = want + '\n' + + +class Failure(Exception): + pass + + +class Output: + + def __init__(self): + self._checker = doctest.OutputChecker() + self._optionflags = ( + doctest.NORMALIZE_WHITESPACE | + doctest.ELLIPSIS | + doctest.REPORT_ONLY_FIRST_FAILURE + ) + + def check(self, want, got, optionflags=None): + if optionflags is None: + optionflags = self._optionflags + success = self._checker.check_output(want, got, optionflags) + if not success: + raise Failure(self._checker.output_difference( + Example(want), + got, optionflags + )) + + +@pytest.fixture(scope='session') +def output(): + return Output() + + +class AppBlend: + + def __init__(self): + self.tempdir = None + self.tear_down() + + @property + def current_scene(self): + return bge.logic.getCurrentScene() + + def next_frame(self): + time.sleep(1 / 30) + bge.logic.NextFrame() + + def wait_until(self, condition): + while not condition(): + self.next_frame() + + def reset_app(self): + # remove game objects + for obj in list(self.current_scene.objects): + if obj.name == 'Camera': + continue + obj.endObject() + # remove blender objects + for obj in list(bpy.data.objects): + if obj.name == 'Camera': + continue + bpy.data.objects.remove(obj) + # remove id data blocks + for g in list(bpy.data.node_groups): + bpy.data.node_groups.remove(g) + + def check_initial_state(self): + # check if application data in initial state + assert len(self.current_scene.objects) == 1 + assert len(bpy.data.objects) == 1 + assert len(bpy.data.node_groups) == 0 + + def set_up(self): + self.tempdir = tempfile.mkdtemp() + + def tear_down(self): + if self.tempdir is not None: + shutil.rmtree(self.tempdir) + self.tempdir = None + # reset application data + self.reset_app() + # wait for actual object removal + self.wait_until(lambda: len(self.current_scene.objects) == 1) + # check if cleanup was successful + self.check_initial_state() + + +_app_blend = None + + +def get_app_blend(): + global _app_blend + if _app_blend is None: + _app_blend = AppBlend() + return _app_blend + + +@pytest.fixture() +def app_blend(): + app = get_app_blend() + app.set_up() + yield app + app.tear_down() diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py new file mode 100644 index 0000000..a36ac5e --- /dev/null +++ b/tests/test_fixtures.py @@ -0,0 +1,19 @@ +from conftest import AppBlend +from pathlib import Path +import bpy + + +def test_AppBlend_fixture(output): + app_blend = AppBlend() + assert app_blend.tempdir is None + + app_blend.set_up() + assert app_blend.tempdir is not None + assert Path(app_blend.tempdir).exists() is True + output.check('/tmp/...', app_blend.tempdir) + + tempdir = app_blend.tempdir + app_blend.tear_down() + assert app_blend.tempdir is None + assert Path(tempdir).exists() is False + assert len(bpy.data.objects) == 1 diff --git a/utils/build.py b/utils/build.py new file mode 100644 index 0000000..069e18d --- /dev/null +++ b/utils/build.py @@ -0,0 +1,202 @@ +import bpy +import os +import shutil +import tempfile + + +def copytree(src, dst, symlinks=False, ignore=None): + """Custom copytree implementation to handle cases + """ + for item in os.listdir(src): + s = os.path.join(src, item) + d = os.path.join(dst, item) + if os.path.isdir(s): + shutil.copytree(s, d, symlinks, ignore, dirs_exist_ok=True) + else: + shutil.copy2(s, d) + + +def new_folder(path): + """Create a new folder if it doesn't exist yet + """ + try: + os.mkdir(path) + except FileExistsError: + pass + + +# Remove old build files +try: + os.remove(bpy.path.abspath('//run')) +except Exception: + pass +try: + shutil.rmtree(bpy.path.abspath('//3.6')) +except Exception: + pass +try: + shutil.rmtree(bpy.path.abspath('//license')) +except Exception: + pass + + +path = bpy.path.abspath('//') +OUTPUT_PATH = bpy.path.abspath('//run') + +def CopyPythonLibs(dst, overwrite_lib, report=print): + import platform + + # use python module to find python's libpath + src = os.path.dirname(platform.__file__) + + # dst points to lib/, but src points to current python's library path, eg: + # '/usr/lib/python3.2' vs '/usr/lib' + # append python's library dir name to destination, so only python's + # libraries would be copied + if os.name == 'posix': + dst = os.path.join(dst, os.path.basename(src)) + + if os.path.exists(src): + write = False + if os.path.exists(dst): + if overwrite_lib: + shutil.rmtree(dst) + write = True + else: + write = True + if write: + shutil.copytree(src, dst, ignore=lambda dir, contents: [i for i in contents if i == '__pycache__']) + else: + report({'WARNING'}, "Python not found in %r, skipping python copy" % src) + + +def WriteRuntime(): + import struct + + # Setup main folders + blender_dir = os.path.dirname(bpy.app.binary_path) + runtime_dir = os.path.dirname(OUTPUT_PATH) + + # Extract new version string. Only take first 3 digits (i.e 3.0) + string = bpy.app.version_string.split()[0] + version_string = string[:3] + + # Create temporal directory + tempdir = tempfile.mkdtemp() + blender_bin_path = bpy.app.binary_path + blender_bin_dir = os.path.dirname(blender_bin_path) + ext = os.path.splitext(blender_bin_path)[-1].lower() + blenderplayer_name = 'blenderplayer' + player_path_temp = os.path.join(blender_bin_dir, blenderplayer_name + ext) + + # Get the player's binary and the offset for the blend + file = open(player_path_temp, 'rb') + player_d = file.read() + offset = file.tell() + file.close() + + # Create a tmp blend file (Blenderplayer doesn't like compressed blends) + blend_path = os.path.join(tempdir, bpy.path.clean_name(OUTPUT_PATH)) + bpy.ops.wm.save_as_mainfile(filepath=blend_path, + relative_remap=False, + compress=False, + copy=True, + ) + + # Get the blend data + blend_file = open(blend_path, 'rb') + blend_d = blend_file.read() + blend_file.close() + + # Get rid of the tmp blend, we're done with it + os.remove(blend_path) + os.rmdir(tempdir) + + # Create a new file for the bundled runtime + output = open(OUTPUT_PATH, 'wb') + + # Write the player and blend data to the new runtime + print("Writing runtime...", end=" ") + output.write(player_d) + output.write(blend_d) + + # Store the offset (an int is 4 bytes, so we split it up into 4 bytes and save it) + output.write(struct.pack('B', (offset>>24)&0xFF)) + output.write(struct.pack('B', (offset>>16)&0xFF)) + output.write(struct.pack('B', (offset>>8)&0xFF)) + output.write(struct.pack('B', (offset>>0)&0xFF)) + + # Stuff for the runtime + output.write(b'BRUNTIME') + output.close() + + print("done") + + # Make the runtime executable on Linux + if os.name == 'posix': + os.chmod(OUTPUT_PATH, 0o755) + + print("Copying Python files...", end=" ") + py_folder = os.path.join(version_string, "python", "lib") + dst = os.path.join(runtime_dir, py_folder) + CopyPythonLibs(dst, True, print) + print("done") + + # Copy datafiles folder + print("Copying datafiles...", end=" ") + datafiles_folder = os.path.join(version_string, "datafiles", "gamecontroller") + src = os.path.join(blender_dir, datafiles_folder) + dst = os.path.join(runtime_dir, datafiles_folder) + if os.path.isdir(dst): + shutil.rmtree(dst) + shutil.copytree(src, dst) + datafiles_folder = os.path.join(version_string, "datafiles", "colormanagement") + src = os.path.join(blender_dir, datafiles_folder) + dst = os.path.join(runtime_dir, datafiles_folder) + if os.path.isdir(dst): + shutil.rmtree(dst) + shutil.copytree(src, dst) + datafiles_folder = os.path.join(version_string, "datafiles", "fonts") + src = os.path.join(blender_dir, datafiles_folder) + dst = os.path.join(runtime_dir, datafiles_folder) + if os.path.isdir(dst): + shutil.rmtree(dst) + shutil.copytree(src, dst) + datafiles_folder = os.path.join(version_string, "datafiles", "studiolights") + src = os.path.join(blender_dir, datafiles_folder) + dst = os.path.join(runtime_dir, datafiles_folder) + if os.path.isdir(dst): + shutil.rmtree(dst) + shutil.copytree(src, dst) + print("done") + + print("Copying scripts and modules...", end=" ") + scripts_folder = os.path.join(version_string, "scripts") + src = os.path.join(blender_dir, scripts_folder) + dst = os.path.join(runtime_dir, scripts_folder) + shutil.copytree(src, dst) + print("done") + + # Copy license folder + print("Copying UPBGE license folder...", end=" ") + src = os.path.join(blender_dir, "license") + dst = os.path.join(runtime_dir, "engine.license") + if os.path.isdir(dst): + shutil.rmtree(dst) + shutil.copytree(src, dst) + license_folder = os.path.join(runtime_dir, "engine.license") + src = os.path.join(blender_dir, "copyright.txt") + dst = os.path.join(license_folder, "copyright.txt") + shutil.copy2(src, dst) + print("done") + + +import time +print("Saving runtime to %r" % bpy.path.abspath('//')) +start_time = time.time() +WriteRuntime() +print("Finished in %.4fs" % (time.time() - start_time)) + +blpath = bpy.utils.resource_path(type='LOCAL') +new_folder(bpy.path.abspath('//lib')) +copytree(os.path.join(blpath, '..', 'lib'), bpy.path.abspath('//lib')) diff --git a/utils/run.blend b/utils/run.blend new file mode 100644 index 0000000..56e8c4a Binary files /dev/null and b/utils/run.blend differ