Finished initial development and testing.

development
Shawn Davis 4 years ago
parent 6fec406d04
commit 1376d33e0a
  1. 3
      .coveragerc
  2. 2
      README.markdown
  3. 2
      VERSION.txt
  4. 229
      docs/Makefile
  5. 65
      docs/generate_command_signatures.py
  6. 272
      docs/make.bat
  7. 3
      docs/requirements.pip
  8. 440
      docs/source/_command-examples.rst
  9. 3
      docs/source/_data/cloc.csv
  10. 10
      docs/source/_static/css/custom.css
  11. BIN
      docs/source/_static/images/slack-1.jpg
  12. BIN
      docs/source/_static/images/slack-2.jpg
  13. BIN
      docs/source/_static/images/slack-3.jpg
  14. 243
      docs/source/conf.py
  15. 166
      docs/source/configuration.rst
  16. 110
      docs/source/developer.rst
  17. 17
      docs/source/index.rst
  18. 27
      docs/source/introduction.rst
  19. 54
      docs/source/tests.rst
  20. 12
      sandbox/tease.py
  21. 204
      scripttease/cli/__init__.py
  22. 58
      scripttease/cli/initialize.py
  23. 76
      scripttease/cli/subcommands.py
  24. 47
      scripttease/data/overlays/ubuntu.ini
  25. 22
      scripttease/factory.py
  26. 224
      scripttease/library/_overlays.py
  27. 2
      scripttease/library/commands/__init__.py
  28. 134
      scripttease/library/commands/apache.py
  29. 62
      scripttease/library/commands/base.py
  30. 119
      scripttease/library/commands/factory.py
  31. 4
      scripttease/library/commands/mappings.py
  32. 23
      scripttease/library/commands/packages.py
  33. 43
      scripttease/library/commands/python.py
  34. 19
      scripttease/library/overlays/common.py
  35. 164
      scripttease/library/overlays/django.py
  36. 292
      scripttease/library/overlays/pgsql.py
  37. 475
      scripttease/library/overlays/posix.py
  38. 83
      scripttease/library/overlays/ubuntu.py
  39. 12
      scripttease/library/scripts.py
  40. 83
      scripttease/parsers/__init__.py
  41. 16
      scripttease/parsers/base.py
  42. 50
      scripttease/parsers/ini.py
  43. 110
      scripttease/parsers/utils.py
  44. 159
      scripttease/utils.py
  45. 10
      setup.py
  46. 12
      tests/examples/apache_examples.ini
  47. 2
      tests/examples/bad_command.ini
  48. 2
      tests/examples/bad_examples.ini
  49. 6
      tests/examples/bad_template_example.ini
  50. 25
      tests/examples/function_examples.ini
  51. 192
      tests/examples/kitchen_sink.ini
  52. 3
      tests/examples/python_examples.ini
  53. 2
      tests/examples/template_example.ini
  54. 40
      tests/test_factory.py
  55. 15
      tests/test_library_commands_base.py
  56. 20
      tests/test_library_commands_factory.py
  57. 18
      tests/test_library_commands_python.py
  58. 17
      tests/test_library_overlays_common.py
  59. 61
      tests/test_library_overlays_django.py
  60. 60
      tests/test_library_overlays_pgsql.py
  61. 209
      tests/test_library_overlays_posix.py
  62. 86
      tests/test_library_overlays_ubuntu.py
  63. 31
      tests/test_library_scripts.py
  64. 19
      tests/test_overlays.py
  65. 22
      tests/test_parsers_base.py
  66. 45
      tests/test_parsers_ini.py
  67. 42
      tests/test_parsers_utils.py

@ -1,8 +1,9 @@
[run] [run]
omit = omit =
docs/* docs/*
scripttease/cli/__init__.py scripttease/cli/*
sandbox sandbox
setup.py setup.py
tests/*
tmp/* tmp/*
tmp.* tmp.*

@ -2,6 +2,6 @@
![](https://img.shields.io/badge/status-active-green.svg) ![](https://img.shields.io/badge/status-active-green.svg)
![](https://img.shields.io/badge/stage-development-blue.svg) ![](https://img.shields.io/badge/stage-development-blue.svg)
![](https://img.shields.io/badge/coverage-55%25-yellow.svg) ![](https://img.shields.io/badge/coverage-100%25-green.svg)
A collection of classes and commands for automated command line scripting using Python. A collection of classes and commands for automated command line scripting using Python.

@ -1 +1 @@
5.8.18-d 6.0.0-d

@ -0,0 +1,229 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
.PHONY: help
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " applehelp to make an Apple Help Book"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " epub3 to make an epub3"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
.PHONY: clean
clean:
rm -rf $(BUILDDIR)/*
.PHONY: html
html:
#./generate_command_signatures.py > source/_command-examples.rst
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
.PHONY: dirhtml
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
.PHONY: singlehtml
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
.PHONY: pickle
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
.PHONY: json
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
.PHONY: htmlhelp
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
.PHONY: qthelp
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/BasisHR.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/BasisHR.qhc"
.PHONY: applehelp
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
@echo "N.B. You won't be able to view it unless you put it in" \
"~/Library/Documentation/Help or install it in your application" \
"bundle."
.PHONY: devhelp
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/BasisHR"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/BasisHR"
@echo "# devhelp"
.PHONY: epub
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
.PHONY: epub3
epub3:
$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
@echo
@echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
.PHONY: latex
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
.PHONY: latexpdf
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: latexpdfja
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: text
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
.PHONY: man
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
.PHONY: texinfo
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
.PHONY: info
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
.PHONY: gettext
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
.PHONY: changes
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
.PHONY: linkcheck
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
.PHONY: doctest
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
.PHONY: coverage
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
.PHONY: xml
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
.PHONY: pseudoxml
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
.PHONY: robots
robots:
cp source/robots.txt build/html/;

@ -0,0 +1,65 @@
#! /usr/bin/env python
from collections import OrderedDict
import inspect
import sys
sys.path.append("../")
from script_tease.mappings import MAPPING
# https://stackoverflow.com/a/52003056/241720
def get_signature(fn):
params = inspect.signature(fn).parameters
args = []
kwargs = OrderedDict()
for p in params.values():
if p.default is p.empty:
args.append(p.name)
else:
kwargs[p.name] = p.default
return args, kwargs
keys = list(MAPPING.keys())
keys.sort()
for key in keys:
cls = MAPPING[key]
print(key)
print("." * len(key))
print("")
extra = cls.get_docs()
if extra is not None:
print(extra)
print("")
# if cls.__init__.__doc__:
# print(cls.__init__.__doc__)
# print("")
print(".. code-block:: cfg")
print("")
if cls.__doc__:
print(" [%s]" % cls.__doc__.strip().replace(".", "").lower())
else:
print(" [run a %s command]" % cls.__name__.lower())
args, kwargs = get_signature(cls.__init__)
line = list()
for a in args:
if a not in ("self", "kwargs"):
line.append(a)
print(" %s = %s" % (key, " ".join(line)))
for option, value in kwargs.items():
print(" %s = %s" % (option, value))
print("")

@ -0,0 +1,272 @@
@ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source
set I18NSPHINXOPTS=%SPHINXOPTS% source
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. epub3 to make an epub3
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. xml to make Docutils-native XML files
echo. pseudoxml to make pseudoxml-XML files for display purposes
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
echo. coverage to run coverage check of the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
REM Check if sphinx-build is available and fallback to Python version if any
%SPHINXBUILD% 1>NUL 2>NUL
if errorlevel 9009 goto sphinx_python
goto sphinx_ok
:sphinx_python
set SPHINXBUILD=python -m sphinx.__init__
%SPHINXBUILD% 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.http://sphinx-doc.org/
exit /b 1
)
:sphinx_ok
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\BasisHR.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\BasisHR.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "epub3" (
%SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub3 file is in %BUILDDIR%/epub3.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdf" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf
cd %~dp0
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdfja" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf-ja
cd %~dp0
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
if "%1" == "coverage" (
%SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
if errorlevel 1 exit /b 1
echo.
echo.Testing of coverage in the sources finished, look at the ^
results in %BUILDDIR%/coverage/python.txt.
goto end
)
if "%1" == "xml" (
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The XML files are in %BUILDDIR%/xml.
goto end
)
if "%1" == "pseudoxml" (
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
goto end
)
:end

@ -0,0 +1,3 @@
sphinx
sphinx_rtd_theme
git+https://github.com/develmaycare/sphinx-helpers

@ -0,0 +1,440 @@
apache.disable_module
.....................
.. code-block:: ini
[disable an apache module]
apache.disable_module = module_name
apache.disable_site
...................
.. code-block:: ini
[disable a virtual host]
apache.disable_site = domain_name
apache.enable_module
....................
.. code-block:: ini
[enable an apache module]
apache.enable_module = module_name
apache.enable_site
..................
.. code-block:: ini
[enable a virtual host]
apache.enable_site = domain_name
apache.test
...........
.. code-block:: ini
[run an apache config test]
apache: test
append
......
.. code-block:: ini
[append to a file]
append = path
content = None
archive
.......
.. code-block:: ini
[create an archive file]
archive = from_path
absolute = False
exclude = None
file_name = archive.tgz
strip = 0
to_path = .
view = False
certbot
.......
Alias: ssl
.. code-block:: ini
[get new ssl certificate from let's encrypt]
certbot = domain_name
email = None
webroot = None
copy
....
.. code-block:: ini
[copy a file or directory]
copy = from_path to_path
overwrite = False
recursive = False
django
......
.. code-block:: ini
[run a django management command]
django = name
django.dumpdata
...............
.. code-block:: ini
[export django fixtures]
django.dumpdata = app_name
file_name = initial
indent = 4
natural_foreign = False
natural_primary = False
path = None
django.loaddata
...............
.. code-block:: ini
[load django fixtures]
django.loaddata = app_name
file_name = initial
path = None
extract
.......
.. code-block:: ini
[extract an archive]
extract = from_path
absolute = False
exclude = None
file_name = archive.tgz
strip = 0
to_path = None
view = False
install
.......
.. code-block:: ini
[install a package using apt-get]
apt = package
remove = False
makedir
.......
.. code-block:: ini
[create a directory]
makedir = path
mode = None
recursive = True
message
.......
.. code-block:: ini
[run a message command]
message = output
back_title = Message
dialog = False
height = 15
width = 100
mkdir
.....
.. code-block:: ini
[create a directory]
mkdir = path
mode = None
recursive = True
move
....
.. code-block:: ini
[move a file or directory]
move = from_path to_path
perms
.....
.. code-block:: ini
[set permissions on a file or directory]
perms = path
group = None
mode = None
owner = None
recursive = False
pg.createdb
...........
.. code-block:: ini
[create a postgresql database]
pg.createdb = name
admin_pass = None
admin_user = postgres
host = localhost
owner = None
port = 5432
template = None
pg.createuser
.............
.. code-block:: ini
[create a postgresql user]
pg.createuser = name
admin_pass = None
admin_user = postgres
host = localhost
password = None
port = 5432
pg.db
.....
.. code-block:: ini
[create a postgresql database]
pg.db = name
admin_pass = None
admin_user = postgres
host = localhost
owner = None
port = 5432
template = None
pg.dropdb
.........
.. code-block:: ini
[remove a postgresql database]
pg.dropdb = name
admin_pass = None
admin_user = postgres
host = localhost
port = 5432
pg.dropuser
...........
.. code-block:: ini
[remove a postgres user]
pg.dropuser = name
admin_pass = None
admin_user = postgres
host = localhost
port = 5432
pg.dump
.......
.. code-block:: ini
[export a postgres database]
pg.dump = name
admin_pass = None
admin_user = postgres
file_name = None
host = localhost
port = 5432
pg.exists
.........
.. code-block:: ini
[determine if a postgres database exists]
pg.exists = name
admin_pass = None
admin_user = postgres
host = localhost
port = 5432
pip
...
.. code-block:: ini
[install a python package using pip]
pip = package
remove = False
upgrade = False
psql
....
.. code-block:: ini
[execute a psql command]
psql = sql
database = template1
host = localhost
password = None
port = 5432
user = postgres
reload
......
.. code-block:: ini
[reload a service]
reload = service
remove
......
.. code-block:: ini
[remove a file or directory]
remove = path
force = False
recursive = False
restart
.......
.. code-block:: ini
[restart a service]
restart = service
rsync
.....
.. code-block:: ini
[synchronize files from a local to remote directory]
rsync = source target
delete = False
guess = False
host = None
key_file = None
links = True
port = 22
recursive = True
user = None
run
...
.. code-block:: ini
[a command to be executed]
run = statement
comment = None
condition = None
cd = None
environments = None
function = None
prefix = None
register = None
shell = None
stop = False
sudo = None
tags = None
scopy
.....
.. code-block:: ini
[copy a file from the local (machine) to the remote host]
scp = from_path to_path
host = None
key_file = None
port = 22
user = None
sed
...
.. code-block:: ini
[replace text in a file]
sed = path
backup = .b
change = None
delimiter = /
find = None
start
.....
.. code-block:: ini
[start a service]
start = service
stop
....
.. code-block:: ini
[stop a service]
stop = service
symlink
.......
.. code-block:: ini
[create a symlink]
symlink = source
force = False
target = None
touch
.....
.. code-block:: ini
[touch a file or directory]
touch = path
virtualenv
..........
.. code-block:: ini
[create a python virtual environment]
virtualenv = name
write
.....
.. code-block:: ini
[write to a file]
write = path
content = None
overwrite = False

@ -0,0 +1,3 @@
files,language,blank,comment,code
17,Python,705,569,1156
17,SUM,705,569,1156
unable to load file from base commit

@ -0,0 +1,10 @@
/* override table width restrictions */
.wy-table-responsive table td, .wy-table-responsive table th {
white-space: normal;
}
.wy-table-responsive {
margin-bottom: 24px;
max-width: 100%;
overflow: visible;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

@ -0,0 +1,243 @@
# -*- coding: utf-8 -*-
#
# Script Tease documentation build configuration file, created by
# sphinx-quickstart on Mon Apr 11 17:36:02 2016.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import os
from sphinx_helpers import get_release, get_version
import sys
# 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.
sys.path.insert(0, os.path.abspath("../../"))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx.ext.todo',
'sphinx.ext.coverage',
'sphinx.ext.ifconfig',
'sphinx.ext.viewcode',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Script Tease (Python)'
copyright = u'Pleasant Tents, LLC'
author = u'Shawn Davis'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = get_version("../../VERSION.txt")
# The full version, including alpha/beta/rc tags.
release = get_release("../../VERSION.txt")
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = []
# The reST default role (used for this markup: `text`) to use for all
# documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
# html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents.
# "<project> v<release> documentation" by default.
#html_title = u'Ninjas v%s' % version
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (relative to this directory) to use as a favicon of
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
def setup(app):
# Some csv-table tables in the RTD theme were scrolling horizontally.
# See https://github.com/snide/sphinx_rtd_theme/issues/117
# Apparently the placement of this function is significant. It wasn't
# working at the end of the file.
# See https://github.com/syncany/syncany-docs/blob/82166518720051e413231d621c3b23b9ac35cbf6/source/conf.py#L135
# app.add_javascript("js/custom.js")
app.add_stylesheet("css/custom.css")
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#html_extra_path = []
# If not None, a 'Last updated on:' timestamp is inserted at every page
# bottom, using the given strftime format.
# The empty string is equivalent to '%b %d, %Y'.
#html_last_updated_fmt = None
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh'
#html_search_language = 'en'
# A dictionary with options for the search language support, empty by default.
# 'ja' uses this config value.
# 'zh' user can custom change `jieba` dictionary path.
#html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
#html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = 'ScriptTeaseDoc'
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'script_tease', u'Script Tease Documentation',
[author], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'https://docs.python.org/': None}
# Read the Docs Theme
import sphinx_rtd_theme
html_theme = "sphinx_rtd_theme"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]

