def delete_log(self, log_type, index=0): if index: print( snakesay( "Deleting old (archive number {index}) {type} log file for {domain} via API" .format(index=index, type=log_type, domain=self.domain))) else: print( snakesay( "Deleting current {type} log file for {domain} via API". format(type=log_type, domain=self.domain))) if index == 1: url = get_api_endpoint().format( username=getpass.getuser(), flavor="files") + "path/var/log/{domain}.{type}.log.1/".format( domain=self.domain, type=log_type) elif index > 1: url = get_api_endpoint().format( username=getpass.getuser(), flavor="files" ) + "path/var/log/{domain}.{type}.log.{index}.gz/".format( domain=self.domain, type=log_type, index=index) else: url = get_api_endpoint().format( username=getpass.getuser(), flavor="files") + "path/var/log/{domain}.{type}.log/".format( domain=self.domain, type=log_type) response = call_api(url, "delete") if not response.ok: raise Exception( "DELETE log file via API failed, got {response}:{response_text}" .format(response=response, response_text=response.text))
def update_schedule(self, params, *, porcelain=False): """Updates existing task using `params`. *Note*: use this method on `Task.from_id` instance. `params` should be one at least one of: command, enabled, interval, hour, minute. `interval` takes precedence over `hour` meaning that `hour` param will be ignored if `interval` is set to 'hourly'. :param params: dictionary of specs to update :param porcelain: when True don't use `snakesay` in stdout messages (defaults to False)""" specs = { "command": self.command, "enabled": self.enabled, "interval": self.interval, "hour": self.hour, "minute": self.minute, } specs.update(params) if ((specs["interval"] != "daily") or (params.get("interval") == "daily" and self.hour) or (params.get("hour") == self.hour)): specs.pop("hour") if params.get("minute") == self.minute: specs.pop("minute") new_specs = self.schedule.update(self.task_id, specs) diff = { key: (getattr(self, key), new_specs[key]) for key in specs if getattr(self, key) != new_specs[key] } def make_spec_str(key, old_spec, new_spec): return f"<{key}> from '{old_spec}' to '{new_spec}'" updated = [ make_spec_str(key, val[0], val[1]) for key, val in diff.items() ] def make_msg(join_with): fill = " " if join_with == ", " else join_with intro = f"Task {self.task_id} updated:{fill}" return f"{intro}{join_with.join(updated)}" if updated: if porcelain: logger.info(make_msg(join_with="\n")) else: logger.info(snakesay(make_msg(join_with=", "))) self.update_specs(new_specs) else: logger.warning(snakesay("Nothing to update!"))
def delete(self): """Returns `True` when `self.path` successfully deleted on PythonAnywhere, `False` otherwise.""" try: self.api.path_delete(self.path) logger.info(snakesay(f"{self.path} deleted!")) return True except Exception as e: logger.warning(snakesay(str(e))) return False
def unshare(self): """Returns `True` when file unshared or has not been shared, `False` otherwise.""" already_shared = self.get_sharing_url(quiet=True) if already_shared: result = self.api.sharing_delete(self.path) if result == 204: logger.info(snakesay(f"{self.path} is no longer shared!")) return True logger.warning(snakesay(f"Could not unshare {self.path}... :(")) return False logger.info(snakesay(f"{self.path} is not being shared, no need to stop sharing...")) return True
def share(self): """Returns PythonAnywhere sharing link for `self.path` or an empty string when share not successful.""" try: code, url = self.api.sharing_post(self.path) except Exception as e: logger.warning(snakesay(str(e))) return "" msg = {200: "was already", 201: "successfully"}[code] sharing_url = self._make_sharing_url(url) logger.info(snakesay(f"{self.path} {msg} shared at {sharing_url}")) return sharing_url
def get_sharing_url(self, quiet=False): """Returns PythonAnywhere sharing url for `self.path` if file is shared, empty string otherwise.""" url = self.api.sharing_get(self.path) if url: sharing_url = self._make_sharing_url(url) if not quiet: logger.info(snakesay(f"{self.path} is shared at {sharing_url}")) return sharing_url logger.info(snakesay(f"{self.path} has not been shared")) return ""
def test_two_lines(): two_lines = 'a' * 81 message = snakesay(two_lines) print(message) assert r'/ aaaaaa' in message assert 'aaaaaa \\' in message assert r'\ aaaaaa' in message
def delete_schedule(self): """Deletes existing task. *Note*: use this method on `Task.from_id` instance.""" if self.schedule.delete(self.task_id): logger.info(snakesay(f"Task {self.task_id} deleted!"))
def create_schedule(self): """Creates new scheduled task. *Note* use this method on `Task.to_be_created` instance.""" params = { "command": self.command, "enabled": self.enabled, "interval": self.interval, "minute": self.minute, } if self.hour: params["hour"] = self.hour self.update_specs(self.schedule.create(params)) mode = "will" if self.enabled else "may be enabled to" msg = ("Task '{command}' successfully created with id {task_id} " "and {mode} be run {interval} at {printable_time}").format( command=self.command, task_id=self.task_id, mode=mode, interval=self.interval, printable_time=self.printable_time, ) logger.info(snakesay(msg))
def sanity_checks(self, nuke): print(snakesay("Running API sanity checks")) token = os.environ.get("API_TOKEN") if not token: raise SanityException( dedent( """ Could not find your API token. You may need to create it on the Accounts page? You will also need to close this console and open a new one once you've done that. """ ) ) if nuke: return url = get_api_endpoint().format(username=getpass.getuser(), flavor="webapps") + self.domain + "/" response = call_api(url, "get") if response.status_code == 200: raise SanityException( "You already have a webapp for {domain}.\n\nUse the --nuke option if you want to replace it.".format( domain=self.domain ) )
def contents(self): """When `self.path` points to a PythonAnywhere user directiory, returns a dictionary of its files and directories, where file/directory names are keys and values contain information about type and API endpoint. Otherwise (when `self.path` points to a file) contents of the file are returned as bytes. >>> PAPath('/home/username').contents >>> {'.bashrc': {'type': 'file', 'url': 'https://www.pythonanywhere.com/api/v0/user/username/files/path/home/username/.bashrc'}, '.local': {'type': 'directory', 'url': 'https://www.pythonanywhere.com/api/v0/user/username/files/path/home/username/.local'}, ... } >>> PAPath('/home/username/README.txt').contents >>> b"some README.txt contents..." """ try: content = self.api.path_get(self.path) return content if isinstance(content, dict) else content.decode("utf-8") except Exception as e: logger.warning(snakesay(str(e))) return None
def list_( tablefmt: str = typer.Option( "simple", "-f", "--format", help="Table format", callback=tablefmt_callback ) ): """Get list of user's scheduled tasks as a table with columns: id, interval, at (hour:minute/minute past), status (enabled/disabled), command. Note: This script provides an overview of all tasks. Once a task id is known and some specific data is required it's more convenient to get it using `pa schedule get` command instead of parsing the table. """ logger = get_logger(set_info=True) headers = "id", "interval", "at", "status", "command" attrs = "task_id", "interval", "printable_time", "enabled", "command" def stringify_values(task, attr): value = getattr(task, attr) if attr == "enabled": value = "enabled" if value else "disabled" return value table = [[stringify_values(task, attr) for attr in attrs] for task in TaskList().tasks] msg = tabulate(table, headers, tablefmt=tablefmt) if table else snakesay("No scheduled tasks") logger.info(msg)
def create( domain_name: str = typer.Option( "your-username.pythonanywhere.com", "-d", "--domain", help="Domain name, eg www.mydomain.com", ), python_version: str = typer.Option( "3.6", "-p", "--python-version", help="Python version, eg '3.8'", ), nuke: bool = typer.Option( False, help= "*Irrevocably* delete any existing web app config on this domain. Irrevocably.", ), ): domain = ensure_domain(domain_name) project = Project(domain, python_version) project.sanity_checks(nuke=nuke) project.virtualenv.create(nuke=nuke) project.create_webapp(nuke=nuke) project.add_static_file_mappings() project.webapp.reload() typer.echo( snakesay( f"All done! Your site is now live at https://{domain}. " f"Your web app config screen is here: https://www.pythonanywhere.com/user/{getpass.getuser().lower()}" f"/webapps/{domain.replace('.', '_')}"))
def pip_install(self, packages): print( snakesay( 'Pip installing {packages} (this may take a couple of minutes)' .format(packages=packages))) commands = [str(self.path / 'bin/pip'), 'install'] + packages.split() subprocess.check_call(commands)
def main(domain_name, certificate_file, private_key_file, suppress_reload): if not os.path.exists(certificate_file): print("Could not find certificate file {certificate_file}".format( certificate_file=certificate_file)) sys.exit(1) with open(certificate_file, "r") as f: certificate = f.read() if not os.path.exists(private_key_file): print("Could not find private key file {private_key_file}".format( private_key_file=private_key_file)) sys.exit(1) with open(private_key_file, "r") as f: private_key = f.read() webapp = Webapp(domain_name) webapp.set_ssl(certificate, private_key) if not suppress_reload: webapp.reload() ssl_details = webapp.get_ssl_info() print( snakesay("That's all set up now :-)\n" "Your new certificate will expire on {expiry:%d %B %Y},\n" "so shortly before then you should renew it\n" "and install the new certificate.".format( expiry=ssl_details["not_after"])))
def update_settings_file(self): print(snakesay('Updating settings.py')) with self.settings_path.open() as f: settings = f.read() new_settings = settings.replace('ALLOWED_HOSTS = []', f'ALLOWED_HOSTS = [{self.domain!r}]') new_django = version.parse( self.virtualenv.get_version("django")) >= version.parse("3.1") if re.search(r'^MEDIA_ROOT\s*=', settings, flags=re.MULTILINE) is None: new_settings += "\nMEDIA_URL = '/media/'" if re.search(r'^STATIC_ROOT\s*=', settings, flags=re.MULTILINE) is None: if new_django: new_settings += "\nSTATIC_ROOT = Path(BASE_DIR / 'static')" else: new_settings += "\nSTATIC_ROOT = os.path.join(BASE_DIR, 'static')" if re.search(r'^MEDIA_ROOT\s*=', settings, flags=re.MULTILINE) is None: if new_django: new_settings += "\nMEDIA_ROOT = Path(BASE_DIR / 'media')" else: new_settings += "\nMEDIA_ROOT = os.path.join(BASE_DIR, 'media')" with self.settings_path.open('w') as f: f.write(new_settings)
def create(self, python_version, virtualenv_path, project_path, nuke): print(snakesay("Creating web app via API")) if nuke: webapp_url = get_api_endpoint().format( username=getpass.getuser(), flavor="webapps") + self.domain + "/" call_api(webapp_url, "delete") post_url = get_api_endpoint().format(username=getpass.getuser(), flavor="webapps") patch_url = post_url + self.domain + "/" response = call_api(post_url, "post", data={ "domain_name": self.domain, "python_version": PYTHON_VERSIONS[python_version] }) if not response.ok or response.json().get("status") == "ERROR": raise Exception( "POST to create webapp via API failed, got {response}:{response_text}" .format(response=response, response_text=response.text)) response = call_api(patch_url, "patch", data={ "virtualenv_path": virtualenv_path, "source_directory": project_path }) if not response.ok: raise Exception( "PATCH to set virtualenv path and source directory via API failed," "got {response}:{response_text}".format( response=response, response_text=response.text))
def main(domain, django_version, python_version, nuke): if domain == 'your-username.pythonanywhere.com': username = getpass.getuser().lower() pa_domain = os.environ.get('PYTHONANYWHERE_DOMAIN', 'pythonanywhere.com') domain = '{username}.{pa_domain}'.format(username=username, pa_domain=pa_domain) project = DjangoProject(domain, python_version) project.sanity_checks(nuke=nuke) project.create_virtualenv(django_version, nuke=nuke) project.run_startproject(nuke=nuke) project.find_django_files() project.update_settings_file() project.run_collectstatic() project.create_webapp(nuke=nuke) project.add_static_file_mappings() project.update_wsgi_file() project.webapp.reload() print( snakesay('All done! Your site is now live at https://{domain}'.format( domain=domain)))
def run_migrate(self): print(snakesay('Running migrate database')) subprocess.check_call([ str(Path(self.virtualenv.path) / 'bin/python'), str(self.manage_py_path), 'migrate', ])
def main(*, task_id, **kwargs): logger = get_logger(set_info=True) task = get_task_from_id(task_id) print_snake = kwargs.pop("snake") print_only_values = kwargs.pop("no_spec") specs = ( {spec: getattr(task, spec) for spec in kwargs if kwargs[spec]} if any([val for val in kwargs.values()]) else {spec: getattr(task, spec) for spec in kwargs} ) # get user path instead of server path: if specs.get("logfile"): specs.update({"logfile": task.logfile.replace("/user/{}/files".format(task.user), "")}) intro = "Task {} specs: ".format(task_id) if print_only_values: specs = "\n".join([str(val) for val in specs.values()]) logger.info(specs) elif print_snake: specs = ["<{}>: {}".format(spec, value) for spec, value in specs.items()] specs.sort() logger.info(snakesay(intro + ", ".join(specs))) else: table = [[spec, val] for spec, val in specs.items()] table.sort(key=lambda x: x[0]) logger.info(intro) logger.info(tabulate(table, tablefmt="simple"))
def main(*, task_id, **kwargs): logger = get_logger() if kwargs.pop("hourly"): kwargs["interval"] = "hourly" if kwargs.pop("daily"): kwargs["hour"] = kwargs["hour"] if kwargs["hour"] else datetime.now( ).hour kwargs["interval"] = "daily" def parse_opts(*opts): candidates = [key for key in opts if kwargs.pop(key, None)] return candidates[0] if candidates else None if not parse_opts("quiet"): logger.setLevel(logging.INFO) porcelain = parse_opts("porcelain") enable_opt = parse_opts("toggle_enabled", "disable", "enable") task = get_task_from_id(task_id) params = {key: val for key, val in kwargs.items() if val} if enable_opt: enabled = { "toggle_enabled": not task.enabled, "disable": False, "enable": True }[enable_opt] params.update({"enabled": enabled}) try: task.update_schedule(params, porcelain=porcelain) except Exception as e: logger.warning(snakesay(str(e)))
def pip_install(self, packages): print( snakesay( f"Pip installing {packages} (this may take a couple of minutes)" )) commands = [str(self.path / "bin/pip"), "install"] + packages.split() subprocess.check_call(commands)
def create(self, python_version, virtualenv_path, project_path, nuke): print(snakesay('Creating web app via API')) if nuke: webapp_url = get_api_endpoint().format(username=getpass.getuser()) + self.domain + '/' call_api(webapp_url, 'delete') post_url = get_api_endpoint().format(username=getpass.getuser()) patch_url = post_url + self.domain + '/' response = call_api(post_url, 'post', data={ 'domain_name': self.domain, 'python_version': PYTHON_VERSIONS[python_version]}, ) if not response.ok or response.json().get('status') == 'ERROR': raise Exception( 'POST to create webapp via API failed, got {response}:{response_text}'.format( response=response, response_text=response.text, ) ) response = call_api( patch_url, 'patch', data={'virtualenv_path': virtualenv_path, 'source_directory': project_path} ) if not response.ok: raise Exception( "PATCH to set virtualenv path and source directory via API failed," "got {response}:{response_text}".format( response=response, response_text=response.text, ) )
def start_bash(self): print( snakesay( 'Starting Bash shell with activated virtualenv in project directory. Press Ctrl+D to exit.' )) unique_id = str(uuid.uuid4()) launch_bash_in_virtualenv(self.virtualenv.path, unique_id, self.project_path)
def update_wsgi_file(self): print( snakesay('Updating wsgi file at {wsgi_file_path}'.format( wsgi_file_path=self.wsgi_file_path))) template = (Path(__file__).parent / 'wsgi_file_template.py').open().read() with self.wsgi_file_path.open('w') as f: f.write(template.format(project=self))
def run_collectstatic(self): print(snakesay('Running collectstatic')) subprocess.check_call([ str(Path(self.virtualenv.path) / 'bin/python'), str(self.manage_py_path), 'collectstatic', '--noinput', ])
def add_default_static_files_mappings(self, project_path): print(snakesay("Adding static files mappings for /static/ and /media/")) url = ( get_api_endpoint().format(username=getpass.getuser(), flavor="webapps") + self.domain + "/static_files/" ) call_api(url, "post", json=dict(url="/static/", path=str(Path(project_path) / "static"))) call_api(url, "post", json=dict(url="/media/", path=str(Path(project_path) / "media")))
def test_two_lines(): two_lines = 'a' * 81 message = snakesay(two_lines) print(message) assert '/ ' in message assert '| aaaaa' in message assert 'aaaaa |' in message assert '\\ ' in message
def create(self, nuke): print( snakesay(f"Creating virtualenv with Python{self.python_version}")) command = f"mkvirtualenv --python=python{self.python_version} {self.domain}" if nuke: command = f"rmvirtualenv {self.domain} && {command}" subprocess.check_call( ["bash", "-c", f"source virtualenvwrapper.sh && {command}"]) return self
def run_startproject(self, nuke): print(snakesay('Starting Django project')) if nuke and self.project_path.exists(): shutil.rmtree(self.project_path) self.project_path.mkdir() subprocess.check_call([ Path(self.virtualenv.path) / 'bin/django-admin.py', 'startproject', 'mysite', self.project_path ])