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-04-02 16:36:06 +00:00
|
|
|
from relay import __version__, logger as logging
|
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
|
|
|
|
2024-02-24 00:58:35 +00:00
|
|
|
try:
|
|
|
|
from watchdog.observers import Observer
|
|
|
|
from watchdog.events import PatternMatchingEventHandler
|
|
|
|
|
|
|
|
except ImportError:
|
2024-03-13 21:43:57 +00:00
|
|
|
class PatternMatchingEventHandler: # type: ignore
|
2024-02-24 00:58:35 +00:00
|
|
|
pass
|
|
|
|
|
|
|
|
|
2024-04-02 16:36:06 +00:00
|
|
|
REPO = Path(__file__).parent
|
2024-02-24 00:58:35 +00:00
|
|
|
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'))
|
2024-03-13 21:43:57 +00:00
|
|
|
@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()
|
2024-04-01 17:05:32 +00:00
|
|
|
|
|
|
|
if watch:
|
|
|
|
handle_run_watcher([sys.executable, "-m", "relay.dev", "lint", str(path)], wait = True)
|
|
|
|
return
|
|
|
|
|
2024-04-02 16:36:06 +00:00
|
|
|
flake8 = [sys.executable, '-m', 'flake8', "dev.py", str(path)]
|
|
|
|
mypy = [sys.executable, '-m', 'mypy', "dev.py", str(path)]
|
2024-03-13 21:43:57 +00:00
|
|
|
|
|
|
|
if strict:
|
2024-03-31 15:00:31 +00:00
|
|
|
mypy.append('--strict')
|
2024-03-13 21:43:57 +00:00
|
|
|
|
2024-03-31 15:00:31 +00:00
|
|
|
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
|
2024-04-02 16:36:06 +00:00
|
|
|
if str(REPO)[0] == tmp[0]:
|
2024-03-04 10:39:21 +00:00
|
|
|
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')
|
2024-03-16 01:17:20 +00:00
|
|
|
@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()
|
2024-04-02 16:36:06 +00:00
|
|
|
watcher.schedule(handler, str(REPO), recursive=True)
|
2024-02-24 00:58:35 +00:00
|
|
|
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-16 01:17:20 +00:00
|
|
|
|
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()
|