Skip to main content

A tool to generate project structure and contents as Markdown

Project description

PackItUp

๐Ÿ“ฆ Project Structure Generator and Documentation Tool

PackItUp is a powerful and flexible command-line tool designed to generate comprehensive documentation of your project's structure. It creates a detailed overview of your project's files and directories, making it easier to understand, share, and maintain your codebase.

๐ŸŒŸ Features

  • ๐ŸŒณ Generate project structure in tree format
  • ๐Ÿ“„ Include file contents (optional)
  • ๐Ÿ“Š Provide file metadata (size, last modified date, permissions)
  • ๐ŸŽจ Multiple output formats (Markdown, JSON, YAML)
  • ๐Ÿงน Respect .gitignore patterns
  • ๐Ÿšซ Customizable file/directory exclusions
  • ๐Ÿ“ฆ Compress output (optional)
  • ๐Ÿ“ Customizable file size limits
  • ๐Ÿ“‹ Automatic clipboard copy of output

๐Ÿ›  Installation

PackItUp is available on PyPI and can be installed with pip:

pip install packitup

This will install PackItUp and all its dependencies.

๐Ÿš€ Usage

After installation, you can use PackItUp directly from the command line:

packitup [OPTIONS] [PATH]

If no path is specified, PackItUp will use the current directory.

๐ŸŽ› Options

  • -t, --tree: Generate file tree only (no file contents)
  • -l, --list: List files and directories in the specified directory
  • -s, --singlefile: Ignore file splitting and return as a single file
  • -p, --purge: Purge all saved PackItUp data
  • -f, --format {markdown,json,yaml}: Output format (default: markdown)
  • -m, --max-size BYTES: Maximum file size in bytes (default: 5MB)
  • -c, --compress: Compress the output files
  • -e, --exclude PATTERN [PATTERN ...]: Additional patterns to exclude
  • --no-content: Exclude file contents, only include metadata

๐Ÿ“š Examples

1. Basic Usage

Generate a Markdown report of the current directory:

packitup

This will create a Markdown file with the project structure, including file contents (up to 5MB per file) and metadata.

2. Specify a Different Directory

Generate a report for a specific project:

packitup /path/to/your/project

3. Generate Only the File Tree

To get a quick overview without file contents:

packitup -t

4. Change Output Format

Generate a JSON report:

packitup -f json

Or a YAML report:

packitup -f yaml

5. Customize File Size Limit

Set the maximum file size to 10MB:

packitup -m 10000000

6. Exclude Specific Patterns

Exclude all .log and .tmp files:

packitup -e *.log *.tmp

7. Generate a Compressed Output

Create a zip file containing the report:

packitup -c

8. Exclude File Contents

Generate a report with only metadata, no file contents:

packitup --no-content
# Project Structure
  • init.py
    
    
  • packitup.py
    import os
    

import mimetypes import pyperclip import argparse from datetime import datetime import shutil import colorama from colorama import Fore, Style, Back import json import yaml import fnmatch from tqdm import tqdm import zipfile