@ -0,0 +1,166 @@
.. _configuration:
*************
Configuration
*************
Generating Commands From a File
===============================
The :py:class:`scripttease.parsers.ini.Config` class may instantiate commands by loading a configuration file.
.. note::
Additional formats such as JSON or YAML may be supported in the future.
An example file:
.. code-block:: ini
[install apache]
install: apache2
[create the web site directory]
mkdir: /var/www/domains/example_com/www
recursive: yes
[set permissions on the website directory]
perms: /var/www/domains/example_com/www
group: www-data
mode: 775
owner: www-data
Notes regarding this format:
- This is the standard format for Python's ConfigParser. If you prefer, you may use ``=`` instead of ``:``.
- The first line is the INI section and is used as the default comment.
- The command name must be the *first* option in the section.
- The arguments for the command appear as the value of the first option in the section. Arguments are separated by a
space.
- Arguments that should be treated as a single value should be enclosed in double quotes.
- ``yes`` and ``no`` are interpreted as boolean values.
- List values, where required, are separated by commas.
Common Parameters
-----------------
All commands support the following common parameters:
- ``comment``: A comment regarding the command.
- ``condition``: A condition for execution. For example, ``! -f /path/to/some/file.txt``
- ``cd``: The path from which a command should be executed.
- ``environments``: A string or list of strings indicating the operational environments in which the command runs. This
is *not* used by default, but may be used to programmatically filter commands for a specific environment. For example,
development versus live.
- ``prefix``: A statement to be added prior to executing the command.
- ``register``: A variable name to which the the success or failure (exit code) of the statement is captured.
- ``shell``: The shell used to run the commands. For example, ``/bin/bash``. This is generally not important, but can
be a problem when attempting to execute some commands (such as Django management commands).
- ``stop``: ``True`` indicates no other commands should be executed if the given command fails.
- ``sudo``: ``True`` indicates the command should be automatically prefixed with ``sudo``. If provided as a string, the
command is also prefixed with a specific user name.
- ``tags``: A list of tags used to classify the command.
Defining an "Itemized" Command
------------------------------
Certain command definitions may be repeated by defining a list of items.
Example of an "itemized" command:
.. code-block:: ini
[touch a bunch of files]
touch = /var/www/domains/example_com/www/$item
items = index.html, assets/index.html, content/index.html
.. note::
Command itemization may vary with the command type.
Available Commands
------------------
The following commands instantiate command instances. Each example is shown with the defaults.
.. include:: _command-examples.rst
Pre-Parsing Command Files as Templates
======================================
Configuration file may be pre-processed as a Jinja2 template by providing a context dictionary:
.. code-block:: ini
[install apache]
install: apache
[create the website directory]
mkdir: /var/www/domains/{{ domain_tld }}/www
recursive: yes
[set permissions on the website directory]
perms: /var/www/domains/{{ domain_tld }}/www
group: www-data
mode: 775
owner: www-data
Then with a config instance:
.. code-block:: python
context = {
'domain_tld': "example_com",
}
config = Config("commands.ini", context=context)
config.load()
for command in config.get_commands():
print(command.get_statement(cd=True))
print("")
Using the Tease Command
=======================
The ``tease`` command may be used to parse a configuration file, providing additional utilities for working with
commands.
.. code-block:: text
positional arguments:
path The path to the configuration file.
optional arguments:
-h, --help show this help message and exit
-c, --color Enable code highlighting for terminal output.
-C= VARIABLES, --context= VARIABLES
Context variables for use in pre-parsing the config and templates. In the form of: name:value
-d, --docs Output documentation instead of code.
-D, --debug Enable debug output.
-f= FILTERS, --filter= FILTERS
Filter the commands in the form of: attribute:value
-O= OPTIONS, --option= OPTIONS
Common command options in the form of: name:value
-s, --script Output commands as a script.
-T= TEMPLATE_LOCATIONS, --template-path= TEMPLATE_LOCATIONS
The location of template files that may be used with the template command.
-w= OUTPUT_FILE, --write= OUTPUT_FILE
Write the output to disk.
-V= VARIABLES_FILE, --variables-file= VARIABLES_FILE
Load variables from a file.
-v Show version number and exit.
--version Show verbose version information and exit.
The ``path`` argument defaults to ``commands.ini``.
Loading Variables from a File
-----------------------------
Context variables may be loaded from a file:
.. code-block:: ini
[domain]
name = example.com
tld = example_com
The variables above are available as ``section_key``. For example, ``domain_name`` is ``example.com``.

@ -0,0 +1,110 @@
.. _developer-reference:
*******************
Developer Reference
*******************
Constants
=========
.. automodule:: scripttease.constants
:members:
:show-inheritance:
:special-members: __init__
Library
=======
Commands
--------
.. automodule:: scripttease.library.commands.base
:members:
:show-inheritance:
:special-members: __init__
Overlays
--------
Common
......
.. automodule:: scripttease.library.overlays.common
:members:
:show-inheritance:
:special-members: __init__
Django
......
.. automodule:: scripttease.library.overlays.django
:members:
:show-inheritance:
:special-members: __init__
Postgres
........
.. automodule:: scripttease.library.overlays.pgsql
:members:
:show-inheritance:
:special-members: __init__
Posix
.....
.. automodule:: scripttease.library.overlays.posix
:members:
:show-inheritance:
:special-members: __init__
Ubuntu
......
.. automodule:: scripttease.library.overlays.ubuntu
:members:
:show-inheritance:
:special-members: __init__
Scripts
-------
.. automodule:: scripttease.library.scripts
:members:
:show-inheritance:
:special-members: __init__
Factory
=======
.. automodule:: scripttease.factory
:members:
:show-inheritance:
:special-members: __init__
Parsers
=======
Base
----
.. automodule:: scripttease.parsers.base
:members:
:show-inheritance:
:special-members: __init__
Config (INI)
------------
.. automodule:: scripttease.parsers.ini
:members:
:show-inheritance:
:special-members: __init__
Utils
-----
.. automodule:: scripttease.parsers.utils
:members:
:show-inheritance:
:special-members: __init__

@ -0,0 +1,17 @@
Python Script Tease
===================
.. toctree::
:maxdepth: 2
Introduction <introduction>
Configuration <configuration>
Developer Reference <developer>
Tests <tests>
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

@ -0,0 +1,27 @@
.. _introduction:
************
Introduction
************
Script Tease is a library and command line tool for generating commands programmatically or using configuration files.
Concepts
========
Generating Commands
-------------------
Script Tease may be used in two (2) ways:
1. Using the library to programmatically define commands and export them as command line statements. See
:ref:`developer-reference`.
2. Using the ``tease`` command to generate commands from a configuration file. See :ref:`configuration`.
Overlays
--------
An *overlay* is a set of command meta functions that define the capabilities of a specific operating system.
.. note::
At present, the only fully defined overlay is for Ubuntu.

@ -0,0 +1,54 @@
*****
Tests
*****
Coverage Requirements
=====================
100% coverage is required for the ``master`` branch.
See `current coverage report <coverage/index.html>`_.
.. csv-table:: Lines of Code
:file: _data/cloc.csv
Set Up for Testing
==================
Install requirements:
.. code-block:: bash
pip install tests/requirements.pip
Running Tests
=============
.. tip::
You may use the ``tests`` target of the ``Makefile`` to run tests with coverage:
``make tests;``
To run unit tests:
.. code-block:: bash
python -m pytest;
Run a specific test:
.. code-block:: bash
python -m pytest tests/units/path/to/test.py
To allow output from print statements within a test method, add the ``-s`` switch:
.. code-block:: bash
python -m pytest -s tests/units/path/to/test.py
Reference
=========
- `coverage <https://coverage.readthedocs.io/en/v4.5.x/>`_
- `pytest <https://pytest.org>`_

@ -0,0 +1,12 @@
#! /usr/bin/env python
import re
import sys
sys.path.insert(0, "../")
from scripttease.cli import main_command
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main_command())

@ -0,0 +1,204 @@
# Imports
from argparse import ArgumentParser, RawDescriptionHelpFormatter
from superpython.logging import LoggingHelper
from ..constants import LOGGER_NAME
from . import initialize
from . import subcommands
DEBUG = 10
logging = LoggingHelper(colorize=True, name=LOGGER_NAME)
log = logging.setup()
# Commands
def main_command():
"""Process script configurations."""
__author__ = "Shawn Davis <shawn@develmaycare.com>"
__date__ = "2020-07-21"
__help__ = """NOTES
This command is used to parse configuration files and output the commands.
"""
__version__ = "0.10.0-d"
# Main argument parser from which sub-commands are created.
parser = ArgumentParser(description=__doc__, epilog=__help__, formatter_class=RawDescriptionHelpFormatter)
parser.add_argument(
"path",
default="commands.ini",
nargs="?",
help="The path to the configuration file."
)
parser.add_argument(
"-c",
"--color",
action="store_true",
dest="color_enabled",
help="Enable code highlighting for terminal output."
)
parser.add_argument(
"-C=",
"--context=",
action="append",
dest="variables",
help="Context variables for use in pre-parsing the config and templates. In the form of: name:value"
)
parser.add_argument(
"-d",
"--docs",
action="store_true",
dest="docs_enabled",
help="Output documentation instead of code."
)
# parser.add_argument(
# "-d=",
# "--docs=",
# choices=["html", "markdown", "plain", "rst"],
# dest="docs_enabled",
# help="Output documentation instead of code."
# )
parser.add_argument(
"-D",
"--debug",
action="store_true",
dest="debug_enabled",
help="Enable debug output."
)
parser.add_argument(
"-f=",
"--filter=",
action="append",
dest="filters",
help="Filter the commands in the form of: attribute:value"
)
parser.add_argument(
"-O=",
"--option=",
action="append",
dest="options",
help="Common command options in the form of: name:value"
)
# parser.add_argument(
# "-O=",
# "--output=",
# # default=os.path.join("prototype", "output"),
# dest="output_path",
# help="Output to the given directory. Defaults to ./prototype/output/"
# )
parser.add_argument(
"-s",
"--script",
action="store_true",
dest="script_enabled",
help="Output commands as a script."
)
parser.add_argument(
"-T=",
"--template-path=",
action="append",
dest="template_locations",
help="The location of template files that may be used with the template command."
)
parser.add_argument(
"-w=",
"--write=",
dest="output_file",
help="Write the output to disk."
)
parser.add_argument(
"-V=",
"--variables-file=",
dest="variables_file",
help="Load variables from a file."
)
# Access to the version number requires special consideration, especially
# when using sub parsers. The Python 3.3 behavior is different. See this
# answer: http://stackoverflow.com/questions/8521612/argparse-optional-subparser-for-version
parser.add_argument(
"-v",
action="version",
help="Show version number and exit.",
version=__version__
)
parser.add_argument(
"--version",
action="version",
help="Show verbose version information and exit.",
version="%(prog)s" + " %s %s by %s" % (__version__, __date__, __author__)
)
# Parse arguments.
args = parser.parse_args()
if args.debug_enabled:
log.setLevel(DEBUG)
log.debug("Namespace: %s" % args)
# Load context.
context = dict()
if args.variables:
context = initialize.context_from_cli(args.variables)
# Handle filters.
filters = None
if args.filters:
filters = initialize.filters_from_cli(args.filters)
# Handle options.
options = None
if args.options:
options = initialize.options_from_cli(args.options)
if args.variables_file:
variables = initialize.variable_from_file(args.variables_file)
if variables:
context.update(variables)
if args.docs_enabled:
exit_code = subcommands.output_docs(
args.path,
context=context,
filters=filters,
locations=args.template_locations,
options=options
)
elif args.script_enabled:
exit_code = subcommands.output_script(
args.path,
color_enabled=args.color_enabled,
context=context,
locations=args.template_locations,
options=options
)
else:
exit_code = subcommands.output_commands(
args.path,
color_enabled=args.color_enabled,
context=context,
filters=filters,
locations=args.template_locations,
options=options
)
exit(exit_code)

@ -0,0 +1,58 @@
# Imports
from configparser import ConfigParser
import logging
import os
from superpython.utils import smart_cast
from ..constants import LOGGER_NAME
log = logging.getLogger(LOGGER_NAME)
# Functions
def context_from_cli(variables):
context = dict()
for i in variables:
key, value = i.split(":")
context[key] = smart_cast(value)
return context
def filters_from_cli(filters):
_filters = dict()
for i in filters:
key, value = i.split(":")
if key not in filters:
_filters[key] = list()
_filters[key].append(value)
return _filters
def options_from_cli(options):
_options = dict()
for i in options:
key, value = i.split(":")
_options[key] = smart_cast(value)
return _options
def variable_from_file(path):
if not os.path.exists(path):
log.warning("Variables file does not exist: %s" % path)
return None
ini = ConfigParser()
ini.read(path)
variables = dict()
for section in ini.sections():
for key, value in ini.items(section):
key = "%s_%s" % (section, key)
variables[key] = smart_cast(vaue)
return variables

