├── watchdog_tricks ├── __init__.py ├── ctagswatcher.py ├── lesswatcher.py ├── utils.py ├── batch.py └── compiler.py ├── .gitignore ├── setup.py └── README.md /watchdog_tricks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | #Mr Developer 27 | .mr.developer.cfg 28 | 29 | *.swp 30 | *.un~ 31 | -------------------------------------------------------------------------------- /watchdog_tricks/ctagswatcher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from argh import arg, dispatch_command 3 | from watchdog.watchmedo import observe_with 4 | from watchdog_tricks.compiler import CtagsTrick 5 | from watchdog_tricks import utils 6 | 7 | def main(): 8 | @arg('filetypes', metavar='FILETYPE', nargs='+', help='Included file types for generating targs') 9 | @arg('--rebuild', default=False, help='Force rebuild all tags at start') 10 | @arg('--ctags', default='ctags', help='Path to ctags program.(default: ctags)') 11 | def _watcher(args): 12 | from watchdog.observers import Observer 13 | handler = CtagsTrick(filetypes=args.filetypes, ctags=args.ctags, rebuild=args.rebuild) 14 | observer = Observer(timeout=1.0) 15 | observe_with(observer, handler, ['.'], True) 16 | dispatch_command(_watcher) 17 | 18 | if __name__ == '__main__': 19 | main() 20 | -------------------------------------------------------------------------------- /watchdog_tricks/lesswatcher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from argh import arg, dispatch_command 3 | from watchdog.watchmedo import observe_with 4 | from watchdog_tricks.compiler import LessTrick 5 | 6 | def main(): 7 | @arg('LESS_DIR', help='Directory of less source files') 8 | @arg('CSS_DIR', help='Directory of generated css files') 9 | @arg('--lessc-path', default='lessc', help='Path to less compiler.(default: lessc)') 10 | def _watcher(args): 11 | "Watch changes in less directory and auto-compile modified file to css" 12 | from watchdog.observers import Observer 13 | handler = LessTrick(src_dir=args.LESS_DIR, dest_dir=args.CSS_DIR, compiler=args.lessc_path) 14 | observer = Observer(timeout=1.0) 15 | observe_with(observer, handler, [args.LESS_DIR], True) 16 | dispatch_command(_watcher) 17 | 18 | if __name__ == '__main__': 19 | main() 20 | -------------------------------------------------------------------------------- /watchdog_tricks/utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import subprocess 4 | from glob import glob 5 | from functools import wraps 6 | 7 | def trace_event(func): 8 | @wraps(func) 9 | def _traced_func(self, event): 10 | if hasattr(event, 'dest_path'): 11 | print '%s: %s -> %s' % (event.event_type, event.src_path, event.dest_path) 12 | else: 13 | print '%s %s' % (event.event_type, event.src_path) 14 | return func(self, event) 15 | return _traced_func 16 | 17 | def exec_cmd(cmd, echo=True): 18 | if echo: 19 | sys.stdout.write('Execute command: %s\n' % cmd) 20 | sys.stdout.flush() 21 | process = subprocess.Popen(cmd, shell=True) 22 | process.wait() 23 | 24 | def build_tags(basedir, filetypes, ctags='ctags', recursive=False): 25 | olddir = os.getcwd() 26 | os.chdir(basedir) 27 | files = sum([glob('*.' + ftype) for ftype in filetypes], []) 28 | if files: 29 | print 'Generate tags for %s file(s) in %s' % (len(files), basedir) 30 | ctags_cmd = '%s %s' % (ctags, ' '.join(files)) 31 | exec_cmd(ctags_cmd, echo=False) 32 | if recursive: 33 | for root, dirs, files in os.walk(basedir): 34 | for d in dirs: 35 | build_tags(os.path.join(root, d), filetypes, ctags, recursive) 36 | os.chdir(olddir) 37 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools, sys 2 | 3 | setuptools.setup( 4 | name="watchdog-tricks", 5 | version='0.1.1', 6 | license="MIT", 7 | 8 | author="Ryan Ye", 9 | author_email="yejianye@gmail.com", 10 | url="https://github.com/yejianye/watchdog-tricks", 11 | 12 | description="Common tricks for watchdog (Python file system monitoring tool), including watcher for LessCss, CoffeeScript etc", 13 | long_description=open("README.md").read(), 14 | keywords=["watchdog","watcher","tricks"], 15 | classifiers=[ 16 | "Environment :: Console", 17 | "Development Status :: 3 - Alpha", 18 | "Programming Language :: Python", 19 | "Programming Language :: Python :: 2.7", 20 | "Intended Audience :: Developers", 21 | "License :: OSI Approved :: MIT License", 22 | "Natural Language :: English", 23 | "Operating System :: OS Independent", 24 | "Environment :: Other Environment", 25 | "Topic :: Utilities", 26 | "Topic :: System :: Monitoring", 27 | "Topic :: System :: Filesystems", 28 | ], 29 | entry_points={ 30 | 'console_scripts': [ 31 | 'lesswatcher = watchdog_tricks.lesswatcher:main', 32 | 'ctagswatcher = watchdog_tricks.ctagswatcher:main', 33 | ] 34 | }, 35 | install_requires=['watchdog==0.6.0'], 36 | packages=['watchdog_tricks'], 37 | ) 38 | -------------------------------------------------------------------------------- /watchdog_tricks/batch.py: -------------------------------------------------------------------------------- 1 | import Queue 2 | import subprocess 3 | from watchdog.tricks import Trick 4 | from watchdog_tricks import utils 5 | from threading import Thread 6 | 7 | class BatchTrick(Trick): 8 | """ Batching events within a time window and send it as a single event 9 | This would be particularly useful for things like restarting a server after code changes. 10 | You don't want to restart the server mutliple times when multiple files are modified due to a save command 11 | in the text editor or more commonly when the user does a git pull. 12 | """ 13 | def __init__(self, *args, **kwargs): 14 | self.timeout = kwargs.pop('time_interval', 0.2) 15 | super(BatchTrick, self).__init__(*args, **kwargs) 16 | self.event_queue = Queue.Queue() 17 | self.timer_thread = Thread(target=self.timer_loop) 18 | self.timer_thread.daemon = True 19 | self.timer_thread.start() 20 | 21 | @utils.trace_event 22 | def on_modified(self, event): 23 | self.event_queue.put(event) 24 | 25 | @utils.trace_event 26 | def on_deleted(self, event): 27 | self.event_queue.put(event) 28 | 29 | @utils.trace_event 30 | def on_created(self, event): 31 | self.event_queue.put(event) 32 | 33 | @utils.trace_event 34 | def on_moved(self, event): 35 | self.event_queue.put(event) 36 | 37 | def timer_loop(self): 38 | events = [] 39 | while True: 40 | try: 41 | event = self.event_queue.get(timeout=self.timeout) 42 | events.append(event) 43 | except Queue.Empty: 44 | if events: 45 | self.on_multiple_events(events) 46 | events = [] 47 | 48 | def on_multiple_events(self, events): 49 | raise NotImplementedError() 50 | 51 | class ServerRestartTrick(BatchTrick): 52 | def __init__(self, restart_command, **kwargs): 53 | super(ServerRestartTrick, self).__init__(**kwargs) 54 | self.restart_command = restart_command 55 | 56 | def on_multiple_events(self, events): 57 | subprocess.call(self.restart_command, shell=True) 58 | -------------------------------------------------------------------------------- /watchdog_tricks/compiler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | from string import Template 5 | from watchdog.tricks import Trick 6 | from watchdog_tricks import utils 7 | 8 | class AutoCompileTrick(Trick): 9 | def __init__(self, src_dir, dest_dir, **kwargs): 10 | self.src_dir = os.path.abspath(src_dir) 11 | self.dest_dir = os.path.abspath(dest_dir) 12 | self.compiler = kwargs.pop('compiler') 13 | self.src_ext = kwargs.pop('src_ext') 14 | self.dest_ext = kwargs.pop('dest_ext') 15 | self.compile_command = kwargs.pop('compile_command') 16 | self.compile_opts = kwargs.pop('compile_opts', '') 17 | kwargs.setdefault('patterns', [os.path.join('*', self.src_dir, '*.' + self.src_ext)]) 18 | super(AutoCompileTrick, self).__init__(**kwargs) 19 | 20 | @utils.trace_event 21 | def on_modified(self, event): 22 | self.compile(event.src_path) 23 | 24 | @utils.trace_event 25 | def on_deleted(self, event): 26 | self.remove(event.src_path) 27 | 28 | @utils.trace_event 29 | def on_created(self, event): 30 | self.compile(event.src_path) 31 | 32 | @utils.trace_event 33 | def on_moved(self, event): 34 | self.remove(event.src_path) 35 | if event.dest_path.endswith(self.src_ext) and event.dest_path.startswith(self.src_dir): 36 | self.compile(event.dest_path) 37 | 38 | def get_dest_fname(self, src_fname): 39 | return src_fname.replace(self.src_dir, self.dest_dir).rsplit('.', 1)[0] + '.' + self.dest_ext 40 | 41 | def compile(self, filename): 42 | utils.exec_cmd(self.assemble_compile_cmdline(filename, self.get_dest_fname(filename))) 43 | 44 | def assemble_compile_cmdline(self, src, dst): 45 | return Template(self.compile_command).substitute({ 46 | 'src' : src, 47 | 'dst' : dst, 48 | 'compiler' : self.compiler, 49 | 'opts' : self.compile_opts 50 | }) 51 | 52 | def remove(self, filename): 53 | utils.exec_cmd('rm ' + self.get_dest_fname(filename)) 54 | 55 | class LessTrick(AutoCompileTrick): 56 | def __init__(self, src_dir, dest_dir, **kwargs): 57 | kwargs.setdefault('compiler', 'lessc') 58 | super(LessTrick, self).__init__(src_dir, dest_dir, 59 | src_ext = 'less', 60 | dest_ext = 'css', 61 | compile_command = '$compiler $src > $dst', 62 | **kwargs 63 | ) 64 | 65 | class CoffeeScriptTrick(AutoCompileTrick): 66 | def __init__(self, src_dir, dest_dir, **kwargs): 67 | kwargs.setdefault('compiler', 'coffee') 68 | super(CoffeeScriptTrick, self).__init__(src_dir, dest_dir, 69 | src_ext = 'coffee', 70 | dest_ext = 'js', 71 | compile_command = '$compiler -cp $opts $src > $dst', 72 | **kwargs 73 | ) 74 | 75 | class CtagsTrick(Trick): 76 | def __init__(self, filetypes, ctags='ctags', rebuild=False, **kwargs): 77 | kwargs.setdefault('patterns', ['*.%s' % ext for ext in filetypes]) 78 | super(CtagsTrick, self).__init__(**kwargs) 79 | self.ctags = ctags 80 | self.filetypes = filetypes 81 | if rebuild: 82 | self.rebuild_all() 83 | 84 | @utils.trace_event 85 | def on_any_event(self, event): 86 | src_dir = os.path.dirname(event.src_path) 87 | self.rebuild_tags(src_dir) 88 | if hasattr(event, 'dest_path'): 89 | dest_dir = os.path.dirname(event.src_path) 90 | if dest_dir != src_dir: 91 | self.rebuild_tags(dest_dir) 92 | 93 | def rebuild_tags(self, fdir): 94 | utils.build_tags(fdir, self.filetypes, ctags=self.ctags, recursive=False) 95 | 96 | def rebuild_all(self): 97 | utils.build_tags('.', self.filetypes, ctags=self.ctags, recursive=True) 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | watchdog-tricks 2 | =============== 3 | 4 | This package includes several useful `Trick` for `watchdog` (Python API for monitoring file system events, https://github.com/gorakhargosh/watchdog). 5 | 6 | Tricks could be running in standalone mode or combined via a configuration file. They will perform specific tasks upon file change event. 7 | 8 | - LessTrick: Auto recompiling LessCSS to CSS 9 | - CoffeeScriptTrick: Auto recompiling CoffeeScript to Javascript 10 | - AutoCompileTrick: A generic trick to auto recompiling source code 11 | - ServerRestartTrick: Restart a daemon process on source code changes 12 | 13 | Standalone watcher - lesswatcher 14 | -------------------------------- 15 | `lesswatcher` is a standalone script that auto recompiling lessCSS files. An example usage is 16 | 17 | $ lesswatcher --lessc-path /path/to/lessc static/less static/css 18 | 19 | if `lessc` is already in your search path, you could just skip `--lessc-path` arg. 20 | 21 | Configuration File - trick.yaml 22 | ------------------------------- 23 | The true power of watchdog tricks is shown via configuration files, which could combine different tricks together and give you a fully automated development and building system. Here is an example of trick configuration file 24 | 25 | tricks: 26 | - watchdog.tricks.ShellCommandTrick: 27 | patterns: ["*.py","*.html"] 28 | shell_command: "cat gunicorn.pid | xargs kill -HUP" 29 | wait_for_process: true 30 | - watchdog_tricks.compiler.LessTrick: 31 | src_dir: 'static/less' 32 | dest_dir: 'static/css' 33 | - watchdog_tricks.compiler.CoffeeScriptTrick: 34 | src_dir: 'static/cs' 35 | dest_dir: 'static/js' 36 | compile_opts: '-b' 37 | 38 | Put the configuration file to the root directory that needs to be monitored. Assume the filename is `tricks.yaml`. To run watchdog with this configuration file 39 | 40 | $ watchmedo tricks tricks.yaml 41 | 42 | API Documentation 43 | ----------------- 44 | watchdog_tricks.compiler.LessTrick: 45 | - src_dir: Directory of LessCSS source files 46 | - dest_dir: Directory of output CSS files 47 | - compile_opts: Command-line options for LessCSS compiler 48 | - compiler: LessCSS compiler path. Default is 'lessc' 49 | 50 | watchdog_tricks.compiler.CoffeeScriptTrick: 51 | - src_dir: Directory of CoffeeScript source files 52 | - dest_dir: Directory of output JavaScript files 53 | - compile_opts: Command-line options for CoffeeScript compiler 54 | - compiler: CoffeeScript compiler path. Default is 'coffee' 55 | 56 | `AutoCompileTrick` provides a generic way to auto recompile your source files once it's been modified. Both `LessTricks` and `CoffeeScriptTrick` are extended from `AutoCompileTrick`. 57 | watchdog_tricks.compiler.AutoCompileTrick: 58 | - src_dir: Directory of source files 59 | - dest_dir: Directory of compiled files 60 | - src_ext: Extension of source files 61 | - dest_ext: Extension of compiled files 62 | - compile_opts: Command-line options for the compiler 63 | - compiler: Path to the compiler 64 | - compile_command: Template to build the compile command. See details below. 65 | 66 | `compile_command` takes several predefined variables which you might need to form a compile command. Those variables include: 67 | - src: Path to the source file 68 | - dst: Path to the compiled file 69 | - compiler: Path toe the compiler 70 | - opts: Command-line options for the compiler 71 | 72 | An example configuration file to auto recompile markdown files to html for example.com 73 | 74 | tricks: 75 | - watchdog_tricks.compiler.AutoCompileTrick: 76 | src_dir: 'docs' 77 | dest_dir: 'build/docs' 78 | src_ext: 'md' 79 | dest_ext: 'html' 80 | compiler: 'markdown' 81 | opts: '-b http://example.com' 82 | compile_command: '$compiler $opts < $src > $dst' 83 | 84 | watchdog_tricks.compiler.CtagsTrick: 85 | - filetypes: file types to be monitored 86 | - rebuild: whether rebuilding all tags on startup 87 | 88 | An example configuration file to auto re-generate tag files for python and coffeescript source files 89 | 90 | tricks: 91 | - watchdog_tricks.compiler.CtagsTrick: 92 | filetypes: ['py', 'coffee'] 93 | rebuild: true 94 | 95 | watchdog_tricks.batch.ServerRestartTrick: 96 | - restart_command: the command to restart the server 97 | 98 | An example to restart gunicorn web servers on python source code changes 99 | 100 | tricks: 101 | - watchdog_tricks.batch.ServerRestartTrick: 102 | patterns: ['*.py'] 103 | restart_command: sudo kill -HUP /srv/logs/gunicorn.pid 104 | 105 | Installation 106 | ------------ 107 | 108 | Install directly from github.com 109 | 110 | $ pip install git+git://github.com/yejianye/watchdog-tricks.git 111 | 112 | Or clone this repository and run 113 | 114 | $ python setup.py install 115 | 116 | --------------------------------------------------------------------------------