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.
Files changed (77) hide show
  1. structkit/__init__.py +6 -0
  2. structkit/commands/__init__.py +17 -0
  3. structkit/commands/completion.py +65 -0
  4. structkit/commands/generate.py +397 -0
  5. structkit/commands/generate_schema.py +67 -0
  6. structkit/commands/import.py +63 -0
  7. structkit/commands/info.py +87 -0
  8. structkit/commands/init.py +52 -0
  9. structkit/commands/list.py +89 -0
  10. structkit/commands/mcp.py +100 -0
  11. structkit/commands/validate.py +129 -0
  12. structkit/completers.py +54 -0
  13. structkit/content_fetcher.py +249 -0
  14. structkit/contribs/README.md +271 -0
  15. structkit/contribs/ansible-playbook.yaml +38 -0
  16. structkit/contribs/chef-cookbook.yaml +51 -0
  17. structkit/contribs/ci-cd-pipelines.yaml +67 -0
  18. structkit/contribs/cloudformation-files.yaml +21 -0
  19. structkit/contribs/configs/chglog.yaml +31 -0
  20. structkit/contribs/configs/codeowners.yaml +3 -0
  21. structkit/contribs/configs/devcontainer.yaml +35 -0
  22. structkit/contribs/configs/editor-config.yaml +11 -0
  23. structkit/contribs/configs/eslint.yaml +30 -0
  24. structkit/contribs/configs/jshint.yaml +11 -0
  25. structkit/contribs/configs/kubectl.yaml +23 -0
  26. structkit/contribs/configs/prettier.yaml +19 -0
  27. structkit/contribs/docker-files.yaml +27 -0
  28. structkit/contribs/documentation-template.yaml +33 -0
  29. structkit/contribs/git-hooks.yaml +19 -0
  30. structkit/contribs/github/chatmodes/plan.yaml +18 -0
  31. structkit/contribs/github/instructions/generic.yaml +5 -0
  32. structkit/contribs/github/prompts/generic.yaml +4 -0
  33. structkit/contribs/github/prompts/react-form.yaml +17 -0
  34. structkit/contribs/github/prompts/security-api.yaml +8 -0
  35. structkit/contribs/github/prompts/struct.yaml +90 -0
  36. structkit/contribs/github/templates.yaml +91 -0
  37. structkit/contribs/github/workflows/codeql.yaml +88 -0
  38. structkit/contribs/github/workflows/execute-tf-workflow.yaml +39 -0
  39. structkit/contribs/github/workflows/labeler.yaml +77 -0
  40. structkit/contribs/github/workflows/pre-commit.yaml +27 -0
  41. structkit/contribs/github/workflows/release-drafter.yaml +77 -0
  42. structkit/contribs/github/workflows/run-struct.yaml +30 -0
  43. structkit/contribs/github/workflows/stale.yaml +16 -0
  44. structkit/contribs/helm-chart.yaml +160 -0
  45. structkit/contribs/kubernetes-manifests.yaml +103 -0
  46. structkit/contribs/project/custom-structures.yaml +24 -0
  47. structkit/contribs/project/generic.yaml +309 -0
  48. structkit/contribs/project/go.yaml +104 -0
  49. structkit/contribs/project/java.yaml +85 -0
  50. structkit/contribs/project/n8n.yaml +100 -0
  51. structkit/contribs/project/nodejs.yaml +101 -0
  52. structkit/contribs/project/python.yaml +136 -0
  53. structkit/contribs/project/ruby.yaml +130 -0
  54. structkit/contribs/project/rust.yaml +106 -0
  55. structkit/contribs/prompts/run-struct-trigger.yaml +18 -0
  56. structkit/contribs/terraform/apps/aws-accounts.yaml +21 -0
  57. structkit/contribs/terraform/apps/environments.yaml +41 -0
  58. structkit/contribs/terraform/apps/generic.yaml +41 -0
  59. structkit/contribs/terraform/apps/github-organization.yaml +40 -0
  60. structkit/contribs/terraform/apps/init.yaml +11 -0
  61. structkit/contribs/terraform/modules/generic.yaml +58 -0
  62. structkit/contribs/vagrant-files.yaml +21 -0
  63. structkit/file_item.py +182 -0
  64. structkit/filters.py +112 -0
  65. structkit/input_store.py +35 -0
  66. structkit/logging_config.py +36 -0
  67. structkit/main.py +85 -0
  68. structkit/mcp_server.py +347 -0
  69. structkit/model_wrapper.py +47 -0
  70. structkit/template_renderer.py +258 -0
  71. structkit/utils.py +36 -0
  72. structkit-3.0.0.dist-info/METADATA +182 -0
  73. structkit-3.0.0.dist-info/RECORD +77 -0
  74. structkit-3.0.0.dist-info/WHEEL +5 -0
  75. structkit-3.0.0.dist-info/entry_points.txt +2 -0
  76. structkit-3.0.0.dist-info/licenses/LICENSE +201 -0
  77. 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.")
@@ -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()