From 77ba4c758dae43b8769d323afb822928735cb180 Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Sun, 10 Jun 2018 10:11:22 +0300 Subject: [PATCH] Initial commit --- .gitignore | 7 +++ README.md | 24 ++++++++ ictmpl/__init__.py | 11 ++++ ictmpl/__version__.py | 11 ++++ ictmpl/app.py | 8 +++ ictmpl/argparser.py | 26 +++++++++ ictmpl/config.py | 6 ++ ictmpl/helpers/__init__.py | 0 ictmpl/helpers/fsutil.py | 102 +++++++++++++++++++++++++++++++++ ictmpl/helpers/git.py | 28 +++++++++ ictmpl/ictmpl.py | 43 ++++++++++++++ ictmpl/methods/__init__.py | 0 ictmpl/methods/create.py | 113 +++++++++++++++++++++++++++++++++++++ setup.py | 31 ++++++++++ 14 files changed, 410 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 ictmpl/__init__.py create mode 100644 ictmpl/__version__.py create mode 100644 ictmpl/app.py create mode 100644 ictmpl/argparser.py create mode 100644 ictmpl/config.py create mode 100644 ictmpl/helpers/__init__.py create mode 100644 ictmpl/helpers/fsutil.py create mode 100644 ictmpl/helpers/git.py create mode 100644 ictmpl/ictmpl.py create mode 100644 ictmpl/methods/__init__.py create mode 100644 ictmpl/methods/create.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0fc4542 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/venv/ +__pycache__/ +*.pyc +*.egg-info/ + +/dist +/build \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..01a3f4d --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# About ictmpl + +This script allows a create new project using a ready-made templates. + + +# Installing + + pip install ictmpl + +or globally: + + sudo -H pip install ictmpl + + +# Usage + + ictmpl create + + ictmpl create flaskproject flask-blank + + ictmpl create myproject ../my-local-template/ + + ictmpl create /var/www/myproject https://github.com/ictmpl/flask-blank.git + diff --git a/ictmpl/__init__.py b/ictmpl/__init__.py new file mode 100644 index 0000000..d1300dc --- /dev/null +++ b/ictmpl/__init__.py @@ -0,0 +1,11 @@ +from .config import Config +from .ictmpl import Ictmpl + + +ictmpl = Ictmpl(Config()) + +if __name__ == '__main__': + ictmpl.run() + + + \ No newline at end of file diff --git a/ictmpl/__version__.py b/ictmpl/__version__.py new file mode 100644 index 0000000..e609533 --- /dev/null +++ b/ictmpl/__version__.py @@ -0,0 +1,11 @@ +# ICE TEMPLE + +__title__ = 'ictmpl' +__description__ = 'Generate projects from templates' +__url__ = 'https://github.com/ideascup/ictmpl/' +__version__ = '1.0.0' +__build__ = 0x010000 +__author__ = 'Dmitriy Pleshevskiy' +__author_email__ = 'dmitriy@ideascup.me' +__license__ = 'MIT' +__copyright__ = 'Copyright 2018 Dmitriy Pleshevskiy' \ No newline at end of file diff --git a/ictmpl/app.py b/ictmpl/app.py new file mode 100644 index 0000000..1770df9 --- /dev/null +++ b/ictmpl/app.py @@ -0,0 +1,8 @@ +def run(): + from ictmpl.ictmpl import Ictmpl + from ictmpl.config import Config + Ictmpl(Config()).run() + + +if __name__ == '__main__': + run() \ No newline at end of file diff --git a/ictmpl/argparser.py b/ictmpl/argparser.py new file mode 100644 index 0000000..6ecd518 --- /dev/null +++ b/ictmpl/argparser.py @@ -0,0 +1,26 @@ +import os +import sys +import json +from argparse import ArgumentParser +from .__version__ import __version__ + +__all__ = ('init_parser',) + + +def init_parser(app): + parser = ArgumentParser(os.path.basename(sys.argv[0]), + description='Project template manager') + + parser.add_argument('-v', '--version', action='version', + version='%(prog)s {}'.format(__version__), + help='Show version of script') + + _subparser = parser.add_subparsers(help='List of commands') + + _create = _subparser.add_parser( + 'create', help='Create new project by template') + _create.add_argument('path', help='Path to new project') + _create.add_argument('template', help='Template name or git repository') + _create.set_defaults(func='create.create_project') + + return parser diff --git a/ictmpl/config.py b/ictmpl/config.py new file mode 100644 index 0000000..aedcc14 --- /dev/null +++ b/ictmpl/config.py @@ -0,0 +1,6 @@ +class Config: + VERSION = '0.1.0' + + REPOSITORY = 'https://github.com/ictmpl/' + + METHODS_MODULE_PREFIX = 'ictmpl.methods' \ No newline at end of file diff --git a/ictmpl/helpers/__init__.py b/ictmpl/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ictmpl/helpers/fsutil.py b/ictmpl/helpers/fsutil.py new file mode 100644 index 0000000..6a5d7a0 --- /dev/null +++ b/ictmpl/helpers/fsutil.py @@ -0,0 +1,102 @@ +import os +from re import compile, sub +from os.path import abspath, isdir, join +from shutil import copytree, rmtree + +__all__ = ('parse_ignore_file', 'copytree_with_ignore', 'treewalk') + + +RE_DOUBLESTAR = compile(r'\*\*') +RE_STAR = compile(r'\*') + + +def parse_ignore_file(filepath): + regexes = [] + try: + with open(abspath(filepath)) as file: + lines = [line.strip() for line in file.readlines()] + except: + return regexes + + for line in lines: + if not line or line.startswith('#'): + continue + + line = RE_DOUBLESTAR.sub('.+?', line) + line = RE_STAR.sub('[\w-]+?', line) + if line.startswith('/'): + line = '^'+line + + regexes.append(compile(line)) + return regexes + + +def treewalk(path): + PATH_LEN = len(path) + + for root, dirs, files in os.walk(path): + src_files = dirs + files + filepaths = [ + '%s/%s' % (root[PATH_LEN:], filename) + for filename in ([dir+'/' for dir in dirs] + files) + ] + + yield root, dirs, files, zip(filepaths, src_files) + + +def copytree_with_ignore(src, dst, ignore_filepath='.ictmplignore', **kwargs): + ignore_filepath = abspath(join(src, ignore_filepath)) + ignore_regexes = parse_ignore_file(ignore_filepath) + SRC_PATHLEN = len(src) + + # TODO: Need refactoring on treewalk method + def ignore(path, files): + ignores = [] + files = (('%s/%s' % (path, file), file) for file in files) + for filepath, file in files: + if isdir(filepath): + filepath += '/' + filepath = filepath[SRC_PATHLEN:] + + for regex in ignore_regexes: + if regex.search(filepath): + ignores.append(file) + break + + return ignores + + return copytree(src, dst, ignore=ignore, **kwargs) + + +def rmtree_without_ignore(path, ignore_filepath='.ictmplignore'): + ignore_filepath = abspath(join(path, ignore_filepath)) + ignore_regexes = parse_ignore_file(ignore_filepath) + + if not ignore_regexes: + return + + for root, dirs, files, all in treewalk(path): + rmfiles = [] + for filepath, srcname in all: + for regex in ignore_regexes: + if regex.search(filepath): + rmfiles.append([filepath, srcname]) + break + + for filepath, filename in rmfiles: + if filename in dirs: + rmtree(path+filepath, ignore_errors=True) + elif filename in files: + os.remove(path+filepath) + + +def replace_template_file(filepath, app): + params = app.params + with open(filepath, 'r') as file: + filedata = file.read() + for key, value in params.items(): + filedata = sub(r'%%{}%%'.format(key), value, filedata) + + with open(filepath, 'w') as file: + file.write(filedata) + \ No newline at end of file diff --git a/ictmpl/helpers/git.py b/ictmpl/helpers/git.py new file mode 100644 index 0000000..10243ed --- /dev/null +++ b/ictmpl/helpers/git.py @@ -0,0 +1,28 @@ +from subprocess import Popen, PIPE, CalledProcessError +from shutil import rmtree + +__all__ = ('Git', 'GitError') + + +class GitError(Exception): + pass + + +class Git: + def __init__(self, repository): + self.repository = repository + + def clone_to(self, project_path): + command = 'git clone {repository} {path} -q --depth=1'.format( + repository=self.repository, + path=project_path) + + pipes = Popen(command, shell=True, stdout=PIPE, stderr=PIPE) + stdout, stderr = pipes.communicate() + if stderr: + stderr = stderr.decode('utf-8').strip().split('\n')[-1] + raise GitError(stderr) + + rmtree('%s/.git' % project_path, ignore_errors=True) + + \ No newline at end of file diff --git a/ictmpl/ictmpl.py b/ictmpl/ictmpl.py new file mode 100644 index 0000000..179c561 --- /dev/null +++ b/ictmpl/ictmpl.py @@ -0,0 +1,43 @@ +import sys + +from .config import Config +from .argparser import init_parser +from importlib import import_module + + +__all__ = ('Ictmpl',) + + +class Ictmpl: + config = None + + def __init__(self, config:Config): + self.config = config + self.parser = init_parser(self) + self.params = {} + + def getconf(self, name, default=''): + return getattr(self.config, name, default) + + def run(self, **args): + args = self.parser.parse_args(**args) + try: + if len(sys.argv) == 1: + self.parser.print_help() + elif callable(args.func): + args.func(args, self) + elif isinstance(args.func, str): + module_name, fn_name = args.func.rsplit('.', 1) + prefix = self.getconf('METHODS_MODULE_PREFIX') + if prefix: + module_name = '%s.%s' % (prefix, module_name) + + module = import_module(module_name) + func = getattr(module, fn_name) + if callable(func): + func(args, self) + + except Exception as e: + print("%s :: %s" % (e.__class__.__name__, str(e))) + exit() + diff --git a/ictmpl/methods/__init__.py b/ictmpl/methods/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ictmpl/methods/create.py b/ictmpl/methods/create.py new file mode 100644 index 0000000..6cecf65 --- /dev/null +++ b/ictmpl/methods/create.py @@ -0,0 +1,113 @@ +import os +from re import compile +from os.path import abspath, isdir, join +from json import loads +from subprocess import Popen, PIPE, DEVNULL, check_output, check_call +from uuid import uuid4 +from textwrap import dedent +from shutil import rmtree + +from ictmpl.helpers.git import Git +from ictmpl.helpers.fsutil import ( + copytree_with_ignore, rmtree_without_ignore, replace_template_file) + + +__all__ = ('create_project',) + + +RE_LOCALDIR = compile(r'^(?:\.{1,2}/|/)') +RE_GITLINK = compile(r'^(?:git@|https?://)') + +RE_COMMAND_ENV = compile(r'#!(.+)') + + +def create_project(args, app): + project_path = abspath(args.path) + + tmpl = args.template + if RE_LOCALDIR.match(tmpl): + template_path = abspath(tmpl) + if not isdir(template_path): + raise Exception("Template wasn't found") + + copytree_with_ignore(template_path, project_path) + elif RE_GITLINK.match(tmpl): + Git(tmpl).clone_to(project_path) + else: + root_repository = app.getconf('REPOSITORY', None) + if not root_repository: + raise Exception("Template wasn't found") + + Git(root_repository + tmpl).clone_to(project_path) + + rmtree_without_ignore(project_path) + os.chdir(project_path) + + rc = {} + try: + with open('.ictmplrc', 'r') as file: + exec(file.read(), rc) + except: + return + + dependencies = rc.get('sysDependencies', None) + if dependencies and isinstance(dependencies, dict): + commands = [] + for key, packages in dependencies.items(): + pipes = Popen('command -v %s' % key, shell=True, stdout=PIPE) + stdout, _ = pipes.communicate() + if not stdout: + commands.append(packages) + + if commands: + # TODO: Need check platform + command = 'sudo apt-get install %s' % ' '.join(commands) + Popen(command, shell=True) + + for key in ('name', 'version', 'description', 'author', 'author_email', + 'author_website', 'licence'): + app.params['__{}__'.format(key.upper())] = rc.get(key, '') + + params = rc.get('params', {}) + if params: + print('Configure template:') + for param, default in params.items(): + question = 'SET {}: ({}) '.format(param, default) + app.params[param] = input(question) or default + print('------------------\n') + + print('Walking files and replace params') + templates = rc.get('templates', []) + if templates: + templates = map(lambda p: join(project_path, p), templates) + for filepath in templates: + replace_template_file(filepath, app) + else: + for root, dirs, files in os.walk(project_path): + for filename in files: + replace_template_file(join(root, filename), app) + + setup_command = rc.get('commands', {}).get('setup', None) + if setup_command: + if isinstance(setup_command, (list, tuple)): + setup_command = '\n'.join(setup_command) + + print('Run setup command') + setup_command = dedent(setup_command).strip() + env = RE_COMMAND_ENV.search(setup_command) + if not env: + env = '/bin/bash' + + env = env.group(1).strip() + tmpfilename = '/tmp/ictmpl_%s' % uuid4().hex + with open(tmpfilename, 'w') as file: + file.write(setup_command) + + try: + proc = Popen([env, tmpfilename], stderr=DEVNULL) + proc.communicate() + except KeyboardInterrupt: + proc.kill() + rmtree(project_path) + finally: + os.remove(tmpfilename) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6cd4e48 --- /dev/null +++ b/setup.py @@ -0,0 +1,31 @@ +import os + +from setuptools import setup, find_packages + + +here = os.path.abspath(os.path.dirname(__file__)) +packages = find_packages() + +about = {} +with open(os.path.join(here, 'ictmpl', '__version__.py'), 'r') as f: + exec(f.read(), about) + +with open('README.md', 'r') as f: + long_description = f.read() + +setup( + name=about['__title__'], + version=about['__version__'], + description=about['__description__'], + long_description=long_description, + long_description_content_type='text/markdown', + author=about['__author__'], + author_email=about['__author_email__'], + url=about['__url__'], + zip_safe=False, + packages=find_packages(), + entry_points=""" + [console_scripts] + ictmpl=ictmpl.app:run + """ +) \ No newline at end of file