Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ docker run \
-v $(pwd):/workdir \
-e OPENAI_API_KEY=your-key \
-u $(id -u):$(id -g) \
ghcr.io/httpdss/struct:main \
ghcr.io/httpdss/struct:main generate \
/workdir/example/structure.yaml \
/workdir/example_output
```
Expand All @@ -97,7 +97,7 @@ cd example/
touch structure.yaml
vim structure.yaml # copy the content from the example folder
export OPENAI_API_KEY=something
struct structure.yaml .
struct generate structure.yaml .
```

## 📝 Usage
Expand All @@ -121,13 +121,13 @@ usage: struct [-h] [--log LOG] [--dry-run] [--vars VARS] [--backup BACKUP] [--fi
### Simple Example

```sh
struct /path/to/your/structure.yaml /path/to/your/output/directory
struct generate /path/to/your/structure.yaml /path/to/your/output/directory
```

### More Complete Example

```sh
struct \
struct generate \
--log=DEBUG \
--dry-run \
--vars="project_name=MyProject,author_name=JohnDoe" \
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ services:
env_file:
- .env
command: [
"generate",
"--log=DEBUG",
"--dry-run",
"--vars=project_name=MyProject,author_name=JohnDoe",
Expand All @@ -24,6 +25,7 @@ services:
env_file:
- .env
command: [
"generate",
"--log=DEBUG",
"--vars=project_name=MyProject,author_name=JohnDoe",
"--backup=/app/backup",
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def parse_requirements(filename):
install_requires=parse_requirements('requirements.txt'),
entry_points={
'console_scripts': [
'struct = struct_module:main',
'struct = struct_module.main:main',
],
},
)
16 changes: 16 additions & 0 deletions struct_module/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import logging

# Base command class
class Command:
def __init__(self, parser):
self.parser = parser
self.logger = logging.getLogger(__name__)
self.add_common_arguments()

def add_common_arguments(self):
self.parser.add_argument('-l', '--log', type=str, default='INFO', help='Set the logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)')
self.parser.add_argument('-c', '--config-file', type=str, help='Path to a configuration file')
self.parser.add_argument('-i', '--log-file', type=str, help='Path to a log file')

def execute(self, args):
raise NotImplementedError("Subclasses should implement this!")
58 changes: 58 additions & 0 deletions struct_module/commands/generate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from struct_module.commands import Command
import os
import yaml
from struct_module.file_item import FileItem

# Generate command class
class GenerateCommand(Command):
def __init__(self, parser):
super().__init__(parser)
parser.add_argument('yaml_file', type=str, help='Path to the YAML configuration file')
parser.add_argument('base_path', type=str, help='Base path where the structure will be created')
parser.add_argument('-d', '--dry-run', action='store_true', help='Perform a dry run without creating any files or directories')
parser.add_argument('-v', '--vars', type=str, help='Template variables in the format KEY1=value1,KEY2=value2')
parser.add_argument('-b', '--backup', type=str, help='Path to the backup folder')
parser.add_argument('-f', '--file-strategy', type=str, choices=['overwrite', 'skip', 'append', 'rename', 'backup'], default='overwrite', help='Strategy for handling existing files')
parser.add_argument('-p', '--global-system-prompt', type=str, help='Global system prompt for OpenAI')
parser.set_defaults(func=self.execute)

def execute(self, args):
self.logger.info(f"Generating structure at {args.base_path} with config {args.yaml_file}")

if args.backup and not os.path.exists(args.backup):
os.makedirs(args.backup)

if args.base_path and not os.path.exists(args.base_path):
self.logger.info(f"Creating base path: {args.base_path}")
os.makedirs(args.base_path)

self._create_structure(args)


def _create_structure(self, args):
with open(args.yaml_file, 'r') as f:
config = yaml.safe_load(f)

template_vars = dict(item.split('=') for item in args.vars.split(',')) if args.vars else None
structure = config.get('structure', [])

for item in structure:
self.logger.debug(f"Processing item: {item}")
for name, content in item.items():
self.logger.debug(f"Processing name: {name}, content: {content}")
if isinstance(content, dict):
content["name"] = name
content["global_system_prompt"] = args.global_system_prompt
file_item = FileItem(content)
file_item.fetch_content()
elif isinstance(content, str):
file_item = FileItem({"name": name, "content": content})

file_item.apply_template_variables(template_vars)
file_item.process_prompt(args.dry_run)
file_item.create(
args.base_path,
args.dry_run or False,
args.backup_path or None,
args.file_strategy or 'overwrite'
)
15 changes: 15 additions & 0 deletions struct_module/commands/info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from struct_module.commands import Command
# Info command class
class InfoCommand(Command):
def __init__(self, parser):
super().__init__(parser)
parser.set_defaults(func=self.execute)

def execute(self, args):
print("STRUCT")
print("")
print("Generate project structure from YAML configuration.")
print("Commands:")
print(" generate Generate the project structure")
print(" validate Validate the YAML configuration file")
print(" info Show information about the package")
54 changes: 54 additions & 0 deletions struct_module/commands/validate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import os
import yaml
from dotenv import load_dotenv
from struct_module.commands import Command

load_dotenv()

openai_api_key = os.getenv("OPENAI_API_KEY")
openai_model = os.getenv("OPENAI_MODEL")

# Validate command class
class ValidateCommand(Command):
def __init__(self, parser):
super().__init__(parser)
parser.add_argument('yaml_file', type=str, help='Path to the YAML configuration file')
parser.set_defaults(func=self.execute)

def execute(self, args):
self.logger.info(f"Validating {args.yaml_file}")

with open(args.yaml_file, 'r') as f:
config = yaml.safe_load(f)

self._validate_configuration(config.get('structure', []))


def _validate_configuration(self, structure):
if not isinstance(structure, list):
raise ValueError("The 'structure' key must be a list.")
for item in structure:
if not isinstance(item, dict):
raise ValueError("Each item in the 'structure' list must be a dictionary.")
for name, content in item.items():
if not isinstance(name, str):
raise ValueError("Each name in the 'structure' item must be a string.")
if isinstance(content, dict):
# Check that any of the keys 'content', 'file' or 'prompt' is present
if 'content' not in content and 'file' not in content and 'user_prompt' not in content:
raise ValueError(f"Dictionary item '{name}' must contain either 'content' or 'file' or 'user_prompt' key.")
# Check if 'file' key is present and its value is a string
if 'file' in content and not isinstance(content['file'], str):
raise ValueError(f"The 'file' value for '{name}' must be a string.")
# Check if 'permissions' key is present and its value is a string
if 'permissions' in content and not isinstance(content['permissions'], str):
raise ValueError(f"The 'permissions' value for '{name}' must be a string.")
# Check if 'prompt' key is present and its value is a string
if 'prompt' in content and not isinstance(content['prompt'], str):
raise ValueError(f"The 'prompt' value for '{name}' must be a string.")
# Check if 'prompt' key is present but no OpenAI API key is found
if 'prompt' in content and not openai_api_key:
raise ValueError("Using prompt property and no OpenAI API key was found. Please set it in the .env file.")
elif not isinstance(content, str):
raise ValueError(f"The content of '{name}' must be a string or dictionary.")
self.logger.info("Configuration validation passed.")
53 changes: 19 additions & 34 deletions struct_module/main.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import os
import logging
import yaml
from dotenv import load_dotenv
from .utils import read_config_file, merge_configs, validate_configuration, create_structure
from struct_module.utils import read_config_file, merge_configs
from struct_module.commands.generate import GenerateCommand
from struct_module.commands.info import InfoCommand
from struct_module.commands.validate import ValidateCommand

import argparse

load_dotenv()

Expand All @@ -14,59 +18,40 @@


def main():
import argparse

parser = argparse.ArgumentParser(
description="Generate project structure from YAML configuration.",
prog="struct",
epilog="Thanks for using %(prog)s! :)",

)
parser.add_argument('yaml_file', type=str, help='Path to the YAML configuration file')
parser.add_argument('base_path', type=str, help='Base path where the structure will be created')
parser.add_argument('-c', '--config-file', type=str, help='Path to a configuration file')
parser.add_argument('-l', '--log', type=str, default='INFO', help='Set the logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)')
parser.add_argument('-d', '--dry-run', action='store_true', help='Perform a dry run without creating any files or directories')
parser.add_argument('-v', '--vars', type=str, help='Template variables in the format KEY1=value1,KEY2=value2')
parser.add_argument('-b', '--backup', type=str, help='Path to the backup folder')
parser.add_argument('-f', '--file-strategy', type=str, choices=['overwrite', 'skip', 'append', 'rename', 'backup'], default='overwrite', help='Strategy for handling existing files')
parser.add_argument('-i', '--log-file', type=str, help='Path to a log file')
parser.add_argument('-p', '--global-system-prompt', type=str, help='Global system prompt for OpenAI')

# Create subparsers
subparsers = parser.add_subparsers()


InfoCommand(subparsers.add_parser('info', help='Show information about the package'))
ValidateCommand(subparsers.add_parser('validate', help='Validate the YAML configuration file'))
GenerateCommand(subparsers.add_parser('generate', help='Generate the project structure'))
args = parser.parse_args()

# Check if a subcommand was provided
if not hasattr(args, 'func'):
parser.print_help()
parser.exit()

# Read config file if provided
if args.config_file:
file_config = read_config_file(args.config_file)
args = argparse.Namespace(**merge_configs(file_config, args))

logging_level = getattr(logging, args.log.upper(), logging.INFO)
template_vars = dict(item.split('=') for item in args.vars.split(',')) if args.vars else None
backup_path = args.backup

if backup_path and not os.path.exists(backup_path):
os.makedirs(backup_path)

if args.base_path and not os.path.exists(args.base_path):
logging.info(f"Creating base path: {args.base_path}")
os.makedirs(args.base_path)

logging.basicConfig(
level=logging_level,
filename=args.log_file,
format='[%(asctime)s][%(levelname)s][struct] >>> %(message)s',
)
logging.info(f"Starting to create project structure from {args.yaml_file} in {args.base_path}")
logging.debug(f"YAML file path: {args.yaml_file}, Base path: {args.base_path}, Dry run: {args.dry_run}, Template vars: {template_vars}, Backup path: {backup_path}")

with open(args.yaml_file, 'r') as f:
config = yaml.safe_load(f)

validate_configuration(config.get('structure', []))
create_structure(args.base_path, config.get('structure', []), args.dry_run, template_vars, backup_path, args.file_strategy, args.global_system_prompt)

logging.info("Finished creating project structure")

args.func(args)

if __name__ == "__main__":
main()
44 changes: 1 addition & 43 deletions struct_module/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,52 +10,10 @@
openai_model = os.getenv("OPENAI_MODEL")


def validate_configuration(structure):
if not isinstance(structure, list):
raise ValueError("The 'structure' key must be a list.")
for item in structure:
if not isinstance(item, dict):
raise ValueError("Each item in the 'structure' list must be a dictionary.")
for name, content in item.items():
if not isinstance(name, str):
raise ValueError("Each name in the 'structure' item must be a string.")
if isinstance(content, dict):
# Check that any of the keys 'content', 'file' or 'prompt' is present
if 'content' not in content and 'file' not in content and 'user_prompt' not in content:
raise ValueError(f"Dictionary item '{name}' must contain either 'content' or 'file' or 'user_prompt' key.")
# Check if 'file' key is present and its value is a string
if 'file' in content and not isinstance(content['file'], str):
raise ValueError(f"The 'file' value for '{name}' must be a string.")
# Check if 'permissions' key is present and its value is a string
if 'permissions' in content and not isinstance(content['permissions'], str):
raise ValueError(f"The 'permissions' value for '{name}' must be a string.")
# Check if 'prompt' key is present and its value is a string
if 'prompt' in content and not isinstance(content['prompt'], str):
raise ValueError(f"The 'prompt' value for '{name}' must be a string.")
# Check if 'prompt' key is present but no OpenAI API key is found
if 'prompt' in content and not openai_api_key:
raise ValueError("Using prompt property and no OpenAI API key was found. Please set it in the .env file.")
elif not isinstance(content, str):
raise ValueError(f"The content of '{name}' must be a string or dictionary.")
logging.info("Configuration validation passed.")


def create_structure(base_path, structure, dry_run=False, template_vars=None, backup_path=None, file_strategy='overwrite', global_system_prompt=None):
for item in structure:
logging.debug(f"Processing item: {item}")
for name, content in item.items():
logging.debug(f"Processing name: {name}, content: {content}")
if isinstance(content, dict):
content["name"] = name
content["global_system_prompt"] = global_system_prompt
file_item = FileItem(content)
file_item.fetch_content()
elif isinstance(content, str):
file_item = FileItem({"name": name, "content": content})

file_item.apply_template_variables(template_vars)
file_item.process_prompt(dry_run)
file_item.create(base_path, dry_run, backup_path, file_strategy)



def read_config_file(file_path):
Expand Down
Loading