relay/relay/dev.py

219 lines
4.5 KiB
Python
Raw Normal View History

2024-02-24 00:58:35 +00:00
import click
2024-03-04 10:39:21 +00:00
import platform
2024-04-01 11:18:10 +00:00
import shutil
2024-02-24 00:58:35 +00:00
import subprocess
import sys
import time
2024-03-31 15:00:31 +00:00
from datetime import datetime, timedelta
2024-02-24 00:58:35 +00:00
from pathlib import Path
2024-03-04 10:39:21 +00:00
from tempfile import TemporaryDirectory
2024-03-31 15:00:31 +00:00
from typing import Sequence
2024-03-04 10:39:21 +00:00
from . import __version__
from . import logger as logging
2024-02-24 00:58:35 +00:00
try:
from watchdog.observers import Observer
from watchdog.events import PatternMatchingEventHandler
except ImportError:
class PatternMatchingEventHandler: # type: ignore
2024-02-24 00:58:35 +00:00
pass
SCRIPT = Path(__file__).parent
REPO = SCRIPT.parent
IGNORE_EXT = {
'.py',
'.pyc'
}
@click.group('cli')
def cli():
'Useful commands for development'
@cli.command('install')
def cli_install():
cmd = [
sys.executable, '-m', 'pip', 'install',
'-r', 'requirements.txt',
'-r', 'dev-requirements.txt'
]
subprocess.run(cmd, check = False)
@cli.command('lint')
2024-04-01 11:18:10 +00:00
@click.argument('path', required = False, type = Path, default = REPO.joinpath('relay'))
@click.option('--strict', '-s', is_flag = True, help = 'Enable strict mode for mypy')
2024-03-31 15:00:31 +00:00
@click.option('--watch', '-w', is_flag = True,
help = 'Automatically, re-run the linters on source change')
2024-04-01 11:18:10 +00:00
def cli_lint(path: Path, strict: bool, watch: bool) -> None:
path = path.expanduser().resolve()
flake8 = [sys.executable, '-m', 'flake8', str(path)]
mypy = [sys.executable, '-m', 'mypy', str(path)]
if strict:
2024-03-31 15:00:31 +00:00
mypy.append('--strict')
2024-03-31 15:00:31 +00:00
if watch:
handle_run_watcher(mypy, flake8, wait = True)
return
click.echo('----- flake8 -----')
subprocess.run(flake8)
click.echo('\n\n----- mypy -----')
subprocess.run(mypy)
2024-02-24 00:58:35 +00:00
2024-04-01 11:18:10 +00:00
@cli.command('clean')
def cli_clean():
dirs = {
'dist',
'build',
'dist-pypi'
}
for directory in dirs:
shutil.rmtree(directory, ignore_errors = True)
for path in REPO.glob('*.egg-info'):
shutil.rmtree(path)
for path in REPO.glob('*.spec'):
path.unlink()
2024-02-24 00:58:35 +00:00
@cli.command('build')
def cli_build():
2024-03-04 10:39:21 +00:00
with TemporaryDirectory() as tmp:
arch = 'amd64' if sys.maxsize >= 2**32 else 'i386'
cmd = [
sys.executable, '-m', 'PyInstaller',
'--collect-data', 'relay',
'--collect-data', 'aiohttp_swagger',
'--hidden-import', 'pg8000',
'--hidden-import', 'sqlite3',
'--name', f'activityrelay-{__version__}-{platform.system().lower()}-{arch}',
'--workpath', tmp,
'--onefile', 'relay/__main__.py',
]
if platform.system() == 'Windows':
cmd.append('--console')
# putting the spec path on a different drive than the source dir breaks
if str(SCRIPT)[0] == tmp[0]:
cmd.extend(['--specpath', tmp])
else:
cmd.append('--strip')
cmd.extend(['--specpath', tmp])
subprocess.run(cmd, check = False)
2024-02-24 00:58:35 +00:00
@cli.command('run')
@click.option('--dev', '-d', is_flag = True)
def cli_run(dev: bool):
2024-02-24 00:58:35 +00:00
print('Starting process watcher')
2024-03-31 15:00:31 +00:00
cmd = [sys.executable, '-m', 'relay', 'run']
if dev:
cmd.append('-d')
handle_run_watcher(cmd)
def handle_run_watcher(*commands: Sequence[str], wait: bool = False):
handler = WatchHandler(*commands, wait = wait)
handler.run_procs()
2024-02-24 00:58:35 +00:00
watcher = Observer()
watcher.schedule(handler, str(SCRIPT), recursive=True)
watcher.start()
try:
while True:
2024-03-31 15:00:31 +00:00
time.sleep(1)
2024-02-24 00:58:35 +00:00
except KeyboardInterrupt:
pass
2024-03-31 15:00:31 +00:00
handler.kill_procs()
2024-02-24 00:58:35 +00:00
watcher.stop()
watcher.join()
class WatchHandler(PatternMatchingEventHandler):
patterns = ['*.py']
2024-03-31 15:00:31 +00:00
def __init__(self, *commands: Sequence[str], wait: bool = False):
2024-02-24 00:58:35 +00:00
PatternMatchingEventHandler.__init__(self)
2024-03-31 15:00:31 +00:00
self.commands: Sequence[Sequence[str]] = commands
self.wait: bool = wait
self.procs: list[subprocess.Popen] = []
self.last_restart: datetime = datetime.now()
2024-02-24 00:58:35 +00:00
2024-03-31 15:00:31 +00:00
def kill_procs(self):
for proc in self.procs:
if proc.poll() is not None:
continue
2024-02-24 00:58:35 +00:00
2024-03-31 15:00:31 +00:00
logging.info(f'Terminating process {proc.pid}')
proc.terminate()
sec = 0.0
2024-02-24 00:58:35 +00:00
2024-03-31 15:00:31 +00:00
while proc.poll() is None:
time.sleep(0.1)
sec += 0.1
2024-02-24 00:58:35 +00:00
2024-03-31 15:00:31 +00:00
if sec >= 5:
logging.error('Failed to terminate. Killing process...')
proc.kill()
break
2024-02-24 00:58:35 +00:00
2024-03-31 15:00:31 +00:00
logging.info('Process terminated')
2024-02-24 00:58:35 +00:00
2024-03-31 15:00:31 +00:00
def run_procs(self, restart: bool = False):
if restart:
if datetime.now() - timedelta(seconds = 3) < self.last_restart:
2024-02-24 00:58:35 +00:00
return
2024-03-31 15:00:31 +00:00
self.kill_procs()
self.last_restart = datetime.now()
2024-02-24 00:58:35 +00:00
2024-03-31 15:00:31 +00:00
if self.wait:
self.procs = []
2024-03-31 15:00:31 +00:00
for cmd in self.commands:
logging.info('Running command: %s', ' '.join(cmd))
subprocess.run(cmd)
2024-02-24 00:58:35 +00:00
2024-03-31 15:00:31 +00:00
else:
self.procs = list(subprocess.Popen(cmd) for cmd in self.commands)
pids = (str(proc.pid) for proc in self.procs)
logging.info('Started processes with PIDs: %s', ', '.join(pids))
2024-02-24 00:58:35 +00:00
def on_any_event(self, event):
if event.event_type not in ['modified', 'created', 'deleted']:
return
2024-03-31 15:00:31 +00:00
self.run_procs(restart = True)
2024-02-24 00:58:35 +00:00
if __name__ == '__main__':
cli()