structkit 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- structkit/__init__.py +6 -0
- structkit/commands/__init__.py +17 -0
- structkit/commands/completion.py +65 -0
- structkit/commands/generate.py +397 -0
- structkit/commands/generate_schema.py +67 -0
- structkit/commands/import.py +63 -0
- structkit/commands/info.py +87 -0
- structkit/commands/init.py +52 -0
- structkit/commands/list.py +89 -0
- structkit/commands/mcp.py +100 -0
- structkit/commands/validate.py +129 -0
- structkit/completers.py +54 -0
- structkit/content_fetcher.py +249 -0
- structkit/contribs/README.md +271 -0
- structkit/contribs/ansible-playbook.yaml +38 -0
- structkit/contribs/chef-cookbook.yaml +51 -0
- structkit/contribs/ci-cd-pipelines.yaml +67 -0
- structkit/contribs/cloudformation-files.yaml +21 -0
- structkit/contribs/configs/chglog.yaml +31 -0
- structkit/contribs/configs/codeowners.yaml +3 -0
- structkit/contribs/configs/devcontainer.yaml +35 -0
- structkit/contribs/configs/editor-config.yaml +11 -0
- structkit/contribs/configs/eslint.yaml +30 -0
- structkit/contribs/configs/jshint.yaml +11 -0
- structkit/contribs/configs/kubectl.yaml +23 -0
- structkit/contribs/configs/prettier.yaml +19 -0
- structkit/contribs/docker-files.yaml +27 -0
- structkit/contribs/documentation-template.yaml +33 -0
- structkit/contribs/git-hooks.yaml +19 -0
- structkit/contribs/github/chatmodes/plan.yaml +18 -0
- structkit/contribs/github/instructions/generic.yaml +5 -0
- structkit/contribs/github/prompts/generic.yaml +4 -0
- structkit/contribs/github/prompts/react-form.yaml +17 -0
- structkit/contribs/github/prompts/security-api.yaml +8 -0
- structkit/contribs/github/prompts/struct.yaml +90 -0
- structkit/contribs/github/templates.yaml +91 -0
- structkit/contribs/github/workflows/codeql.yaml +88 -0
- structkit/contribs/github/workflows/execute-tf-workflow.yaml +39 -0
- structkit/contribs/github/workflows/labeler.yaml +77 -0
- structkit/contribs/github/workflows/pre-commit.yaml +27 -0
- structkit/contribs/github/workflows/release-drafter.yaml +77 -0
- structkit/contribs/github/workflows/run-struct.yaml +30 -0
- structkit/contribs/github/workflows/stale.yaml +16 -0
- structkit/contribs/helm-chart.yaml +160 -0
- structkit/contribs/kubernetes-manifests.yaml +103 -0
- structkit/contribs/project/custom-structures.yaml +24 -0
- structkit/contribs/project/generic.yaml +309 -0
- structkit/contribs/project/go.yaml +104 -0
- structkit/contribs/project/java.yaml +85 -0
- structkit/contribs/project/n8n.yaml +100 -0
- structkit/contribs/project/nodejs.yaml +101 -0
- structkit/contribs/project/python.yaml +136 -0
- structkit/contribs/project/ruby.yaml +130 -0
- structkit/contribs/project/rust.yaml +106 -0
- structkit/contribs/prompts/run-struct-trigger.yaml +18 -0
- structkit/contribs/terraform/apps/aws-accounts.yaml +21 -0
- structkit/contribs/terraform/apps/environments.yaml +41 -0
- structkit/contribs/terraform/apps/generic.yaml +41 -0
- structkit/contribs/terraform/apps/github-organization.yaml +40 -0
- structkit/contribs/terraform/apps/init.yaml +11 -0
- structkit/contribs/terraform/modules/generic.yaml +58 -0
- structkit/contribs/vagrant-files.yaml +21 -0
- structkit/file_item.py +182 -0
- structkit/filters.py +112 -0
- structkit/input_store.py +35 -0
- structkit/logging_config.py +36 -0
- structkit/main.py +85 -0
- structkit/mcp_server.py +347 -0
- structkit/model_wrapper.py +47 -0
- structkit/template_renderer.py +258 -0
- structkit/utils.py +36 -0
- structkit-3.0.0.dist-info/METADATA +182 -0
- structkit-3.0.0.dist-info/RECORD +77 -0
- structkit-3.0.0.dist-info/WHEEL +5 -0
- structkit-3.0.0.dist-info/entry_points.txt +2 -0
- structkit-3.0.0.dist-info/licenses/LICENSE +201 -0
- structkit-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import yaml
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from structkit.commands import Command
|
|
6
|
+
|
|
7
|
+
# Info command class for exposing information about the structure
|
|
8
|
+
class InfoCommand(Command):
|
|
9
|
+
def __init__(self, parser):
|
|
10
|
+
super().__init__(parser)
|
|
11
|
+
parser.description = "Show information about the package or structure definition"
|
|
12
|
+
parser.add_argument('structure_definition', type=str, help='Name of the structure definition')
|
|
13
|
+
parser.add_argument('-s', '--structures-path', type=str, help='Path to structure definitions')
|
|
14
|
+
parser.add_argument('--mcp', action='store_true', help='Enable MCP (Model Context Protocol) integration')
|
|
15
|
+
|
|
16
|
+
parser.set_defaults(func=self.execute)
|
|
17
|
+
|
|
18
|
+
def execute(self, args):
|
|
19
|
+
self.logger.info(f"Getting info for structure {args.structure_definition}")
|
|
20
|
+
|
|
21
|
+
if args.mcp:
|
|
22
|
+
self._get_info_mcp(args)
|
|
23
|
+
else:
|
|
24
|
+
self._get_info(args)
|
|
25
|
+
|
|
26
|
+
def _get_info(self, args):
|
|
27
|
+
if args.structure_definition.startswith("file://") and args.structure_definition.endswith(".yaml"):
|
|
28
|
+
with open(args.structure_definition[7:], 'r') as f:
|
|
29
|
+
config = yaml.safe_load(f)
|
|
30
|
+
else:
|
|
31
|
+
if args.structures_path is None:
|
|
32
|
+
this_file = os.path.dirname(os.path.realpath(__file__))
|
|
33
|
+
file_path = os.path.join(this_file, "..", "contribs", f"{args.structure_definition}.yaml")
|
|
34
|
+
else:
|
|
35
|
+
file_path = os.path.join(args.structures_path, f"{args.structure_definition}.yaml")
|
|
36
|
+
# show error if file is not found
|
|
37
|
+
if not os.path.exists(file_path):
|
|
38
|
+
self.logger.error(f"❗ File not found: {file_path}")
|
|
39
|
+
return
|
|
40
|
+
with open(file_path, 'r') as f:
|
|
41
|
+
config = yaml.safe_load(f)
|
|
42
|
+
|
|
43
|
+
print("📒 Structure definition\n")
|
|
44
|
+
print(f" 📌 Name: {args.structure_definition}\n")
|
|
45
|
+
print(f" 📌 Description: {config.get('description', 'No description')}\n")
|
|
46
|
+
|
|
47
|
+
if config.get('files'):
|
|
48
|
+
print(f" 📌 Files:")
|
|
49
|
+
for item in config.get('files', []):
|
|
50
|
+
for name, content in item.items():
|
|
51
|
+
print(f" - {name} ")
|
|
52
|
+
# indent all lines of content
|
|
53
|
+
# for line in content.get('content', content.get('file', 'Not defined')).split('\n'):
|
|
54
|
+
# print(f" {line}")
|
|
55
|
+
|
|
56
|
+
if config.get('folders'):
|
|
57
|
+
print(f" 📌 Folders:")
|
|
58
|
+
for folder in config.get('folders', []):
|
|
59
|
+
print(f" - {folder}")
|
|
60
|
+
# print(f" - {folder}: {folder.get('struct', 'No structure')}")
|
|
61
|
+
|
|
62
|
+
def _get_info_mcp(self, args):
|
|
63
|
+
"""Get structure info using MCP integration."""
|
|
64
|
+
try:
|
|
65
|
+
from structkit.mcp_server import StructMCPServer
|
|
66
|
+
|
|
67
|
+
async def run_mcp_info():
|
|
68
|
+
server = StructMCPServer()
|
|
69
|
+
arguments = {
|
|
70
|
+
"structure_name": args.structure_definition
|
|
71
|
+
}
|
|
72
|
+
if args.structures_path:
|
|
73
|
+
arguments["structures_path"] = args.structures_path
|
|
74
|
+
|
|
75
|
+
result = await server._handle_get_structure_info(arguments)
|
|
76
|
+
return result.content[0].text
|
|
77
|
+
|
|
78
|
+
result_text = asyncio.run(run_mcp_info())
|
|
79
|
+
print(result_text)
|
|
80
|
+
|
|
81
|
+
except ImportError:
|
|
82
|
+
self.logger.error("MCP support not available. Install with: pip install mcp")
|
|
83
|
+
self._get_info(args)
|
|
84
|
+
except Exception as e:
|
|
85
|
+
self.logger.error(f"MCP integration error: {e}")
|
|
86
|
+
self.logger.info("Falling back to standard info mode")
|
|
87
|
+
self._get_info(args)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from structkit.commands import Command
|
|
2
|
+
import os
|
|
3
|
+
import textwrap
|
|
4
|
+
|
|
5
|
+
BASIC_STRUCTKIT_YAML = textwrap.dedent(
|
|
6
|
+
"""
|
|
7
|
+
# Generated by `structkit init`
|
|
8
|
+
# Edit as needed to customize your project bootstrap
|
|
9
|
+
|
|
10
|
+
pre_hooks:
|
|
11
|
+
- echo "Starting structkit generation"
|
|
12
|
+
|
|
13
|
+
post_hooks:
|
|
14
|
+
- echo "Structkit generation completed"
|
|
15
|
+
|
|
16
|
+
files:
|
|
17
|
+
- README.md: |
|
|
18
|
+
# Project
|
|
19
|
+
|
|
20
|
+
Initialized with struct.
|
|
21
|
+
|
|
22
|
+
folders:
|
|
23
|
+
- ./:
|
|
24
|
+
struct:
|
|
25
|
+
- github/workflows/run-struct
|
|
26
|
+
"""
|
|
27
|
+
).lstrip()
|
|
28
|
+
|
|
29
|
+
class InitCommand(Command):
|
|
30
|
+
def __init__(self, parser):
|
|
31
|
+
super().__init__(parser)
|
|
32
|
+
parser.description = "Initialize a basic .struct.yaml in the target directory"
|
|
33
|
+
parser.add_argument('path', nargs='?', default='.', help='Directory to initialize (default: current directory)')
|
|
34
|
+
parser.set_defaults(func=self.execute)
|
|
35
|
+
|
|
36
|
+
def execute(self, args):
|
|
37
|
+
base_path = os.path.abspath(args.path or '.')
|
|
38
|
+
target = os.path.join(base_path, '.struct.yaml')
|
|
39
|
+
|
|
40
|
+
os.makedirs(base_path, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
# If file exists, do not overwrite without explicit confirmation behavior (keep simple: skip)
|
|
43
|
+
if os.path.exists(target):
|
|
44
|
+
self.logger.info(f".struct.yaml already exists at {target}, skipping creation")
|
|
45
|
+
print(f"⚠️ .struct.yaml already exists at: {target}")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
with open(target, 'w') as f:
|
|
49
|
+
f.write(BASIC_STRUCTKIT_YAML)
|
|
50
|
+
|
|
51
|
+
print("✅ Created .struct.yaml")
|
|
52
|
+
print(f" - {target}")
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from structkit.commands import Command
|
|
2
|
+
import os
|
|
3
|
+
import yaml
|
|
4
|
+
import asyncio
|
|
5
|
+
from structkit.file_item import FileItem
|
|
6
|
+
from structkit.utils import project_path
|
|
7
|
+
|
|
8
|
+
# List command class
|
|
9
|
+
class ListCommand(Command):
|
|
10
|
+
def __init__(self, parser):
|
|
11
|
+
super().__init__(parser)
|
|
12
|
+
parser.description = "List available structures"
|
|
13
|
+
parser.add_argument('-s', '--structures-path', type=str, help='Path to structure definitions')
|
|
14
|
+
parser.add_argument('--names-only', action='store_true', help='Print only structure names, one per line (for shell completion)')
|
|
15
|
+
parser.add_argument('--mcp', action='store_true', help='Enable MCP (Model Context Protocol) integration')
|
|
16
|
+
parser.set_defaults(func=self.execute)
|
|
17
|
+
|
|
18
|
+
def execute(self, args):
|
|
19
|
+
self.logger.info(f"Listing available structures")
|
|
20
|
+
if args.mcp:
|
|
21
|
+
self._list_structures_mcp(args)
|
|
22
|
+
else:
|
|
23
|
+
self._list_structures(args)
|
|
24
|
+
|
|
25
|
+
def _list_structures(self, args):
|
|
26
|
+
this_file = os.path.dirname(os.path.realpath(__file__))
|
|
27
|
+
contribs_path = os.path.join(this_file, "..", "contribs")
|
|
28
|
+
|
|
29
|
+
if args.structures_path:
|
|
30
|
+
final_path = args.structures_path
|
|
31
|
+
paths_to_list = [final_path, contribs_path]
|
|
32
|
+
else:
|
|
33
|
+
paths_to_list = [contribs_path]
|
|
34
|
+
|
|
35
|
+
all_structures = set()
|
|
36
|
+
for path in paths_to_list:
|
|
37
|
+
for root, _, files in os.walk(path):
|
|
38
|
+
for file in files:
|
|
39
|
+
file_path = os.path.join(root, file)
|
|
40
|
+
rel_path = os.path.relpath(file_path, path)
|
|
41
|
+
if file.endswith(".yaml"):
|
|
42
|
+
rel_path = rel_path[:-5]
|
|
43
|
+
# Mark custom path entries with '+ ' unless names-only requested
|
|
44
|
+
if not args.names_only and path != contribs_path:
|
|
45
|
+
rel_path = f"+ {rel_path}"
|
|
46
|
+
all_structures.add(rel_path)
|
|
47
|
+
|
|
48
|
+
sorted_list = sorted(all_structures)
|
|
49
|
+
|
|
50
|
+
if args.names_only:
|
|
51
|
+
# Print plain names without bullets or headers, remove '+ ' marker
|
|
52
|
+
for structure in sorted_list:
|
|
53
|
+
if structure.startswith('+ '):
|
|
54
|
+
print(structure[2:])
|
|
55
|
+
else:
|
|
56
|
+
print(structure)
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
print("📃 Listing available structures\n")
|
|
60
|
+
for structure in sorted_list:
|
|
61
|
+
print(f" - {structure}")
|
|
62
|
+
|
|
63
|
+
print("\nUse 'structkit generate' to generate the structure")
|
|
64
|
+
print("Note: Structures with '+' sign are custom structures")
|
|
65
|
+
|
|
66
|
+
def _list_structures_mcp(self, args):
|
|
67
|
+
"""List structures using MCP integration."""
|
|
68
|
+
try:
|
|
69
|
+
from structkit.mcp_server import StructMCPServer
|
|
70
|
+
|
|
71
|
+
async def run_mcp_list():
|
|
72
|
+
server = StructMCPServer()
|
|
73
|
+
arguments = {}
|
|
74
|
+
if args.structures_path:
|
|
75
|
+
arguments["structures_path"] = args.structures_path
|
|
76
|
+
|
|
77
|
+
result = await server._handle_list_structures(arguments)
|
|
78
|
+
return result.content[0].text
|
|
79
|
+
|
|
80
|
+
result_text = asyncio.run(run_mcp_list())
|
|
81
|
+
print(result_text)
|
|
82
|
+
|
|
83
|
+
except ImportError:
|
|
84
|
+
self.logger.error("MCP support not available. Install with: pip install mcp")
|
|
85
|
+
self._list_structures(args)
|
|
86
|
+
except Exception as e:
|
|
87
|
+
self.logger.error(f"MCP integration error: {e}")
|
|
88
|
+
self.logger.info("Falling back to standard list mode")
|
|
89
|
+
self._list_structures(args)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from structkit.commands import Command
|
|
4
|
+
from structkit.mcp_server import StructMCPServer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# MCP command class for starting the MCP server (FastMCP stdio only)
|
|
8
|
+
class MCPCommand(Command):
|
|
9
|
+
def __init__(self, parser):
|
|
10
|
+
super().__init__(parser)
|
|
11
|
+
parser.description = "MCP (Model Context Protocol) using FastMCP transports (stdio, http, sse)"
|
|
12
|
+
parser.add_argument('--server', action='store_true',
|
|
13
|
+
help='Start the MCP server')
|
|
14
|
+
parser.add_argument('--transport', choices=['stdio', 'http', 'sse'], default='stdio',
|
|
15
|
+
help='Transport protocol for the MCP server (default: stdio)')
|
|
16
|
+
# HTTP/SSE options
|
|
17
|
+
parser.add_argument('--host', type=str, default='127.0.0.1', help='Host to bind for HTTP/SSE transports')
|
|
18
|
+
parser.add_argument('--port', type=int, default=8000, help='Port to bind for HTTP/SSE transports')
|
|
19
|
+
parser.add_argument('--path', type=str, default='/mcp', help='Endpoint path for HTTP/SSE transports')
|
|
20
|
+
parser.add_argument('--uvicorn-log-level', dest='uvicorn_log_level', type=str, default=None,
|
|
21
|
+
help='Log level for the HTTP server (e.g., info, warning, error)')
|
|
22
|
+
parser.add_argument('--stateless-http', action='store_true', default=None,
|
|
23
|
+
help='Use stateless HTTP mode (HTTP transport only)')
|
|
24
|
+
parser.add_argument('--no-banner', dest='show_banner', action='store_false', default=True,
|
|
25
|
+
help='Disable FastMCP startup banner')
|
|
26
|
+
# Debugging options
|
|
27
|
+
parser.add_argument('--debug', action='store_true', help='Enable debug mode (sets structkit and FastMCP loggers to DEBUG by default)')
|
|
28
|
+
parser.add_argument('--fastmcp-log-level', dest='fastmcp_log_level', type=str, default=None,
|
|
29
|
+
help='Log level for FastMCP internals (e.g., DEBUG, INFO). Overrides --debug for FastMCP if provided')
|
|
30
|
+
parser.set_defaults(func=self.execute)
|
|
31
|
+
|
|
32
|
+
def execute(self, args):
|
|
33
|
+
if args.server:
|
|
34
|
+
self.logger.info(
|
|
35
|
+
f"Starting FastMCP server for structkit tool (transport={args.transport})"
|
|
36
|
+
)
|
|
37
|
+
asyncio.run(self._start_mcp_server(args))
|
|
38
|
+
else:
|
|
39
|
+
print("MCP (Model Context Protocol) support for structkit tool (FastMCP)")
|
|
40
|
+
print("\nAvailable options:")
|
|
41
|
+
print(" --server Start the MCP server")
|
|
42
|
+
print(" --transport {stdio|http|sse} Transport protocol (default: stdio)")
|
|
43
|
+
print(" --host HOST Host for HTTP/SSE (default: 127.0.0.1)")
|
|
44
|
+
print(" --port PORT Port for HTTP/SSE (default: 8000)")
|
|
45
|
+
print(" --path /PATH Endpoint path for HTTP/SSE (default: /mcp)")
|
|
46
|
+
print(" --stateless-http Enable stateless HTTP mode (HTTP only)")
|
|
47
|
+
print(" --no-banner Disable FastMCP banner")
|
|
48
|
+
print(" --debug Enable debug mode (structkit + FastMCP DEBUG; uvicorn=debug)")
|
|
49
|
+
print(" --fastmcp-log-level LVL Set FastMCP logger level (overrides --debug for FastMCP)")
|
|
50
|
+
print("\nMCP tools available:")
|
|
51
|
+
print(" - list_structures: List all available structure definitions")
|
|
52
|
+
print(" - get_structure_info: Get detailed information about a structure")
|
|
53
|
+
print(" - generate_structure: Generate structures with various options")
|
|
54
|
+
print(" - validate_structure: Validate structure configuration files")
|
|
55
|
+
print("\nExamples:")
|
|
56
|
+
print(" structkit mcp --server --transport stdio --debug")
|
|
57
|
+
print(" structkit mcp --server --transport http --host 127.0.0.1 --port 9000 --path /mcp --uvicorn-log-level debug")
|
|
58
|
+
print(" structkit mcp --server --transport sse --host 0.0.0.0 --port 8080 --path /events --fastmcp-log-level DEBUG")
|
|
59
|
+
|
|
60
|
+
async def _start_mcp_server(self, args=None):
|
|
61
|
+
"""Start the MCP server using the selected transport."""
|
|
62
|
+
try:
|
|
63
|
+
server = StructMCPServer()
|
|
64
|
+
transport = getattr(args, 'transport', 'stdio') if args else 'stdio'
|
|
65
|
+
# Map CLI args to server.run kwargs
|
|
66
|
+
run_kwargs = {
|
|
67
|
+
"transport": transport,
|
|
68
|
+
"show_banner": getattr(args, 'show_banner', True) if args else True,
|
|
69
|
+
}
|
|
70
|
+
# Determine FastMCP logger level
|
|
71
|
+
fastmcp_log_level = None
|
|
72
|
+
if args:
|
|
73
|
+
fastmcp_log_level = getattr(args, 'fastmcp_log_level', None)
|
|
74
|
+
if not fastmcp_log_level and getattr(args, 'debug', False):
|
|
75
|
+
fastmcp_log_level = 'DEBUG'
|
|
76
|
+
if fastmcp_log_level:
|
|
77
|
+
run_kwargs["fastmcp_log_level"] = fastmcp_log_level
|
|
78
|
+
|
|
79
|
+
if transport in {"http", "sse"}:
|
|
80
|
+
# uvicorn expects lowercase levels like "info"/"debug"
|
|
81
|
+
uvicorn_level = None
|
|
82
|
+
if args:
|
|
83
|
+
uvicorn_level = getattr(args, 'uvicorn_log_level', None)
|
|
84
|
+
if not uvicorn_level and getattr(args, 'debug', False):
|
|
85
|
+
uvicorn_level = 'debug'
|
|
86
|
+
if not uvicorn_level:
|
|
87
|
+
# Default to args.log if provided, else None
|
|
88
|
+
uvicorn_level = getattr(args, 'log', None)
|
|
89
|
+
run_kwargs.update({
|
|
90
|
+
"host": getattr(args, 'host', None),
|
|
91
|
+
"port": getattr(args, 'port', None),
|
|
92
|
+
"path": getattr(args, 'path', None),
|
|
93
|
+
"log_level": (uvicorn_level.lower() if isinstance(uvicorn_level, str) else uvicorn_level),
|
|
94
|
+
})
|
|
95
|
+
if transport == "http":
|
|
96
|
+
run_kwargs["stateless_http"] = getattr(args, 'stateless_http', None)
|
|
97
|
+
await server.run(**run_kwargs)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
self.logger.error(f"Error starting MCP server: {e}")
|
|
100
|
+
raise
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import yaml
|
|
3
|
+
from dotenv import load_dotenv
|
|
4
|
+
from structkit.commands import Command
|
|
5
|
+
|
|
6
|
+
load_dotenv()
|
|
7
|
+
|
|
8
|
+
# Validate command class
|
|
9
|
+
class ValidateCommand(Command):
|
|
10
|
+
def __init__(self, parser):
|
|
11
|
+
super().__init__(parser)
|
|
12
|
+
parser.description = "Validate a YAML configuration file for structure definitions"
|
|
13
|
+
parser.add_argument('yaml_file', type=str, help='Path to the YAML configuration file')
|
|
14
|
+
parser.set_defaults(func=self.execute)
|
|
15
|
+
|
|
16
|
+
def execute(self, args):
|
|
17
|
+
self.logger.info(f"Validating {args.yaml_file}")
|
|
18
|
+
|
|
19
|
+
with open(args.yaml_file, 'r') as f:
|
|
20
|
+
config = yaml.safe_load(f)
|
|
21
|
+
|
|
22
|
+
# Validate pre_hooks and post_hooks if present
|
|
23
|
+
for hook_key in ["pre_hooks", "post_hooks"]:
|
|
24
|
+
if hook_key in config:
|
|
25
|
+
if not isinstance(config[hook_key], list):
|
|
26
|
+
raise ValueError(f"The '{hook_key}' key must be a list of shell commands (strings).")
|
|
27
|
+
for cmd in config[hook_key]:
|
|
28
|
+
if not isinstance(cmd, str):
|
|
29
|
+
raise ValueError(f"Each item in '{hook_key}' must be a string (shell command).")
|
|
30
|
+
|
|
31
|
+
if 'structure' in config and 'files' in config:
|
|
32
|
+
self.logger.warning("Both 'structure' and 'files' keys exist. Prioritizing 'structure'.")
|
|
33
|
+
self._validate_structure_config(config.get('structure') or config.get('files', []))
|
|
34
|
+
self._validate_folders_config(config.get('folders', []))
|
|
35
|
+
self._validate_variables_config(config.get('variables', []))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Validate the 'folders' key in the configuration file
|
|
39
|
+
# folders should be defined as a list of dictionaries
|
|
40
|
+
# each dictionary should have a 'struct' key
|
|
41
|
+
#
|
|
42
|
+
# Example:
|
|
43
|
+
# folders:
|
|
44
|
+
# - .devops/modules/my_module_one:
|
|
45
|
+
# struct: terraform/module
|
|
46
|
+
# - .devops/modules/my_module_two:
|
|
47
|
+
# struct: terraform/module
|
|
48
|
+
# with:
|
|
49
|
+
# module_name: my_module_two
|
|
50
|
+
def _validate_folders_config(self, folders):
|
|
51
|
+
if not isinstance(folders, list):
|
|
52
|
+
raise ValueError("The 'folders' key must be a list.")
|
|
53
|
+
for item in folders:
|
|
54
|
+
if not isinstance(item, dict):
|
|
55
|
+
raise ValueError("Each item in the 'folders' list must be a dictionary.")
|
|
56
|
+
for name, content in item.items():
|
|
57
|
+
if not isinstance(name, str):
|
|
58
|
+
raise ValueError("Each name in the 'folders' item must be a string.")
|
|
59
|
+
if not isinstance(content, dict):
|
|
60
|
+
raise ValueError(f"The content of '{name}' must be a dictionary.")
|
|
61
|
+
if 'struct' not in content:
|
|
62
|
+
raise ValueError(f"Dictionary item '{name}' must contain a 'struct' key.")
|
|
63
|
+
if not isinstance(content['struct'], str) and not isinstance(content['struct'], list):
|
|
64
|
+
raise ValueError(f"The 'struct' value for '{name}' must be a string.")
|
|
65
|
+
if 'with' in content and not isinstance(content['with'], dict):
|
|
66
|
+
raise ValueError(f"The 'with' value for '{name}' must be a dictionary.")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Validate the 'variables' key in the configuration file
|
|
70
|
+
# variables should be defined as a list of dictionaries
|
|
71
|
+
# each dictionary should have a 'name' key and optionall 'default' value
|
|
72
|
+
#
|
|
73
|
+
# Example:
|
|
74
|
+
# variables:
|
|
75
|
+
# - session_name:
|
|
76
|
+
# type: string
|
|
77
|
+
# default: my_session
|
|
78
|
+
# - project_name:
|
|
79
|
+
# type: string
|
|
80
|
+
# default: my_project
|
|
81
|
+
# help: The name of the project
|
|
82
|
+
def _validate_variables_config(self, variables):
|
|
83
|
+
if not isinstance(variables, list):
|
|
84
|
+
raise ValueError("The 'variables' key must be a list.")
|
|
85
|
+
for item in variables:
|
|
86
|
+
if not isinstance(item, dict):
|
|
87
|
+
raise ValueError("Each item in the 'variables' list must be a dictionary.")
|
|
88
|
+
for name, content in item.items():
|
|
89
|
+
if not isinstance(name, str):
|
|
90
|
+
raise ValueError("Each name in the 'variables' item must be a string.")
|
|
91
|
+
if not isinstance(content, dict):
|
|
92
|
+
raise ValueError(f"The content of '{name}' must be a dictionary.")
|
|
93
|
+
if 'type' not in content:
|
|
94
|
+
raise ValueError(f"Dictionary item '{name}' must contain a 'type' key.")
|
|
95
|
+
if content['type'] not in ['string', 'number', 'boolean']:
|
|
96
|
+
raise ValueError(f"Invalid type for '{name}'. Must be 'string', 'number' or 'boolean'.")
|
|
97
|
+
if 'default' in content and content['type'] == 'boolean' and not isinstance(content['default'], bool):
|
|
98
|
+
raise ValueError(f"Invalid default value for '{name}'. Must be a boolean.")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _validate_structure_config(self, structure):
|
|
102
|
+
if not isinstance(structure, list):
|
|
103
|
+
raise ValueError("The 'structure' key must be a list.")
|
|
104
|
+
for item in structure:
|
|
105
|
+
if not isinstance(item, dict):
|
|
106
|
+
raise ValueError("Each item in the 'structure' list must be a dictionary.")
|
|
107
|
+
for name, content in item.items():
|
|
108
|
+
if not isinstance(name, str):
|
|
109
|
+
raise ValueError("Each name in the 'structure' item must be a string.")
|
|
110
|
+
if isinstance(content, dict):
|
|
111
|
+
# Check that any of the keys 'content', 'file' or 'prompt' is present
|
|
112
|
+
if 'content' not in content and 'file' not in content and 'user_prompt' not in content:
|
|
113
|
+
raise ValueError(f"Dictionary item '{name}' must contain either 'content' or 'file' or 'user_prompt' key.")
|
|
114
|
+
# Check if 'file' key is present and its value is a string
|
|
115
|
+
if 'file' in content and not isinstance(content['file'], str):
|
|
116
|
+
raise ValueError(f"The 'file' value for '{name}' must be a string.")
|
|
117
|
+
# Check if 'permissions' key is present and its value is a string
|
|
118
|
+
if 'permissions' in content and not isinstance(content['permissions'], str):
|
|
119
|
+
raise ValueError(f"The 'permissions' value for '{name}' must be a string.")
|
|
120
|
+
# Check if 'prompt' key is present and its value is a string
|
|
121
|
+
if 'prompt' in content and not isinstance(content['prompt'], str):
|
|
122
|
+
raise ValueError(f"The 'prompt' value for '{name}' must be a string.")
|
|
123
|
+
if 'skip' in content and not isinstance(content['skip'], bool):
|
|
124
|
+
raise ValueError(f"The 'skip' value for '{name}' must be a string.")
|
|
125
|
+
if 'skip_if_exists' in content and not isinstance(content['skip_if_exists'], bool):
|
|
126
|
+
raise ValueError(f"The 'skip_if_exists' value for '{name}' must be a string.")
|
|
127
|
+
elif not isinstance(content, str):
|
|
128
|
+
raise ValueError(f"The content of '{name}' must be a string or dictionary.")
|
|
129
|
+
self.logger.info("Configuration validation passed.")
|
structkit/completers.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
class ChoicesCompleter(object):
|
|
4
|
+
def __init__(self, choices):
|
|
5
|
+
self.choices = choices
|
|
6
|
+
|
|
7
|
+
def __call__(self, **kwargs):
|
|
8
|
+
return self.choices
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StructuresCompleter(object):
|
|
12
|
+
"""Dynamic completer for available structure names."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, structures_path=None):
|
|
15
|
+
self.structures_path = structures_path
|
|
16
|
+
|
|
17
|
+
def __call__(self, prefix, parsed_args, **kwargs):
|
|
18
|
+
"""Return list of available structure names for completion."""
|
|
19
|
+
return self._get_available_structures(parsed_args)
|
|
20
|
+
|
|
21
|
+
def _get_available_structures(self, parsed_args):
|
|
22
|
+
"""Get list of available structure names, similar to ListCommand logic."""
|
|
23
|
+
# Get the directory where the commands are located
|
|
24
|
+
this_file = os.path.dirname(os.path.realpath(__file__))
|
|
25
|
+
contribs_path = os.path.join(this_file, "contribs")
|
|
26
|
+
|
|
27
|
+
# Check if custom structures path is provided via parsed_args
|
|
28
|
+
structures_path = getattr(parsed_args, 'structures_path', None) or self.structures_path
|
|
29
|
+
|
|
30
|
+
if structures_path:
|
|
31
|
+
paths_to_list = [structures_path, contribs_path]
|
|
32
|
+
else:
|
|
33
|
+
paths_to_list = [contribs_path]
|
|
34
|
+
|
|
35
|
+
all_structures = set()
|
|
36
|
+
for path in paths_to_list:
|
|
37
|
+
if not os.path.exists(path):
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
for root, _, files in os.walk(path):
|
|
41
|
+
for file in files:
|
|
42
|
+
if file.endswith(".yaml"):
|
|
43
|
+
file_path = os.path.join(root, file)
|
|
44
|
+
rel_path = os.path.relpath(file_path, path)
|
|
45
|
+
# Remove .yaml extension
|
|
46
|
+
structure_name = rel_path[:-5]
|
|
47
|
+
all_structures.add(structure_name)
|
|
48
|
+
|
|
49
|
+
return sorted(list(all_structures))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
log_level_completer = ChoicesCompleter(['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'])
|
|
53
|
+
file_strategy_completer = ChoicesCompleter(['overwrite', 'skip', 'append', 'rename', 'backup'])
|
|
54
|
+
structures_completer = StructuresCompleter()
|