Initial commit
This commit is contained in:
parent
a524ffc1c3
commit
77ba4c758d
14 changed files with 410 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
/venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.egg-info/
|
||||||
|
|
||||||
|
/dist
|
||||||
|
/build
|
24
README.md
Normal file
24
README.md
Normal file
|
@ -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 <project_path> <template_name>
|
||||||
|
|
||||||
|
ictmpl create flaskproject flask-blank
|
||||||
|
|
||||||
|
ictmpl create myproject ../my-local-template/
|
||||||
|
|
||||||
|
ictmpl create /var/www/myproject https://github.com/ictmpl/flask-blank.git
|
||||||
|
|
11
ictmpl/__init__.py
Normal file
11
ictmpl/__init__.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
from .config import Config
|
||||||
|
from .ictmpl import Ictmpl
|
||||||
|
|
||||||
|
|
||||||
|
ictmpl = Ictmpl(Config())
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
ictmpl.run()
|
||||||
|
|
||||||
|
|
||||||
|
|
11
ictmpl/__version__.py
Normal file
11
ictmpl/__version__.py
Normal file
|
@ -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'
|
8
ictmpl/app.py
Normal file
8
ictmpl/app.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
def run():
|
||||||
|
from ictmpl.ictmpl import Ictmpl
|
||||||
|
from ictmpl.config import Config
|
||||||
|
Ictmpl(Config()).run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run()
|
26
ictmpl/argparser.py
Normal file
26
ictmpl/argparser.py
Normal file
|
@ -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
|
6
ictmpl/config.py
Normal file
6
ictmpl/config.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
class Config:
|
||||||
|
VERSION = '0.1.0'
|
||||||
|
|
||||||
|
REPOSITORY = 'https://github.com/ictmpl/'
|
||||||
|
|
||||||
|
METHODS_MODULE_PREFIX = 'ictmpl.methods'
|
0
ictmpl/helpers/__init__.py
Normal file
0
ictmpl/helpers/__init__.py
Normal file
102
ictmpl/helpers/fsutil.py
Normal file
102
ictmpl/helpers/fsutil.py
Normal file
|
@ -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)
|
||||||
|
|
28
ictmpl/helpers/git.py
Normal file
28
ictmpl/helpers/git.py
Normal file
|
@ -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)
|
||||||
|
|
||||||
|
|
43
ictmpl/ictmpl.py
Normal file
43
ictmpl/ictmpl.py
Normal file
|
@ -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()
|
||||||
|
|
0
ictmpl/methods/__init__.py
Normal file
0
ictmpl/methods/__init__.py
Normal file
113
ictmpl/methods/create.py
Normal file
113
ictmpl/methods/create.py
Normal file
|
@ -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)
|
31
setup.py
Normal file
31
setup.py
Normal file
|
@ -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
|
||||||
|
"""
|
||||||
|
)
|
Reference in a new issue