@ -0,0 +1,76 @@
# Imports
from superpython.shell import EXIT
from superpython.utils import highlight_code
from ..parsers import load_commands, load_config
# Functions
def output_commands(path, color_enabled=False, context=None, filters=None, locations=None, options=None):
commands = load_commands(
path,
context=context,
filters=filters,
locations=locations,
options=options
)
if commands is None:
return EXIT.ERROR
output = list()
for command in commands:
statement = command.get_statement(cd=True)
if statement is None:
continue
output.append(statement)
output.append("")
if color_enabled:
print(highlight_code("\n".join(output), language="bash"))
else:
print("\n".join(output))
return EXIT.OK
def output_docs(path, context=None, filters=None, locations=None, options=None):
commands = load_commands(
path,
context=context,
filters=filters,
locations=locations,
options=options
)
if commands is None:
return EXIT.ERROR
count = 1
output = list()
for command in commands:
output.append("%s. %s" % (count, command.comment))
count += 1
print("\n".join(output))
return EXIT.OK
def output_script(path, color_enabled=False, context=None, filters=None, locations=None, options=None):
config = load_config(
path,
context=context,
locations=locations,
options=options
)
if config is None:
return EXIT.ERROR
script = config.as_script()
if color_enabled:
print(highlight_code(script.to_string(), language="bash"))
else:
print(script)
return EXIT.OK

@ -1,47 +0,0 @@
[apache]
disable_module = a2dismod {{ module_name }}
disable_site = a2dissite {{ domain_name }}.conf
enable_module = a2enmod {{ module_name }}
enable_site = a2ensite {{ domain_name }}.conf
reload = service apache2 reload
restart = service apache2 restart
start = service apache2 start
stop = service apache2 stop
test = apachectl configtest
[package_install]
system = apt-get install -y {{ package_name }}
pip = pip3 install{% if upgrade %} --upgrade{% endif %} --quiet {{ package_name }}
[package_remove]
system = apt-get uninstall -y {{ package_name }}
pip = pip3 uninstall --quiet {{ package_name }}
[system]
install = apt-get install -y {{ package_name }}
reboot = reboot
remove = apt-get uninstall -y {{ package_name }}
update = apt-get update -y
upgrade = apt-get upgrade -y
[python]
virtualenv = virtualenv {{ name }}
install = pip3 install{% if upgrade %} --upgrade{% endif %} --quiet {{ package_name }}
remove = pip3 uninstall --quiet {{ package_name }}
[files]
append = echo "{{ content }}" >> {{ path }}
chgrp = chgrp{% if recursive %} -R{% endif %} {{ group }} {{ path }}
chmod = chmod{% if recursive %} -R{% endif %} {{ owner }} {{ path }}
chown = chown{% if recursive %} -R{% endif %} {{ mode }} {{ path }}
copy = cp{% if recursive %} -R{% endif %}{% if overwrite %} -n{% endif %} {{ from_path }} {{ to_path }}
mkdir = mkdir{% if mode %} -m {{ mode }}{% endif %}{% if recursive %} -p{% endif %} {{ path }}
move = move {{ from_path }} {{ to_path }}
rename = move {{ from_path }} {{ to_path }}
remove = rm{% if force %} -f{% endif %}{% if recursive %} -r{% endif %} {{ path }}
;rsync = ?
;scopy = ?
;sed = ?
symlink = ln -s{% if force %} -f{% endif %} {{ source }} {{ target }}
touch = touch {{ path }}
;write = ?

@ -1,6 +1,11 @@
# Imports # Imports
import logging
from importlib import import_module from importlib import import_module
from .constants import LOGGER_NAME
from .library.commands import ItemizedCommand
log = logging.getLogger(LOGGER_NAME)
# Exports # Exports
@ -25,6 +30,9 @@ class Factory(object):
self.overlay = None self.overlay = None
self._overlay = overlay self._overlay = overlay
def __repr__(self):
return "<%s %s>" % (self.__class__.__name__, self._overlay)
def get_command(self, name, *args, **kwargs): def get_command(self, name, *args, **kwargs):
"""Get a command. """Get a command.
@ -37,19 +45,19 @@ class Factory(object):
""" """
if not self.overlay.command_exists(name): if not self.overlay.command_exists(name):
# log.warning("Command does not exist in %s overlay: %s" % (overlay.name, name)) log.warning("Command does not exist in %s overlay: %s" % (self._overlay, name))
return None return None
callback = self.overlay.MAPPINGS[name] callback = self.overlay.MAPPINGS[name]
try: try:
# items = kwargs.pop("items", None) items = kwargs.pop("items", None)
# if items is not None: if items is not None:
# return ItemizedCommand(callback, items, *args, **kwargs) return ItemizedCommand(callback, items, *args, **kwargs)
return callback(*args, **kwargs) return callback(*args, **kwargs)
except (KeyError, TypeError, ValueError) as e: except (KeyError, NameError, TypeError, ValueError) as e:
# log.critical("Failed to load %s command: %s" % (name, e)) log.critical("Failed to load %s command: %s" % (name, e))
return None return None
def load(self): def load(self):
@ -62,7 +70,7 @@ class Factory(object):
self.overlay = import_module("scripttease.library.overlays.%s" % self._overlay) self.overlay = import_module("scripttease.library.overlays.%s" % self._overlay)
self.is_loaded = True self.is_loaded = True
except ImportError as e: except ImportError as e:
# log.error("The %s overlay could not be imported: %s" % (overlay, str(e))) log.error("The %s overlay could not be imported: %s" % (self._overlay, str(e)))
pass pass
return self.is_loaded return self.is_loaded

@ -1,224 +0,0 @@
# Imports
from configparser import RawConfigParser
import os
from superpython.utils import parse_jinja_string
from ..constants import PATH_TO_SCRIPT_TEASE
# Exports
__all__ = (
"Overlay",
)
# Classes
class Overlay(object):
"""An overlay applies commands specific to a given operating system or platform."""
def __init__(self, name):
self.is_loaded = False
self._name = name
self._path = os.path.join(PATH_TO_SCRIPT_TEASE, "data", "overlays", "%s.ini" % name)
self._sections = dict()
def __repr__(self):
return "<%s %s>" % (self.__class__.__name__, self._name)
@property
def exists(self):
"""Indicates whether the overlay file exists.
:rtype: bool
"""
return os.path.exists(self._path)
def get(self, section, key, **kwargs):
"""Get the command statement for the given section and key.
:param section: The section name.
:type section: str
:param key: The key within the section.
:type key: str
kwargs are used to parse the value of the key within the section.
:rtype: str | None
"""
if not self.has(section, key):
return None
template = self._sections[section][key]
return parse_jinja_string(template, kwargs)
def has(self, section, key):
"""Determine whether the overlay contains a given section and key.
:param section: The section name.
:type section: str
:param key: The key within the section.
:type key: str
:rtype: bool
"""
if section not in self._sections:
return False
if key not in self._sections[section]:
return False
return True
def load(self):
"""Load the overlay.
:rtype: bool
"""
if not self.exists:
return False
ini = RawConfigParser()
ini.read(self._path)
for section in ini.sections():
self._sections[section] = dict()
for key, value in ini.items(section):
self._sections[section][key] = value
self.is_loaded = True
return True
def to_mapping(self):
"""Export the overlay as a dictionary with command names as values.
:rtype: dict
"""
d = dict()
for section in self._sections:
d[section] = list()
for command_name, statement in self._sections[section].items():
d[section].append(command_name)
class Overlay2(object):
"""An overlay applies commands specific to a given operating system or platform."""
def __init__(self, name):
self.is_loaded = False
self._name = name
self._path = os.path.join(PATH_TO_SCRIPT_TEASE, "data", "overlays", "%s.ini" % name)
self._sections = dict()
self.exists = os.path.exists(self._path)
def __repr__(self):
return "<%s %s>" % (self.__class__.__name__, self._name)
def command_exists(self, name):
"""Determine whether a given command exists.
:param name: The name of the command to check.
:type name: str
:rtype: bool
"""
section = None
if "." in name:
section, name = name.split(".")
if section is not None:
if section in self._sections:
return name in self._sections[section]
for section in self._sections.keys():
if name in self._sections[section]:
return True
return False
def get_statement(self, name, *args, **kwargs):
pass
def get(self, section, key, **kwargs):
"""Get the command statement for the given section and key.
:param section: The section name.
:type section: str
:param key: The key within the section.
:type key: str
kwargs are used to parse the value of the key within the section.
:rtype: str | None
"""
if not self.has(section, key):
return None
template = self._sections[section][key]
return parse_jinja_string(template, kwargs)
def has(self, section, key):
"""Determine whether the overlay contains a given section and key.
:param section: The section name.
:type section: str
:param key: The key within the section.
:type key: str
:rtype: bool
"""
if section not in self._sections:
return False
if key not in self._sections[section]:
return False
return True
def load(self):
"""Load the overlay.
:rtype: bool
"""
if not self.exists:
return False
ini = RawConfigParser()
ini.read(self._path)
for section in ini.sections():
self._sections[section] = dict()
for command_name, statement_template in ini.items(section):
self._sections[section][command_name] = statement_template
self.is_loaded = True
return True
def to_mapping(self):
"""Export the overlay as a dictionary with command names as values.
:rtype: dict
"""
d = dict()
for section in self._sections:
d[section] = list()
for command_name, statement in self._sections[section].items():
d[section].append(command_name)

@ -1,2 +1,2 @@
from .base import Command, ItemizedCommand from .base import Command, ItemizedCommand
from .factory import command_factory # from .factory import command_factory

@ -1,134 +0,0 @@
# Imports
import logging
from ...constants import LOGGER_NAME
from .base import Command
log = logging.getLogger(LOGGER_NAME)
# Exports
__all__ = (
"MAPPING",
"ConfigTest",
"DisableModule",
"DisableSite",
"EnableModule",
"EnableSite",
"Reload",
"Restart",
"Start",
"Stop",
)
# Classes
class ConfigTest(Command):
"""Run an apache config test."""
def __init__(self, overlay=None, **kwargs):
"""There is no argument."""
if overlay is not None:
statement = overlay.get("apache", "test")
else:
statement = "apachectl configtest"
kwargs.setdefault('register', "apache_checks_out")
super().__init__(statement, **kwargs)
class DisableModule(Command):
"""Disable an Apache module."""
def __init__(self, module_name, overlay=None, **kwargs):
"""Initialize the command.
:param module_name: The module name.
:type module_name: str
"""
if overlay is not None:
statement = overlay.get("apache", "disable_module", module_name=module_name)
statement = "a2dismod %s" % module_name
super().__init__(statement, **kwargs)
class DisableSite(Command):
"""Disable a virtual host."""
def __init__(self, domain_name, **kwargs):
"""Initialize the command.
:param domain_name: The domain name.
:type domain_name: str
"""
statement = "a2dissite %s.conf" % domain_name
super().__init__(statement, **kwargs)
class Enable(Command):
def __init__(self, what, name, **kwargs):
if what in ("mod", "module"):
statement = EnableModule(name, **kwargs).statement
elif what == "site":
statement = EnableSite(name, **kwargs).statement
else:
raise ValueError("Invalid Apache item to be enabled: %s" % what)
super().__init__(statement, **kwargs)
class EnableModule(Command):
"""Enable an Apache module."""
def __init__(self, module_name, **kwargs):
"""Initialize the command.
:param module_name: The module name.
:type module_name: str
"""
statement = "a2enmod %s" % module_name
super().__init__(statement, **kwargs)
class EnableSite(Command):
"""Enable a virtual host."""
def __init__(self, domain_name, **kwargs):
"""Initialize the command.
:param domain_name: The domain name.
:type domain_name: str
"""
statement = "a2ensite %s.conf" % domain_name
super().__init__(statement, **kwargs)
MAPPING = {
# 'apache': Apache,
'apache.check': ConfigTest,
'apache.config': ConfigTest,
'apache.configtest': ConfigTest,
'apache.disable': Disable,
'apache.disable_mod': DisableModule,
'apache.disable_module': DisableModule,
'apache.disable_site': DisableSite,
'apache.enable': Enable,
'apache.enable_mod': EnableModule,
'apache.enable_module': EnableModule,
'apache.enable_site': EnableSite,
'apache.mod': EnableModule,
'apache.module': EnableModule,
'apache.test': ConfigTest,
}

@ -2,9 +2,52 @@
class Command(object): class Command(object):
"""A command line statement."""
def __init__(self, statement, comment=None, condition=None, cd=None, environments=None, function=None, prefix=None, def __init__(self, statement, comment=None, condition=None, cd=None, environments=None, function=None, prefix=None,
register=None, shell=None, stop=False, sudo=None, tags=None, **kwargs): register=None, shell=None, stop=False, sudo=None, tags=None, **kwargs):
"""Initialize a command.
:param statement: The statement to be executed.
:type statement: str
:param comment: A comment regarding the statement.
:type comment: str
:param condition: A (system-specific) condition for the statement to be executed.
:type condition: str
:param cd: The direction from which the statement should be executed.
:type cd: str
:param environments: A list of target environments where the statement should be executed.
:type environments: list[str]
:param function: The name of the function in which the statement is executed.
:type function: str
:param prefix: A statement to execute before the main statement is executed.
:type prefix: str
:param register: A variable name to use for capture the success for failure of the statement's execution.
:type register: str
:param shell: The shell execute through which the statement is executed.
:type shell: str
:param stop: Indicates process should stop if the statement fails to execute.
:type stop: bool | None
:param sudo: Indicates whether sudo should be invoked for the statement. Given as a bool or user name or
:py:class:`scripttease.library.commands.base.Sudo` instance.
:type sudo: bool | str | Sudo
:param tags: A list of tags describing the statement.
:type tags: list[str]
Additional kwargs are available as dynamic attributes of the Command instance.
"""
self.comment = comment self.comment = comment
self.condition = condition self.condition = condition
self.cd = cd self.cd = cd
@ -37,12 +80,15 @@ class Command(object):
return "<%s>" % self.__class__.__name__ return "<%s>" % self.__class__.__name__
def get_statement(self, cd=False): def get_statement(self, cd=False, suppress_comment=False):
"""Get the full statement. """Get the full statement.
:param cd: Include the directory change, if given. :param cd: Include the directory change, if given.
:type cd: bool :type cd: bool
:param suppress_comment: Don't include the comment.
:type suppress_comment: bool
:rtype: str :rtype: str
""" """
@ -65,7 +111,7 @@ class Command(object):
a.append(")") a.append(")")
b = list() b = list()
if self.comment is not None: if self.comment is not None and not suppress_comment:
b.append("# %s" % self.comment) b.append("# %s" % self.comment)
if self.condition is not None: if self.condition is not None:
@ -95,12 +141,12 @@ class Command(object):
class ItemizedCommand(object): class ItemizedCommand(object):
"""An itemized command represents multiple commands of with the same statement but different parameters."""
def __init__(self, command_class, items, *args, **kwargs): def __init__(self, callback, items, *args, **kwargs):
"""Initialize the command. """Initialize the command.
:param command_class: The command class to be used. :param callback: The function to be used to generate the command.
:type command_class: class
:param items: The command arguments. :param items: The command arguments.
:type items: list[str] :type items: list[str]
@ -111,7 +157,7 @@ class ItemizedCommand(object):
""" """
self.args = args self.args = args
self.command_class = command_class self.callback = callback
self.items = items self.items = items
self.kwargs = kwargs self.kwargs = kwargs
@ -119,7 +165,7 @@ class ItemizedCommand(object):
return self.kwargs.get(item) return self.kwargs.get(item)
def __repr__(self): def __repr__(self):
return "<%s %s>" % (self.__class__.__name__, self.command_class.__name__) return "<%s %s>" % (self.__class__.__name__, self.callback.__name__)
def get_commands(self): def get_commands(self):
"""Get the commands to be executed. """Get the commands to be executed.
@ -135,7 +181,7 @@ class ItemizedCommand(object):
for arg in self.args: for arg in self.args:
args.append(arg.replace("$item", item)) args.append(arg.replace("$item", item))
command = self.command_class(*args, **kwargs) command = self.callback(*args, **kwargs)
a.append(command) a.append(command)
return a return a