SKIP_FOLDERS = [ # Version control ".git", ".svn", ".hg", ".bzr", # Dependencies and package managers "node_modules", "bower_components", "jspm_packages", "vendor", "packages", "composer.phar", # Python "venv", ".venv", "env", ".env", "pycache", ".pytest_cache", "pip-wheel-metadata", ".eggs", ".egg-info", # Build outputs "dist", "build", "out", "target", "bin", "obj", # IDE and editor specific ".idea", ".vscode", ".vs", ".sublime-", ".atom", ".eclipse", ".settings", # OS specific ".DS_Store", "Thumbs.db", # Logs and temporary files "logs", ".log", "tmp", "temp", # Configuration and local settings ".env.local", ".env..local", # Docker ".docker", # Coverage reports "coverage", ".coverage", "htmlcov", # Documentation builds "docs/_build", "site", # Compiled source ".com", ".class", ".dll", ".exe", ".o", "*.so", ]

MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB CHUNK_SIZE = 1 * 1024 * 1024 # 1 MB

def load_gitignore(path): gitignore_patterns = [] gitignore_path = os.path.join(path, ".gitignore") if os.path.exists(gitignore_path): with open(gitignore_path, "r") as f: gitignore_patterns = [ line.strip() for line in f if line.strip() and not line.startswith("#") ] return gitignore_patterns

def should_skip(path, skip_patterns, gitignore_patterns): name = os.path.basename(path) if any( fnmatch.fnmatch(name, pattern) for pattern in skip_patterns + gitignore_patterns ): return True return False

def get_file_metadata(file_path): stats = os.stat(file_path) return { "size": stats.st_size, "modified": datetime.fromtimestamp(stats.st_mtime).isoformat(), "permissions": oct(stats.st_mode)[-3:], }

def get_file_content( file_path, output_dir, relative_path, single_file=False, include_content=True ): if not include_content: return get_file_metadata(file_path)

file_size = os.path.getsize(file_path)
if file_size > MAX_FILE_SIZE and not single_file:
    return handle_large_file(file_path, output_dir, relative_path)

mime_type, _ = mimetypes.guess_type(file_path)

if mime_type and mime_type.startswith("text"):
    try:
        with open(file_path, "r", encoding="utf-8") as file:
            content = file.read()
        return {**get_file_metadata(file_path), "content": content}
    except UnicodeDecodeError:
        try:
            with open(file_path, "r", encoding="iso-8859-1") as file:
                content = file.read()
            return {**get_file_metadata(file_path), "content": content}
        except Exception:
            return {
                **get_file_metadata(file_path),
                "content": f"[Unable to read {os.path.basename(file_path)}]",
            }
else:
    return {
        **get_file_metadata(file_path),
        "content": f"[{os.path.splitext(file_path)[1]} file]",
    }

def handle_large_file(file_path, output_dir, relative_path): file_name = os.path.basename(file_path) parts_dir = os.path.join(output_dir, f"{file_name}_parts") os.makedirs(parts_dir, exist_ok=True) part_num = 1 with open(file_path, "rb") as f: while True: chunk = f.read(CHUNK_SIZE) if not chunk: break part_file = os.path.join(parts_dir, f"{file_name}.part{part_num}") with open(part_file, "wb") as part: part.write(chunk) part_num += 1

return {
    **get_file_metadata(file_path),
    "content": f"[Large file split into {part_num - 1} parts. See {os.path.relpath(parts_dir, output_dir)}]",
}

def generate_content( path, output_dir, skip_patterns, gitignore_patterns, tree_only=False, single_file=False, include_content=True, ): content = {} for item in sorted(os.listdir(path)): item_path = os.path.join(path, item) if should_skip(item_path, skip_patterns, gitignore_patterns): continue if os.path.isdir(item_path): content[item] = generate_content( item_path, output_dir, skip_patterns, gitignore_patterns, tree_only, single_file, include_content, ) else: if tree_only: content[item] = None else: content[item] = get_file_content( item_path, output_dir, item, single_file, include_content ) return content

def content_to_markdown(content, indent=""): md_content = [] for key, value in content.items(): if value is None or isinstance(value, dict) and "content" not in value: md_content.append(f"{indent}- {key}/") if value: md_content.extend(content_to_markdown(value, indent + " ")) else: md_content.append(f"{indent}- {key}") if isinstance(value, dict) and "content" in value: md_content.append(f"{indent} ") md_content.append(f"{indent} {value['content']}") md_content.append(f"{indent} ") return md_content

def compress_output(output_dir, compressed_file): with zipfile.ZipFile(compressed_file, "w", zipfile.ZIP_DEFLATED) as zipf: for root, _, files in os.walk(output_dir): for file in files: zipf.write( os.path.join(root, file), os.path.relpath(os.path.join(root, file), output_dir), )

def print_styled_header(text): print(f"\n{Back.CYAN}{Fore.BLACK}{text:^50}{Style.RESET_ALL}")

def print_styled_section(title, content): print(f"\n{Fore.CYAN}{title}:") for line in content: print(f" {line}")

def make_clickable(path): return f"\033]8;;file://{path}\033\{path}\033]8;;\033\"

def list_directory(path): print(f"\n{Fore.CYAN}Contents of {Fore.YELLOW}{path}{Fore.RESET}:") for item in sorted(os.listdir(path)): if os.path.isdir(os.path.join(path, item)): print(f" {Fore.BLUE}๐Ÿ“ {item}/{Fore.RESET}") else: print(f" {Fore.GREEN}๐Ÿ“„ {item}{Fore.RESET}") print()

def purge_data(): home_dir = os.path.expanduser("~") home_packitup_dir = os.path.join(home_dir, ".packitup")

if os.path.exists(home_packitup_dir):
    try:
        shutil.rmtree(home_packitup_dir)
        print(
            f"{Fore.GREEN}โœ… Successfully purged all PackItUp data from {home_packitup_dir}"
        )
    except Exception as e:
        print(f"{Fore.RED}โŒ Error while purging data: {e}")
else:
    print(f"{Fore.YELLOW}โš ๏ธ No PackItUp data found to purge.")

# Also remove any local output directories in the current working directory
cwd = os.getcwd()
for item in os.listdir(cwd):
    if os.path.isdir(item) and item.endswith("_structure_"):
        try:
            shutil.rmtree(item)
            print(f"{Fore.GREEN}โœ… Removed local output directory: {item}")
        except Exception as e:
            print(f"{Fore.RED}โŒ Error while removing {item}: {e}")

def main(): colorama.init(autoreset=True) parser = argparse.ArgumentParser( description="Generate project structure and contents." ) parser.add_argument( "path", nargs="?", default=".", help="Path to pack (default: current directory)" ) parser.add_argument( "-t", "--tree", action="store_true", help="Generate file tree only (no file contents)", ) parser.add_argument( "-l", "--list", action="store_true", help="List files and directories in the specified directory", ) parser.add_argument( "-s", "--singlefile", action="store_true", help="Ignore file splitting and return as a single file", ) parser.add_argument( "-p", "--purge", action="store_true", help="Purge all saved PackItUp data" ) parser.add_argument( "-f", "--format", choices=["markdown", "json", "yaml"], default="markdown", help="Output format (default: markdown)", ) parser.add_argument( "-m", "--max-size", type=int, help="Maximum file size in bytes (default: 5MB)" ) parser.add_argument( "-c", "--compress", action="store_true", help="Compress the output files" ) parser.add_argument( "-e", "--exclude", nargs="+", help="Additional patterns to exclude" ) parser.add_argument( "--no-content", action="store_true", help="Exclude file contents, only include metadata", ) args = parser.parse_args()

if args.purge:
    purge_data()
    return

if args.max_size:
    global MAX_FILE_SIZE
    MAX_FILE_SIZE = args.max_size

root_dir = os.path.abspath(args.path)

if args.list:
    list_directory(root_dir)
    return

gitignore_patterns = load_gitignore(root_dir)
skip_patterns = SKIP_FOLDERS + (args.exclude or [])

timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
root_folder_name = os.path.basename(root_dir)

output_dir = f"{root_folder_name}_structure_{timestamp}"
os.makedirs(output_dir, exist_ok=True)
output_file = os.path.join(
    output_dir, f"{root_folder_name}_structure_{timestamp}.{args.format}"
)

print(f"Generating project structure for {root_dir}...")
content = generate_content(
    root_dir,
    output_dir,
    skip_patterns,
    gitignore_patterns,
    args.tree,
    args.singlefile,
    not args.no_content,
)

if args.format == "markdown":
    output_content = "\n".join(
        ["# Project Structure", ""] + content_to_markdown(content)
    )
elif args.format == "json":
    output_content = json.dumps(content, indent=2)
else:  # yaml
    output_content = yaml.dump(content, default_flow_style=False)

with open(output_file, "w", encoding="utf-8") as f:
    f.write(output_content)

if args.compress:
    compressed_file = f"{output_file}.zip"
    print(f"Compressing output to {compressed_file}...")
    compress_output(output_dir, compressed_file)
    print(f"Compressed file saved to {compressed_file}")

pyperclip.copy(output_content)

print_styled_header("PackItUp Completed Successfully!")

print_styled_section(
    "๐Ÿ“ Output Location",
    [f"๐Ÿ“Œ Local: {Fore.YELLOW}{make_clickable(os.path.abspath(output_dir))}"],
)

print_styled_section(
    "๐Ÿ“„ Main File",
    [f"๐Ÿ“Œ Local: {Fore.YELLOW}{make_clickable(os.path.abspath(output_file))}"],
)

print_styled_section(
    "โ„น๏ธ  Info",
    [
        f"๐Ÿ“‹ Content has been copied to clipboard.",
        f"๐Ÿ“‚ Large files have been split and saved in parts if necessary.",
        f"๐Ÿ—‚๏ธ  Skipped patterns: {', '.join(skip_patterns + gitignore_patterns)}",
    ],
)

if name == "main": main()

### 9. Combine Multiple Options

Generate a compressed JSON report with a 10MB file size limit, excluding .log files and file contents:

packitup -f json -m 10000000 -c -e *.log --no-content


## ๐Ÿ“‚ Output

PackItUp generates its output in a new directory named `<project_name>_structure_<timestamp>`. The main output file will be in this directory, named `<project_name>_structure_<timestamp>.<format>`.

If the compress option is used, a zip file will be created in the same directory.

The generated content is automatically copied to your clipboard for easy sharing.

## ๐Ÿงน Cleaning Up

To remove all PackItUp generated data:

packitup -p


This will delete the `.packitup` directory in your home folder and any local output directories.

## ๐Ÿ”„ Updating

To update PackItUp to the latest version, use pip:

pip install --upgrade packitup


## ๐Ÿ› Troubleshooting

If you encounter any issues while using PackItUp, try the following:

1. Ensure you have the latest version installed.
2. Check that you have the necessary permissions to read the files and directories you're trying to document.
3. If you're having issues with specific file types, try using the `--no-content` option to exclude file contents.

If problems persist, please open an issue on our GitHub repository.

## ๐Ÿค Contributing

Contributions are welcome! Please feel free to submit a Pull Request on our [GitHub repository](https://github.com/yourusername/packitup).

## ๐Ÿ“„ License

This project is licensed under the MIT License - see the [LICENSE](https://github.com/yourusername/packitup/blob/main/LICENSE) file for details.

## ๐Ÿ™ Acknowledgements

- Thanks to all the open-source libraries that made this project possible.
- Special thanks to the Python community for their continuous support and inspiration.

---

Happy documenting! ๐Ÿ“šโœจ

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

packitup-1.0.9.tar.gz (11.6 kB view hashes)

Uploaded Source

Built Distribution

packitup-1.0.9-py3-none-any.whl (10.8 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page