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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
OPENAI_API_KEY=your-api-key-here
1 change: 1 addition & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
patreon: structproject
custom: ['https://www.paypal.me/httpdss']
31 changes: 31 additions & 0 deletions .github/release-drafter.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name-template: 'v$RESOLVED_VERSION 🌈'
tag-template: 'v$RESOLVED_VERSION'
categories:
- title: '🚀 Features'
labels:
- 'feature'
- 'enhancement'
- title: '🐛 Bug Fixes'
labels:
- 'fix'
- 'bugfix'
- 'bug'
- title: '🧰 Maintenance'
label: 'chore'
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
version-resolver:
major:
labels:
- 'major'
minor:
labels:
- 'minor'
patch:
labels:
- 'patch'
default: patch
template: |
## Changes

$CHANGES
15 changes: 15 additions & 0 deletions .github/workflows/pre-commit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: pre-commit

on:
pull_request:
branches: [main]
push:
branches: [main]

jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: pre-commit/action@v3.0.1
51 changes: 51 additions & 0 deletions .github/workflows/push-to-registry.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: push-to-registry

on:
push:
branches: ['main']

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

- name: Build and push Docker image
id: push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

- name: Generate artifact attestation
uses: actions/attest-build-provenance@v1
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
22 changes: 22 additions & 0 deletions .github/workflows/release-drafter.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: release-drafter

on:
push:
branches:
- main
pull_request:
types: [opened, reopened, synchronize]

permissions:
contents: read

jobs:
update_release_draft:
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
10 changes: 5 additions & 5 deletions .github/workflows/test-script.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ on:
push:
branches:
- main
pull_request:
branches:
- main
# pull_request:
# branches:
# - main

jobs:
build:
Expand All @@ -17,10 +17,10 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python 3.10.14
- name: Set up Python 3.10
uses: actions/setup-python@v5
with:
python-version: 3.10.14
python-version: '3.10'
cache: pip

- name: Install dependencies
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
.venv
__pycache__
*.egg-info
.env
*.log
example_project/
7 changes: 7 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,36 @@ Here is an example of a YAML configuration file:
structure:
- README.md:
content: |
# {project_name}
# ${project_name}
This is a template repository.
- script.sh:
permissions: '0777'
content: |
#!/bin/bash
echo "Hello, {author_name}!"
echo "Hello, ${author_name}!"
- LICENSE:
file: https://raw.githubusercontent.com/nishanths/license/master/LICENSE
```

## Development

To get started with development, follow these steps:

1. Clone the repository
2. Create a virtual environment

```sh
python3 -m venv .venv
source .venv/bin/activate
```

3. Install the dependencies

```sh
pip install -r requirements.txt
pip install -r requirements.dev.txt
```

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
Expand Down
6 changes: 0 additions & 6 deletions _struct

This file was deleted.

8 changes: 4 additions & 4 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
version: '3.8'

services:
struct:
build: .
volumes:
- .:/app
entrypoint: ["python", "struct_module/main.py"]
env_file:
- .env
command: [
"--log=DEBUG",
"--dry-run",
"--vars=project_name=MyProject,author_name=JohnDoe",
"--backup=/app/backup",
"--file-strategy=rename",
"--log-file=/app/logfile.log",
"/app/project_structure.yaml",
"/app/project"
"/app/example/structure.yaml",
"/app/example_project"
]
7 changes: 5 additions & 2 deletions example/structure.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
structure:
- README.md:
content: |
# {project_name}
# ${project_name}
This is a template repository.
- script.sh:
permissions: '0777'
content: |
#!/bin/bash
echo "Hello, {author_name}!"
echo "Hello, ${author_name}!"
- LICENSE:
file: https://raw.githubusercontent.com/nishanths/license/master/LICENSE
- .gitignore:
user_prompt: |
Generate the content for a .gitignore file that is appropriate for a Python project. Include common directories and file types that should be ignored.
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
PyYAML
requests
openai
python-dotenv
81 changes: 78 additions & 3 deletions struct_module/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@
import logging
from string import Template
import time
import yaml
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()

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

if not openai_api_key:
logging.warning("OpenAI API key not found. Skipping processing prompt.")

class FileItem:
def __init__(self, properties):
Expand All @@ -12,14 +23,60 @@ def __init__(self, properties):
self.remote_location = properties.get("file")
self.permissions = properties.get("permissions")

self.system_prompt = properties.get("system_prompt")
self.user_prompt = properties.get("user_prompt")
self.openai_client = OpenAI(
api_key=openai_api_key
)

if not openai_model:
logging.info("OpenAI model not found. Using default model.")
self.openai_model = "gpt-3.5-turbo"
else:
logging.debug(f"Using OpenAI model: {openai_model}")
self.openai_model = openai_model

def process_prompt(self, dry_run=False):
if self.user_prompt:
logging.debug(f"Using user prompt: {self.user_prompt}")

if not openai_api_key:
logging.warning("Skipping processing prompt as OpenAI API key is not set.")
return

if not self.system_prompt:
system_prompt = "You are a software developer working on a project. You need to create a file with the following content:"
else:
system_prompt = self.system_prompt

if dry_run:
logging.info("[DRY RUN] Would generate content using OpenAI API.")
self.content = "[DRY RUN] Generating content using OpenAI"
return

completion = self.openai_client.chat.completions.create(
model=self.openai_model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": self.user_prompt}
]
)

self.content = completion.choices[0].message.content
logging.debug(f"Generated content: {self.content}")

def fetch_content(self):
if self.remote_location:
logging.debug(f"Fetching content from: {self.remote_location}")
response = requests.get(self.remote_location)
logging.debug(f"Response status code: {response.status_code}")
response.raise_for_status()
self.content = response.text
logging.debug(f"Fetched content: {self.content}")

def apply_template_variables(self, template_vars):
if self.content and template_vars:
logging.debug(f"Applying template variables: {template_vars}")
template = Template(self.content)
self.content = template.substitute(template_vars)

Expand Down Expand Up @@ -65,12 +122,21 @@ def validate_configuration(structure):
if not isinstance(name, str):
raise ValueError("Each name in the 'structure' item must be a string.")
if isinstance(content, dict):
if 'content' not in content and 'file' not in content:
raise ValueError(f"Dictionary item '{name}' must contain either 'content' or 'file' key.")
# 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.")
Expand All @@ -88,6 +154,7 @@ def create_structure(base_path, structure, dry_run=False, template_vars=None, ba
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 main():
Expand All @@ -112,7 +179,15 @@ def main():
if backup_path and not os.path.exists(backup_path):
os.makedirs(backup_path)

logging.basicConfig(level=logging_level, filename=args.log_file)
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='%(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}")

Expand Down
Loading