@ -1,119 +0,0 @@
# Imports
from importlib import import_module
import logging
# from ..scripts import Function
from ...constants import LOGGER_NAME
# from .base import ItemizedCommand
# from .mappings import MAPPING
log = logging.getLogger(LOGGER_NAME)
# Functions
def command_factory(name, comment, overlay, *args, **kwargs):
# try:
# _overlay = import_module("scripttease.library.overlays.%s" % overlay)
# except ImportError as e:
# log.error("The %s overlay could not be imported: %s" % (overlay, str(e)))
# return None
if not overlay.command_exists(name):
log.warning("Command does not exist in %s overlay: %s" % (overlay.name, name))
return None
kwargs['comment'] = comment
callback = overlay.MAPPINGS[name]
return callback(*args, **kwargs)
'''
def command_exists(name):
"""Indicates whether the named command exists.
:param name: The name of the command to be checked.
:type name: str
:rtype: bool
"""
return name in MAPPING
def command_factory(name, comment, overlay, *args, **kwargs):
# if name in ("func", "function"):
# kwargs['comment'] = comment
# return Function(*args, **kwargs)
if not command_exists(name):
log.warning("No mapping for command: %s" % name)
return None
_args = list(args)
kwargs['comment'] = comment
kwargs['overlay'] = overlay
log.debug("%s: %s" % (comment, kwargs))
command_class = MAPPING[name]
try:
items = kwargs.pop("items", None)
if items is not None:
return ItemizedCommand(command_class, items, *_args, **kwargs)
return command_class(*_args, **kwargs)
except (KeyError, TypeError, ValueError) as e:
log.critical("Failed to load %s command: %s" % (name, e))
return None
'''
#
#
#
# MAPPINGS = {
# 'apache.disable_module': apache_disable_module,
# 'apache.disable_site': apache_disable_site,
# 'apache.enable_module': apache_enable_module,
# 'apache.enable_site': apache_enable_site,
# 'apache.reload': apache_reload,
# 'apache.restart': apache_restart,
# 'apache.start': apache_start,
# 'apache.stop': apache_stop,
# 'apache.test': apache_test,
# 'copy': file_copy,
# 'pip': python_pip,
# 'virtualenv': python_virtualenv,
# # 'python': ("pip", "virtualenv"),
# # 'apache': ("disable_module", "disable_site", "enable_module", "enable_site", "test"),
# }
def nother_command_exists(name):
return name in MAPPINGS
def other_command_exists(name, section=None):
if section is not None:
if section not in MAPPINGS:
return False
return name in MAPPINGS[section]
for _section, commands in MAPPINGS.items():
if name in commands:
return True
return False
def other_command_factory(name, comment, overlay, *args, **kwargs):
if not overlay.command_exists(name):
log.warning("The %s overlay does not have a mapping for command: %s" % (overlay, name))
return None
items = kwargs.pop("items", None)
if items is not None:
return ItemizedCommand

@ -1,4 +0,0 @@
from .python import MAPPING as PYTHON_MAPPING
MAPPING = dict()
MAPPING.update(PYTHON_MAPPING)

@ -1,23 +0,0 @@
# Classes
class Install(object):
def __init__(self, name, manager="pip", overlay=None, upgrade=False, **kwargs):
if overlay is not None:
statement = overlay.get("package_install", manager, package_name=name, upgrade=upgrade)
else:
statement = "%s install %s" % (manager, name)
self.statement = statement
class Remove(object):
def __init__(self, name, manager="pip", overlay=None):
if overlay is not None:
statement = overlay.get("package_remove", manager, package_name=name)
else:
statement = "%s uninstall %s" % (manager, name)

@ -1,43 +0,0 @@
# Imports
from .base import Command
# Exports
__all__ = (
"Pip",
"VirtualEnv",
)
# Classes
class Pip(Command):
def __init__(self, name, op="install", overlay=None, upgrade=False, venv=None, **kwargs):
if overlay is not None:
statement = overlay.get("python", op, package_name=name, upgrade=upgrade)
else:
statement = "pip %s -y %s" % (op, name)
if venv is not None:
kwargs['prefix'] = "source %s/bin/activate" % venv
kwargs.setdefault("comment", "%s %s" % (op, name))
super().__init__(statement, **kwargs)
class VirtualEnv(Command):
def __init__(self, name="python", overlay=None, **kwargs):
kwargs.setdefault("comment", "create %s virtual environment" % name)
statement = "virtualenv %s" % name
super().__init__(statement, **kwargs)
MAPPING = {
'pip': Pip,
'virtualenv': VirtualEnv,
}

@ -1,5 +1,17 @@
# Imports
from ..commands import Command from ..commands import Command
# Exports
__all__ = (
"COMMON_MAPPINGS",
"python_pip",
"python_virtualenv",
)
# Functions
def python_pip(name, op="install", upgrade=False, venv=None, **kwargs): def python_pip(name, op="install", upgrade=False, venv=None, **kwargs):
if upgrade: if upgrade:
@ -19,3 +31,10 @@ def python_virtualenv(name="python", **kwargs):
kwargs.setdefault("comment", "create %s virtual environment" % name) kwargs.setdefault("comment", "create %s virtual environment" % name)
return Command("virtualenv %s" % name, **kwargs) return Command("virtualenv %s" % name, **kwargs)
# Mappings
COMMON_MAPPINGS = {
'pip': python_pip,
'virtualenv': python_virtualenv,
}

@ -0,0 +1,164 @@
# Imports
import os
from ..commands import Command
# Exports
__all__ = (
"DJANGO_MAPPINGS",
"django",
"django_check",
"django_collect_static",
"django_dumpdata",
"django_loaddata",
"django_migrate",
)
# Functions
def _django(name, *args, venv=None, **kwargs):
if venv is not None:
kwargs['prefix'] = "source %s/bin/activate" % venv
kwargs.setdefault("comment", "run %s django management command" % name)
# Base parameters need to be captured, because all others are assumed to be switches for the management command.
_kwargs = {
'comment': kwargs.pop("comment", None),
'condition': kwargs.pop("condition", None),
'cd': kwargs.pop("cd", None),
'environments': kwargs.pop("environments", None),
'function': kwargs.pop("function", None),
# 'local': kwargs.pop("local", False),
'prefix': kwargs.pop("prefix", None),
'register': kwargs.pop("register", None),
'shell': kwargs.pop("shell", "/bin/bash"),
'stop': kwargs.pop("stop", False),
'sudo': kwargs.pop('sudo', False),
'tags': kwargs.pop("tags", None),
}
statement = list()
statement.append("./manage.py %s" % name)
# Remaining kwargs are assumed to be switches.
for key, value in kwargs.items():
key = key.replace("_", "-")
if type(value) is bool:
if value is True:
statement.append("--%s" % key)
else:
statement.append("--%s=%s" % (key, value))
if len(args) > 0:
statement.append(" ".join(args))
return Command(" ".join(statement), **_kwargs)
def django(name, *args, venv=None, **kwargs):
if name == "check":
return django_check(venv=venv, **kwargs)
elif name in ("collectstatic", "static"):
return django_collect_static(venv=venv, **kwargs)
elif name == "migrate":
return django_migrate(venv=venv, **kwargs)
else:
return _django(name, *args, venv=venv, **kwargs)
def django_check(venv=None, **kwargs):
kwargs.setdefault("comment", "run django checks")
kwargs.setdefault("register", "django_checks_out")
return _django("check", venv=venv, **kwargs)
def django_collect_static(venv=None, **kwargs):
kwargs.setdefault("comment", "collect static files")
return _django("collectstatic", venv=venv, **kwargs)
def django_dumpdata(app_name, base_path="local", file_name="initial", indent=4, natural_foreign=False,
natural_primary=False, path=None, venv=None, **kwargs):
"""Initialize the command.
:param app_name: The name (app label) of the app. ``app_label.ModelName`` may also be given.
:type app_name: str
:param file_name: The file name to which the data will be dumped.
:type file_name: str
:param indent: Indentation of the exported fixtures.
:type indent: int
:param natural_foreign: Use the natural foreign parameter.
:type natural_foreign: bool
:param natural_primary: Use the natural primary parameter.
:type natural_primary: bool
:param path: The path to the data file.
:type path: str
"""
kwargs.setdefault("comment", "export fixtures for %s" % app_name)
output_format = kwargs.pop("format", "json")
_path = path or os.path.join(base_path, app_name, "fixtures", "%s.%s" % (file_name, output_format))
return _django(
"dumpdata",
app_name,
"> %s" % _path,
format=output_format,
indent=indent,
natural_foreign=natural_foreign,
natural_primary=natural_primary,
venv=venv,
**kwargs
)
def django_loaddata(app_name, base_path="local", file_name="initial", path=None, venv=None, **kwargs):
"""Initialize the command.
:param app_name: The name (app label) of the app.
:type app_name: str
:param file_name: The file name to which the data will be dumped.
:type file_name: str
:param path: The path to the data file.
:type path: str
"""
kwargs.setdefault("comment", "load fixtures for %s" % app_name)
output_format = kwargs.pop("format", "json")
_path = path or os.path.join(base_path, app_name, "fixtures", "%s.%s" % (file_name, output_format))
return _django("loaddata", _path, venv=venv, **kwargs)
def django_migrate(venv=None, **kwargs):
kwargs.setdefault("comment", "run django database migrations")
return _django("migrate", venv=venv, **kwargs)
# Mapping
DJANGO_MAPPINGS = {
'django': django,
'django.check': django_check,
'django.collect_static': django_collect_static,
'django.dumpdata': django_dumpdata,
'django.loaddata': django_loaddata,
'django.migrate': django_migrate,
}

@ -0,0 +1,292 @@
# Imports
from ..commands import Command
# Exports
__all__ = (
"PGSQL_MAPPINGS",
"pg_create_database",
"pg_create_user",
"pg_database_exists",
"pg_drop_database",
"pg_drop_user",
"pg_dump_database",
"psql",
)
# Functions
def _get_pgsql_command(name, host="localhost", password=None, port=5432, user="postgres"):
a = list()
if password:
a.append('export PGPASSWORD="%s" &&' % password)
a.append(name)
a.append("--host=%s" % host)
a.append("--port=%s" % port)
a.append("--username=%s" % user)
return a
def pg_create_database(name, admin_pass=None, admin_user="postgres", host="localhost", owner=None, port=5432,
template=None, **kwargs):
"""Create a PostgreSQL database.
:param name: The database name.
:type name: str
:param admin_pass: The password for the user with sufficient access privileges to execute the command.
:type admin_pass: str
:param admin_user: The name of the user with sufficient access privileges to execute the command.
:type admin_user: str
:param host: The database host name or IP address.
:type host: str
:param owner: The owner (user/role name) of the new database.
:type owner: str
:param port: The port number of the Postgres service running on the host.
:type port: int
:param template: The database template name to use, if any.
:type template: str
"""
_owner = owner or admin_user
# Postgres commands always run without sudo because the -U may be provided.
kwargs['sudo'] = False
# Assemble the command.
base = _get_pgsql_command("createdb", host=host, password=admin_pass, port=port)
base.append("--owner=%s" % _owner)
if template is not None:
base.append("--template=%s" % template)
base.append(name)
return Command(" ".join(base), **kwargs)
def pg_create_user(name, admin_pass=None, admin_user="postgres", host="localhost", password=None, port=5432, **kwargs):
"""Create a PostgreSQL user.
:param name: The user name.
:type name: str
:param admin_pass: The password for the user with sufficient access privileges to execute the command.
:type admin_pass: str
:param admin_user: The name of the user with sufficient access privileges to execute the command.
:type admin_user: str
:param host: The database host name or IP address.
:type host: str
:param password: The password for the new user.
:type password: str
:param port: The port number of the Postgres service running on the host.
:type port: int
"""
# Postgres commands always run without sudo because the -U may be provided.
kwargs['sudo'] = False
# Assemble the command.
base = _get_pgsql_command("createuser", host=host, password=admin_pass, port=port)
base.append("-DRS")
base.append(name)
if password is not None:
base.append("&& psql -h %s -U %s" % (host, admin_user))
base.append("-c \"ALTER USER %s WITH ENCRYPTED PASSWORD '%s';\"" % (name, password))
return Command(" ".join(base), **kwargs)
def pg_database_exists(name, admin_pass=None, admin_user="postgres", host="localhost", port=5432, **kwargs):
"""Determine if a Postgres database exists.
:param name: The database name.
:type name: str
:param admin_pass: The password for the user with sufficient access privileges to execute the command.
:type admin_pass: str
:param admin_user: The name of the user with sufficient access privileges to execute the command.
:type admin_user: str
:param host: The database host name or IP address.
:type host: str
:param port: The port number of the Postgres service running on the host.
:type port: int
"""
# Postgres commands always run without sudo because the -U may be provided. However, sudo may be required for
# file writing.
# kwargs['sudo'] = False
kwargs.setdefault("register", "%s_db_exists" % name)
base = _get_pgsql_command("psql", host=host, password=admin_pass, port=port, user=admin_user)
base.append(r"-lqt | cut -d \| -f 1 | grep -qw %s" % name)
return Command(" ".join(base), **kwargs)
def pg_drop_database(name, admin_pass=None, admin_user="postgres", host="localhost", port=5432, **kwargs):
"""Remove a PostgreSQL database.
:param name: The database name.
:type name: str
:param admin_pass: The password for the user with sufficient access privileges to execute the command.
:type admin_pass: str
:param admin_user: The name of the user with sufficient access privileges to execute the command.
:type admin_user: str
:param host: The database host name or IP address.
:type host: str
:param port: The port number of the Postgres service running on the host.
:type port: int
"""
# Postgres commands always run without sudo because the -U may be provided.
kwargs['sudo'] = False
# Assemble the command.
base = _get_pgsql_command("dropdb", host=host, password=admin_pass, port=port, user=admin_user)
base.append(name)
return Command(" ".join(base), **kwargs)
def pg_drop_user(name, admin_pass=None, admin_user="postgres", host="localhost", port=5432, **kwargs):
"""Remove a Postgres user.
:param name: The user name.
:type name: str
:param admin_pass: The password for the user with sufficient access privileges to execute the command.
:type admin_pass: str
:param admin_user: The name of the user with sufficient access privileges to execute the command.
:type admin_user: str
:param host: The database host name or IP address.
:type host: str
:param port: The port number of the Postgres service running on the host.
:type port: int
"""
# Postgres commands always run without sudo because the -U may be provided.
kwargs['sudo'] = False
# Assemble the command.
base = _get_pgsql_command("dropuser", host=host, password=admin_pass, port=port, user=admin_user)
base.append(name)
return Command(" ".join(base), **kwargs)
def pg_dump_database(name, admin_pass=None, admin_user="postgres", file_name=None, host="localhost", port=5432,
**kwargs):
"""Export a Postgres database.
:param name: The database name.
:type name: str
:param admin_pass: The password for the user with sufficient access privileges to execute the command.
:type admin_pass: str
:param admin_user: The name of the user with sufficient access privileges to execute the command.
:type admin_user: str
:param host: The database host name or IP address.
:type host: str
:param file_name: The name (including the path, if desired) of the export file. Defaults to the
``database_name`` plus ".sql"
:type file_name: str
:param port: The port number of the Postgres service running on the host.
:type port: int
"""
_file_name = file_name or "%s.sql" % name
# Postgres commands always run without sudo because the -U may be provided.
kwargs['sudo'] = False
# Assemble the command.
base = _get_pgsql_command("pg_dump", host=host, password=admin_pass, port=port, user=admin_user)
base.append("--column-inserts")
base.append("--file=%s" % _file_name)
base.append(name)
return Command(" ".join(base), **kwargs)
def psql(sql, database="template1", host="localhost", password=None, port=5432, user="postgres", **kwargs):
"""Execute a psql command.
:param sql: The SQL to be executed.
:type sql: str
:param database: The database name.
:type database: str
:param password: The password for the user with sufficient access privileges to execute the command.
:type password: str
:param host: The database host name or IP address.
:type host: str
:param port: The port number of the Postgres service running on the host.
:type port: int
:param user: The name of the user with sufficient access privileges to execute the command.
:type user: str
"""
# Postgres commands always run without sudo because the -U may be provided.
kwargs['sudo'] = False
# Assemble the command.
base = _get_pgsql_command("psql", host=host, password=password, port=port, user=user)
base.append("--dbname=%s" % database)
base.append('-c "%s"' % sql)
return Command(" ".join(base), **kwargs)
PGSQL_MAPPINGS = {
'pg.client': psql,
'pg.createdatabase': pg_create_database,
'pg.createdb': pg_create_database,
'pg.createuser': pg_create_user,
'pg.database': pg_create_database,
'pg.database_exists': pg_database_exists,
'pg.db': pg_create_database,
'pg.dropdatabase': pg_drop_database,
'pg.dropdb': pg_drop_database,
'pg.dropuser': pg_drop_user,
'pg.dump': pg_dump_database,
'pg.dumpdb': pg_dump_database,
'pg.exists': pg_database_exists,
'pg.user': pg_create_user,
'psql': psql,
}

