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
structkit/file_item.py ADDED
@@ -0,0 +1,182 @@
1
+ # FILE: file_item.py
2
+ import requests
3
+ import os
4
+ import shutil
5
+ import logging
6
+ import time
7
+ from dotenv import load_dotenv
8
+ from structkit.template_renderer import TemplateRenderer
9
+ from structkit.content_fetcher import ContentFetcher
10
+ from structkit.model_wrapper import ModelWrapper
11
+
12
+ load_dotenv()
13
+
14
+ class FileItem:
15
+ def __init__(self, properties):
16
+ self.logger = logging.getLogger(__name__)
17
+ self.name = properties.get("name")
18
+ self.file_directory = self._get_file_directory()
19
+ self.content = properties.get("content")
20
+ self.config_variables = properties.get("config_variables")
21
+ self.content_location = properties.get("file")
22
+ self.permissions = properties.get("permissions")
23
+ self.input_store = properties.get("input_store")
24
+ self.non_interactive = properties.get("non_interactive")
25
+ self.skip = properties.get("skip", False)
26
+ self.skip_if_exists = properties.get("skip_if_exists", False)
27
+
28
+ self.content_fetcher = ContentFetcher()
29
+
30
+ self.system_prompt = properties.get("system_prompt") or properties.get("global_system_prompt")
31
+ self.user_prompt = properties.get("user_prompt")
32
+ self.mappings = properties.get("mappings", {})
33
+
34
+ self.model_wrapper = ModelWrapper(self.logger)
35
+
36
+ self.template_renderer = TemplateRenderer(
37
+ self.config_variables,
38
+ self.input_store,
39
+ self.non_interactive,
40
+ self.mappings
41
+ )
42
+ # internal flags used for reporting
43
+ self._last_action = None
44
+
45
+ def _get_file_directory(self):
46
+ return os.path.dirname(self.name)
47
+
48
+ def process_prompt(self, dry_run=False, existing_content=None):
49
+ if self.user_prompt:
50
+
51
+ if not self.system_prompt:
52
+ system_prompt = "You are a software developer working on a project. You need to create a file with the following content:"
53
+ else:
54
+ system_prompt = self.system_prompt
55
+
56
+ user_prompt = self.user_prompt
57
+ if existing_content:
58
+ user_prompt += f"\n\nCurrent file content (if any):\n```\n{existing_content}\n```\n\nPlease modify existing content so that it meets the new requirements. Your output should be plain text, without any code blocks or formatting. Do not include any explanations or comments. Just provide the final content of the file."
59
+
60
+ self.logger.debug(f"Using system prompt: {system_prompt}")
61
+ self.logger.debug(f"Using user prompt: {user_prompt}")
62
+
63
+ self.content = self.model_wrapper.generate_content(
64
+ system_prompt,
65
+ user_prompt,
66
+ dry_run=dry_run
67
+ )
68
+ self.logger.debug(f"Generated content: \n\n{self.content}")
69
+
70
+ def fetch_content(self):
71
+ if self.content_location:
72
+ self.logger.debug(f"Fetching content from: {self.content_location}")
73
+ try:
74
+ raw_content = self.content_fetcher.fetch_content(
75
+ self.content_location)
76
+ self.logger.debug(f"Fetched content: {raw_content}")
77
+ # Render the fetched content using the template renderer
78
+ template_vars = self._merge_default_template_vars(
79
+ self.config_variables)
80
+ missing_vars = self.template_renderer.prompt_for_missing_vars(
81
+ raw_content, template_vars)
82
+ template_vars.update(missing_vars)
83
+ self.content = self.template_renderer.render_template(
84
+ raw_content, template_vars)
85
+ self.logger.debug(f"Rendered content: {self.content}")
86
+ except Exception as e:
87
+ self.logger.error(f"❗ Failed to fetch content from {self.content_location}: {e}")
88
+
89
+ def _merge_default_template_vars(self, template_vars):
90
+ default_vars = {
91
+ "file_name": self.name,
92
+ "file_directory": self.file_directory,
93
+ }
94
+ if not template_vars:
95
+ return default_vars
96
+ return {**default_vars, **template_vars}
97
+
98
+ def apply_template_variables(self, template_vars):
99
+ vars = self._merge_default_template_vars(template_vars)
100
+ self.logger.debug(f"Applying template variables: {vars}")
101
+
102
+ missing_vars = self.template_renderer.prompt_for_missing_vars(self.content, vars)
103
+ vars.update(missing_vars)
104
+
105
+ self.vars = vars
106
+ self.logger.debug(f"Final template variables: {self.vars}")
107
+
108
+ self.content = self.template_renderer.render_template(self.content, vars)
109
+
110
+ def create(self, base_path, dry_run=False, backup_path=None, file_strategy='overwrite'):
111
+ file_path = os.path.join(base_path, self.name)
112
+
113
+ file_path = self.template_renderer.render_template(file_path, self.vars)
114
+
115
+ # default result
116
+ result = {"action": None, "path": file_path}
117
+
118
+ if self.skip:
119
+ self.logger.info(f"⏭️ Skipped (skip=true): {file_path}")
120
+ result["action"] = "skipped"
121
+ return result
122
+
123
+ if dry_run:
124
+ self.logger.info(f"[DRY RUN] Would create/update: {file_path}")
125
+ result["action"] = "dry_run"
126
+ return result
127
+
128
+ if self.skip_if_exists and os.path.exists(file_path):
129
+ self.logger.info(f"⏭️ Skipped (exists and skip_if_exists=true): {file_path}")
130
+ result["action"] = "skipped"
131
+ return result
132
+
133
+ # Create the directory if it does not exist
134
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
135
+
136
+ existed_before = os.path.exists(file_path)
137
+ renamed_from = None
138
+ backed_up_to = None
139
+
140
+ if existed_before:
141
+ if file_strategy == 'backup' and backup_path:
142
+ backup_file_path = os.path.join(backup_path, os.path.basename(file_path))
143
+ shutil.copy2(file_path, backup_file_path)
144
+ backed_up_to = backup_file_path
145
+ self.logger.info(f"🗄️ Backed up: {file_path} -> {backup_file_path}")
146
+ elif file_strategy == 'skip':
147
+ self.logger.info(f"⏭️ Skipped (exists): {file_path}")
148
+ result["action"] = "skipped"
149
+ return result
150
+ elif file_strategy == 'append':
151
+ with open(file_path, 'a') as f:
152
+ f.write(f"{self.content}\n")
153
+ self.logger.info(f"📝 Appended: {file_path}")
154
+ result.update({"action": "appended"})
155
+ return result
156
+ elif file_strategy == 'rename':
157
+ new_name = f"{file_path}.{int(time.time())}"
158
+ os.rename(file_path, new_name)
159
+ renamed_from = new_name
160
+ self.logger.info(f"🔁 Renamed: {file_path} -> {new_name}")
161
+
162
+ # Write/overwrite the file
163
+ with open(file_path, 'w') as f:
164
+ f.write(f"{self.content}\n")
165
+
166
+ action = "created" if not existed_before else "updated"
167
+ if action == "created":
168
+ self.logger.info(f"✅ Created: {file_path}")
169
+ else:
170
+ self.logger.info(f"✅ Updated: {file_path}")
171
+ self.logger.debug(f"Content: \n\n{self.content}")
172
+
173
+ if self.permissions:
174
+ os.chmod(file_path, int(self.permissions, 8))
175
+ self.logger.info(f"🔐 Set permissions: {self.permissions} on {file_path}")
176
+
177
+ result.update({
178
+ "action": action,
179
+ "renamed_from": renamed_from,
180
+ "backed_up_to": backed_up_to,
181
+ })
182
+ return result
structkit/filters.py ADDED
@@ -0,0 +1,112 @@
1
+ import os
2
+ import re
3
+ import json
4
+ from uuid import uuid4
5
+ from datetime import datetime, timezone
6
+ from typing import Any
7
+
8
+ import yaml
9
+ from github import Github
10
+ from cachetools import TTLCache, cached
11
+
12
+ cache = TTLCache(maxsize=100, ttl=600)
13
+
14
+ @cached(cache)
15
+ def get_latest_release(repo_name):
16
+ token = os.getenv('GITHUB_TOKEN')
17
+
18
+ # Use the token if available, otherwise proceed without authentication
19
+ if token:
20
+ g = Github(token)
21
+ else:
22
+ g = Github()
23
+
24
+ try:
25
+ # Get the repository object
26
+ repo = g.get_repo(repo_name)
27
+ # Get the latest release
28
+ latest_release = repo.get_latest_release()
29
+ return latest_release.tag_name
30
+ except Exception:
31
+ # If an error occurs, return the default branch name
32
+ try:
33
+ default_branch = repo.default_branch
34
+ return default_branch
35
+ except Exception as e:
36
+ return "LATEST_RELEASE_ERROR"
37
+
38
+ @cached(cache)
39
+ def get_default_branch(repo_name):
40
+ token = os.getenv('GITHUB_TOKEN')
41
+
42
+ if token:
43
+ g = Github(token)
44
+ else:
45
+ g = Github()
46
+
47
+ try:
48
+ repo = g.get_repo(repo_name)
49
+ return repo.default_branch
50
+ except Exception:
51
+ return "DEFAULT_BRANCH_ERROR"
52
+
53
+ def slugify(value):
54
+ # Convert to lowercase
55
+ value = value.lower()
56
+ # Replace spaces with hyphens
57
+ value = re.sub(r'\s+', '-', value)
58
+ # Remove any non-alphanumeric characters (except hyphens)
59
+ value = re.sub(r'[^a-z0-9-]', '', value)
60
+ return value
61
+
62
+ # -----------------------------
63
+ # Additional helpers/filters
64
+ # -----------------------------
65
+
66
+ def gen_uuid() -> str:
67
+ return str(uuid4())
68
+
69
+
70
+ def now_iso() -> str:
71
+ # UTC ISO8601 string
72
+ return datetime.now(timezone.utc).isoformat()
73
+
74
+
75
+ def env(name: str, default: str = "") -> str:
76
+ return os.getenv(name, default)
77
+
78
+
79
+ def read_file(path: str, encoding: str = "utf-8") -> str:
80
+ try:
81
+ with open(path, "r", encoding=encoding) as f:
82
+ return f.read()
83
+ except Exception:
84
+ return ""
85
+
86
+
87
+ def to_yaml(obj: Any) -> str:
88
+ try:
89
+ return yaml.safe_dump(obj, sort_keys=False)
90
+ except Exception:
91
+ return ""
92
+
93
+
94
+ def from_yaml(s: str) -> Any:
95
+ try:
96
+ return yaml.safe_load(s)
97
+ except Exception:
98
+ return None
99
+
100
+
101
+ def to_json(obj: Any, indent: int | None = None) -> str:
102
+ try:
103
+ return json.dumps(obj, indent=indent)
104
+ except Exception:
105
+ return ""
106
+
107
+
108
+ def from_json(s: str) -> Any:
109
+ try:
110
+ return json.loads(s)
111
+ except Exception:
112
+ return None
@@ -0,0 +1,35 @@
1
+ import json
2
+ import os
3
+
4
+ class InputStore:
5
+
6
+ def __init__(self, input_file):
7
+ self.input_file = input_file
8
+ self.data = None
9
+
10
+ # create directory if it doesn't exist
11
+ directory = os.path.dirname(input_file)
12
+ if not os.path.exists(directory):
13
+ os.makedirs(directory)
14
+
15
+ # create file if it doesn't exist
16
+ if not os.path.exists(input_file):
17
+ with open(input_file, 'w') as f:
18
+ json.dump({}, f)
19
+
20
+ def load(self):
21
+ with open(self.input_file, 'r') as f:
22
+ self.data = json.load(f)
23
+
24
+ def get_data(self):
25
+ return self.data
26
+
27
+ def get_value(self, key):
28
+ return self.data[key]
29
+
30
+ def set_value(self, key, value):
31
+ self.data[key] = value
32
+
33
+ def save(self):
34
+ with open(self.input_file, 'w') as f:
35
+ json.dump(self.data, f, indent=2)
@@ -0,0 +1,36 @@
1
+ # FILE: structkit/logging_config.py
2
+ import logging
3
+ import colorlog
4
+
5
+ def configure_logging(level=logging.INFO, log_file=None):
6
+ """Configure logging with colorlog."""
7
+ handler = colorlog.StreamHandler()
8
+
9
+ line_format = "%(log_color)s%(message)s"
10
+ if level == logging.DEBUG:
11
+ line_format = "%(log_color)s[%(asctime)s][%(levelname)s][%(filename)s:%(lineno)d] >> %(message)s"
12
+
13
+ handler.setFormatter(colorlog.ColoredFormatter(
14
+ line_format,
15
+ datefmt='%Y-%m-%d %H:%M:%S',
16
+ log_colors={
17
+ 'DEBUG': 'cyan',
18
+ 'INFO': 'green',
19
+ 'WARNING': 'yellow',
20
+ 'ERROR': 'red',
21
+ 'CRITICAL': 'bold_red',
22
+ }
23
+ ))
24
+
25
+ logging.basicConfig(
26
+ level=level,
27
+ handlers=[handler],
28
+ )
29
+
30
+ if log_file:
31
+ file_handler = logging.FileHandler(log_file)
32
+ file_handler.setFormatter(logging.Formatter(
33
+ "[%(asctime)s][%(levelname)s][struct] >>> %(message)s",
34
+ datefmt='%Y-%m-%d %H:%M:%S'
35
+ ))
36
+ logging.getLogger().addHandler(file_handler)
structkit/main.py ADDED
@@ -0,0 +1,85 @@
1
+ import argparse
2
+ import logging
3
+ import os
4
+ from dotenv import load_dotenv
5
+ from structkit.utils import read_config_file, merge_configs
6
+ from structkit.commands.generate import GenerateCommand
7
+ from structkit.commands.info import InfoCommand
8
+ from structkit.commands.validate import ValidateCommand
9
+ from structkit.commands.list import ListCommand
10
+ from structkit.commands.generate_schema import GenerateSchemaCommand
11
+ from structkit.commands.mcp import MCPCommand
12
+ from structkit.logging_config import configure_logging
13
+
14
+ # Optional dependency: shtab for static shell completion generation
15
+ try:
16
+ import shtab # type: ignore
17
+ except Exception: # pragma: no cover - optional at runtime
18
+ shtab = None
19
+
20
+ load_dotenv()
21
+
22
+ def get_parser():
23
+ parser = argparse.ArgumentParser(
24
+ description="Generate project structure from YAML configuration.",
25
+ prog="structkit",
26
+ epilog="Thanks for using %(prog)s! :)",
27
+ )
28
+
29
+ # Create subparsers
30
+ subparsers = parser.add_subparsers()
31
+
32
+ InfoCommand(subparsers.add_parser('info', help='Show information about the package'))
33
+ ValidateCommand(subparsers.add_parser('validate', help='Validate the YAML configuration file'))
34
+ GenerateCommand(subparsers.add_parser('generate', help='Generate the project structure'))
35
+ ListCommand(subparsers.add_parser('list', help='List available structures'))
36
+ GenerateSchemaCommand(subparsers.add_parser('generate-schema', help='Generate JSON schema for available structures'))
37
+ MCPCommand(subparsers.add_parser('mcp', help='MCP (Model Context Protocol) support'))
38
+
39
+ # init to create a basic .struct.yaml
40
+ from structkit.commands.init import InitCommand
41
+ InitCommand(subparsers.add_parser('init', help='Initialize a basic .struct.yaml in the target directory'))
42
+
43
+ # completion manager
44
+ from structkit.commands.completion import CompletionCommand
45
+ CompletionCommand(subparsers.add_parser('completion', help='Manage shell completions'))
46
+
47
+ # Add shtab completion printing flags if available
48
+ if shtab is not None:
49
+ # Adds --print-completion and --shell flags
50
+ shtab.add_argument_to(parser)
51
+
52
+ return parser
53
+
54
+ def main():
55
+ parser = get_parser()
56
+
57
+ args = parser.parse_args()
58
+
59
+ # Check if a subcommand was provided
60
+ if not hasattr(args, 'func'):
61
+ parser.print_help()
62
+ parser.exit()
63
+
64
+ # Read config file if provided
65
+ if getattr(args, 'config_file', None):
66
+ file_config = read_config_file(args.config_file)
67
+ args = argparse.Namespace(**merge_configs(file_config, args))
68
+
69
+ # Resolve logging level precedence: STRUCTKIT_LOG_LEVEL env > --debug (if present) > --log
70
+ env_level = os.getenv('STRUCTKIT_LOG_LEVEL')
71
+ if env_level:
72
+ logging_level = getattr(logging, env_level.upper(), logging.INFO)
73
+ else:
74
+ # Some commands (like mcp) may add a --debug flag; respect it
75
+ if getattr(args, 'debug', False):
76
+ logging_level = logging.DEBUG
77
+ else:
78
+ logging_level = getattr(logging, getattr(args, 'log', 'INFO').upper(), logging.INFO)
79
+
80
+ configure_logging(level=logging_level, log_file=getattr(args, 'log_file', None))
81
+
82
+ args.func(args)
83
+
84
+ if __name__ == "__main__":
85
+ main()