diff --git a/.dockerignore b/.dockerignore index 83cee50..821c19d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1 @@ -test/ .github \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50b4ba9..4a5cdbf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,11 @@ jobs: - name: Checkout VCS uses: actions/checkout@v1 + - name: 'Unit tests' + run: | + docker build --tag tests . + docker run --entrypoint python tests unit-tests.py + - name: '[run] simple' uses: ./ with: diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..52729dd --- /dev/null +++ b/.gitignore @@ -0,0 +1,146 @@ + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +*.pyc + + + + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +pytestdebug.log + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ +doc/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +pythonenv* + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# profiling data +.prof + +# End of https://www.toptal.com/developers/gitignore/api/python diff --git a/Dockerfile b/Dockerfile index 80afbfb..bb76dd3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,6 @@ RUN poetry config virtualenvs.create false COPY poetry.lock pyproject.toml ./ RUN poetry install -COPY entrypoint.py ./entrypoint.py +COPY . ./ ENTRYPOINT ["./entrypoint.py"] diff --git a/entrypoint.py b/entrypoint.py index 8271a05..2a59afc 100755 --- a/entrypoint.py +++ b/entrypoint.py @@ -1,39 +1,12 @@ #!/usr/bin/env python3 import os -from jinja2 import Template, StrictUndefined -from j2cli.context import read_context_data -def guess_format(file_name): - _, extension = os.path.splitext(file_name) - print(extension) - formats = { - '.yaml': 'yaml', - '.yml': 'yaml', - '.json': 'json', - '.ini': 'ini', - '.env': 'env', - } - return formats.get(extension, 'env') +from main import Context -variables = {'env': os.environ} -for variable in os.environ.get('INPUT_VARIABLES', '').split('\n'): - clean_variable = bytes(variable.strip(), 'utf-8').decode('unicode_escape') - if clean_variable != '': - name, value = clean_variable.split('=', 1) - variables.update({name: value}) - -data_file = os.environ.get('INPUT_DATA_FILE') -if data_file: - format = os.environ.get('INPUT_DATA_FORMAT', guess_format(data_file)) - with open(data_file, 'r') as file: - variables.update(read_context_data(format, file, None)) - -with open(os.environ['INPUT_TEMPLATE'], 'r') as file: - template_kwargs = {} - if os.environ.get('INPUT_STRICT') == 'true': - template_kwargs.update({'undefined': StrictUndefined}) - template = Template(str(file.read()), **template_kwargs) - -with open(os.environ['INPUT_OUTPUT_FILE'], 'w') as file: - file.write(template.render(**variables) + '\n') +if __name__ == '__main__': + context = Context(os.environ) + context.load_from_env() + context.load_from_input() + context.load_from_data_file() + context.render_template() diff --git a/enums.py b/enums.py new file mode 100644 index 0000000..4915c07 --- /dev/null +++ b/enums.py @@ -0,0 +1,13 @@ +from enum import Enum + + +class GitHubActionsInput(str, Enum): + DATA_FILE = 'INPUT_DATA_FILE' + DATA_FORMAT = 'INPUT_DATA_FORMAT' + OUTPUT_FILE = 'INPUT_OUTPUT_FILE' + STRICT = 'INPUT_STRICT' + TEMPLATE = 'INPUT_TEMPLATE' + VARIABLES = 'INPUT_VARIABLES' + + def __str__(self): + return self.value diff --git a/main.py b/main.py new file mode 100644 index 0000000..e46e0f5 --- /dev/null +++ b/main.py @@ -0,0 +1,53 @@ +import os +from j2cli.context import read_context_data +from jinja2 import Template, StrictUndefined +from enums import GitHubActionsInput + +class Context: + def __init__(self, environ): + self._variables = {} + self._environ = environ + + def load_from_env(self): + self._variables.update({'env': self._environ}) + + def load_from_input(self): + for variable in self._environ.get(GitHubActionsInput.VARIABLES, '').split('\n'): + clean_variable = bytes(variable.strip(), 'utf-8').decode('unicode_escape') + if clean_variable != '': + name, value = clean_variable.split('=', 1) + self._variables.update({name: value}) + + def load_from_data_file(self): + data_file = self._environ.get(GitHubActionsInput.DATA_FILE) + if data_file: + format = self._environ.get( + GitHubActionsInput.DATA_FORMAT, + self._guess_format(data_file), + ) + with open(data_file, 'r') as file: + self._variables.update(read_context_data(format, file, None)) + + + def render_template(self): + with open(self._environ[GitHubActionsInput.TEMPLATE], 'r') as file: + template_kwargs = {} + if self._environ.get(GitHubActionsInput.STRICT) == 'true': + template_kwargs.update({'undefined': StrictUndefined}) + template = Template(str(file.read()), **template_kwargs) + + with open(self._environ[GitHubActionsInput.OUTPUT_FILE], 'w') as file: + file.write(template.render(**self._variables) + '\n') + + + def _guess_format(self, file_name): + _, extension = os.path.splitext(file_name) + formats = { + '.yaml': 'yaml', + '.yml': 'yaml', + '.json': 'json', + '.ini': 'ini', + '.env': 'env', + } + return formats.get(extension, 'env') + diff --git a/unit-tests.py b/unit-tests.py new file mode 100644 index 0000000..43bb5f3 --- /dev/null +++ b/unit-tests.py @@ -0,0 +1,69 @@ +import os +import unittest + +from enums import GitHubActionsInput +from main import Context + + +class TestContext(unittest.TestCase): + TEST_FILE = 'test_output' + + def test_load_from_env(self): + env_vars = { + 'foo': 'bar', + GitHubActionsInput.VARIABLES.value: 'name=John\nsurname=Doe' + } + + context = Context(env_vars) + context.load_from_env() + expected = {'env': env_vars} + + self.assertEqual(context._variables, expected) + + def test_load_from_input(self): + context = Context({GitHubActionsInput.VARIABLES.value: 'name=John\nsurname=Doe'}) + context.load_from_input() + + expected = { + 'name': 'John', + 'surname': 'Doe', + } + self.assertEqual(context._variables, expected) + + + def test_load_from_data_file(self): + context = Context({ + GitHubActionsInput.DATA_FILE.value: 'test/data-files/data.yml' + }) + + context.load_from_data_file() + + expected = { + 'foo': 'bar', + 'baz': 'cux', + } + self.assertEqual(context._variables, expected) + + def test_render_template(self): + context = Context({ + GitHubActionsInput.DATA_FILE.value: 'test/data-files/data.json', + GitHubActionsInput.OUTPUT_FILE.value: self.TEST_FILE, + GitHubActionsInput.TEMPLATE.value: 'test/many-variables/template', + }) + + context.load_from_data_file() + context.render_template() + + expected = "\nbar\ncux\n" + + with open('test_output', 'r') as file: + result = file.read() + self.assertEqual(result, expected) + + @classmethod + def tearDownClass(cls): + if os.path.exists(cls.TEST_FILE): + os.remove(cls.TEST_FILE) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file