@ -1,5 +1,172 @@
# Imports
import os
from superpython.utils import indent
from ..commands import Command from ..commands import Command
# Exports
__all__ = (
"POSIX_MAPPINGS",
"archive",
"certbot",
"extract",
"file_append",
"file_copy",
"file_write",
"mkdir",
"move",
"perms",
"remove",
"rsync",
"run",
"scopy",
"sed",
"symlink",
"touch",
"Function",
)
# Functions
def archive(from_path, absolute=False, exclude=None, file_name="archive.tgz", strip=None, to_path=".", view=False,
**kwargs):
"""Create a file archive.
:param from_path: The path that should be archived.
:type from_path: str
:param absolute: By default, the leading slash is stripped from each path. Set to ``True`` to preserve the
absolute path.
:type absolute: bool
:param bzip2: Compress using bzip2.
:type bzip2: bool
:param exclude: A pattern to be excluded from the archive.
:type exclude: str
:param format: The command to use for the operation.
:type format: str
:param gzip: Compress using gzip.
:type gzip: bool
:param strip: Remove the specified number of leading elements from the path. Paths with fewer elements will be
silently skipped.
:type strip: int
:param to_path: Where the archive should be created. This should *not* include the file name.
:type to_path: str
:param view: View the output of the command as it happens.
:type view: bool
"""
tokens = ["tar"]
switches = ["-cz"]
if absolute:
switches.append("P")
if view:
switches.append("v")
tokens.append("".join(switches))
if exclude:
tokens.append("--exclude %s" % exclude)
if strip:
tokens.append("--strip-components %s" % strip)
to_path = os.path.join(to_path, file_name)
tokens.append('-f %s %s' % (to_path, from_path))
name = " ".join(tokens)
return Command(name, **kwargs)
def certbot(domain_name, email=None, webroot=None, **kwargs):
"""Get new SSL certificate from Let's Encrypt.
:param domain_name: The domain name for which the SSL certificate is requested.
:type domain_name: str
:param email: The email address of the requester sent to the certificate authority. Required.
:type email: str
:param webroot: The directory where the challenge file will be created.
:type webroot: str
"""
_email = email or os.environ.get("SCRIPTTEASE_CERTBOT_EMAIL", None)
_webroot = webroot or os.path.join("/var", "www", "domains", domain_name.replace(".", "_"), "www")
if not _email:
raise ValueError("Email is required for certbot command.")
template = "certbot certonly --agree-tos --email %(email)s -n --webroot -w %(webroot)s -d %(domain_name)s"
name = template % {
'domain_name': domain_name,
'email': _email,
'webroot': _webroot,
}
return Command(name, **kwargs)
def extract(from_path, absolute=False, exclude=None, strip=None, to_path=None, view=False, **kwargs):
"""Extract a file archive.
:param from_path: The path to the archive file.
:type from_path: str
:param absolute: By default, the leading slash is stripped from each path. Set to ``True`` to preserve the
absolute path.
:type absolute: bool
:param exclude: A pattern to be excluded from the archive.
:type exclude: str
:param strip: Remove the specified number of leading elements from the path. Paths with fewer elements will be
silently skipped.
:type strip: int
:param to_path: Where the archive should be extracted.
:type to_path: str
:param view: View the output of the command as it happens.
:type view: bool
"""
_to_path = to_path or "./"
tokens = ["tar"]
switches = ["-xz"]
if absolute:
switches.append("P")
if view:
switches.append("v")
tokens.append("".join(switches))
if exclude:
tokens.append("--exclude %s" % exclude)
if strip:
tokens.append("--strip-components %s" % strip)
tokens.append('-f %s %s' % (from_path, _to_path))
name = " ".join(tokens)
return Command(name, **kwargs)
def file_append(path, content=None, **kwargs): def file_append(path, content=None, **kwargs):
"""Append content to a file. """Append content to a file.
@ -11,7 +178,6 @@ def file_append(path, content=None, **kwargs):
:type content: str :type content: str
""" """
print("HERE")
kwargs.setdefault("comment", "append to %s" % path) kwargs.setdefault("comment", "append to %s" % path)
statement = 'echo "%s" >> %s' % (content or "", path) statement = 'echo "%s" >> %s' % (content or "", path)
@ -52,6 +218,32 @@ def file_copy(from_path, to_path, overwrite=False, recursive=False, **kwargs):
return Command(" ".join(a), **kwargs) return Command(" ".join(a), **kwargs)
def file_write(path, content=None, **kwargs):
"""Initialize the command.
:param path: The file to be written.
:type path: str
:param content: The content to be written. Note: If omitted, this command is equivalent to :py:class:`Touch`.
:type content: str
"""
_content = content or ""
kwargs.setdefault("comment", "write to %s" % path)
a = list()
if len(_content.split("\n")) > 1:
a.append("cat > %s << EOF" % path)
a.append(_content)
a.append("EOF")
else:
a.append('echo "%s" > %s' % (_content, path))
return Command(" ".join(a), **kwargs)
def mkdir(path, mode=None, recursive=True, **kwargs): def mkdir(path, mode=None, recursive=True, **kwargs):
"""Initialize the command. """Initialize the command.
@ -59,7 +251,7 @@ def mkdir(path, mode=None, recursive=True, **kwargs):
:type path: str :type path: str
:param mode: The access permissions of the new directory. :param mode: The access permissions of the new directory.
:type mode: str :type mode: int | str
:param recursive: Create all directories along the path. :param recursive: Create all directories along the path.
:type recursive: bool :type recursive: bool
@ -96,7 +288,7 @@ def perms(path, group=None, mode=None, owner=None, recursive=False, **kwargs):
:type group: str :type group: str
:param mode: The access permissions of the file or directory. :param mode: The access permissions of the file or directory.
:type mode: str :type mode: int | str
:param owner: The name of the user to be applied. :param owner: The name of the user to be applied.
:type owner: str :type owner: str
@ -107,6 +299,8 @@ def perms(path, group=None, mode=None, owner=None, recursive=False, **kwargs):
""" """
commands = list() commands = list()
kwargs['comment'] = "set permissions on %s" % path
if group is not None: if group is not None:
statement = ["chgrp"] statement = ["chgrp"]
@ -144,7 +338,7 @@ def perms(path, group=None, mode=None, owner=None, recursive=False, **kwargs):
a = list() a = list()
for c in commands: for c in commands:
a.append(c.get_statement()) a.append(c.get_statement(suppress_comment=True))
return Command("\n".join(a), **kwargs) return Command("\n".join(a), **kwargs)
@ -175,3 +369,276 @@ def remove(path, force=False, recursive=False, **kwargs):
statement.append(path) statement.append(path)
return Command(" ".join(statement), **kwargs) return Command(" ".join(statement), **kwargs)
def rsync(source, target, delete=False, exclude=None, host=None, key_file=None, links=True, port=22,
recursive=True, user=None, **kwargs):
"""Initialize the command.
:param source: The source directory.
:type source: str
:param target: The target directory.
:type target: str
:param delete: Indicates target files that exist in source but not in target should be removed.
:type delete: bool
:param exclude: The path to an exclude file.
:type exclude: str
:param host: The host name or IP address. This causes the command to run over SSH and may require a
``key_file``, ``port``, and ``user``.
:type host: str
:param key_file: The path to the private SSH key to use for remove connections. User expansion is
automatically applied.
:type key_file: str
:param links: Include symlinks in the sync.
:type links: bool
:param port: The SSH port to use for remote connections.
:type port: int
:param recursive: Indicates source contents should be recursively synchronized.
:type recursive: bool
:param user: The user name to use for remote connections.
"""
# :param guess: When ``True``, the ``host``, ``key_file``, and ``user`` will be guessed based on the base name of
# the source path.
# :type guess: bool
# if guess:
# host = host or os.path.basename(source).replace("_", ".")
# key_file = key_file or os.path.expanduser(os.path.join("~/.ssh", os.path.basename(source)))
# user = user or os.path.basename(source)
# else:
# host = host
# key_file = key_file
# user = user
kwargs.setdefault("comment", "copy %s to remote %s" % (source, target))
# rsync -e "ssh -i $(SSH_KEY) -p $(SSH_PORT)" -P -rvzc --delete
# $(OUTPUTH_PATH) $(SSH_USER)@$(SSH_HOST):$(UPLOAD_PATH) --cvs-exclude;
tokens = list()
tokens.append('rsync')
tokens.append("--cvs-exclude")
tokens.append("--checksum")
tokens.append("--compress")
if links:
tokens.append("--copy-links")
if delete:
tokens.append("--delete")
if exclude is not None:
tokens.append("--exclude-from=%s" % exclude)
# --partial and --progress
tokens.append("-P")
if recursive:
tokens.append("--recursive")
tokens.append(source)
conditions = [
host is not None,
key_file is not None,
user is not None,
]
if all(conditions):
tokens.append('-e "ssh -i %s -p %s"' % (key_file, port))
tokens.append("%s@%s:%s" % (user, host, target))
else:
tokens.append(target)
statement = " ".join(tokens)
return Command(statement, **kwargs)
def run(statement, **kwargs):
"""Run any statement."""
kwargs.setdefault("comment", "run statement")
return Command(statement, **kwargs)
def scopy(from_path, to_path, host=None, key_file=None, port=22, user=None, **kwargs):
"""Initialize the command.
:param from_path: The source directory.
:type from_path: str
:param to_path: The target directory.
:type to_path: str
:param host: The host name or IP address. Required.
:type host: str
:param key_file: The path to the private SSH key to use for remove connections. User expansion is
automatically applied.
:type key_file: str
:param port: The SSH port to use for remote connections.
:type port: int
:param user: The user name to use for remote connections.
"""
kwargs.setdefault("comment", "copy %s to remote %s" % (from_path, to_path))
# TODO: What to do to force local versus remote commands?
# kwargs['local'] = True
kwargs['sudo'] = False
statement = ["scp"]
if key_file is not None:
statement.append("-i %s" % key_file)
statement.append("-P %s" % port)
statement.append(from_path)
if host is not None and user is not None:
statement.append("%s@%s:%s" % (user, host, to_path))
elif host is not None:
statement.append("%s:%s" % (host, to_path))
else:
raise ValueError("Host is a required keyword argument.")
return Command(" ".join(statement), **kwargs)
def sed(path, backup=".b", delimiter="/", find=None, replace=None, **kwargs):
"""Find and replace text in a file.
:param path: The path to the file to be edited.
:type path: str
:param backup: The backup file extension to use.
:type backup: str
:param delimiter: The pattern delimiter.
:param find: The old text. Required.
:type find: str
:param replace: The new text. Required.
:type replace: str
"""
kwargs.setdefault("comment", "find and replace in %s" % path)
context = {
'backup': backup,
'delimiter': delimiter,
'path': path,
'pattern': find,
'replace': replace,
}
template = "sed -i %(backup)s 's%(delimiter)s%(pattern)s%(delimiter)s%(replace)s%(delimiter)sg' %(path)s"
statement = template % context
return Command(statement, **kwargs)
def symlink(source, force=False, target=None, **kwargs):
"""Initialize the command.
:param source: The source of the link.
:type source: str
:param force: Force the creation of the link.
:type force: bool
:param target: The name or path of the target. Defaults to the base name of the source path.
:type target: str
"""
_target = target or os.path.basename(source)
kwargs.setdefault("comment", "link to %s" % source)
statement = ["ln -s"]
if force:
statement.append("-f")
statement.append(source)
statement.append(_target)
return Command(" ".join(statement), **kwargs)
def touch(path, **kwargs):
"""Initialize the command.
:param path: The file or directory to touch.
:type path: str
"""
kwargs.setdefault("comment", "touch %s" % path)
return Command("touch %s" % path, **kwargs)
# Classes
class Function(object):
"""A function that may be used to organize related commands to be called together."""
def __init__(self, name, commands=None, comment=None):
self.commands = commands or list()
self.comment = comment
self.name = name
def to_string(self):
a = list()
if self.comment is not None:
a.append("# %s" % self.comment)
a.append("function %s()" % self.name)
a.append("{")
for command in self.commands:
a.append(indent(command.get_statement(cd=True)))
a.append("")
a.append("}")
return "\n".join(a)
# Mappings
POSIX_MAPPINGS = {
'append': file_append,
'archive': archive,
'certbot': certbot,
'copy': file_copy,
'extract': extract,
'func': Function,
'function': Function,
'mkdir': mkdir,
'move': move,
'perms': perms,
'remove': remove,
'rsync': rsync,
'run': run,
'scopy': scopy,
'sed': sed,
'ssl': certbot,
'symlink': symlink,
'touch': touch,
'write': file_write,
}

