Initial commit

This commit is contained in:
Dmitriy Pleshevskiy 2018-06-10 10:11:22 +03:00
parent a524ffc1c3
commit 77ba4c758d
14 changed files with 410 additions and 0 deletions

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
/venv/
__pycache__/
*.pyc
*.egg-info/
/dist
/build

24
README.md Normal file
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,6 @@
class Config:
VERSION = '0.1.0'
REPOSITORY = 'https://github.com/ictmpl/'
METHODS_MODULE_PREFIX = 'ictmpl.methods'

View file

102
ictmpl/helpers/fsutil.py Normal file
View 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
View 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
View 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()

View file

113
ictmpl/methods/create.py Normal file
View 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
View 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
"""
)