Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Added support for http/https urls as specs

  • Loading branch information...
commit 34e5c9ed9e13352d7df45322094541bcfdc4dcf1 1 parent 0cbff8d
@dvarrazzo authored
View
4 CHANGES
@@ -6,7 +6,9 @@ PGXN Client changes log
pgxnclient 1.2
==============
-- ``gmake`` if used in favour of ``make`` for platforms where the two are
+- Packages can be downloaded, installed, loaded specifying an URL
+ (ticket #15).
+- Use ``gmake`` in favour of ``make`` for platforms where the two are
distinct, such as BSD (ticket #14).
- Added ``--make`` option to select the make executable (ticket #16).
View
38 docs/usage.rst
@@ -65,6 +65,9 @@ directory containing a distribution: in this case the specification should
contain at least a path separator to disambiguate it from a distribution name,
for instance ``pgxn install ./foo.zip``.
+A few commands also allow specifying a package with an URL. Currently the
+schemas ``http://`` and ``https://`` are supported.
+
.. _install:
@@ -85,7 +88,8 @@ Usage:
The program takes a `package specification`_ identifying the distribution to
work with. The download phase is skipped if the distribution specification
-refers to a local directory or package.
+refers to a local directory or package. The package may be specified with an
+URL.
Note that the built extension is not loaded in any database: use the command
`load`_ for this purpose.
@@ -144,8 +148,9 @@ Usage:
*SPEC*
The command takes a `package specification`_ identifying the distribution to
-work with, which can also be a local file or directory. The distribution is
-unpacked if required and the ``installcheck`` make target is run.
+work with, which can also be a local file or directory or an URL. The
+distribution is unpacked if required and the ``installcheck`` make target is
+run.
.. note::
The command doesn't run ``make all`` before ``installcheck``: if any file
@@ -224,11 +229,11 @@ Usage:
*SPEC* [*EXT* [*EXT* ...]]
The distribution is specified according to the `package specification`_ and
-can refer to a local directory or file. No consistency check is performed
-between the packages specified in the ``install`` and ``load`` command: the
-specifications should refer to compatible packages. The specified distribution
-is only used to read the metadata: only installed files are actually used to
-issue database commands.
+can refer to a local directory or file or to an URL. No consistency check is
+performed between the packages specified in the ``install`` and ``load``
+command: the specifications should refer to compatible packages. The specified
+distribution is only used to read the metadata: only installed files are
+actually used to issue database commands.
The database to install into can be specified using options
``-d``/``--dbname``, ``-h``/``--host``, ``-p``/``--port``,
@@ -334,11 +339,12 @@ Usage:
[--target *PATH*]
*SPEC*
-The distribution is specified according to the `package specification`_. The
-file is saved in the current directory with name usually
-:samp:`{distribution}-{version}.zip`. If a file with the same name exists, a
-suffix ``-1``, ``-2`` etc. is added to the name, before the extension. A
-different directory or name can be specified using the ``--target`` option.
+The distribution is specified according to the `package specification`_ and
+can be represented by an URL. The file is saved in the current directory with
+name usually :samp:`{distribution}-{version}.zip`. If a file with the same
+name exists, a suffix ``-1``, ``-2`` etc. is added to the name, before the
+extension. A different directory or name can be specified using the
+``--target`` option.
.. _pgxn-search:
@@ -419,9 +425,9 @@ Usage:
[--details | --meta | --readme | --versions]
*SPEC*
-The distribution is specified according to the `package specification`_.
-The command output is a list of values obtained by the distribution's
-``META.json`` file, for example:
+The distribution is specified according to the `package specification`_. It
+cannot be a local dir or file nor an URL. The command output is a list of
+values obtained by the distribution's ``META.json`` file, for example:
.. code-block:: console
View
43 pgxnclient/commands/__init__.py
@@ -20,10 +20,12 @@
from pgxnclient.utils import load_json, argparse, find_executable
from pgxnclient import __version__
+from pgxnclient import network
from pgxnclient import Spec, SemVer
from pgxnclient.api import Api
from pgxnclient.i18n import _, gettext
from pgxnclient.errors import NotFound, PgxnClientException, ProcessError, ResourceNotFound, UserAbort
+from pgxnclient.utils.temp import temp_dir
logger = logging.getLogger('pgxnclient.commands')
@@ -265,7 +267,7 @@ def customize_parser(self, parser, subparsers,
return subp
- def get_spec(self, _can_be_local=False):
+ def get_spec(self, _can_be_local=False, _can_be_url=False):
"""
Return the package specification requested.
@@ -283,6 +285,10 @@ def get_spec(self, _can_be_local=False):
raise PgxnClientException(
_("you cannot use a local resource with this command"))
+ if not _can_be_url and spec.is_url():
+ raise PgxnClientException(
+ _("you cannot use an url with this command"))
+
return spec
def get_best_version(self, data, spec, quiet=False):
@@ -359,7 +365,7 @@ def get_meta(self, spec):
Return the object obtained parsing the JSON.
"""
- if not spec.is_local():
+ if spec.is_name():
# Get the metadata from the API
try:
data = self.api.dist(spec.name)
@@ -387,6 +393,14 @@ def get_meta(self, spec):
# Get the metadata from a zip file
return get_meta_from_zip(spec.filename)
+ elif spec.is_url():
+ with network.get_file(spec.url) as fin:
+ with temp_dir() as dir:
+ fn = network.download(fin, dir)
+ return get_meta_from_zip(fn)
+
+ else:
+ assert False
class WithSpecLocal(WithSpec):
"""
@@ -405,8 +419,29 @@ def customize_parser(self, parser, subparsers, epilog=None, **kwargs):
return subp
- def get_spec(self):
- return super(WithSpecLocal, self).get_spec(_can_be_local=True)
+ def get_spec(self, **kwargs):
+ kwargs['_can_be_local'] = True
+ return super(WithSpecLocal, self).get_spec(**kwargs)
+
+class WithSpecUrl(WithSpec):
+ """
+ Mixin to implement commands that can also refer to a URL.
+ """
+
+ @classmethod
+ def customize_parser(self, parser, subparsers, epilog=None, **kwargs):
+ epilog = _("""
+SPEC may also be an url specifying a protocol such as 'http://' or 'https://'.
+""") + (epilog or "")
+
+ subp = super(WithSpecUrl, self).customize_parser(
+ parser, subparsers, epilog=epilog, **kwargs)
+
+ return subp
+
+ def get_spec(self, **kwargs):
+ kwargs['_can_be_url'] = True
+ return super(WithSpecUrl, self).get_spec(**kwargs)
class WithPgConfig(object):
View
23 pgxnclient/commands/install.py
@@ -22,7 +22,7 @@
from pgxnclient.utils import sha1, b
from pgxnclient.errors import BadChecksum, PgxnClientException, InsufficientPrivileges
from pgxnclient.commands import Command, WithDatabase, WithMake, WithPgConfig
-from pgxnclient.commands import WithSpec, WithSpecLocal, WithSudo
+from pgxnclient.commands import WithSpecUrl, WithSpecLocal, WithSudo
from pgxnclient.utils.zip import unpack
from pgxnclient.utils.temp import temp_dir
from pgxnclient.utils.strings import Identifier
@@ -30,7 +30,7 @@
logger = logging.getLogger('pgxnclient.commands')
-class Download(WithSpec, Command):
+class Download(WithSpecUrl, Command):
name = 'download'
description = N_("download a distribution from the network")
@@ -45,6 +45,11 @@ def customize_parser(self, parser, subparsers, **kwargs):
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:
@@ -59,6 +64,12 @@ def run(self):
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()
@@ -80,7 +91,7 @@ def verify_checksum(self, fn, chk):
raise BadChecksum(_("bad sha1 in downloaded file"))
-class InstallUninstall(WithMake, WithSpecLocal, Command):
+class InstallUninstall(WithMake, WithSpecUrl, WithSpecLocal, Command):
"""
Base class to implement the ``install`` and ``uninstall`` commands.
"""
@@ -94,10 +105,12 @@ def _run(self, dir):
pdir = os.path.abspath(spec.dirname)
elif spec.is_file():
pdir = unpack(spec.filename, dir)
- else: # download
+ elif not spec.is_local():
self.opts.target = dir
fn = Download(self.opts).run()
pdir = unpack(fn, dir)
+ else:
+ assert False
self.maybe_run_configure(pdir)
@@ -213,7 +226,7 @@ def _inun(self, pdir):
raise
-class LoadUnload(WithPgConfig, WithDatabase, WithSpecLocal, Command):
+class LoadUnload(WithPgConfig, WithDatabase, WithSpecUrl, WithSpecLocal, Command):
"""
Base class to implement the ``load`` and ``unload`` commands.
"""
View
17 pgxnclient/spec.py
@@ -33,14 +33,18 @@ class Spec(object):
'stable': STABLE, }
def __init__(self, name=None, op=None, ver=None,
- dirname=None, filename=None):
+ dirname=None, filename=None, url=None):
self.name = name and name.lower()
self.op = op
self.ver = ver
- # point to local files
+ # point to local files or specific resources
self.dirname = dirname
self.filename = filename
+ self.url = url
+
+ def is_name(self):
+ return self.name is not None
def is_dir(self):
return self.dirname is not None
@@ -48,11 +52,14 @@ def is_dir(self):
def is_file(self):
return self.filename is not None
+ def is_url(self):
+ return self.url is not None
+
def is_local(self):
return self.is_dir() or self.is_file()
def __str__(self):
- name = self.name or self.filename or self.dirname or "???"
+ name = self.name or self.filename or self.dirname or self.url or "???"
if self.op is None:
return name
else:
@@ -64,6 +71,10 @@ def parse(self, spec):
Raise BadSpecError if couldn't parse.
"""
+ # TODO: handle file:// too
+ if spec.startswith('http://') or spec.startswith('https://'):
+ return Spec(url=spec)
+
if os.sep in spec:
# This is a local thing, let's see what
if os.path.isdir(spec):
View
50 pgxnclient/tests/test_commands.py
@@ -149,6 +149,20 @@ def test_download_testing(self, mock):
ifunlink(fn)
@patch('pgxnclient.network.get_file')
+ def test_download_url(self, mock):
+ mock.side_effect = fake_get_file
+
+ fn = 'foobar-0.43.2b1.zip'
+ self.assert_(not os.path.exists(fn))
+
+ from pgxnclient.cli import main
+ try:
+ main(['download', 'http://api.pgxn.org/dist/foobar/0.43.2b1/foobar-0.43.2b1.zip'])
+ self.assert_(os.path.exists(fn))
+ finally:
+ ifunlink(fn)
+
+ @patch('pgxnclient.network.get_file')
def test_download_ext(self, mock):
mock.side_effect = fake_get_file
@@ -366,6 +380,17 @@ def test_install_local(self):
self.assertCallArgs([self.make], self.mock_popen.call_args_list[0][0][0][:1])
self.assertCallArgs([self.make], self.mock_popen.call_args_list[1][0][0][:1])
+ def test_install_url(self):
+ self.mock_pgconfig.side_effect = fake_pg_config(
+ libdir=os.environ['HOME'], bindir='/')
+
+ from pgxnclient.cli import main
+ main(['install', 'http://api.pgxn.org/dist/foobar/0.42.1/foobar-0.42.1.zip'])
+
+ self.assertEquals(self.mock_popen.call_count, 2)
+ self.assertCallArgs([self.make], self.mock_popen.call_args_list[0][0][0][:1])
+ self.assertCallArgs([self.make], self.mock_popen.call_args_list[1][0][0][:1])
+
def test_install_fails(self):
self.mock_popen.return_value.returncode = 1
self.mock_pgconfig.side_effect = fake_pg_config(
@@ -477,6 +502,13 @@ def test_check_latest(self):
self.assertEquals(self.mock_popen.call_count, 1)
self.assertCallArgs([self.make], self.mock_popen.call_args_list[0][0][0][:1])
+ def test_check_url(self):
+ from pgxnclient.cli import main
+ main(['check', 'http://api.pgxn.org/dist/foobar/0.42.1/foobar-0.42.1.zip'])
+
+ self.assertEquals(self.mock_popen.call_count, 1)
+ self.assertCallArgs([self.make], self.mock_popen.call_args_list[0][0][0][:1])
+
def test_check_fails(self):
self.mock_popen.return_value.returncode = 1
@@ -618,6 +650,24 @@ def test_load_local_dir(self, mock_get):
self.assertEquals(communicate.call_args[0][0],
'CREATE EXTENSION foobar;')
+ @patch('pgxnclient.commands.install.unpack')
+ @patch('pgxnclient.network.get_file')
+ def test_load_url(self, mock_get, mock_unpack):
+ mock_get.side_effect = fake_get_file
+ from pgxnclient.utils.zip import unpack
+ mock_unpack.side_effect = unpack
+
+ from pgxnclient.cli import main
+ main(['load', '--yes',
+ 'http://api.pgxn.org/dist/foobar/0.42.1/foobar-0.42.1.zip'])
+
+ self.assertEquals(mock_unpack.call_count, 0)
+ self.assertEquals(self.mock_popen.call_count, 1)
+ self.assert_('psql' in self.mock_popen.call_args[0][0][0])
+ communicate = self.mock_popen.return_value.communicate
+ self.assertEquals(communicate.call_args[0][0],
+ 'CREATE EXTENSION foobar;')
+
def test_load_extensions_order(self):
tdir = tempfile.mkdtemp()
try:
Please sign in to comment.
Something went wrong with that request. Please try again.