@ -1,14 +1,59 @@
from ..commands import Command # Imports
from .common import python_pip, python_virtualenv
from .posix import file_append, file_copy, mkdir, move, perms, remove
name = "ubuntu" from ..commands import Command
from .common import COMMON_MAPPINGS
from .django import DJANGO_MAPPINGS
from .pgsql import PGSQL_MAPPINGS
from .posix import POSIX_MAPPINGS, Function
# Exports
__all__ = (
"MAPPINGS",
"apache",
"apache_disable_module",
"apache_disable_site",
"apache_enable_module",
"apache_enable_site",
"apache_reload",
"apache_restart",
"apache_start",
"apache_stop",
"apache_test",
"command_exists",
"service_reload",
"service_restart",
"service_start",
"service_stop",
"system",
"system_install",
"system_reboot",
"system_update",
"system_upgrade",
"system_uninstall",
"Function",
)
def command_exists(name): def command_exists(name):
return name in MAPPINGS return name in MAPPINGS
def apache(op, **kwargs):
if op == "reload":
return apache_reload(**kwargs)
elif op == "restart":
return apache_restart(**kwargs)
elif op == "start":
return apache_start(**kwargs)
elif op == "stop":
return apache_stop(**kwargs)
elif op == "test":
return apache_test(**kwargs)
else:
raise NameError("Unrecognized or unsupported apache operation: %s" % op)
def apache_disable_module(name, **kwargs): def apache_disable_module(name, **kwargs):
kwargs.setdefault("comment", "disable %s apache module" % name) kwargs.setdefault("comment", "disable %s apache module" % name)
@ -30,7 +75,7 @@ def apache_enable_module(name, **kwargs):
def apache_enable_site(name, **kwargs): def apache_enable_site(name, **kwargs):
kwargs.setdefault("comment", "enable %s apache module" % name) kwargs.setdefault("comment", "enable %s apache module" % name)
return Command("a2densite %s" % name, **kwargs) return Command("a2ensite %s" % name, **kwargs)
def apache_reload(**kwargs): def apache_reload(**kwargs):
@ -78,7 +123,7 @@ def service_restart(name, **kwargs):
kwargs.setdefault("comment", "restart %s service" % name) kwargs.setdefault("comment", "restart %s service" % name)
kwargs.setdefault("register", "%s_restarted" % name) kwargs.setdefault("register", "%s_restarted" % name)
return Command("service %s reload" % name, **kwargs) return Command("service %s restart" % name, **kwargs)
def service_start(name, **kwargs): def service_start(name, **kwargs):
@ -95,6 +140,17 @@ def service_stop(name, **kwargs):
return Command("service %s stop" % name, **kwargs) return Command("service %s stop" % name, **kwargs)
def system(op, **kwargs):
if op == "reboot":
return system_reboot(**kwargs)
elif op == "update":
return system_update(**kwargs)
elif op == "upgrade":
return system_upgrade(**kwargs)
else:
raise NameError("Unrecognized or unsupported system operation: %s" % op)
def system_install(name, **kwargs): def system_install(name, **kwargs):
kwargs.setdefault("comment", "install system package %s" % name) kwargs.setdefault("comment", "install system package %s" % name)
@ -126,6 +182,7 @@ def system_upgrade(**kwargs):
MAPPINGS = { MAPPINGS = {
'apache': apache,
'apache.disable_module': apache_disable_module, 'apache.disable_module': apache_disable_module,
'apache.disable_site': apache_disable_site, 'apache.disable_site': apache_disable_site,
'apache.enable_module': apache_enable_module, 'apache.enable_module': apache_enable_module,
@ -135,21 +192,19 @@ MAPPINGS = {
'apache.start': apache_start, 'apache.start': apache_start,
'apache.stop': apache_stop, 'apache.stop': apache_stop,
'apache.test': apache_test, 'apache.test': apache_test,
'append': file_append,
'copy': file_copy,
'install': system_install, 'install': system_install,
'mkdir': mkdir,
'move': move,
'perms': perms,
'pip': python_pip,
'reboot': system_reboot, 'reboot': system_reboot,
'reload': service_reload, 'reload': service_reload,
'remove': remove,
'restart': service_restart, 'restart': service_restart,
'start': service_start, 'start': service_start,
'stop': service_stop, 'stop': service_stop,
'system': system,
'update': system_update, 'update': system_update,
'uninstall': system_uninstall, 'uninstall': system_uninstall,
'upgrade': system_upgrade, 'upgrade': system_upgrade,
'virtualenv': python_virtualenv,
} }
MAPPINGS.update(COMMON_MAPPINGS)
MAPPINGS.update(DJANGO_MAPPINGS)
MAPPINGS.update(PGSQL_MAPPINGS)
MAPPINGS.update(POSIX_MAPPINGS)

@ -11,7 +11,7 @@ class Script(object):
:type name: str :type name: str
:param commands: The commands to be included. :param commands: The commands to be included.
:type commands: list[BaseType[Command]] :type commands: list[scripttease.library.commands.base.Command]
:param functions: The functions to be included. :param functions: The functions to be included.
:type functions: list[Function] :type functions: list[Function]
@ -49,21 +49,21 @@ class Script(object):
a = list() a = list()
if shebang is not None: if shebang is not None:
a.append("%s" % {'shell': self.shell}) a.append(shebang % {'shell': self.shell})
a.append("") a.append("")
if self.functions is not None: if self.functions is not None:
for function in self.functions: for function in self.functions:
a.append(function.preview()) a.append(function.to_string())
a.append("") a.append("")
for function in self.functions: # for function in self.functions:
a.append("%s;" % function.name) # a.append("%s;" % function.name)
a.append("") a.append("")
for command in self.commands: for command in self.commands:
a.append(command.preview(cwd=True)) a.append(command.get_statement(cd=True))
a.append("") a.append("")
return "\n".join(a) return "\n".join(a)

@ -1,85 +1,4 @@
# Imports # Imports
import logging
from superpython.utils import any_list_item
from ..constants import LOGGER_NAME
from .ini import Config from .ini import Config
from .utils import filter_commands, load_commands, load_config
log = logging.getLogger(LOGGER_NAME)
# Exports
__all__ = (
"filter_commands",
"load_commands",
)
# Functions
def filter_commands(commands, environments=None, tags=None):
"""Filter commands based on the given criteria.
:param commands: The commands to be filtered.
:type commands: list
:param environments: Environment names to be matched.
:type environments: list[str]
:param tags: Tag names to be matched.
:type tags
:return:
"""
filtered = list()
for command in commands:
if environments is not None:
if not any_list_item(environments, command.environments):
continue
if tags is not None:
if not any_list_item(tags, command.tags):
continue
filtered.append(command)
return filtered
def load_commands(path, filters=None, overlay=None, **kwargs):
"""Load commands from a configuration file.
:param path: The path to the configuration file.
:type path: str
:param filters: Used to filter commands.
:type filters: dict
:rtype: list[BaseType[Command] | ItemizedCommand] | None
:returns: A list of command instances or ``None`` if the configuration could not be loaded.
kwargs are passed to the configuration class for instantiation.
"""
if path.endswith(".ini"):
_config = Config(path, overlay=overlay, **kwargs)
# elif path.endswith(".yml"):
# _config = YAML(path, **kwargs)
else:
log.warning("Input file format is not currently supported: %s" % path)
return None
if _config.load():
commands = _config.get_commands()
if filters is not None:
criteria = dict()
for attribute, values in filters.items():
criteria[attribute] = values
commands = filter_commands(commands, **criteria)
return commands
log.error("Failed to load config file: %s" % path)
return None

@ -1,13 +1,13 @@
# Imports # Imports
from superpython.utils import File from superpython.utils import File
from ..library.overlays import Overlay from ..factory import Factory
from ..library.scripts import Script from ..library.scripts import Script
# Exports # Exports
__all__ = ( __all__ = (
"Parser" "Parser",
) )
# Classes # Classes
@ -16,19 +16,18 @@ __all__ = (
class Parser(File): class Parser(File):
"""Base class for implementing a command parser.""" """Base class for implementing a command parser."""
def __init__(self, path, context=None, locations=None, options=None, overlay=None): def __init__(self, path, context=None, locations=None, options=None, overlay="ubuntu"):
super().__init__(path) super().__init__(path)
self.context = context self.context = context
self.factory = Factory(overlay)
self.is_loaded = False self.is_loaded = False
self.locations = locations or list() self.locations = locations or list()
self.options = options or dict() self.options = options or dict()
self.overlay = overlay or Overlay("ubuntu") self.overlay = overlay
self._commands = list() self._commands = list()
self._functions = list() self._functions = list()
self.overlay.load()
def as_script(self): def as_script(self):
"""Convert loaded commands to a script. """Convert loaded commands to a script.
@ -73,4 +72,9 @@ class Parser(File):
return a return a
def load(self): def load(self):
"""Load the factory and the configuration file.
:rtype: bool
"""
raise NotImplementedError() raise NotImplementedError()

@ -4,7 +4,6 @@ from configparser import ConfigParser, ParsingError
import logging import logging
from superpython.utils import parse_jinja_template, read_file, smart_cast, split_csv from superpython.utils import parse_jinja_template, read_file, smart_cast, split_csv
import os import os
from ..library.commands import command_factory
from ..constants import LOGGER_NAME from ..constants import LOGGER_NAME
from .base import Parser from .base import Parser
@ -23,9 +22,13 @@ class Config(Parser):
"""An INI configuration for loading commands.""" """An INI configuration for loading commands."""
def load(self): def load(self):
"""Load commands from a INI file."""
if not self.exists: if not self.exists:
return False return False
if not self.factory.load():
return False
ini = self._load_ini() ini = self._load_ini()
if ini is None: if ini is None:
return False return False
@ -36,12 +39,15 @@ class Config(Parser):
command_name = None command_name = None
count = 0 count = 0
kwargs = self.options.copy() kwargs = self.options.copy()
kwargs['comment'] = comment
for key, value in ini.items(comment): for key, value in ini.items(comment):
# The first key/value pair is the command name and arguments. # The first key/value pair is the command name and arguments.
if count == 0: if count == 0:
command_name = key command_name = key
# Arguments surrounded by quotes are considered to be one argument. All others are split into a
# list to be passed to the callback.
if value[0] == '"': if value[0] == '"':
args.append(value.replace('"', "")) args.append(value.replace('"', ""))
else: else:
@ -53,8 +59,13 @@ class Config(Parser):
count += 1 count += 1
command = command_factory(command_name, comment, self.overlay, *args, **kwargs) command = self.factory.get_command(command_name, *args, **kwargs)
if command is not None: if command is not None:
if isinstance(command, self.factory.overlay.Function):
self._functions.append(command)
else:
self._commands.append(command)
# if isinstance(command, Function): # if isinstance(command, Function):
# self._functions.append(command) # self._functions.append(command)
# elif isinstance(command, Include): # elif isinstance(command, Include):
@ -70,7 +81,6 @@ class Config(Parser):
# self._commands.append(c) # self._commands.append(c)
# else: # else:
# self._commands.append(command) # self._commands.append(command)
self._commands.append(command)
else: else:
success = False success = False
@ -131,20 +141,20 @@ class Config(Parser):
log.error("Failed to parse %s: %s" % (self.path, e)) log.error("Failed to parse %s: %s" % (self.path, e))
return None return None
def _load_template(self, command): # def _load_template(self, command):
"""Load additional resources for a template command. # """Load additional resources for a template command.
#
:param command: The template command. # :param command: The template command.
:type command: Template # :type command: Template
#
""" # """
# This may produce problems if template kwargs are the same as the given context. # # This may produce problems if template kwargs are the same as the given context.
if self.context is not None: # if self.context is not None:
command.context.update(self.context) # command.context.update(self.context)
#
# Custom locations come before default locations. # # Custom locations come before default locations.
command.locations += self.locations # command.locations += self.locations
#
# This allows template files to be specified relative to the configuration file. # # This allows template files to be specified relative to the configuration file.
command.locations.append(os.path.join(self.directory, "templates")) # command.locations.append(os.path.join(self.directory, "templates"))
command.locations.append(self.directory) # command.locations.append(self.directory)

@ -0,0 +1,110 @@
# Imports
import logging
from superpython.utils import any_list_item
from ..constants import LOGGER_NAME
from .ini import Config
log = logging.getLogger(LOGGER_NAME)
# Exports
__all__ = (
"filter_commands",
"load_commands",
)
# Functions
def filter_commands(commands, environments=None, tags=None):
"""Filter commands based on the given criteria.
:param commands: The commands to be filtered.
:type commands: list
:param environments: Environment names to be matched.
:type environments: list[str]
:param tags: Tag names to be matched.
:type tags
:return:
"""
filtered = list()
for command in commands:
if environments is not None:
if not any_list_item(environments, command.environments):
continue
if tags is not None:
if not any_list_item(tags, command.tags):
continue
filtered.append(command)
return filtered
def load_commands(path, filters=None, overlay="ubuntu", **kwargs):
"""Load commands from a configuration file.
:param path: The path to the configuration file.
:type path: str
:param filters: Used to filter commands.
:type filters: dict
:param overlay: The name of the command overlay to apply to generated commands.
:type overlay: str
:rtype: list[scriptetease.library.commands.base.Command] | scriptetease.library.commands.base.ItemizedCommand] |
None
:returns: A list of command instances or ``None`` if the configuration could not be loaded.
kwargs are passed to the configuration class for instantiation.
"""
_config = load_config(path, overlay, **kwargs)
if _config is None:
return None
commands = _config.get_commands()
if filters is not None:
criteria = dict()
for attribute, values in filters.items():
criteria[attribute] = values
commands = filter_commands(commands, **criteria)
return commands
def load_config(path, overlay="ubuntu", **kwargs):
"""Load a command configuration.
:param path: The path to the configuration file.
:type path: str
:param overlay: The name of the command overlay to apply to generated commands.
:type overlay: str
:rtype: Config | None
kwargs are passed to the configuration class for instantiation.
"""
if path.endswith(".ini"):
_config = Config(path, overlay=overlay, **kwargs)
# elif path.endswith(".yml"):
# _config = YAML(path, **kwargs)
else:
log.warning("Input file format is not currently supported: %s" % path)
return None
if not _config.load():
log.error("Failed to load config file: %s" % path)
return None
return _config

@ -1,159 +0,0 @@
# Imports
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import get_formatter_by_name
BashLexer = get_lexer_by_name("bash")
JSONLexer = get_lexer_by_name("json")
PythonLexer = get_lexer_by_name("python")
TerminalFormatter = get_formatter_by_name("terminal", linenos=True)
# Exports
__all__ = (
"any_list_item",
"filter_commands",
"filter_objects",
"highlight_code",
"split_csv",
)
# Functions
def any_list_item(a, b):
"""Determine whether any item in ``a`` also exists in ``b``.
:param a: The first list to be compared.
:type a: list
:param b: The second list to be compared.
:type b: list
:rtype: bool
"""
for i in a:
for j in b:
if i == j:
return True
return False
def filter_commands(commands, values, attribute="tags"):
"""Filter commands for a given set of values.
:param commands: The commands to be filtered.
:type commands: list[BaseType[Command]]
:param values: The values to be compared.
:type values: list
:param attribute: The name of the command attribute to check. This attribute must be a list or tuple of values of
the same type given in ``values``.
:type attribute: str
:rtype: bool
.. code-block:: python
commands = [
AddUser("bob"),
Apt("apache2", tags=["apache", "www"]),
Reload("postgresql", tags=["database", "pgsql"]),
Touch("/var/www/index.html", tags=["www"]),
]
values = ["apache", "www"]
# Outputs the Apt and Touch commands above.
filtered_commands = filter_commands(command, values)
print(filtered_commands)
"""
filtered = list()
for command in commands:
try:
list_b = getattr(command, attribute)
except AttributeError:
continue
if not any_list_item(values, list_b):
continue
filtered.append(command)
return filtered
def filter_objects(objects, environments=None, scope=None, tags=None):
"""Filter the given objects by the given keys.
:param objects: The objects to be filtered.
:type objects: list
:param environments: The environments to be included.
:type environments: list[str]
:param scope: The scope by which to filter; deploy, provision, tenant.
:type scope: str
:param tags: The tags to be included.
:type tags: list[str]
:rtype: list
:returns: Returns the objects that match the given keys.
"""
filtered = list()
# print("object, object environments, environments, any_list_item")
for o in objects:
# print(o, o.environments, environments, any_list_item(environments, o.environments))
# Apply environment filter.
if environments is not None:
if hasattr(o, "environment"):
if o.environment is not None and o.environment not in environments:
continue
elif hasattr(o, "environments"):
if type(o.environments) in (list, tuple) and not any_list_item(environments, o.environments):
continue
else:
pass
# # Apply scope filter.
# if scope is not None:
# if o.scope not in [None, SCOPE_ALL, scope]:
# continue
# Apply tag filter.
if tags is not None:
if not any_list_item(tags, o.tags):
continue
# The object has passed the tests above.
filtered.append(o)
return filtered
def highlight_code(string, lexer=None):
"""Highlight (colorize) the given string as Python code.
:param string: The string to be highlighted.
:type string: str
:rtype: str
"""
if lexer is None:
lexer = BashLexer
return highlight(string, lexer, TerminalFormatter)

@ -12,22 +12,22 @@ def read_file(path):
setup( setup(
name='python-script-tease', name='python-scripttease',
version=read_file("VERSION.txt"), version=read_file("VERSION.txt"),
description=read_file("DESCRIPTION.txt"), description=read_file("DESCRIPTION.txt"),
long_description=read_file("README.markdown"), long_description=read_file("README.markdown"),
author='Shawn Davis', author='Shawn Davis',
author_email='shawn@myninjas.net', author_email='shawn@develmaycare.com',
url='https://bitbucket.com/myninjas/python-script-tease', url='https://github.com/develmaycare/python-scripttease',
packages=find_packages(), packages=find_packages(),
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=[
"jinja2", "jinja2",
"pygments", "pygments",
"python-myninjas", "superpython",
], ],
dependency_links=[ dependency_links=[
"https://bitbucket.com/myninjas/python-myninjas/master.tar.gz#python-myninjas", "https://github.com/develmaycare/superpython",
], ],
classifiers=[ classifiers=[
'Development Status :: 2 - Pre-Alpha', 'Development Status :: 2 - Pre-Alpha',

@ -15,15 +15,3 @@ apache.enable_module = ssl
[atlernative syntax disable the default site] [atlernative syntax disable the default site]
apache = site default apache = site default
state = disabled state = disabled
[apache]
disable_module = a2dismod {{ module_name }}
disable_site = a2dissite {{ domain_name }}.conf
enable_module = a2enmod {{ module_name }}
enable_site = a2ensite {{ domain_name }}.conf
reload = service apache2 reload
restart = service apache2 restart
start = service apache2 start
stop = service apache2 stop
test = apachectl configtest

@ -0,0 +1,2 @@
[this command will fail to load]
nonexistent = testing

@ -0,0 +1,2 @@
[this command will fail to load]
run - testing

@ -0,0 +1,6 @@
[create the site directory]
mkdir: /var/www/domains/{{ domain_tld }}
[this will cause template parsing to fail]
touch: /path/to/{% if this.will.break %}testing

@ -0,0 +1,25 @@
[function for setting up apache]
func = apache_setup
[install apache]
install: apache2
func: apache_setup
[install wsgi]
install: mod_wsgi
func: apache_setup
[enable wsgi]
apache.enable_module: mod_wsgi
func: apache_setup
[disable the default site]
apache.disable_site = default
func: apache_setup
[restart apache]
apache: restart
func: apache_setup
[call apache setup]
run: apache_setup

@ -0,0 +1,192 @@
[update system repos]
system: update
[upgrade the system]
system: upgrade
[reboot the system]
system: reboot
[function for setting up apache]
func = apache_setup
[install apache]
install: apache2
func: apache_setup
[install wsgi]
install: mod_wsgi
func: apache_setup
[enable wsgi]
apache.enable_module: mod_wsgi
func: apache_setup
[disable the default site in function]
apache.disable_site = default
func: apache_setup
[restart apache]
apache: restart
func: apache_setup
[call apache setup]
run: apache_setup
[disable the default site]
apache.disable_site = default
[enable mod SSL]
apache.enable_module = mod_ssl
[enable more than one apache module at once]
apache.enable_module = $item
items = mod_wsgi, mod_rewrite
[make sure apache can be reloaded]
apache: test
[restart apache outside of function]
apache: restart
condition: $apache_checks_out -eq 0
[install the virtualenv package]
pip = virtualenv
[install django debug toolbar]
pip: django-debug-toolbar
env: development
tags: python, project
[create a virtual environment]
virtualenv = python
cd = /path/to/project
tags = python, project
[install pillow]
pip = Pillow
cd = /path/to/project
upgrade = yes
venv = python
tags = python, project
[apply database migrations]
django: migrate
cd: /path/to/project
venv: python
[run a custom django command]
django = custom_command arg1 arg2
cd = /path/to/project
venv = python
settings = tenants.example_app.settings
quiet = yes
[collect the project's static files]
django: collectstatic
cd: /path/to/project
venv: python
[load data fixtures]
django.loaddata: categories
cd: /path/to/project
venv: python
[dump data fixtures]
django.dumpdata: projects
cd: /path/to/project
venv: python
[reload a service]
reload: postfix
[restart a service]
restart: postfix
[stop a service]
stop: postfix
[start a service]
start: postfix
[install a package]
install: python3
[remove a package]
uninstall: apache-top
[add to a file]
append: /path/to/file.txt
content: this is a test
[copy a file]
copy: /path/to/file.txt /new/path/to/file.txt
[write (overwrite) a file]
write: /path/to/file.txt
content: this replaces all text in the file
[create a directory]
mkdir: /path/to/dir
mode: 755
[move a file]
move: /path/to/file.txt /path/to/file.txt.b
[set permissions on a file]
perms: /path/to/file.txt
group: www-data
mode: 755
owner: deploy
recursive: yes
[remove a file]
remove: /path/to/file.txt
[sync a directory]
rsync: /path/to/source /path/to/target
[copy a file to remote server]
scopy: /path/to/file.txt /path/to/server/file.txt
host: example.com
[replace text in a file]
sed: /path/to/file.txt
find: logging = no
replace: logging = yes
[create a symlink]
symlink: /var/www/domains
[touch a file]
touch: /path/to/file.txt
[create a postgres user/role]
pg.user: example_app
[create a postgres database]
pg.db: example_app
owner: example_app
[determine whether a postgres database exists]
pg.database_exists: example_app
[export a postgres database]
pg.dump: testing
[drop a postgres user/role]
pg.dropuser: testing
[drop a postgres database]
pg.dropdb: testing
[run an SQL command on a postgres database]
psql: "SELECT * FROM projects WHERE category = 'testing'"
database: example_app
owner: example_app
[create a file archive]
archive: /var/www/domains/example_com
[extract a file archive]
extract: /var/www/domains/example_com.tgz

@ -1,12 +1,15 @@
[install the virtualenv package] [install the virtualenv package]
pip = virtualenv pip = virtualenv
tags = python-support
[create a virtual environment] [create a virtual environment]
virtualenv = python virtualenv = python
cd = /path/to/project cd = /path/to/project
tags = python-support
[install pillow] [install pillow]
pip = Pillow pip = Pillow
cd = /path/to/project cd = /path/to/project
upgrade = yes upgrade = yes
venv = python venv = python
tags = depends

@ -0,0 +1,2 @@
[create the site directory]
mkdir: /var/www/domains/{{ domain_tld }}

@ -0,0 +1,40 @@
from scripttease.library.commands import Command, ItemizedCommand
from scripttease.factory import Factory
class TestFactory(object):
def test_get_command(self):
f = Factory("ubuntu")
f.load()
# Non-existent command.
c = f.get_command("nonexistent")
assert c is None
# A good command with itemized parameters.
c = f.get_command(
"pip",
"$item",
items=["Pillow", "psycopg2-binary", "django"]
)
assert isinstance(c, ItemizedCommand)
# A good, normal command.
c = f.get_command("pip", "django")
assert isinstance(c, Command)
# Command exists, but given bad arguments.
c = f.get_command("pip")
assert c is None
def test_load(self):
f = Factory("nonexistent")
assert f.load() is False
f = Factory("ubuntu")
assert f.load() is True
def test_repr(self):
f = Factory("centos")
assert repr(f) == "<Factory centos>"

@ -1,6 +1,5 @@
from scripttease.library.commands.base import Command, ItemizedCommand, Sudo from scripttease.library.commands.base import Command, ItemizedCommand, Sudo
from scripttease.library.commands.python import Pip from scripttease.library.overlays.common import python_pip
from scripttease.library.overlays import Overlay
class TestCommand(object): class TestCommand(object):
@ -65,22 +64,22 @@ class TestCommand(object):
class TestItemizedCommand(object): class TestItemizedCommand(object):
def test_getattr(self): def test_getattr(self):
c = ItemizedCommand(Pip, ["Pillow", "psycopg2-binary", "django"], "$item", extra=True) c = ItemizedCommand(python_pip, ["Pillow", "psycopg2-binary", "django"], "$item", extra=True)
assert c.extra is True assert c.extra is True
def test_get_commands(self): def test_get_commands(self):
c = ItemizedCommand(Pip, ["Pillow", "psycopg2-binary", "django"], "$item") c = ItemizedCommand(python_pip, ["Pillow", "psycopg2-binary", "django"], "$item")
commands = c.get_commands() commands = c.get_commands()
for i in commands: for i in commands:
assert isinstance(i, Pip) assert isinstance(i, Command)
def test_get_statement(self): def test_get_statement(self):
c = ItemizedCommand(Pip, ["Pillow", "psycopg2-binary", "django"], "$item") c = ItemizedCommand(python_pip, ["Pillow", "psycopg2-binary", "django"], "$item")
statement = c.get_statement() statement = c.get_statement()
assert "Pillow" in statement assert "Pillow" in statement
assert "psycopg2-binary" in statement assert "psycopg2-binary" in statement
assert "django" in statement assert "django" in statement
def test_repr(self): def test_repr(self):
c = ItemizedCommand(Pip, ["Pillow", "psycopg2-binary", "django"], "$item") c = ItemizedCommand(python_pip, ["Pillow", "psycopg2-binary", "django"], "$item")
assert repr(c) == "<ItemizedCommand Pip>" assert repr(c) == "<ItemizedCommand python_pip>"

@ -1,20 +0,0 @@
from scripttease.library.commands import command_factory, ItemizedCommand
from scripttease.library.commands.python import Pip
from scripttease.library.overlays import Overlay
def test_command_factory():
overlay = Overlay("ubuntu")
overlay.load()
command = command_factory("nonexistent", "non existent command", overlay)
assert command is None
command = command_factory("pip", "install pillow", overlay)
assert command is None
command = command_factory("pip", "install pillow", overlay, "Pillow")
assert isinstance(command, Pip)
command = command_factory("pip", "install various", overlay, "$item", items=["Pillow", "pyscopg2-binary", "django"])
assert isinstance(command, ItemizedCommand)

@ -1,18 +0,0 @@
from scripttease.library.commands.python import *
from scripttease.library.overlays import Overlay
def test_pip():
pip = Pip("Pillow")
assert "pip install -y Pillow" in pip.get_statement()
overlay = Overlay("ubuntu")
overlay.load()
pip = Pip("Pillow", op="remove", overlay=overlay, venv="python")
assert "source python/bin/activate && pip3 uninstall --quiet Pillow" in pip.get_statement()
def test_virtualenv():
virt = VirtualEnv()
assert "virtualenv python" in virt.get_statement()

@ -0,0 +1,17 @@
from scripttease.library.overlays.common import *
def test_python_pip():
c = python_pip("Pillow")
assert "pip install -y Pillow" in c.get_statement()
c = python_pip("Pillow", upgrade=True)
assert "--upgrade" in c.get_statement()
c = python_pip("Pillow", venv="python")
assert "source python/bin/activate" in c.get_statement()
def test_python_virtual_env():
c = python_virtualenv()
assert "virtualenv python" in c.get_statement()

@ -0,0 +1,61 @@
from scripttease.library.overlays.django import *
def test_django():
c = django("check")
assert "./manage.py check" in c.get_statement()
c = django("collectstatic")
assert "./manage.py collectstatic" in c.get_statement()
c = django("migrate")
assert "./manage.py migrate" in c.get_statement()
c = django("custom", "arg1", "arg2", venv="python", settings="tenants.example.settings", quiet=True)
s = c.get_statement()
assert "./manage.py custom" in s
assert "arg1" in s
assert "arg2" in s
assert "--settings=" in s
assert "source python/bin/activate" in s
assert "--quiet" in s
def test_django_check():
c = django_check(venv="python")
s = c.get_statement()
assert "./manage.py check" in s
assert "source python/bin/activate" in s
def test_django_collect_static():
c = django_collect_static(venv="python")
s = c.get_statement()
assert "./manage.py collectstatic" in s
assert "source python/bin/activate" in s
def test_django_dumpdata():
c = django_dumpdata("projects")
s = c.get_statement()
assert "./manage.py dumpdata" in s
assert "projects >" in s
assert "--format=json" in s
assert "--indent=4" in s
assert "local/projects/fixtures/initial.json" in s
def test_django_loaddata():
c = django_loaddata("projects")
s = c.get_statement()
print(s)
assert "./manage.py loaddata" in s
assert "local/projects/fixtures/initial.json" in s
def test_django_migrate():
c = django_migrate(cd="/path/to/project/", venv="python")
s = c.get_statement(cd=True)
assert "./manage.py migrate" in s
assert "source python/bin/activate" in s
assert "cd /path/to/project/" in s

@ -0,0 +1,60 @@
from scripttease.library.overlays.pgsql import *
def test_pg_create_database():
c = pg_create_database("testing", admin_pass="secret", template="mytemplate")
s = c.get_statement()
assert "createdb" in s
assert "export PGPASSWORD=" in s
assert "--host=" in s
assert "--port=" in s
assert "--username=" in s
assert "--owner=" in s
assert "--template=mytemplate" in s
assert "testing" in s
def test_pg_create_user():
c = pg_create_user("testing", password="secret")
s = c.get_statement()
assert "createuser" in s
assert "-DRS" in s
assert "testing" in s
assert "ALTER USER testing" in s
def test_pg_database_exists():
c = pg_database_exists("testing")
s = c.get_statement()
assert "psql" in s
assert "testing_db_exists" in s
def test_pg_drop_database():
c = pg_drop_database("testing")
s = c.get_statement()
assert "dropdb" in s
assert "testing" in s
def test_pg_drop_user():
c = pg_drop_user("testing")
s = c.get_statement()
assert "dropuser" in s
assert "testing" in s
def test_pg_dump_database():
c = pg_dump_database("testing")
s = c.get_statement()
assert "pg_dump" in s
assert "--column-inserts" in s
assert "--file=testing.sql" in s
def test_psql():
c = psql("SELECT * FROM projects", database="testing")
s = c.get_statement()
assert "psql" in s
assert "--dbname=testing" in s
assert '-c "SELECT * FROM projects"' in s

@ -0,0 +1,209 @@
import pytest
from scripttease.library.overlays.posix import *
def test_archive():
c = archive(
"/path/to/target",
absolute=True,
exclude="*.log",
strip=1,
view=True
)
s = c.get_statement()
print(s)
# tar -czPv --exclude *.log --strip-components 1 -f ./archive.tgz /path/to/target
assert "tar -czPv --exclude *.log --strip-components 1" in s
assert "-f ./archive.tgz /path/to/target" in s
def test_certbot():
with pytest.raises(ValueError):
c = certbot("example.com")
c = certbot("example.com", email="webmaster@example.com")
s = c.get_statement()
assert "certbot certonly --agree-tos --email webmaster@example.com -n" in s
assert "--webroot -w /var/www/domains/example_com/www -d example.com" in s
def test_extract():
c = extract(
"/path/to/archive.tgz",
absolute=True,
exclude="*.log",
strip=1,
view=True
)
s = c.get_statement()
assert "tar -xzPv --exclude *.log --strip-components 1" in s
assert "-f /path/to/archive.tgz ./" in s
def test_file_append():
c = file_append("/path/to/file.txt", content="testing = yes")
assert 'echo "testing = yes" >> /path/to/file.txt' in c.get_statement()
def test_file_copy():
c = file_copy("/path/to/file.txt", "/path/to/new-file.txt")
s = c.get_statement()
assert "cp" in s
assert "-n" in s
assert "/path/to/file.txt /path/to/new-file.txt" in s
c = file_copy("/path/to/dir", "/path/to/newdir", recursive=True)
s = c.get_statement()
assert "cp" in s
assert "-R" in s
assert "/path/to/dir /path/to/newdir" in s
def test_file_write():
c = file_write("/path/to/file.txt", content="testing 123")
assert 'echo "testing 123" > /path/to/file.txt' in c.get_statement()
content = [
"I am testing",
"I am testing",
"I am testing",
"testing 123",
]
c = file_write("/path/to/file.txt", content="\n".join(content))
s = c.get_statement()
assert "cat > /path/to/file.txt << EOF" in s
assert "I am testing" in s
assert "testing 123" in s
def test_mkdir():
c = mkdir("/path/to/dir", mode=755, recursive=True)
s = c.get_statement()
assert "mkdir" in s
assert "-m 755" in s
assert "-p" in s
assert "/path/to/dir" in s
def test_move():
c = move("/path/to/file.txt", "/path/to/file.txt.b")
assert "mv /path/to/file.txt /path/to/file.txt.b" in c.get_statement()
def test_perms():
c = perms("/path/to/dir", group="www-data", mode=755, owner="deploy", recursive=True)
s = c.get_statement()
assert "chgrp -R www-data /path/to/dir" in s
assert "chown -R deploy /path/to/dir" in s
assert "chmod -R 755 /path/to/dir" in s
def test_remove():
c = remove("/path/to/dir", force=True, recursive=True)
s = c.get_statement()
assert "rm" in s
assert "-f" in s
assert "-r" in s
assert "/path/to/dir" in s
def test_rsync():
c = rsync(
"/path/to/local/",
"/path/to/remote",
links=True,
delete=True,
exclude="deploy/exclude.txt",
recursive=True,
host="example.com",
key_file="~/.ssh/deploy",
user="deploy"
)
s = c.get_statement()
assert "rsync --cvs-exclude --checksum --compress --copy-links --delete" in s
assert "--exclude-from=deploy/exclude.txt" in s
assert "-P" in s
assert "--recursive /path/to/local/" in s
assert '-e "ssh -i ~/.ssh/deploy -p 22"' in s
assert "deploy@example.com:/path/to/remote" in s
c = rsync(
"/path/to/local/",
"/path/to/remote",
links=True,
delete=True,
exclude="deploy/exclude.txt",
recursive=True,
)
s = c.get_statement()
assert "rsync --cvs-exclude --checksum --compress --copy-links --delete" in s
assert "--exclude-from=deploy/exclude.txt" in s
assert "-P" in s
assert "--recursive" in s
assert "/path/to/local/" in s
assert "/path/to/remote" in s
def test_run():
c = run("ls -ls")
assert "ls -ls" in c.get_statement()
def test_scopy():
with pytest.raises(ValueError):
c = scopy("/path/to/local/file.txt", "/path/to/remote/file.txt")
c = scopy(
"/path/to/local/file.txt",
"/path/to/remote/file.txt",
key_file="~/.ssh/deploy",
host="example.com",
user="deploy"
)
s = c.get_statement()
assert "scp -i ~/.ssh/deploy" in s
assert "-P 22" in s
assert "/path/to/local/file.txt" in s
assert "deploy@example.com:/path/to/remote/file.txt" in s
c = scopy(
"/path/to/local/file.txt",
"/path/to/remote/file.txt",
host="example.com",
)
s = c.get_statement()
assert "scp -P 22" in s
assert "/path/to/local/file.txt" in s
assert "example.com:/path/to/remote/file.txt" in s
def test_sed():
c = sed("/path/to/file.txt", find="testing", replace="123")
s = c.get_statement()
assert "sed -i .b" in s
assert "s/testing/123/g" in s
assert "/path/to/file.txt" in s
def test_symlink():
c = symlink("/var/www/domains", force=True)
s = c.get_statement()
assert "ln -s" in s
assert "-f" in s
assert "/var/www/domains" in s
def test_touch():
c = touch("/path/to/file.txt")
assert "touch /path/to/file.txt" in c.get_statement()
class TestFunction(object):
def test_to_string(self):
f = Function("testing", comment="A test function.")
f.commands.append(touch("/path/to/file.txt"))
s = f.to_string()
assert "# A test function." in s
assert "function testing()" in s
assert "touch /path/to/file.txt" in s

@ -0,0 +1,86 @@
import pytest
from scripttease.library.overlays.ubuntu import *
def test_apache():
c = apache("reload")
assert "service apache2 reload" in c.get_statement()
c = apache("restart")
assert "service apache2 restart" in c.get_statement()
c = apache("start")
assert "service apache2 start" in c.get_statement()
c = apache("stop")
assert "service apache2 stop" in c.get_statement()
c = apache("test")
assert "apachectl configtest" in c.get_statement()
with pytest.raises(NameError):
apache("nonexistent")
def test_apache_disable_module():
c = apache_disable_module("mod_ssl")
assert "a2dismod mod_ssl" in c.get_statement()
def test_apache_disable_site():
c = apache_disable_site("default")
assert "a2dissite default" in c.get_statement()
def test_apache_enable_module():
c = apache_enable_module("mod_wsgi")
assert "a2enmod mod_wsgi" in c.get_statement()
def test_apache_enable_site():
c = apache_enable_site("example.com")
assert "a2ensite example.com" in c.get_statement()
def test_service_reload():
c = service_reload("postfix")
assert "service postfix reload" in c.get_statement()
def test_service_restart():
c = service_restart("postfix")
assert "service postfix restart" in c.get_statement()
def test_service_start():
c = service_start("postfix")
assert "service postfix start" in c.get_statement()
def test_service_stop():
c = service_stop("postfix")
assert "service postfix stop" in c.get_statement()
def test_system():
c = system("reboot")
assert "reboot" in c.get_statement()
c = system("update")
assert "apt-get update -y" in c.get_statement()
c = system("upgrade")
assert "apt-get upgrade -y" in c.get_statement()
with pytest.raises(NameError):
system("nonexistent")
def test_system_install():
c = system_install("vim")
assert "apt-get install -y vim" in c.get_statement()
def test_system_uninstall():
c = system_uninstall("lftp")
assert "apt-get uninstall -y lftp" in c.get_statement()

@ -0,0 +1,31 @@
from scripttease.library.commands import Command, ItemizedCommand
from scripttease.library.overlays.posix import Function
from scripttease.library.scripts import Script
class TestScript(object):
def test_append(self):
s = Script("testing")
s.append(Command("ls -ls", comment="list some stuff"))
s.append(Command("touch /path/to/file.txt", comment="touch a file"))
s.append(Command("ln -s /path/to/file.txt", comment="link to a file"))
assert len(s.commands) == 3
def test_to_string(self):
s = Script("testing")
s.append(Command("ls -ls", comment="list some stuff"))
s.append(Command("touch /path/to/file.txt", comment="touch a file"))
s.append(Command("ln -s /path/to/file.txt", comment="link to a file"))
s.functions = list()
s.functions.append(Function("testing"))
output = s.to_string()
assert output == str(s)
assert "ls -ls" in output
assert "touch /path/to/file.txt" in output
assert "ln -s /path/to/file.txt" in output
assert "function testing()" in output

@ -1,19 +0,0 @@
from scripttease.library.overlays import Overlay
class TestOverlay(object):
def test_get(self):
overlay = Overlay("ubuntu")
overlay.load()
assert overlay.get("nonexistent", "nonexistent") is None
def test_has(self):
overlay = Overlay("ubuntu")
overlay.load()
assert overlay.has("nonexistent", "nonexistent") is False
assert overlay.has("python", "nonexistent") is False
def test_load(self):
overlay = Overlay("nonexistent")
assert overlay.load() is False

@ -0,0 +1,22 @@
import pytest
from scripttease.library.scripts import Script
# from scripttease.parsers import filter_commands, load_commands
from scripttease.parsers.base import Parser
class TestParser(object):
def test_as_script(self):
p = Parser("/path/to/nonexistent.txt")
assert isinstance(p.as_script(), Script)
# def test_get_commands(self):
# pass
#
# def test_get_functions(self):
# pass
def test_load(self):
p = Parser("/path/to/nonexistent.txt")
with pytest.raises(NotImplementedError):
p.load()

@ -0,0 +1,45 @@
import pytest
from scripttease.parsers.ini import Config
class TestConfig(object):
def test_get_commands(self):
c = Config("tests/examples/kitchen_sink.ini")
assert c.load() is True
assert len(c.get_commands()) > 0
def test_get_functions(self):
c = Config("tests/examples/kitchen_sink.ini")
assert c.load() is True
assert len(c.get_functions()) > 0
def test_load(self):
c = Config("nonexistent.ini")
assert c.load() is False
c = Config("tests/examples/python_examples.ini", overlay="nonexistent")
assert c.load() is False
c = Config("tests/examples/bad_examples.ini")
assert c.load() is False
c = Config("tests/examples/kitchen_sink.ini")
assert c.load() is True
c = Config("tests/examples/bad_command.ini")
assert c.load() is False
context = {
'domain_tld': "example_com",
}
c = Config("tests/examples/template_example.ini", context=context)
assert c.load() is True
context = {
'domain_tld': "example_com",
}
c = Config("tests/examples/bad_template_example.ini", context=context)
assert c.load() is False

@ -0,0 +1,42 @@
import pytest
from scripttease.library.commands import Command, ItemizedCommand
from scripttease.parsers import filter_commands, load_commands
def test_filter_commands():
commands = [
Command("apt-get install apache2 -y", environments=["base"], tags=["web"]),
Command("apt-get install apache-top -y", environments=["live"], tags=["web"]),
Command("pip install django-debug-toolbar", environments=["development"], tags=["django"]),
Command("pip install django", environments=["base"], tags=["django"]),
]
f1 = filter_commands(commands, environments=["base", "live"])
assert len(f1) == 3
f2 = filter_commands(commands, tags=["django"])
assert len(f2) == 2
f3 = filter_commands(commands, environments=["base", "development"])
assert len(f3) == 3
f4 = filter_commands(commands, environments=["base"], tags=["web"])
assert len(f4) == 1
def test_load_commands():
commands = load_commands("nonexistent.xml")
assert commands is None
commands = load_commands("nonexistent.ini")
assert commands is None
commands = load_commands("tests/examples/bad_examples.ini")
assert commands is None
commands = load_commands(
"tests/examples/python_examples.ini",
filters={
'tags': ["python-support"],
}
)
assert len(commands) == 2
Loading…
Cancel
Save