def publish(self, target_url, credentials=None, **extra): if not locate_executable('git'): self.fail('git executable not found; cannot deploy.') branch = self.detect_target_branch(target_url) with _temporary_folder(self.env) as path: ssh_command = None def git(args, **kwargs): kwargs['env'] = _patch_git_env(kwargs.pop('env', None), ssh_command) return Command(['git'] + args, cwd=path, **kwargs) for line in git(['init']).output: yield line ssh_command = self.update_git_config(path, target_url, branch, credentials) for line in git(['remote', 'update']).output: yield line if git(['checkout', '-q', branch], silent=True).wait() != 0: git(['checkout', '-qb', branch], silent=True).wait() self.link_artifacts(path) self.write_cname(path, target_url) for line in git(['add', '-f', '--all', '.']).output: yield line for line in git(['commit', '-qm', 'Synchronized build']).output: yield line for line in git(['push', 'origin', branch]).output: yield line
def process_svg_image( ctx, source_image, dst_filename, width=None, height=None, mode=None, ): if width is None and height is None: raise ValueError("Must specify at least one of width or height.") # Because Lektor doesn't have a generic find program (only imagemagick), # only *nix is supported for simplicity. Imagemagick will be used as the # fallback on Windows, but it won't work well... if os.name == 'nt': return process_image(ctx, source_image, dst_filename, width, height, mode, 85) inkscape = locate_executable('inkscape') cmdline = [inkscape, source_image] if width is not None: cmdline += ["-w", str(width)] if height is not None: cmdline += ["-h", str(height)] # FIXME: This will only work with inkscape 1.0+ cmdline += ["--export-filename", dst_filename] reporter.report_debug_info("inkscape cmd line", cmdline) portable_popen(cmdline).wait()
def merge_pot(self, from_filenames, to_filename): msgcat = locate_executable('msgcat') cmdline = [msgcat, "--use-first"] cmdline.extend(from_filenames) cmdline.extend(("-o", to_filename)) reporter.report_debug_info('msgcat cmd line', cmdline) portable_popen(cmdline).wait()
def merge_pot(self, from_filenames, to_filename): msgcat=locate_executable('msgcat') cmdline=[msgcat, "--use-first"] cmdline.extend(from_filenames) cmdline.extend(("-o", to_filename)) reporter.report_debug_info('msgcat cmd line', cmdline) portable_popen(cmdline).wait()
def parse_templates(self, to_filename): pybabel=locate_executable('pybabel') if pybabel is None: pybabel="/usr/bin/pybabel" cmdline=[pybabel, 'extract', '-F', 'babel.cfg', "-o", to_filename, "./"] reporter.report_debug_info('pybabel cmd line', cmdline) portable_popen(cmdline).wait()
def publish(self, target_url, credentials=None, **extra): if not locate_executable("git"): self.fail("git executable not found; cannot deploy.") branch = self.detect_target_branch(target_url) with _temporary_folder(self.env) as path: ssh_command = None def git(args, **kwargs): kwargs["env"] = _patch_git_env(kwargs.pop("env", None), ssh_command) return Command(["git"] + args, cwd=path, **kwargs) for line in git(["init"]).output: yield line ssh_command = self.update_git_config(path, target_url, branch, credentials) for line in git(["remote", "update"]).output: yield line if git(["checkout", "-q", branch], silent=True).wait() != 0: git(["checkout", "-qb", branch], silent=True).wait() self.link_artifacts(path) self.write_cname(path, target_url) for line in git(["add", "-f", "--all", "."]).output: yield line for line in git(["commit", "-qm", "Synchronized build"]).output: yield line for line in git(["push", "origin", branch]).output: yield line
def publish(self, target_url, credentials=None, **extra): if not locate_executable("git"): self.fail("git executable not found; cannot deploy.") # When pushing to the username.github.io repo we need to push to # master, otherwise to gh-pages if target_url.host + ".github.io" == target_url.path.strip("/"): branch = "master" else: branch = "gh-pages" with _temporary_folder(self.env) as path: ssh_command = None def git(args, **kwargs): kwargs["env"] = _patch_git_env(kwargs.pop("env", None), ssh_command) return Command(["git"] + args, cwd=path, **kwargs).proc() for line in git(["init"]): yield line ssh_command = self.update_git_config(path, target_url, branch, credentials) for line in git(["remote", "update"]): yield line if git(["checkout", "-q", branch], silent=True).wait() != 0: git(["checkout", "-qb", branch], silent=True).wait() self.link_artifacts(path) self.write_cname(path, target_url) for line in git(["add", "-f", "--all", "."]): yield line for line in git(["commit", "-qm", "Synchronized build"]): yield line for line in git(["push", "origin", branch]): yield line
def _msg_init(self): """Generates the first <language>.po file""" msginit = locate_executable('msginit') cmdline = [ msginit, "-i", "contents.pot", "-l", self.language, "-o", self.FILENAME_PATTERN % self.language, "--no-translator" ] reporter.report_debug_info('msginit cmd line', cmdline) portable_popen(cmdline, cwd=self.i18npath).wait()
def _msg_merge(self): """Merges an existing <language>.po file with .pot file""" msgmerge = locate_executable('msgmerge') cmdline = [ msgmerge, self.FILENAME_PATTERN % self.language, "contents.pot", "-U", "-N", "--backup=simple" ] reporter.report_debug_info('msgmerge cmd line', cmdline) portable_popen(cmdline, cwd=self.i18npath).wait()
def _msg_fmt(self, locale_dirname): """Compile an existing <language>.po file into a .mo file""" msgfmt = locate_executable('msgfmt') cmdline = [ msgfmt, self.FILENAME_PATTERN % self.language, "-o", join(locale_dirname, "contents.mo") ] reporter.report_debug_info('msgfmt cmd line', cmdline) portable_popen(cmdline, cwd=self.i18npath).wait()
def parse_templates(self, to_filename): pybabel = locate_executable('pybabel') cmdline = [ pybabel, 'extract', '-F', 'babel.cfg', "-o", to_filename, "./" ] reporter.report_debug_info('pybabel cmd line', cmdline) try: portable_popen(cmdline).wait() except TypeError as err: traceback.print_exc(file=sys.stderr) print(err)
def run_package_manager(self, *args): # Use yarn over npm if it's availabe and there is a yarn lockfile has_yarn_lockfile = os.path.exists( self.get_webpack_folder('yarn.lock')) pkg_manager = 'npm' if locate_executable('yarn') is not None and has_yarn_lockfile: pkg_manager = 'yarn' reporter.report_generic('Running {} {}'.format(pkg_manager, " ".join(args))) return portable_popen([pkg_manager] + list(args), cwd=self.get_webpack_folder())
def install_node_dependencies(self): webpack_root = os.path.join(self.env.root_path, 'webpack') # Use yarn over npm if it's availabe and there is a yarn lockfile has_yarn_lockfile = os.path.exists( os.path.join(webpack_root, 'yarn.lock')) pkg_manager = 'npm' if locate_executable('yarn') is not None and has_yarn_lockfile: pkg_manager = 'yarn' reporter.report_generic('Running {} install'.format(pkg_manager)) portable_popen([pkg_manager, 'install'], cwd=webpack_root).wait()
def get_video_info(filename): """Read video information using ffprobe if available. Returns a dict with: width, height and duration. """ ffprobe = locate_executable("ffprobe") if ffprobe is None: raise RuntimeError("Failed to locate ffprobe") proc = portable_popen( [ ffprobe, "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", filename, ], stdout=subprocess.PIPE, ) stdout, _ = proc.communicate() if proc.returncode != 0: raise RuntimeError("ffprobe exited with code %d" % proc.returncode) ffprobe_data = json.loads(stdout.decode("utf8")) info = { "width": None, "height": None, "duration": None, } # Try to extract total video duration try: info["duration"] = timedelta( seconds=float(ffprobe_data["format"]["duration"])) except (KeyError, TypeError, ValueError): pass # Try to extract width and height from the first found video stream for stream in ffprobe_data["streams"]: if stream["codec_type"] != "video": continue info["width"] = int(stream["width"]) info["height"] = int(stream["height"]) # We currently don't bother with multiple video streams break return info
def get_video_info(filename): """Read video information using ffprobe if available. Returns a dict with: width, height and duration. """ ffprobe = locate_executable('ffprobe') if ffprobe is None: raise RuntimeError('Failed to locate ffprobe') proc = portable_popen([ ffprobe, '-v', 'quiet', '-print_format', 'json', '-show_format', '-show_streams', filename, ], stdout=subprocess.PIPE) stdout, _ = proc.communicate() if proc.returncode != 0: raise RuntimeError('ffprobe exited with code %d' % proc.returncode) ffprobe_data = json.loads(stdout.decode("utf8")) info = { 'width': None, 'height': None, 'duration': None, } # Try to extract total video duration try: info['duration'] = timedelta( seconds=float(ffprobe_data['format']['duration'])) except (KeyError, TypeError, ValueError): pass # Try to extract width and height from the first found video stream for stream in ffprobe_data['streams']: if stream['codec_type'] != 'video': continue info['width'] = int(stream['width']) info['height'] = int(stream['height']) # We currently don't bother with multiple video streams break return info
def find_imagemagick(im=None): """Finds imagemagick and returns the path to it.""" # If it's provided explicitly and it's valid, we go with that one. if im is not None and os.path.isfile(im): return im # On windows, imagemagick was renamed to magick, because # convert is system utility for fs manipulation. imagemagick_exe = "convert" if os.name != "nt" else "magick" rv = locate_executable(imagemagick_exe) if rv is not None: return rv # Give up. raise RuntimeError("Could not locate imagemagick.")
def find_imagemagick(im=None): """Finds imagemagick and returns the path to it.""" # If it's provided explicitly and it's valid, we go with that one. if im is not None and os.path.isfile(im): return im # If we have a shipped imagemagick, then we used this one. if BUNDLE_BIN_PATH is not None: executable = os.path.join(BUNDLE_BIN_PATH, 'convert') if os.name == 'nt': executable += '.exe' if os.path.isfile(executable): return executable # If we're not on windows, we locate the executable like we would # do normally. if os.name != 'nt': rv = locate_executable('convert') if rv is not None: return rv # On windows, we only scan the program files for an image magick # installation, because this is where this usually goes. We do # this because the convert executable is otherwise the system # one which can convert file systems and stuff like this. else: for key in 'ProgramFiles', 'ProgramW6432', 'ProgramFiles(x86)': value = os.environ.get(key) if not value: continue try: for filename in os.listdir(value): if filename.lower().startswith('imagemagick-'): exe = os.path.join(value, filename, 'convert.exe') if os.path.isfile(exe): return exe except OSError: continue # Give up. raise RuntimeError('Could not locate imagemagick.')
def get_default_author_email() -> Optional[str]: """Attempt to guess an email address for the current user. May return an empty string if not reasonable guess can be made. """ git = locate_executable("git") if git: proc = run((git, "config", "user.email"), stdout=PIPE, errors="strict", check=False) if proc.returncode == 0: return proc.stdout.strip() email = os.environ.get("EMAIL", "").strip() if email: return email # We could fall back to f"{getpass.getuser()}@{socket.getfqdn()}", # but it is probably better just to go with no default in that # case. return None
def publish(self, target_url, credentials=None, **extra): if not locate_executable('git'): self.fail('git executable not found; cannot deploy.') # When pushing to the username.github.io repo we need to push to # master, otherwise to gh-pages if target_url.host + '.github.io' == target_url.path.strip('/'): branch = 'master' else: branch = 'gh-pages' with _temporary_folder(self.env) as path: ssh_command = None def git(args, **kwargs): kwargs['env'] = _patch_git_env(kwargs.pop('env', None), ssh_command) return Command(['git'] + args, cwd=path, **kwargs) for line in git(['init']).output: yield line ssh_command = self.update_git_config(path, target_url, branch, credentials) for line in git(['remote', 'update']).output: yield line if git(['checkout', '-q', branch], silent=True).wait() != 0: git(['checkout', '-qb', branch], silent=True).wait() self.link_artifacts(path) self.write_cname(path, target_url) for line in git(['add', '-f', '--all', '.']).output: yield line for line in git(['commit', '-qm', 'Synchronized build']).output: yield line for line in git(['push', 'origin', branch]).output: yield line
def publish(self, target_url, credentials=None): if not locate_executable('git'): raise PublishError('git executable not found; cannot deploy.') # When pushing to the username.github.io repo we need to push to # master, otherwise to gh-pages if target_url.host + '.github.io' == target_url.path.strip('/'): branch = 'master' else: branch = 'gh-pages' with self.temporary_repo() as path: def git(args, capture=True): cmd = Command(['git'] + args, cwd=path, capture=capture) if capture: return cmd.safe_iter() return cmd for line in git(['init']): yield line self.update_git_config(path, target_url, branch, credentials) for line in git(['remote', 'update']): yield line if git(['checkout', '-q', branch], capture=False).wait() != 0: git(['checkout', '-qb', branch], capture=False).wait() self.link_artifacts(path) self.write_cname(path, target_url) for line in git(['add', '-f', '--all', '.']): yield line for line in git(['commit', '-qm', 'Synchronized build']): yield line for line in git(['push', 'origin', branch]): yield line
def find_imagemagick(im=None): """Finds imagemagick and returns the path to it.""" # If it's provided explicitly and it's valid, we go with that one. if im is not None and os.path.isfile(im): return im # On windows, imagemagick was renamed to magick, because # convert is system utility for fs manipulation. imagemagick_exe = 'convert' if os.name != 'nt' else 'magick' # If we have a shipped imagemagick, then we used this one. if BUNDLE_BIN_PATH is not None: executable = os.path.join(BUNDLE_BIN_PATH, imagemagick_exe) if os.name == 'nt': executable += '.exe' if os.path.isfile(executable): return executable rv = locate_executable(imagemagick_exe) if rv is not None: return rv # Give up. raise RuntimeError('Could not locate imagemagick.')
def publish(self, target_url, credentials=None, **extra): if not locate_executable('git'): self.fail('git executable not found; cannot deploy.') # When pushing to the username.github.io repo we need to push to # master, otherwise to gh-pages if target_url.host + '.github.io' == target_url.path.strip('/'): branch = 'master' else: branch = 'gh-pages' with _temporary_folder(self.env) as path: ssh_command = None def git(args, **kwargs): kwargs['env'] = _patch_git_env(kwargs.pop('env', None), ssh_command) return Command(['git'] + args, cwd=path, **kwargs).proc() for line in git(['init']): yield line ssh_command = self.update_git_config(path, target_url, branch, credentials) for line in git(['remote', 'update']): yield line if git(['checkout', '-q', branch], silent=True).wait() != 0: git(['checkout', '-qb', branch], silent=True).wait() self.link_artifacts(path) self.write_cname(path, target_url) for line in git(['add', '-f', '--all', '.']): yield line for line in git(['commit', '-qm', 'Synchronized build']): yield line for line in git(['push', 'origin', branch]): yield line
def test_default_author_from_pwd(mocker): pw_gecos = "Lektor Tester,,555-1212,," mocker.patch("pwd.getpwuid", spec=True, return_value=struct_passwd(pw_gecos=pw_gecos)) assert get_default_author() == "Lektor Tester" def test_default_author_from_username(mocker): mocker.patch("getpass.getuser", spec=True, return_value="lektortester") if os.name != "nt": mocker.patch("os.getuid", spec=True, return_value=-1) assert get_default_author() == "lektortester" @pytest.mark.skipif(locate_executable("git") is None, reason="git not installed") def test_default_author_email(git_config_file): git_config_file.write_text("[user]\n\temail = [email protected]\n") assert get_default_author_email() == "*****@*****.**" @pytest.mark.usefixtures("no_utils") def test_default_author_email_from_EMAIL(monkeypatch): email = "*****@*****.**" monkeypatch.setitem(os.environ, "EMAIL", email) assert get_default_author_email() == email @pytest.mark.usefixtures("no_utils") def test_default_author_email_no_default(monkeypatch):
import os from datetime import timedelta import pytest from lektor.utils import locate_executable from lektor.videotools import Dimensions from lektor.videotools import get_ffmpeg_quality from lektor.videotools import get_timecode has_ffmpeg = locate_executable("ffmpeg") has_ffprobe = locate_executable("ffprobe") require_ffmpeg = pytest.mark.skipif( not has_ffmpeg or not has_ffprobe, reason="requires ffmpeg and ffprobe in path") @pytest.mark.parametrize( "td, tc", [ (timedelta(), "00:00:00"), (timedelta(seconds=1), "00:00:01"), (timedelta(seconds=1.5), "00:00:01.5"), (timedelta(hours=8, minutes=15), "08:15:00"), (timedelta(hours=36, minutes=35, seconds=34.25), "36:35:34.25"), ], ) def test_get_timecode(td, tc): assert get_timecode(td) == tc
def make_video_thumbnail( ctx, source_video, source_url_path, seek, width=None, height=None, mode=ThumbnailMode.DEFAULT, upscale=None, quality=None, format=None, ): if mode != ThumbnailMode.FIT and (width is None or height is None): msg = '"%s" mode requires both `width` and `height` to be defined.' raise ValueError(msg % mode.label) if upscale is None: upscale = { ThumbnailMode.FIT: False, ThumbnailMode.CROP: True, ThumbnailMode.STRETCH: True, }[mode] if format is None: format = "jpg" if format not in THUMBNAIL_FORMATS: raise ValueError('Invalid thumbnail format "%s"' % format) if quality is not None and format != "jpg": raise ValueError( "The quality parameter is only supported for jpeg images") if seek < timedelta(0): raise ValueError("Seek must not be negative") ffmpeg = locate_executable("ffmpeg") if ffmpeg is None: raise RuntimeError("Failed to locate ffmpeg") info = get_video_info(source_video) source_dim = Dimensions(info["width"], info["height"]) resize_dim, crop_dim = source_dim.resize(width, height, mode, upscale) # Construct a filename suffix unique to the given parameters suffix = get_suffix(seek, width, height, mode=mode, quality=quality) dst_url_path = get_dependent_url(source_url_path, suffix, ext=".{}".format(format)) if quality is None and format == "jpg": quality = 95 def build_thumbnail_artifact(artifact): artifact.ensure_dir() vfilter = "thumbnail,scale={rw}:{rh},crop={tw}:{th}".format( rw=resize_dim.width, rh=resize_dim.height, tw=crop_dim.width, th=crop_dim.height, ) cmdline = [ ffmpeg, "-loglevel", "-8", "-ss", get_timecode(seek), # Input seeking since it's faster "-i", source_video, "-vf", vfilter, "-frames:v", "1", "-qscale:v", str(get_ffmpeg_quality(quality)), artifact.dst_filename, ] reporter.report_debug_info("ffmpeg cmd line", cmdline) proc = portable_popen(cmdline) if proc.wait() != 0: raise RuntimeError("ffmpeg exited with code {}".format( proc.returncode)) if not os.path.exists(artifact.dst_filename): msg = ("Unable to create video thumbnail for {!r}. Maybe the seek " "is outside of the video duration?") raise RuntimeError(msg.format(source_video)) ctx.sub_artifact(artifact_name=dst_url_path, sources=[source_video])(build_thumbnail_artifact) return Thumbnail(dst_url_path, crop_dim.width, crop_dim.height)
def parse_templates(self, to_filename): pybabel=locate_executable('pybabel') cmdline=[pybabel, 'extract', '-F', 'babel.cfg', "-o", to_filename, "./"] reporter.report_debug_info('pybabel cmd line', cmdline) portable_popen(cmdline).wait()
def _msg_init(self): """Generates the first <language>.po file""" msginit=locate_executable('msginit') cmdline=[msginit, "-i", "contents.pot", "-l", self.language, "-o", self.FILENAME_PATTERN%self.language, "--no-translator"] reporter.report_debug_info('msginit cmd line', cmdline) portable_popen(cmdline, cwd=self.i18npath).wait()
def _msg_merge(self): """Merges an existing <language>.po file with .pot file""" msgmerge=locate_executable('msgmerge') cmdline=[msgmerge, self.FILENAME_PATTERN%self.language, "contents.pot", "-U", "-N", "--backup=simple"] reporter.report_debug_info('msgmerge cmd line', cmdline) portable_popen(cmdline, cwd=self.i18npath).wait()
def _msg_fmt(self, locale_dirname): """Compile an existing <language>.po file into a .mo file""" msgfmt=locate_executable('msgfmt') cmdline=[msgfmt, self.FILENAME_PATTERN%self.language, "-o", join(locale_dirname,"contents.mo")] reporter.report_debug_info('msgfmt cmd line', cmdline) portable_popen(cmdline, cwd=self.i18npath).wait()
import os from datetime import timedelta import pytest from lektor.utils import locate_executable from lektor.videotools import Dimensions, get_ffmpeg_quality, get_timecode has_ffmpeg = locate_executable('ffmpeg') has_ffprobe = locate_executable('ffprobe') require_ffmpeg = pytest.mark.skipif( not has_ffmpeg or not has_ffprobe, reason='requires ffmpeg and ffprobe in path') @pytest.mark.parametrize("td, tc", [ (timedelta(), "00:00:00"), (timedelta(seconds=1), "00:00:01"), (timedelta(seconds=1.5), "00:00:01.5"), (timedelta(hours=8, minutes=15), "08:15:00"), (timedelta(hours=36, minutes=35, seconds=34.25), "36:35:34.25"), ]) def test_get_timecode(td, tc): assert get_timecode(td) == tc @pytest.mark.parametrize("in_quality, out_quality", [ (0, 31), (100, 2), ]) def test_get_ffmpeg_quality(in_quality, out_quality): assert get_ffmpeg_quality(in_quality) == out_quality