class Check(WithDatabase, InstallUninstall): name = 'check' description = N_("run a distribution's test") def _inun(self, pdir): logger.info(_("checking extension")) upenv = self.get_psql_env() logger.debug("additional env: %s", upenv) env = os.environ.copy() env.update(upenv) cmd = ['installcheck'] if 'PGDATABASE' in upenv: cmd.append("CONTRIB_TESTDB=" + env['PGDATABASE']) try: self.run_make(cmd, dir=pdir, env=env) except PgxnClientException: # if the test failed, copy locally the regression result for ext in ('out', 'diffs'): fn = os.path.join(pdir, 'regression.' + ext) if os.path.exists(fn): dest = './regression.' + ext if not os.path.exists(dest) or not os.path.samefile( fn, dest ): logger.info(_('copying regression.%s'), ext) shutil.copy(fn, dest) raise
class Download(WithSpecUrl, Command): name = 'download' description = N_("download a distribution from the network") @classmethod def customize_parser(self, parser, subparsers, **kwargs): subp = super(Download, self).customize_parser(parser, subparsers, **kwargs) subp.add_argument('--target', metavar='PATH', default='.', help=_('Target directory and/or filename to save')) return subp def run(self): spec = self.get_spec() assert not spec.is_local() if spec.is_url(): return self._run_url(spec) data = self.get_meta(spec) try: chk = data['sha1'] except KeyError: raise PgxnClientException( "sha1 missing from the distribution meta") with self.api.download(data['name'], SemVer(data['version'])) as fin: fn = network.download(fin, self.opts.target) self.verify_checksum(fn, chk) return fn def _run_url(self, spec): with network.get_file(spec.url) as fin: fn = network.download(fin, self.opts.target) return fn def verify_checksum(self, fn, chk): """Verify that a downloaded file has the expected sha1.""" sha = sha1() logger.debug(_("checking sha1 of '%s'"), fn) f = open(fn, "rb") try: while 1: data = f.read(8192) if not data: break sha.update(data) finally: f.close() sha = sha.hexdigest() if sha != chk: os.unlink(fn) logger.error(_("file %s has sha1 %s instead of %s"), fn, sha, chk) raise BadChecksum(_("bad sha1 in downloaded file"))
class Uninstall(SudoInstallUninstall): name = 'uninstall' description = N_("remove a distribution from the system") def _inun(self, pdir): logger.info(_("removing extension")) self.run_make('uninstall', dir=pdir, sudo=self.get_sudo_prog())
class Unload(LoadUnload): name = 'unload' description = N_("unload a distribution's extensions from a database") def run(self): items = self._get_extensions() if not self.opts.extensions: items.reverse() for (name, sql) in items: self.unload_ext(name, sql) def unload_ext(self, name, sqlfile): logger.debug(_("unloading extension '%s' with file: %s"), name, sqlfile) if sqlfile and not sqlfile.endswith('.sql'): logger.info( _("the specified file '%s' doesn't seem SQL:" " assuming '%s' is not a PostgreSQL extension"), sqlfile, name, ) return pgver = self.get_pg_version() if pgver >= (9, 1, 0): if self.is_extension(name): self.drop_extension(name) return else: self.confirm( _("""\ The extension '%s' doesn't contain a control file: will look for an SQL script to unload the objects. Do you want to continue?""") % name) if not sqlfile: sqlfile = name + '.sql' tmp = os.path.split(sqlfile) sqlfile = os.path.join(tmp[0], 'uninstall_' + tmp[1]) fn = self.find_sql_file(name, sqlfile) self.confirm( _("""\ In order to unload the extension '%s' looks like you will have to load the file '%s'. Do you want to execute it?""") % (name, fn)) data = self.patch_for_schema(fn) self.load_sql(data=data) def drop_extension(self, name): # TODO: cascade cmd = "DROP EXTENSION %s;" % Identifier(name) self.load_sql(data=cmd)
class Install(SudoInstallUninstall): name = 'install' description = N_("download, build and install a distribution") def _inun(self, pdir): logger.info(_("building extension")) self.run_make('all', dir=pdir) logger.info(_("installing extension")) self.run_make('install', dir=pdir, sudo=self.get_sudo_prog())
class Mirror(Command): name = 'mirror' description = N_("return information about the available mirrors") @classmethod def customize_parser(self, parser, subparsers, **kwargs): subp = super(Mirror, self).customize_parser( parser, subparsers, **kwargs ) subp.add_argument( 'uri', nargs='?', metavar="URI", help=_( "return detailed info about this mirror." " If not specified return a list of mirror URIs" ), ) subp.add_argument( '--detailed', action="store_true", help=_("return full details for each mirror"), ) return subp def run(self): data = self.api.mirrors() if self.opts.uri: detailed = True data = [d for d in data if d['uri'] == self.opts.uri] if not data: raise ResourceNotFound( _('mirror not found: %s') % self.opts.uri ) else: detailed = self.opts.detailed for i, d in enumerate(data): if not detailed: emit(d['uri']) else: for k in u""" uri frequency location bandwidth organization email timezone src rsync notes """.split(): emit("%s: %s" % (k, d.get(k, ''))) emit()
class Help(Command): name = 'help' description = N_("display help and other program information") @classmethod def customize_parser(self, parser, subparsers, **kwargs): subp = super(Help, self).customize_parser(parser, subparsers, **kwargs) g = subp.add_mutually_exclusive_group() g.add_argument( '--all', action="store_true", help=_("list all the available commands"), ) g.add_argument( '--libexec', action="store_true", help=_("print the location of the scripts directory"), ) g.add_argument( 'command', metavar='CMD', nargs='?', help=_("the command to get help about"), ) # To print the basic help self._parser = parser return subp def run(self): if self.opts.command: from pgxnclient.cli import main main([self.opts.command, '--help']) elif self.opts.all: self.print_all_commands() elif self.opts.libexec: self.print_libexec() else: self._parser.print_help() def print_all_commands(self): cmds = self.find_all_commands() title = _("Available PGXN Client commands") emit(title) emit("-" * len(title)) for cmd in cmds: emit(" " + cmd) def find_all_commands(self): rv = [] path = os.environ.get('PATH', '').split(os.pathsep) path[0:0] = get_scripts_dirs() for p in path: try: files = os.listdir(p) except OSError: # Dir missing, or not readable continue for fn in files: if fn.startswith('pgxn-'): rv.append(fn[5:]) rv.sort() return rv def print_libexec(self): emit(get_public_scripts_dir())
class Load(LoadUnload): name = 'load' description = N_("load a distribution's extensions into a database") def run(self): items = self._get_extensions() for (name, sql) in items: self.load_ext(name, sql) def load_ext(self, name, sqlfile): logger.debug(_("loading extension '%s' with file: %s"), name, sqlfile) if sqlfile and not sqlfile.endswith('.sql'): logger.info( _( "the specified file '%s' doesn't seem SQL:" " assuming '%s' is not a PostgreSQL extension" ), sqlfile, name, ) return pgver = self.get_pg_version() if pgver >= (9, 1, 0): if self.is_extension(name): self.create_extension(name) return else: self.confirm( _( """\ The extension '%s' doesn't contain a control file: it will be installed as a loose set of objects. Do you want to continue?""" ) % name ) confirm = False if not sqlfile: sqlfile = name + '.sql' confirm = True fn = self.find_sql_file(name, sqlfile) if confirm: self.confirm( _( """\ The extension '%s' doesn't specify a SQL file. '%s' is probably the right one. Do you want to load it?""" ) % (name, fn) ) # TODO: is confirmation asked only once? Also, check for repetition # in unload. if self._is_loaded(fn): logger.info(_("file %s already loaded"), fn) else: data = self.patch_for_schema(fn) self.load_sql(data=data) self._register_loaded(fn) def create_extension(self, name): name = Identifier(name) schema = self.opts.schema cmd = ["CREATE EXTENSION", name] if schema: cmd.extend(["SCHEMA", schema]) cmd = " ".join(cmd) + ';' self.load_sql(data=cmd)
class Search(Command): name = 'search' description = N_("search in the available extensions") @classmethod def customize_parser(self, parser, subparsers, **kwargs): subp = super(Search, self).customize_parser(parser, subparsers, **kwargs) g = subp.add_mutually_exclusive_group() g.add_argument( '--docs', dest='where', action='store_const', const='docs', default='docs', help=_("search in documentation [default]"), ) g.add_argument( '--dist', dest='where', action='store_const', const="dists", help=_("search in distributions"), ) g.add_argument( '--ext', dest='where', action='store_const', const='extensions', help=_("search in extensions"), ) subp.add_argument('query', metavar='TERM', nargs='+', help=_("a string to search")) return subp def run(self): data = self.api.search(self.opts.where, self.opts.query) for hit in data['hits']: emit("%s %s" % (hit['dist'], hit['version'])) if 'excerpt' in hit: excerpt = self.clean_excerpt(hit['excerpt']) for line in textwrap.wrap(excerpt, 72): emit(" " + line) emit() def clean_excerpt(self, excerpt): """Clean up the excerpt returned in the json result for output.""" # replace ellipsis with three dots, as there's no chance # to have them printed on non-utf8 consoles. # Also, they suck obscenely on fixed-width output. excerpt = excerpt.replace('…', '...') # TODO: this apparently misses a few entities excerpt = saxutils.unescape(excerpt) excerpt = excerpt.replace('"', '"') # Convert numerical entities excerpt = re.sub(r'\&\#(\d+)\;', lambda c: six.unichr(int(c.group(1))), excerpt) # Hilight found terms # TODO: use proper highlight with escape chars? excerpt = excerpt.replace('<strong></strong>', '') excerpt = excerpt.replace('<strong>', '*') excerpt = excerpt.replace('</strong>', '*') return excerpt
class Info(WithSpec, Command): name = 'info' description = N_("print information about a distribution") @classmethod def customize_parser(self, parser, subparsers, **kwargs): subp = super(Info, self).customize_parser(parser, subparsers, **kwargs) g = subp.add_mutually_exclusive_group() g.add_argument( '--details', dest='what', action='store_const', const='details', default='details', help=_("show details about the distribution [default]"), ) g.add_argument( '--meta', dest='what', action='store_const', const='meta', help=_("show the distribution META.json"), ) g.add_argument( '--readme', dest='what', action='store_const', const='readme', help=_("show the distribution README"), ) g.add_argument( '--versions', dest='what', action='store_const', const='versions', help=_("show the list of available versions"), ) return subp def run(self): spec = self.get_spec() getattr(self, 'print_' + self.opts.what)(spec) def print_meta(self, spec): data = self._get_dist_data(spec.name) ver = self.get_best_version(data, spec, quiet=True) emit(self.api.meta(spec.name, ver, as_json=False)) def print_readme(self, spec): data = self._get_dist_data(spec.name) ver = self.get_best_version(data, spec, quiet=True) emit(self.api.readme(spec.name, ver)) def print_details(self, spec): data = self._get_dist_data(spec.name) ver = self.get_best_version(data, spec, quiet=True) data = self.api.meta(spec.name, ver) for k in u""" name abstract description maintainer license release_status version date sha1 """.split(): try: v = data[k] except KeyError: logger.warning(_("data key '%s' not found"), k) continue if isinstance(v, list): for vv in v: emit("%s: %s" % (k, vv)) elif isinstance(v, dict): for kk, vv in v.items(): emit("%s: %s: %s" % (k, kk, vv)) else: emit("%s: %s" % (k, v)) k = 'provides' for ext, dext in data[k].items(): emit("%s: %s: %s" % (k, ext, dext['version'])) k = 'prereqs' if k in data: for phase, rels in data[k].items(): for rel, pkgs in rels.items(): for pkg, ver in pkgs.items(): emit("%s: %s: %s %s" % (phase, rel, pkg, ver)) def print_versions(self, spec): data = self._get_dist_data(spec.name) name = data['name'] vs = [(SemVer(d['version']), s) for s, ds in data['releases'].items() for d in ds] vs = [(v, s) for v, s in vs if spec.accepted(v)] vs.sort(reverse=True) for v, s in vs: emit("%s %s %s" % (name, v, s)) def _get_dist_data(self, name): try: return self.api.dist(name) except NotFound as e: # maybe the user was looking for an extension instead? try: ext = self.api.ext(name) except NotFound: pass else: vs = ext.get('versions', {}) for extver, ds in vs.items(): for d in ds: if 'dist' not in d: continue dist = d['dist'] distver = d.get('version', 'unknown') logger.info( _("extension %s %s found in distribution %s %s"), name, extver, dist, distver, ) raise e