From 1bf9efd4c055b0815de2a410538285da89a22661 Mon Sep 17 00:00:00 2001 From: NilashishC Date: Tue, 1 Aug 2023 17:55:11 +0530 Subject: [PATCH] Updates Signed-off-by: NilashishC --- .config/dictionary.txt | 125 +++++++ .flake8 | 15 +- .isort.cfg | 5 + .pre-commit-config.yaml | 160 +++++++++ .yamllint | 3 + cspell.config.yaml | 50 +++ pyproject.toml | 7 +- requirements.txt | 4 +- src/ansible_creator/__init__.py | 2 +- src/ansible_creator/actions/create.py | 79 ++++- src/ansible_creator/actions/init.py | 50 +-- src/ansible_creator/cli.py | 38 ++- src/ansible_creator/constants.py | 4 +- src/ansible_creator/schemas/manifest.json | 308 ++++++++---------- .../{ => new_collection}/.isort.cfg.j2 | 0 .../.pre-commit-config.yaml.j2 | 0 .../{ => new_collection}/.prettierignore.j2 | 0 .../templates/{ => new_collection}/LICENSE.j2 | 0 .../{ => new_collection}/README.md.j2 | 0 .../{ => new_collection}/galaxy.yml.j2 | 0 .../{ => new_collection}/pyproject.toml.j2 | 0 .../workflows/test.yml.j2 | 2 +- src/ansible_creator/utils.py | 48 +++ src/ansible_creator/validators.py | 56 ++++ 24 files changed, 752 insertions(+), 204 deletions(-) create mode 100644 .config/dictionary.txt create mode 100644 .isort.cfg create mode 100644 .pre-commit-config.yaml create mode 100644 .yamllint create mode 100644 cspell.config.yaml rename src/ansible_creator/templates/{ => new_collection}/.isort.cfg.j2 (100%) rename src/ansible_creator/templates/{ => new_collection}/.pre-commit-config.yaml.j2 (100%) rename src/ansible_creator/templates/{ => new_collection}/.prettierignore.j2 (100%) rename src/ansible_creator/templates/{ => new_collection}/LICENSE.j2 (100%) rename src/ansible_creator/templates/{ => new_collection}/README.md.j2 (100%) rename src/ansible_creator/templates/{ => new_collection}/galaxy.yml.j2 (100%) rename src/ansible_creator/templates/{ => new_collection}/pyproject.toml.j2 (100%) rename src/ansible_creator/templates/{.github => new_collection}/workflows/test.yml.j2 (98%) create mode 100644 src/ansible_creator/utils.py create mode 100644 src/ansible_creator/validators.py diff --git a/.config/dictionary.txt b/.config/dictionary.txt new file mode 100644 index 0000000..aeca814 --- /dev/null +++ b/.config/dictionary.txt @@ -0,0 +1,125 @@ +COLORTERM +ESCDELAY +Kolkata +Lightbulbs +Mergeable +Regset +Representer +Rocannon's +SCAP +SIGTERM +Towncrier # Changelog generator +ansiblelint +apidoc +argname +argnames +argparser +argspec +argvalues +astimezone +autorefs +basesystem +caplog +cliconf # Ansible network cli abstraction plugin +codeclimate +colname +coltext +colval +commandline +copybutton +datadir +datarootdir +deflist +devel +doctree +dumpable +dunder +editables +extlinks +facelessuser +fedoraproject +fontawesome +fqcn +fromlist +headerlink +helpconfig +hostvars +hrefs +htmlproofer +httpapi +ignorespace +importables +inlinehilite +intersphinx +introspector +isatty +jsonschema +keepends +kegex # An action's regex +kegexes +lentext +levelname +libonig +lightbulbs +linenums +linkcheck +lintable +lintables +magiclink +maskables +maxsplit +mkdocs +mkdocstrings +moduleauthor +mqueue # https://github.com/ansible/ansible-runner/issues/984 +myproject +netcommon # Ansible network collection, seen in tests and README +netconf +nitpicky +nonblocking +oldmask +oneline +onigurumacffi +opensearch +oxfordcomma +pageview +preclear +precommand +premanent +pullable +pymdown +pymdownx +redhatinsights +representer +returndocs +runtimes +scrollback +sectionauthor +setuptools # Used in _version.pyi +smartquotes +somevalue +statemachine +stripspaces +subaction +subschema +superfences +templatable +templated +templating +testhost +testname +topbar +tracebacks +truecolor +usefixtures +userbase +viewcode +volmount +workdir +xmss +SKEL +OKGREEN +ENDC +topdown +addopts +testpaths diff --git a/.flake8 b/.flake8 index 8520d58..f20137a 100644 --- a/.flake8 +++ b/.flake8 @@ -33,11 +33,22 @@ extend-exclude = pip-wheel-metadata, # adjacent venv venv - # project tox directory - .tox + +# IMPORTANT: avoid using ignore option, always use extend-ignore instead +# Completely and unconditionally ignore the following errors: +extend-ignore = F, E203 # Accessibility/large fonts and PEP8 unfriendly: max-line-length = 100 +# Allow certain violations in certain files: +# Please keep both sections of this list sorted, as it will be easier for others to find and add entries in the future +per-file-ignores = + # The following ignores have been researched and should be considered permanent + # each should be preceeded with an explanation of each of the error codes + # If other ignores are added for a specific file in the section following this, + # these will need to be added to that line as well. + + # Count the number of occurrences of each error/warning code and print a report: statistics = true diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000..99abe3c --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,5 @@ +[settings] +line_length=100 +lines_after_imports=2 +lines_between_types=1 +profile=black diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..7080aa4 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,160 @@ +--- +default_language_version: + # ensures that we get same behavior on CI(s) as on local machines + python: python3.10 +exclude: > + (?x)^( + _readthedocs| + .tox + )$ +repos: + - repo: https://github.com/pre-commit/mirrors-prettier + rev: "v3.0.0" + hooks: + - id: prettier + # Original hook implementation is flaky due to *several* bugs described + # in https://github.com/prettier/prettier/issues/12364 + # a) CI=1 needed to avoid incomplete output + # b) two executions are needed because --list-different works correctly + # only when run with --check as with --write the output will also + # include other entries and logging level cannot be used to keep only + # modified files listed (any file is listes using the log level, + # regardless if + # is modified or not). + # c) We avoid letting pre-commit pass each filename in order to avoid + # runing multiple instances in parallel. This also ensures that running + # prettier from the command line behaves identically with the pre-commit + # one. No real performance downsides. + # d) exit with the return code from list-different (0=none, 1=some) + # rather than the write (0=successfully rewrote files). pre-commit.ci + entry: env CI=1 bash -c "prettier --list-different . || ec=$? && prettier --loglevel=error --write . && exit $ec" + pass_filenames: false + args: [] + additional_dependencies: + - prettier + - prettier-plugin-toml + + - repo: https://github.com/psf/black.git + rev: 23.7.0 + hooks: + - id: black + language_version: python3 + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.0.278" + hooks: + - id: ruff + args: + - "--exit-non-zero-on-fix" + + - repo: https://github.com/streetsidesoftware/cspell-cli + rev: v6.31.0 + hooks: + - id: cspell + name: Spell check with cspell + + - repo: https://github.com/Lucas-C/pre-commit-hooks.git + rev: v1.5.1 + hooks: + - id: remove-tabs + + - repo: https://github.com/pre-commit/pre-commit-hooks.git + rev: v4.4.0 + hooks: + # Side-effects: + - id: trailing-whitespace + - id: check-merge-conflict + - id: end-of-file-fixer + - id: requirements-txt-fixer + - id: check-added-large-files + - id: fix-byte-order-marker + - id: check-case-conflict + - id: check-symlinks + - id: check-yaml + exclude: > + (?x)^ + ( + mkdocs.yml + ) + $ + - id: detect-private-key + # Heavy checks: + - id: check-ast + - id: debug-statements + + - repo: https://gitlab.com/bmares/check-json5 + # Allow json comments, trailing commas + # https://github.com/pre-commit/pre-commit-hooks/issues/395 + rev: v1.0.0 + hooks: + - id: check-json5 + + - repo: https://github.com/igorshubovych/markdownlint-cli + rev: v0.35.0 + hooks: + - id: markdownlint + exclude: > + (?x)^ + ( + \.github/ISSUE_TEMPLATE/\w+| + docs/( + faq| + index| + )| + README| + src/ansible_navigator/data/(help|welcome) + )\.md + $ + + - repo: https://github.com/codespell-project/codespell + rev: v2.2.5 + hooks: + - id: codespell + # NOTE: dout is part of the stdout action regex + args: ["-L", "dout"] + # We exclude generated and external files as they are not directly under + # our control, so we cannot fix spelling in them. + exclude: > + (?x)^ + ( + tests/fixtures/integration/actions/.*\.json| + src/ansible_navigator/data/grammar/.*\.json + ) + $ + + - repo: https://github.com/adrienverge/yamllint.git + rev: v1.32.0 + hooks: + - id: yamllint + args: + - --strict + types: [file, yaml] + + - repo: https://github.com/PyCQA/flake8.git + rev: 6.0.0 + hooks: + - id: flake8 + language_version: python3 + additional_dependencies: + - darglint + - flake8-docstrings # uses pydocstyle + + - repo: https://github.com/asottile/pyupgrade + # keep it after flake8 + rev: v3.9.0 + hooks: + - id: pyupgrade + args: ["--py39-plus"] + + - repo: https://github.com/pycqa/pylint.git + rev: v3.0.0a6 + hooks: + - id: pylint + args: + - docs/ + - src/ + - tests/ + additional_dependencies: + - jinja2 + - jsonschema + pass_filenames: false diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..23598e7 --- /dev/null +++ b/.yamllint @@ -0,0 +1,3 @@ +ignore: | + .tox + .pre-commit-config.yaml diff --git a/cspell.config.yaml b/cspell.config.yaml new file mode 100644 index 0000000..0bfc57f --- /dev/null +++ b/cspell.config.yaml @@ -0,0 +1,50 @@ +--- +dictionaryDefinitions: + - name: words + path: .config/dictionary.txt + addWords: true +dictionaries: + - bash + - networking-terms + - python + - words + - "!aws" + - "!backwards-compatibility" + - "!cryptocurrencies" + - "!cpp" +ignorePaths: + # All dot files in the root + - \.* + # This file + - cspell.config.yaml + # The docs requirements file + - docs/requirements.in + # Ignore licenses + - licenses/* + # The mypy configuration file + - mypy.ini + # The shared file for tool configuration + - pyproject.toml + # The setup file + - setup.cfg + # All grammar (text-mate) tokenization files + - src/ansible_navigator/data/grammar/*.json + # Theme files + - src/ansible_navigator/data/themes/*.json + # Anything in the vendored tokenization directory + - src/ansible_navigator/tm_tokenize/* + # requirements.txt is generated + - requirements.txt + # All fixture files in the tests directory + - tests/fixtures/integration/actions/**/*.json + # The tox configuration file + - tox.ini + +ignoreRegExpList: + - "--\\w+" # ansible-navigator long CLI parameters + - "(window|_screen|win)\\.\\w+" # curses calls e.g. getyx + - "\\^.*$" # kegex regular expressions + - "ANSIBLE_\\w+" # ansible environment variables + - "curses\\.\\w+" # curses functions e.g. curses.doupdate + - "monkeypatch\\.\\w+" # monkeypatch functions e.g. monkeypatch.setenv + - "request\\.node\\.\\w+" # pytest request e.g. request.node.nodeid diff --git a/pyproject.toml b/pyproject.toml index c8d011f..442bec9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,4 +49,9 @@ write_to = "src/ansible_creator/_version.py" [tool.pylint] [tool.pylint.format] -max-line-length = 100 \ No newline at end of file +max-line-length = 100 + +[tool.ruff] +# Allow lines to be as long as 120 characters. +line-length = 100 +fix = true \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f048027..0a8ebaa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -Jinja2==3.0.3 -jsonschema \ No newline at end of file +Jinja2==3.0.3 +jsonschema diff --git a/src/ansible_creator/__init__.py b/src/ansible_creator/__init__.py index 4893444..1458106 100644 --- a/src/ansible_creator/__init__.py +++ b/src/ansible_creator/__init__.py @@ -1 +1 @@ -"""The ansible-creator application""" +"""The ansible-creator application.""" diff --git a/src/ansible_creator/actions/create.py b/src/ansible_creator/actions/create.py index 3fda314..94e552f 100644 --- a/src/ansible_creator/actions/create.py +++ b/src/ansible_creator/actions/create.py @@ -1,9 +1,82 @@ -"""Definitions for ansible-creator create action""" +"""Definitions for ansible-creator create action.""" + +import os +import yaml + +from ..utils import creator_exit +from ..validators import JSONSchemaValidator class AnsibleCreatorCreate: + """Class representing ansible-creator create subcommand.""" + def __init__(self, **args): - self.args = args + """Initialize the create action. + + Load and validate the content definition file. + + :param **args: Arguments passed for the create action + """ + self.file_path = args["file"] + self.content_def = self.load_config(self.file_path) + if not self.content_def: + creator_exit( + status="WARNING", + message=( + "The content definition file seems to be empty. No content to scaffold." + ), + ) + else: + # fail early is schema validation fails for any reason + self.validate_config(self.content_def) def run(self): - pass + """Start scaffolding the specified content(s).""" + + def load_config(self, file_path): + """Load the content definition file. + + :param file_path: Path to the content definition file. + :returns: A dictionary of content(s) to scaffold. + """ + content_def = {} + file_path = os.path.abspath(os.path.expanduser(os.path.expandvars(file_path))) + try: + with open(file_path) as content_file: + data = yaml.safe_load(content_file) + content_def = data + except FileNotFoundError: + creator_exit( + status="FAILURE", + message=( + f"Could not detect '{file_path}' file in this directory.\n" + "Use -f to specify a different location for the content definition file." + ), + ) + except (yaml.parser.ParserError, yaml.scanner.ScannerError) as exc: + creator_exit( + status="FAILURE", + message=f"Error occurred while parsing the definition file:\n{str(exc)}", + ) + + return content_def + + def validate_config(self, content_def): + """Validate the content definition against a pre-defined jsonschema. + + :param content_def: A dictionary of content(s) to scaffold. + :returns: True if no validation exceptions occur else False + """ + try: + errors = JSONSchemaValidator( + data=content_def, criteria="manifest.json" + ).validate() + except Exception as exc: + creator_exit(status="FAILURE", message=f"{exc}") + + if errors: + creator_exit( + status="FAILURE", + message="The following schema validation errors were found:\n\n" + + "\n".join(errors), + ) diff --git a/src/ansible_creator/actions/init.py b/src/ansible_creator/actions/init.py index a9e96b6..c5785ba 100644 --- a/src/ansible_creator/actions/init.py +++ b/src/ansible_creator/actions/init.py @@ -1,19 +1,22 @@ -"""Definitions for ansible-creator init action""" - +"""Definitions for ansible-creator init action.""" import os -import sys import shutil + from pathlib import Path -from ..constants import ( - COLLECTION_SKEL_DIRS, - COLLECTION_SKEL_TEMPLATES, - MessageColors, -) + +from ..constants import COLLECTION_SKEL_DIRS, COLLECTION_SKEL_TEMPLATES from ..template_engine import Templar +from ..utils import creator_exit class AnsibleCreatorInit: + """Class representing ansible-creator create subcommand.""" + def __init__(self, **args): + """Initialize the init action. + + :param args: Arguments passed for the init action + """ self._namespace = args["collection_name"].split(".")[0] self._collection_name = args["collection_name"].split(".")[-1] self._init_path = args["init_path"] @@ -21,26 +24,27 @@ def __init__(self, **args): self._templar = Templar() def run(self): + """Start scaffolding collection skeleton.""" col_path = os.path.join(self._init_path, self._namespace, self._collection_name) col_path = os.path.abspath(os.path.expanduser(os.path.expandvars(col_path))) # check if init_path already exists if os.path.exists(col_path): if os.path.isfile(col_path): - print( - f"{MessageColors['FAIL']}" - "- the path %s already exists, but is a file - aborting" % col_path + creator_exit( + status="FAILURE", + message=f"- the path {col_path} already exists, but is a file - aborting", ) - sys.exit(1) elif not self._force: - print( - f"{MessageColors['FAIL']}" - "- The directory %s already exists.\n" - "You can use --force to re-initialize this directory,\n" - "however it will delete ALL existing contents in it." % col_path + creator_exit( + status="FAILURE", + message=( + f"- The directory {col_path} already exists.\n" + "You can use --force to re-initialize this directory,\n" + "however it will delete ALL existing contents in it." + ), ) - sys.exit(1) else: # user requested --force, re-initializing existing directory @@ -80,8 +84,10 @@ def run(self): with open(dest_file, "w") as df: df.write(rendered_content) - print( - f"{MessageColors['OKGREEN']}" - "- Collection %s.%s was created successfully at %s" - % (self._namespace, self._collection_name, self._init_path) + creator_exit( + status="OKGREEN", + message=( + "- Collection %s.%s was created successfully at %s" + % (self._namespace, self._collection_name, self._init_path) + ), ) diff --git a/src/ansible_creator/cli.py b/src/ansible_creator/cli.py index d3ffa5a..1eed03e 100644 --- a/src/ansible_creator/cli.py +++ b/src/ansible_creator/cli.py @@ -1,15 +1,23 @@ -"""The ansible-creator CLI""" +"""The ansible-creator CLI.""" import argparse -from .actions.init import AnsibleCreatorInit + from .actions.create import AnsibleCreatorCreate +from .actions.init import AnsibleCreatorInit class AnsibleCreatorCLI: + """Class representing the ansible-creator CLI.""" + def __init__(self): + """Initialize the CLI and parse CLI args.""" self.args = self.parse_args() def get_version(self): + """Get the running ansible-creator version. + + :returns: The ansible-creator version. + """ try: from ._version import version as __version__ except ImportError: @@ -18,10 +26,13 @@ def get_version(self): return __version__ def parse_args(self): + """Start parsing args passed from CLI. + + :returns: A dictionary of CLI args. + """ parser = argparse.ArgumentParser( description=( - "Tool to scaffold Ansible Content. " - "Get started by looking at the help text." + "Tool to scaffold Ansible Content. Get started by looking at the help text." ) ) @@ -76,15 +87,31 @@ def parse_args(self): create_command_parser.add_argument( "-f", "--file", - default="./ansible-contents.yaml", + default="./content.yaml", help="A YAML file containing definition of Ansible Content(s) to be scaffolded.", ) + sample_command_parser = subparsers.add_parser( + "sample", + help="Generate a sample content.yaml file.", + description=( + "Generate a sample content.yaml file to serve as a reference." + ), + ) + + sample_command_parser.add_argument( + "-f", + "--file", + default="./contents.yaml", + help="Path where the sample content.yaml file will be added. Default: ./", + ) + args = parser.parse_args() return args def run(self): + """Dispatch work to correct action class.""" args = vars(self.args) if args["action"] == "init": AnsibleCreatorInit(**args).run() @@ -93,6 +120,7 @@ def run(self): def main(): + """Entry point for ansible-creator CLI.""" cli = AnsibleCreatorCLI() cli.run() diff --git a/src/ansible_creator/constants.py b/src/ansible_creator/constants.py index a6b5e68..d4facc5 100644 --- a/src/ansible_creator/constants.py +++ b/src/ansible_creator/constants.py @@ -1,8 +1,10 @@ +"""Definition of constants for this package.""" + MessageColors = { "HEADER": "\033[94m", "OKGREEN": "\033[92m", "WARNING": "\033[93m", - "FAIL": "\033[1;31m", + "FAILURE": "\033[1;31m", "OK": "\033[95m", "ENDC": "\033[0m", } diff --git a/src/ansible_creator/schemas/manifest.json b/src/ansible_creator/schemas/manifest.json index b6f82d2..df46083 100644 --- a/src/ansible_creator/schemas/manifest.json +++ b/src/ansible_creator/schemas/manifest.json @@ -1,173 +1,149 @@ { - "description": "JSON Schema for Ansible Content Builder MANIFEST", - "title": "Ansible Content Builder MANIFEST Schema", - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "collection": { - "type": "object", - "properties": { - "path": { - "type": "string" + "description": "JSON Schema for Ansible Content Builder MANIFEST", + "title": "Ansible Content Builder MANIFEST Schema", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "collection": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": ["path", "namespace", "name"] + }, + "license_file": { + "type": "string" + }, + "plugins": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "cache", + "action", + "filter", + "test", + "lookup", + "module_network_cli", + "module_network_netconf", + "module_security_httpapi", + "module_openapi" + ] + }, + "action": { + "type": "string" + }, + "content": { + "type": "string" + }, + "name": { + "type": "string" + }, + "docstring": { + "type": "string" + }, + "overwrite": { + "type": "boolean" + }, + "resource": { + "type": "string" + }, + "examples": { + "type": "string" + }, + "rm_swagger_json": { + "type": "string" + }, + "api_object_path": { + "type": "string" + }, + "module_version": { + "type": "string" + }, + "unique_key": { + "type": "string" + }, + "author": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "allOf": [ + { + "if": { + "properties": { + "type": { + "enum": ["module_network_cli", "module_network_netconf"] + } + } + }, + "then": { + "required": ["resource"] + } + }, + { + "if": { + "properties": { + "type": { + "enum": ["module_openapi"] }, - "namespace": { - "type": "string" + "content": { + "enum": ["amazon_cloud", "vmware_rest"] }, - "name": { - "type": "string" + "action": { + "enum": [ + "generate_all", + "generate_modules", + "generate_schema" + ] } + } }, - "required": [ - "path", - "namespace", - "name" - ] - }, - "license_file": { - "type": "string" - }, - "plugins": { - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "cache", - "action", - "filter", - "test", - "lookup", - "module_network_cli", - "module_network_netconf", - "module_security_httpapi", - "module_openapi" - ] - }, - "action": { - "type": "string" - }, - "content": { - "type": "string" - }, - "name": { - "type": "string" - }, - "docstring": { - "type": "string" - }, - "overwrite": { - "type": "boolean" - }, - "resource": { - "type": "string" - }, - "examples": { - "type": "string" - }, - "rm_swagger_json": { - "type": "string" - }, - "api_object_path": { - "type": "string" - }, - "module_version": { - "type": "string" - }, - "unique_key": { - "type": "string" - }, - "author": { - "type": "string" - }, - "version": { - "type": "string" - } + "then": { + "required": ["api_object_path"] + } + }, + { + "if": { + "properties": { + "type": { + "enum": ["module_openapi"] }, - "allOf": [ - { - "if": { - "properties": { - "type": { - "enum": [ - "module_network_cli", - "module_network_netconf" - ] - } - } - }, - "then": { - "required": [ - "resource" - ] - } - }, - { - "if": { - "properties": { - "type": { - "enum": [ - "module_openapi" - ] - }, - "content": { - "enum": [ - "amazon_cloud", - "vmware_rest" - ] - }, - "action": { - "enum": [ - "generate_all", - "generate_modules", - "generate_schema" - ] - } - } - }, - "then": { - "required": [ - "api_object_path" - ] - } - }, - { - "if": { - "properties": { - "type": { - "enum": [ - "module_openapi" - ] - }, - "not": { - "content": { - "enum": [ - "amazon_cloud", - "vmware_rest" - ] - } - } - } - }, - "then": { - "required": [ - "rm_swagger_json", - "api_object_path", - "module_version", - "unique_key", - "author" - ] - } - } - ], - "required": [ - "name", - "type" - ], - "additionalProperties": false + "not": { + "content": { + "enum": ["amazon_cloud", "vmware_rest"] + } + } + } + }, + "then": { + "required": [ + "rm_swagger_json", + "api_object_path", + "module_version", + "unique_key", + "author" + ] } - } - }, - "additionalProperties": false -} \ No newline at end of file + } + ], + "required": ["name", "type"], + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/src/ansible_creator/templates/.isort.cfg.j2 b/src/ansible_creator/templates/new_collection/.isort.cfg.j2 similarity index 100% rename from src/ansible_creator/templates/.isort.cfg.j2 rename to src/ansible_creator/templates/new_collection/.isort.cfg.j2 diff --git a/src/ansible_creator/templates/.pre-commit-config.yaml.j2 b/src/ansible_creator/templates/new_collection/.pre-commit-config.yaml.j2 similarity index 100% rename from src/ansible_creator/templates/.pre-commit-config.yaml.j2 rename to src/ansible_creator/templates/new_collection/.pre-commit-config.yaml.j2 diff --git a/src/ansible_creator/templates/.prettierignore.j2 b/src/ansible_creator/templates/new_collection/.prettierignore.j2 similarity index 100% rename from src/ansible_creator/templates/.prettierignore.j2 rename to src/ansible_creator/templates/new_collection/.prettierignore.j2 diff --git a/src/ansible_creator/templates/LICENSE.j2 b/src/ansible_creator/templates/new_collection/LICENSE.j2 similarity index 100% rename from src/ansible_creator/templates/LICENSE.j2 rename to src/ansible_creator/templates/new_collection/LICENSE.j2 diff --git a/src/ansible_creator/templates/README.md.j2 b/src/ansible_creator/templates/new_collection/README.md.j2 similarity index 100% rename from src/ansible_creator/templates/README.md.j2 rename to src/ansible_creator/templates/new_collection/README.md.j2 diff --git a/src/ansible_creator/templates/galaxy.yml.j2 b/src/ansible_creator/templates/new_collection/galaxy.yml.j2 similarity index 100% rename from src/ansible_creator/templates/galaxy.yml.j2 rename to src/ansible_creator/templates/new_collection/galaxy.yml.j2 diff --git a/src/ansible_creator/templates/pyproject.toml.j2 b/src/ansible_creator/templates/new_collection/pyproject.toml.j2 similarity index 100% rename from src/ansible_creator/templates/pyproject.toml.j2 rename to src/ansible_creator/templates/new_collection/pyproject.toml.j2 diff --git a/src/ansible_creator/templates/.github/workflows/test.yml.j2 b/src/ansible_creator/templates/new_collection/workflows/test.yml.j2 similarity index 98% rename from src/ansible_creator/templates/.github/workflows/test.yml.j2 rename to src/ansible_creator/templates/new_collection/workflows/test.yml.j2 index 06577fe..5f3eabe 100644 --- a/src/ansible_creator/templates/.github/workflows/test.yml.j2 +++ b/src/ansible_creator/templates/new_collection/workflows/test.yml.j2 @@ -39,4 +39,4 @@ jobs: '${{ needs.sanity.result }}', '${{ needs.unit-galaxy.result }}' ]) == {'success'}" -{% endraw %} \ No newline at end of file +{% endraw %} diff --git a/src/ansible_creator/utils.py b/src/ansible_creator/utils.py new file mode 100644 index 0000000..00b7746 --- /dev/null +++ b/src/ansible_creator/utils.py @@ -0,0 +1,48 @@ +"""Re-usable utility functions used by this package.""" + +import sys + +from importlib import resources + +from .constants import MessageColors + + +def get_file_contents(directory, filename): + """Return contents of a file. + + :param directory: A directory within ansible_creator package. + :param filename: Name of the file to read contents from. + + :raises FileNotFoundError: + :raises TypeError: + """ + package = f"ansible_creator.{directory}" + + try: + with resources.files(package).joinpath(filename).open( + "r", encoding="utf-8" + ) as fh: + content = fh.read() + except (FileNotFoundError, TypeError) as exc: + raise exc + + return content + + +def creator_exit(status, message): + """Helper function for printing a message and exiting the creator process. + + :param status: exit status + :param message: exit message + """ + if status not in MessageColors: + print( + f"{MessageColors['FAILURE']}Invalid exit status: {status}. This is likely a bug." + ) + else: + print(f"{MessageColors[status]}{message}") + + if status == "FAILURE": + sys.exit(1) + else: + sys.exit(0) diff --git a/src/ansible_creator/validators.py b/src/ansible_creator/validators.py new file mode 100644 index 0000000..587ed87 --- /dev/null +++ b/src/ansible_creator/validators.py @@ -0,0 +1,56 @@ +"""A schema validation helper for ansible-creator.""" + +from json import JSONDecodeError, loads + + +try: + from jsonschema import SchemaError + from jsonschema.validators import validator_for + + HAS_JSONSCHEMA = True +except ImportError: + HAS_JSONSCHEMA = False +from .utils import get_file_contents + + +class JSONSchemaValidator: + """Class representing a validation engine for ansible-creator content definition files.""" + + def __init__(self, data, criteria): + """Initialize the validation engine. + + :param data: Content definition as a dictionary + :param criteria: Name of the file that contains the JSON schema. + """ + self.data = data + try: + self.schema = loads(get_file_contents("schemas", criteria)) + except (FileNotFoundError, TypeError, JSONDecodeError) as err: + raise Exception( + f"unable to load schema for validation with error:\n{str(err)}" + ) + + def validate(self): + """Validate data against loaded schema. + + :returns: A list of schema validation errors (if any) + """ + errors = [] + validator = validator_for(self.schema) + try: + validator.check_schema(self.schema) + except SchemaError as schema_err: + raise Exception( + "Sanity check failed for in-built schema. This is likely a bug." + f"\n\n{str(schema_err)}" + ) + + validation_errors = sorted( + validator(self.schema).iter_errors(self.data), key=lambda e: e.path + ) + + for err in validation_errors: + err_msg = str(err.message) + " at " + str(err.instance) + "\n" + errors.append(err_msg) + + return errors