pax_global_header00006660000000000000000000000064135412275020014513gustar00rootroot0000000000000052 comment=ee014b7b86d72fc915459758333f3a9472785a3e pgxnclient-1.3/000077500000000000000000000000001354122750200135315ustar00rootroot00000000000000pgxnclient-1.3/.gitignore000066400000000000000000000001461354122750200155220ustar00rootroot00000000000000build dist docs/html docs/env pgxnclient.egg-info .eggs dbtest/regression.* dbtest/postgresql-*/* env pgxnclient-1.3/.travis.yml000066400000000000000000000002421354122750200156400ustar00rootroot00000000000000language: python dist: xenial sudo: false python: - "2.7" - "3.4" - "3.5" - "3.6" - "3.7" script: python setup.py test notifications: email: false pgxnclient-1.3/AUTHORS000066400000000000000000000012041354122750200145760ustar00rootroot00000000000000Who has contributed to the PGXN client? ======================================= Daniele Varrazzo He rushed to implement a client before David could do it in Perl! David Wheeler He is the PGXN mastermind: a lot of helpful design discussions. Peter Eisentraut First implementation of tarball support. Auto-sudo is not a good idea, I got it. Hitoshi Harada Tricky installation corner cases. Andrey Popp Make selection. Helped the program not to suck on BSD! Also thank you everybody for the useful discussions on the PGXN mailing list, bug reports, proofreading the docs and the general support to the project. pgxnclient-1.3/CHANGES000066400000000000000000000071271354122750200145330ustar00rootroot00000000000000.. _changes: PGXN Client changes log ----------------------- pgxnclient 1.3 ============== - Use https by default to access the PGXN API. - Dropped support for Python < 2.7 and Python 3 < 3.4. - Logging information emitted on stderr instead of stdout. - Exit with nonzero return code after command line parsing errors (ticket #23). - Don't fail if some directories in the ``PATH`` are not readable (ticket #24). - Don't file emitting non-ascii chars with stdout redirected (ticket #26). - Fixed parsing of server version numbers with PostgreSQL beta versions (ticket #29). - Use six to make the codebase portable between Python 2 and 3. pgxnclient 1.2.1 ================ - Fixed traceback on error when a dir doesn't contain META.json (ticket #19). - Handle version numbers both with and without hyphen (ticket #22). pgxnclient 1.2 ============== - Packages can be downloaded, installed, loaded specifying an URL (ticket #15). - Added support for ``.tar`` files (ticket #17). - 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). pgxnclient 1.1 ============== - Dropped support for Python 2.4. - ``sudo`` is not invoked automatically: the ``--sudo`` option must be specified if the user has not permission to write into PostgreSQL's libdir (ticket #13). The ``--sudo`` option can also be invoked without argument. - Make sure the same ``pg_config`` is used both by the current user and by sudo. pgxnclient 1.0.3 ================ - Can deal with extensions whose ``Makefile`` is created by ``configure`` and with makefile not in the package root. Patch provided by Hitoshi Harada (ticket #12). pgxnclient 1.0.2 ================ - Correctly handle PostgreSQL identifiers to be quoted (ticket #10). - Don't crash with a traceback if some external command is not found (ticket #11). pgxnclient 1.0.1 ================ - Fixed simplejson dependency on Python 2.6 (ticket #8). - Added ``pgxn help CMD`` as synonim for ``pgxn CMD --help`` (ticket #7). - Fixed a few compatibility problems with Python 3. pgxnclient 1.0 ============== - Extensions to load/unload from a distribution can be specified on the command line. - ``pgxn help --libexec`` returns a single directory, possibly independent from the client version. pgxnclient 0.3 ============== - ``pgxn`` script converted into a generic dispatcher in order to allow additional commands to be implemented in external scripts and in any language. - commands accept extension names too, not only specs. - Added ``help`` command to get information about program and commands. pgxnclient 0.2.1 ================ - Lowercase search for distributions in the API (issue #3). - Fixed handling of zip files not containing entries for the directory. - More informative error messages when some item is not found on PGXN. pgxnclient 0.2 ============== - Dropped ``list`` command (use ``info --versions`` instead). - Skip extension load/unload if the provided file is not sql. pgxnclient 0.1a4 ================ - The spec can point to a local file/directory for install. - Read the sha1 from the ``META.json`` as it may be different from the one in the ``dist.json``. - Run sudo in the installation phase of the install command. pgxn.client 0.1a3 ================= - Fixed executable mode for scripts unpacked from the zip files. - Added ``list`` and ``info`` commands. pgxn.client 0.1a2 ================= - Added database connection parameters for the ``check`` command. pgxn.client 0.1a1 ================= - Fist version released on PyPI. pgxnclient-1.3/COPYING000066400000000000000000000027141354122750200145700ustar00rootroot00000000000000Copyright (c) 2011-2019, Daniele Varrazzo All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The name of Daniele Varrazzo may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. pgxnclient-1.3/MANIFEST.in000066400000000000000000000003631354122750200152710ustar00rootroot00000000000000include AUTHORS CHANGES COPYING MANIFEST.in README.rst setup.py Makefile include bin/pgxn bin/pgxnclient recursive-include pgxnclient *.py recursive-include testdata * include pgxnclient/libexec/* include docs/conf.py docs/Makefile docs/*.rst pgxnclient-1.3/Makefile000066400000000000000000000010321354122750200151650ustar00rootroot00000000000000# pgxnclient Makefile # # Copyright (C) 2011-2019 Daniele Varrazzo # # This file is part of the PGXN client .PHONY: sdist upload docs PYTHON := python$(PYTHON_VERSION) PYTHON_VERSION ?= $(shell $(PYTHON) -c 'import sys; print ("%d.%d" % sys.version_info[:2])') build: $(PYTHON) setup.py build check: $(PYTHON) setup.py test sdist: $(PYTHON) setup.py sdist --formats=gztar upload: $(PYTHON) setup.py sdist --formats=gztar upload docs: $(MAKE) -C docs clean: rm -rf build pgxnclient.egg-info rm -rf .eggs $(MAKE) -C docs $@ pgxnclient-1.3/README.rst000066400000000000000000000034671354122750200152320ustar00rootroot00000000000000===================================================================== PGXN Client ===================================================================== A command line tool to interact with the PostgreSQL Extension Network ===================================================================== |travis| .. |travis| image:: https://travis-ci.org/pgxn/pgxnclient.svg?branch=master :target: https://travis-ci.org/pgxn/pgxnclient :alt: build status The `PGXN Client `__ is a command line tool designed to interact with the `PostgreSQL Extension Network `__ allowing searching, compiling, installing, and removing extensions in PostgreSQL databases. For example, to install the semver_ extension, the client can be invoked as:: $ pgxn install semver which would download and compile the extension for one of the PostgreSQL servers hosted on the machine and:: $ pgxn load -d somedb semver which would load the extension in one of the databases of the server. The client interacts with the PGXN web service and a ``Makefile`` provided by the extension. The best results are achieved with makefiles using the PostgreSQL `Extension Building Infrastructure`__; however the client tries to degrade gracefully in presence of any package hosted on PGXN. .. _semver: https://pgxn.org/dist/semver .. __: https://www.postgresql.org/docs/current/extend-pgxs.html - Documentation: https://pgxn.github.io/pgxnclient/ - Source repository: https://github.com/pgxn/pgxnclient - Downloads: https://pypi.python.org/pypi/pgxnclient/ - Discussion group: https://groups.google.com/group/pgxn-users/ Please refer to the files in the ``docs`` directory or online__ for instructions about the program installation and usage. .. __: https://pgxn.github.io/pgxnclient/ pgxnclient-1.3/bin/000077500000000000000000000000001354122750200143015ustar00rootroot00000000000000pgxnclient-1.3/bin/pgxn000077500000000000000000000010671354122750200152070ustar00rootroot00000000000000#!/usr/bin/env python """ pgxnclient -- commands dispatcher The script dispatches commands based on the name, e.g. upon the command:: pgxn foo --arg blah ... a script called pgxn-foo is searched and executed with remaining arguments. The commands are looked for by default in the dir ``libexec/pgxnclient/`` sibling of the directory containing this script, then are looked for in the ``PATH`` directories. """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client from pgxnclient.cli import command_dispatch command_dispatch() pgxnclient-1.3/bin/pgxnclient000077500000000000000000000003031354122750200163760ustar00rootroot00000000000000#!/usr/bin/env python """ pgxnclient -- command line interface """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client from pgxnclient.cli import script script() pgxnclient-1.3/dbtest/000077500000000000000000000000001354122750200150165ustar00rootroot00000000000000pgxnclient-1.3/dbtest/README000066400000000000000000000004571354122750200157040ustar00rootroot00000000000000This directory contain test files for high level testing. Currently you should run: - make_env.sh (will compile postgres and create a cluster) - pg_ctl.sh start (will start the test cluster on port 15432) - test_WHAT.sh (will install, test, uninstall the extension WHAT) - pg_ctl.sh stop (guess what) pgxnclient-1.3/dbtest/_test_template.sh000066400000000000000000000007061354122750200203660ustar00rootroot00000000000000echo "INSTALL" ${PGXN} install ${LEVEL} ${EXTENSION} || exit echo "CHECK" ${PGXN} check ${TEST_DSN} ${LEVEL} ${EXTENSION} echo "LOAD/UNLOAD" dropdb -h ${PG_HOST} -p ${PG_PORT} ${TEST_DB} createdb -h ${PG_HOST} -p ${PG_PORT} ${TEST_DB} ${PGXN} load ${TEST_DSN} ${LEVEL} ${EXTENSION} || exit ${PGXN} unload ${TEST_DSN} ${LEVEL} ${EXTENSION} echo "UNINSTALL" dropdb -h ${PG_HOST} -p ${PG_PORT} ${TEST_DB} ${PGXN} uninstall ${LEVEL} ${EXTENSION} || exit pgxnclient-1.3/dbtest/env.sh000066400000000000000000000006261354122750200161460ustar00rootroot00000000000000export PG_VERSION=11.2 export PG_ROOT=`pwd`/postgresql-${PG_VERSION}/root/ export PATH=${PG_ROOT}/bin/:${PATH} export PG_PORT=15432 export PG_HOST=localhost export TEST_DB=contrib_regression export TEST_DSN="-d ${TEST_DB} -h ${PG_HOST} -p ${PG_PORT}" export LEVEL="" # find the pgxn version to be tested # export PYTHONPATH=`pwd`/..:$PYTHONPATH # export PATH=`pwd`/../bin:${PATH} export PGXN=`which pgxn` pgxnclient-1.3/dbtest/make_env.sh000077500000000000000000000011651354122750200171450ustar00rootroot00000000000000#!/bin/bash set -euo pipefail # set -x dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd "$dir" source env.sh wget https://ftp.postgresql.org/pub/source/v${PG_VERSION}/postgresql-${PG_VERSION}.tar.bz2 tar xjvf postgresql-${PG_VERSION}.tar.bz2 rm postgresql-${PG_VERSION}.tar.bz2 cd postgresql-${PG_VERSION} ./configure --prefix=`pwd`/root make make install `pwd`/root/bin/initdb -D data set_param () { # Set a parameter in a postgresql.conf file param=$1 value=$2 sed -i "s/^\s*#\?\s*$param.*/$param = $value/" "data/postgresql.conf" } set_param port "${PG_PORT}" set_param listen_addresses "'*'" pgxnclient-1.3/dbtest/pg_ctl.sh000077500000000000000000000002761354122750200166320ustar00rootroot00000000000000#!/bin/bash set -euo pipefail # set -x dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd "$dir" source env.sh ${PG_ROOT}/bin/pg_ctl -D "${dir}/postgresql-${PG_VERSION}/data" $1 pgxnclient-1.3/dbtest/test_pair.sh000077500000000000000000000001111354122750200173400ustar00rootroot00000000000000#!/bin/bash source env.sh export EXTENSION=pair source _test_template.sh pgxnclient-1.3/dbtest/test_pgmp.sh000077500000000000000000000001111354122750200173500ustar00rootroot00000000000000#!/bin/bash source env.sh export EXTENSION=pgmp source _test_template.sh pgxnclient-1.3/dbtest/test_pyrseas.sh000077500000000000000000000001521354122750200201000ustar00rootroot00000000000000#!/bin/bash source env.sh export EXTENSION=pyrseas export TEST_DB=pyrseas_testdb source _test_template.sh pgxnclient-1.3/dbtest/test_quantile.sh000077500000000000000000000001441354122750200202350ustar00rootroot00000000000000#!/bin/bash source env.sh export EXTENSION=quantile export LEVEL=--testing source _test_template.sh pgxnclient-1.3/dbtest/test_s3_fdw.sh000077500000000000000000000001431354122750200175770ustar00rootroot00000000000000#!/bin/bash source env.sh export EXTENSION=s3_fdw export LEVEL=--unstable source _test_template.sh pgxnclient-1.3/dbtest/test_semver.sh000077500000000000000000000002511354122750200177130ustar00rootroot00000000000000#!/bin/bash set -euo pipefail set -x dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd "$dir" source env.sh export EXTENSION=semver source _test_template.sh pgxnclient-1.3/docs/000077500000000000000000000000001354122750200144615ustar00rootroot00000000000000pgxnclient-1.3/docs/Makefile000066400000000000000000000013401354122750200161170ustar00rootroot00000000000000# PGXN Client -- documentation makefile # # Building docs requires virtualenv already installed in the system. # # Use'make html' to build the HTML documentation. # # Copyright (C) 2011-2019 Daniele Varrazzo PYTHON := python ENV_DIR = $(shell pwd)/env ENV_BIN = $(ENV_DIR)/bin SPHINXOPTS = SPHINXBUILD = $(ENV_BIN)/sphinx-build PAPER = BUILDDIR = . .PHONY: env clean html default: html html: env $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) \ . $(BUILDDIR)/html # The environment is currently required to build the documentation. # It is not clean by 'make clean' env: [ -d "$(ENV_DIR)" ] || ( \ virtualenv -p "$(PYTHON)" "$(ENV_DIR)" \ && "$(ENV_BIN)/pip" install -r requirements.txt) clean: $(RM) -r html pgxnclient-1.3/docs/_static/000077500000000000000000000000001354122750200161075ustar00rootroot00000000000000pgxnclient-1.3/docs/_static/empty000066400000000000000000000000001354122750200171560ustar00rootroot00000000000000pgxnclient-1.3/docs/_templates/000077500000000000000000000000001354122750200166165ustar00rootroot00000000000000pgxnclient-1.3/docs/_templates/empty000066400000000000000000000000001354122750200176650ustar00rootroot00000000000000pgxnclient-1.3/docs/changes.rst000077700000000000000000000000001354122750200200242../CHANGESustar00rootroot00000000000000pgxnclient-1.3/docs/conf.py000066400000000000000000000164001354122750200157610ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # PGXN Client documentation build configuration file, created by # sphinx-quickstart on Tue May 3 00:34:03 2011. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import re # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.todo'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'PGXN Client' copyright = u'2011-2018, Daniele Varrazzo' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. sys.path.insert(0, '..') try: from pgxnclient import __version__ finally: del sys.path[0] # The short X.Y version. version = re.match(r'\d+\.\d+', __version__).group() # The full version, including alpha/beta/rc tags. release = __version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build', 'env'] # The reST default role (used for this markup: `text`) to use for all documents. default_role = 'obj' # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] todo_include_todos = True # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # Disabled because of issue #647 # https://bitbucket.org/birkenfeld/sphinx/issue/647/ html_use_smartypants = False # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'PGXNClientdoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). # latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). # latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ( 'index', 'PGXNClient.tex', u'PGXN Client Documentation', u'Daniele Varrazzo', 'manual', ) ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Additional stuff for the LaTeX preamble. # latex_preamble = '' # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ( 'index', 'pgxnclient', u'PGXN Client Documentation', [u'Daniele Varrazzo'], 1, ) ] pgxnclient-1.3/docs/ext.rst000066400000000000000000000032001354122750200160060ustar00rootroot00000000000000.. _extending: Extending PGXN client ===================== PGXN Client can be easily extended, either adding new builtin commands, to be included in the `!pgxnclient` package, or writing new scripts in any language you want. In order to add new builtin commands, add a Python module into the ``pgxnclient/commands`` containing your command or a set of logically-related commands. The commands are implemented by subclassing the `!Command` class. Your commands will benefit of all the infrastructure available for the other commands. For up-to-date information take a look at the implementation of builtin simple commands, such as the ones in ``info.py``. If you are not into Python and want to add commands written in other languages, you can provide a link (either soft or hard) to your command under one of the ``libexec`` directories. The exact location of the directories depends on the client installation: distribution packagers may decide to move them according to their own policies. The location of one of the directories, which can be considered the "public" one, can always be known using the command ``pgxn help --libexec``. Note that this directory may not exist: in this case the command being installed is responsible to create it. Links are also looked for in the :envvar:`PATH` directories. In order to implement the command :samp:`pgxn {foo}`, the link should be named :samp:`pgxn-{foo}`. The :program:`pgxn` script will dispatch the command and all the options to your script. Note that you can package many commands into the same script by looking at ``argv[0]`` to know the name of the link through which your script has been invoked. pgxnclient-1.3/docs/index.rst000066400000000000000000000030151354122750200163210ustar00rootroot00000000000000PGXN Client's documentation =========================== The `PGXN Client `__ is a command line tool designed to interact with the `PostgreSQL Extension Network `__ allowing searching, compiling, installing, and removing extensions in PostgreSQL databases. For example, to install the semver_ extension, the client can be invoked as:: $ pgxn install semver which would download and compile the extension for one of the PostgreSQL servers hosted on the machine and:: $ pgxn load -d somedb semver which would load the extension in one of the databases of the server. The client interacts with the PGXN web service and a ``Makefile`` provided by the extension. The best results are achieved with makefiles using the PostgreSQL `Extension Building Infrastructure`__; however the client tries to degrade gracefully in presence of any package hosted on PGXN and any package available outside the extension network. .. _semver: https://pgxn.org/dist/semver/ .. __: https://www.postgresql.org/docs/current/extend-pgxs.html - Source repository: https://github.com/pgxn/pgxnclient - Downloads: https://pypi.python.org/pypi/pgxnclient/ - Discussion group: https://groups.google.com/group/pgxn-users/ Contents: .. toctree:: :maxdepth: 2 install usage ext .. toctree:: :hidden: changes Indices and tables ================== * :ref:`Changes Log ` * :ref:`search` * :ref:`genindex` .. * :ref:`modindex` .. To do ===== .. todolist:: pgxnclient-1.3/docs/install.rst000066400000000000000000000034441354122750200166660ustar00rootroot00000000000000Installation ============ Prerequisites ------------- The program is implemented in Python. The current version can run using Python 2.7 and 3.4 onwards. PostgreSQL client-side development tools are required to build and install extensions. Installation from the Python Package Index ------------------------------------------ The PGXN client is `hosted on PyPI`__, therefore the easiest way to install the program is through a Python installation tool such as pip_. For example a system-wide installation can be obtained with:: $ sudo pip install pgxnclient To upgrade from a previous version to the most recent available you may run instead:: $ sudo pip install --upgrade pgxnclient The documentation of the installation tool of your choice will also show how to perform a local installation. .. __: https://pypi.org/project/pgxnclient/ .. _pip: https://pip.pypa.io/en/latest/ Installation from source ------------------------ The program can also be installed from the source, either from a `source package`__ or from the `source repository`__: in this case you can install the program using:: $ python setup.py install .. __: https://pypi.org/project/pgxnclient/ .. __: https://github.com/pgxn/pgxnclient Running from the project directory ---------------------------------- You can also run PGXN Client directly from the project directory, either unpacked from a `source package`__, or cloned from the `source repository`__, without performing any installation. Just make sure that the project directory is in the :envvar:`PYTHONPATH` and run the :program:`bin/pgxn` script:: $ cd /path/to/pgxnclient $ export PYTHONPATH=`pwd` $ ./bin/pgxn --version pgxnclient 1.3.0 # just an example .. __: https://pypi.org/project/pgxnclient/ .. __: https://github.com/pgxn/pgxnclient pgxnclient-1.3/docs/requirements.txt000066400000000000000000000000211354122750200177360ustar00rootroot00000000000000Sphinx>=1.8,<1.9 pgxnclient-1.3/docs/usage.rst000066400000000000000000000434741354122750200163330ustar00rootroot00000000000000Program usage ============= The program entry point is the script called :program:`pgxn`. Usage: .. parsed-literal:: :class: pgxn pgxn [--help] [--version] *COMMAND* [--mirror *URL*] [--verbose] [--yes] ... The script offers several commands, whose list can be obtained using ``pgxn --help``. The options available for each subcommand can be obtained using :samp:`pgxn {COMMAND} --help`. The main commands you may be interested in are `install`_ (to download, build and install an extension distribution into the system) and `load`_ (to load an installed extension into a database). Commands to perform reverse operations are `uninstall`_ and `unload`_. Use `download`_ to get a package from a mirror without installing it. There are also informative commands: `search <#pgxn-search>`_ is used to search the network, `info`_ to get information about a distribution. The `mirror`_ command can be used to get a list of mirrors. A few options are available to all the commands: :samp:`--mirror {URL}` Select a mirror to interact with. If not specified the default is ``https://api.pgxn.org/``. ``--verbose`` Print more information during the process. ``--yes`` Assume affirmative answer to all questions. Useful for unattended scripts. Package specification --------------------- Many commands such as install_ require a *package specification* to operate. In its simple form the specification is just the name of a distribution: ``pgxn install foo`` means "install the most recent stable release of the ``foo`` distribution". If a distribution with given name is not found, many commands will look for an *extension* with the given name, and will work on it. The specification allows specifying an operator and a version number, so that ``pgxn install 'foo<2.0'`` will install the most recent stable release of the distribution before the release 2.0. The version numbers are ordered according to the `Semantic Versioning specification `__. Supported operators are ``=``, ``==`` (alias for ``=``), ``<``, ``<=``, ``>``, ``>=``. Note that you probably need to quote the string as in the example to avoid invoking shell command redirection. Whenever a command takes a specification in input, it also accepts options ``--stable``, ``--testing`` and ``--unstable`` to specify the minimum release status accepted. The default is "stable". A few commands also allow specifying a local archive or local 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``) or it should be specified as an URL with ``file://`` schema. A few commands also allow specifying a remote package with a URL. Currently the schemas ``http://`` and ``https://`` are supported. Currently the client supports ``.zip`` and ``.tar`` archives (eventually with *gzip* and *bz2* compression). .. _install: ``pgxn install`` ---------------- Download, build, and install a distribution on the local system. Usage: .. parsed-literal:: :class: pgxn-install pgxn install [--help] [--stable | --testing | --unstable] [--pg_config *PROG*] [--make *PROG*] [--sudo [*PROG*] | --nosudo] *SPEC* 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. 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. The command will run the ``configure`` script if available in the package, then will perform ``make all`` and ``make install``. It is assumed that the ``Makefile`` provided by the distribution uses PGXS_ to build the extension, but this is not enforced: you may provide any Makefile as long as the expected commands are implemented. .. _PGXS: https://www.postgresql.org/docs/current/extend-pgxs.html If there are many PostgreSQL installations on the system, the extension will be built and installed against the instance whose :program:`pg_config` is first found on the :envvar:`PATH`. A different instance can be specified using the option :samp:`--pg_config {PATH}`. The PGXS_ build system relies on a presence of `GNU Make`__: in many systems it is installed as :program:`gmake` or :program:`make` executable. The program will use the first of them on the path. You can specify an alternative program using ``--make`` option. .. __: https://www.gnu.org/software/make/ If the extension is being installed into a system PostgreSQL installation, the install phase will likely require root privileges to be performed. In this case either run the command under :program:`sudo` or specify the ``--sudo`` option: in the latter case :program:`sudo` will only be invoked during the "install" phase. An optional program :samp:`{PROG}` to elevate the user privileges can be specified as ``--sudo`` option; if none is specified, :program:`sudo` will be used. .. note:: If ``--sudo`` is the last option and no :samp:`{PROG}` is specified, a ``--`` separator may be required to disambiguate the :samp:`{SPEC}`:: pgxn install --sudo -- foobar .. _check: ``pgxn check`` -------------- Run a distribution's unit test. Usage: .. parsed-literal:: :class: pgxn-check pgxn check [--help] [--stable | --testing | --unstable] [--pg_config *PROG*] [--make *PROG*] [-d *DBNAME*] [-h *HOST*] [-p *PORT*] [-U *NAME*] *SPEC* The command takes a `package specification`_ identifying the distribution to 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 required for testing is to be built, it should be listed as ``installcheck`` prerequisite in the ``Makefile``, for instance: .. code-block:: make myext.sql: myext.sql.in some_command installcheck: myext.sql The script exits with non-zero value in case of test failed. In this case, if files ``regression.diff`` and ``regression.out`` are produced (as :program:`pg_regress` does), these files are copied to the local directory where the script is run. The database connection options are similar to the ones in load_, with the difference that the variable :envvar:`PGDATABASE` doesn't influence the database name. See the install_ command for details about the command arguments. .. warning:: At the time of writing, :program:`pg_regress` on Debian and derivatives is affected by `bug #554166`__ which makes *HOST* selection impossible. .. __: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=554166 .. _uninstall: ``pgxn uninstall`` ------------------ Remove a distribution from the system. Usage: .. parsed-literal:: :class: pgxn-uninstall pgxn uninstall [--help] [--stable | --testing | --unstable] [--pg_config *PROG*] [--make *PROG*] [--sudo [*PROG*] | --nosudo] *SPEC* The command does the opposite of the install_ command, removing a distribution's files from the system. It doesn't issue any command to the databases where the distribution's extensions may have been loaded: you should first drop the extension (the unload_ command can do this). The distribution should match what installed via the `install`_ command. See the install_ command for details about the command arguments. .. _load: ``pgxn load`` ------------- Load the extensions included in a distribution into a database. The distribution must be already installed in the system, e.g. via the `install`_ command. Usage: .. parsed-literal:: :class: pgxn-load pgxn load [--help] [--stable | --testing | --unstable] [-d *DBNAME*] [-h *HOST*] [-p *PORT*] [-U *NAME*] [--pg_config *PATH*] [--schema *SCHEMA*] *SPEC* [*EXT* [*EXT* ...]] The distribution is specified according to the `package specification`_ and 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``, ``-U``/``--username``. The default values for these parameters are the regular system ones and can be also set using environment variables :envvar:`PGDATABASE`, :envvar:`PGHOST`, :envvar:`PGPORT`, :envvar:`PGUSER`. The command supports also a ``--pg_config`` option that can be used to specify an alternative :program:`pg_config` to use to look for installation scripts: you may need to specify the parameter if there are many PostgreSQL installations on the system, and should be consistent to the one specified in the ``install`` command. If the specified database version is at least PostgreSQL 9.1, and if the extension specifies a ``.control`` file, it will be loaded using the `CREATE EXTENSION`_ command, otherwise it will be loaded as a loose set of objects. For more information see the `extensions documentation`__. .. _CREATE EXTENSION: https://www.postgresql.org/docs/current/sql-createextension.html .. __: https://www.postgresql.org/docs/current/extend-extensions.html The command is based on the `'provides' section`_ of the distribution's ``META.json``: if a SQL file is specified, that file will be used to load the extension. Note that loading is only attempted if the file extension is ``.sql``: if it's not, we assume that the extension is not really a PostgreSQL extension (it may be for instance a script). If no ``file`` is specified, a file named :samp:`{extension}.sql` will be looked for in a few directories under the PostgreSQL ``shared`` directory and it will be loaded after an user confirmation. If the distribution provides more than one extension, the extensions are loaded in the order in which they are specified in the ``provides`` section of the ``META.json`` file. It is also possible to load only a few of the extensions provided, specifying them after *SPEC*: the extensions will be loaded in the order specified. If a *SCHEMA* is specified, the extensions are loaded in the provided schema. Note that if ``CREATE EXTENSION`` is used, the schema is directly supported; otherwise the ``.sql`` script loaded will be patched to create the objects in the provided schema (a confirmation will be asked before attempting loading). .. _'provides' section: https://pgxn.org/spec/#provides .. _unload: ``pgxn unload`` --------------- Unload a distribution's extensions from a database. Usage: .. parsed-literal:: :class: pgxn-unload pgxn unload [--help] [--stable | --testing | --unstable] [-d *DBNAME*] [-h *HOST*] [-p *PORT*] [-U *NAME*] [--pg_config *PATH*] [--schema *SCHEMA*] *SPEC* [*EXT* [*EXT* ...]] The command does the opposite of the load_ command: it drops a distribution extensions from the specified database, either issuing `DROP EXTENSION`_ commands or running uninstall scripts eventually provided. For every extension specified in the `'provides' section`_ of the distribution ``META.json``, the command will look for a file called :samp:`uninstall_{file.sql}` where :samp:`{file.sql}` is the ``file`` specified. If no file is specified, :samp:`{extension}.sql` is assumed. If a file with extension different from ``.sql`` is specified, it is assumed that the extension is not a PostgreSQL extension so unload is not performed. If a *SCHEMA* is specified, the uninstall script will be patched to drop the objects in the selected schema. However, if the extension was loaded via ``CREATE EXTENSION``, the server will be able to figure out the correct schema itself, so the option will be ignored. If the distribution specifies more than one extension, they are unloaded in reverse order respect to the order in which they are specified in the ``META.json`` file. It is also possible to unload only a few of the extensions provided, specifying them after *SPEC*: the extensions will be unloaded in the order specified. .. _DROP EXTENSION: https://www.postgresql.org/docs/current/sql-dropextension.html See the load_ command for details about the command arguments. .. _download: ``pgxn download`` ----------------- Download a distribution from the network. Usage: .. parsed-literal:: :class: pgxn-download pgxn download [--help] [--stable | --testing | --unstable] [--target *PATH*] *SPEC* 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: ``pgxn search`` --------------- Search in the extensions available on PGXN. Usage: .. parsed-literal:: :class: pgxn-search pgxn search [--help] [--dist | --ext | --docs] *TERM* [*TERM* ...] The command prints on ``stdout`` a list of packages and version matching :samp:`{TERM}`. By default the search is performed in the documentation: alternatively the distributions (using the ``--dist`` option) or the extensions (using the ``--ext`` option) can be searched. Example: .. code-block:: console $ pgxn search --dist integer tinyint 0.1.1 Traditionally, PostgreSQL core has a policy not to have 1 byte *integer* in it. With this module, you can define 1 byte *integer* column on your tables, which will help query performances and... check_updates 1.0.0 ... test2 defined as: CREATE TABLE test2(a *INTEGER*, b *INTEGER*, c *INTEGER*, d *INTEGER*); To make a trigger allowing updates only when c becomes equal to 5: CREATE TRIGGER c_should_be_5 BEFORE UPDATE ON... ssn 1.0.0 INSERT INTO test VALUES('124659876'); The output is always represented using the format with dashes, i.e: 123-45-6789 124-65-9876 Internals: The type is stored as a 4 bytes *integer*. The search will return all the matches containing any of *TERM*. In order to search for items containing more than one word, join the word into a single token. For instance to search for items containing the terms "double precision" or the terms "floating point" use: .. code-block:: console $ pgxn search "double precision" "floating point" semver 0.2.2 ... to semver semver(12.0::real) 12.0.0semver(*double precision*) Cast *double precision* to semver semver(9.2::*double precision*) 9.2.0semver(integer) Cast integer to semver semver(42::integer)... saio 0.0.1 Defaults to true. saio_seed A *floating point* seed for the random numbers generator. saio_equilibrium_factor Scaling factor for the query size, determining the number of loops before equilibrium is... pgTAP 0.25.0 ... ) casts_are( casts[] ) SELECT casts_are( ARRAY[ 'integer AS *double precision*', 'integer AS reltime', 'integer AS numeric', -- ... .. _info: ``pgxn info`` ------------- Print information about a distribution obtained from PGXN. Usage: .. parsed-literal:: :class: pgxn-info pgxn info [--help] [--stable | --testing | --unstable] [--details | --meta | --readme | --versions] *SPEC* 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 $ pgxn info pair name: pair abstract: A key/value pair data type description: This library contains a single PostgreSQL extension, a key/value pair data type called “pair”, along with a convenience function for constructing key/value pairs. maintainer: David E. Wheeler license: postgresql release_status: stable version: 0.1.2 date: 2011-04-20T23:47:22Z sha1: 9988d7adb056b11f8576db44cca30f88a08bd652 provides: pair: 0.1.2 Alternatively the raw ``META.json`` (using the ``--meta`` option) or the distribution README (using the ``--readme`` option) can be obtained. Using the ``--versions`` option, the command prints a list of available versions for the specified distribution, together with their release status. Only distributions respecting :samp:`{SPEC}` and the eventually specified release status options are printed, for example: .. code-block:: console $ pgxn info --versions 'pair<0.1.2' pair 0.1.1 stable pair 0.1.0 stable .. _mirror: ``pgxn mirror`` --------------- Return information about the available mirrors. Usage: .. parsed-literal:: :class: pgxn-mirror pgxn mirror [--help] [--detailed] [*URI*] If no :samp:`URI` is specified, print a list of known mirror URIs. Otherwise print details about the specified mirror. It is also possible to print details for all the known mirrors using the ``--detailed`` option. .. _help: ``pgxn help`` ------------- Display help and other program information. Usage: .. parsed-literal:: :class: pgxn-help pgxn help [--help] [--all | --libexec | *CMD*] Without options show the same information obtained by ``pgxn --help``, which includes a list of builtin commands. With the ``--all`` option print the complete list of commands installed in the system. The option ``--libexec`` prints the full path of the directory containing the external commands scripts: see :ref:`extending` for more information. :samp:`pgxn help {CMD}` is an alias for :samp:`pgxn {CMD} --help`. pgxnclient-1.3/pgxnclient/000077500000000000000000000000001354122750200157045ustar00rootroot00000000000000pgxnclient-1.3/pgxnclient/__init__.py000066400000000000000000000035731354122750200200250ustar00rootroot00000000000000""" pgxnclient -- main package """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client import os from pgxnclient.spec import Spec from pgxnclient.utils.semver import SemVer from pgxnclient.utils.strings import Label, Term, Identifier __version__ = '1.3' # Paths where to find the command executables. # If relative, it's from the `pgxnclient` package directory. # Distribution packagers may move them around if they wish. # # Only one of the paths should be marked as "public": it will be returned by # pgxn help --libexec LIBEXECDIRS = [ # public, path (False, './libexec/'), (True, '/usr/local/libexec/pgxnclient/'), ] assert ( len([x for x in LIBEXECDIRS if x[0]]) == 1 ), "only one libexec directory should be public" __all__ = [ 'Spec', 'SemVer', 'Label', 'Term', 'Identifier', 'get_scripts_dirs', 'get_public_script_dir', 'find_script', ] def get_scripts_dirs(): """ Return the absolute path of the directories containing the client scripts. """ return [ os.path.normpath(os.path.join(os.path.dirname(__file__), p)) for (_, p) in LIBEXECDIRS ] def get_public_scripts_dir(): """ Return the absolute path of the public directory for the client scripts. """ return [ os.path.normpath(os.path.join(os.path.dirname(__file__), p)) for (public, p) in LIBEXECDIRS if public ][0] def find_script(name): """Return the absoulute path of a pgxn script. The script are usually found in the `LIBEXEC` dir, but any script on the path will do (they are usually prefixed by ``pgxn-``). Return `None` if the script is not found. """ path = os.environ.get('PATH', '').split(os.pathsep) path[0:0] = get_scripts_dirs() for p in path: fn = os.path.join(p, name) if os.path.isfile(fn): return fn pgxnclient-1.3/pgxnclient/api.py000066400000000000000000000072661354122750200170420ustar00rootroot00000000000000""" pgxnclient -- client API stub """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client from six.moves.urllib.parse import urlencode from pgxnclient import network from pgxnclient.utils import load_json from pgxnclient.errors import NetworkError, NotFound, ResourceNotFound from pgxnclient.utils.uri import expand_template class Api(object): def __init__(self, mirror): self.mirror = mirror def dist(self, dist, version=''): try: with self.call( version and 'meta' or 'dist', {'dist': dist, 'version': version}, ) as f: return load_json(f) except ResourceNotFound: raise NotFound("distribution '%s' not found" % dist) def ext(self, ext): try: with self.call('extension', {'extension': ext}) as f: return load_json(f) except ResourceNotFound: raise NotFound("extension '%s' not found" % ext) def meta(self, dist, version, as_json=True): with self.call('meta', {'dist': dist, 'version': version}) as f: if as_json: return load_json(f) else: return f.read().decode('utf-8') def readme(self, dist, version): with self.call('readme', {'dist': dist, 'version': version}) as f: return f.read() def download(self, dist, version): dist = dist.lower() version = version.lower() return self.call('download', {'dist': dist, 'version': version}) def mirrors(self): with self.call('mirrors') as f: return load_json(f) def search(self, where, query): """Search into PGXN. :param where: where to search. The server currently supports "docs", "dists", "extensions" :param query: list of strings to search """ # convert the query list into a string q = ' '.join([' ' in s and ('"%s"' % s) or s for s in query]) with self.call('search', {'in': where}, query={'q': q}) as f: return load_json(f) def stats(self, arg): with self.call('stats', {'stats': arg}) as f: return load_json(f) def user(self, username): with self.call('user', {'user': username}) as f: return load_json(f) def call(self, meth, args=None, query=None): url = self.get_url(meth, args, query) try: return network.get_file(url) except ResourceNotFound: # check if it is one of the broken URLs as reported in # https://groups.google.com/group/pgxn-users/browse_thread/thread/e41fbc202680c92c version = args and args.get('version') if not (version and version.trail): raise args = args.copy() args['version'] = str(version).replace('-', '', 1) url = self.get_url(meth, args, query) return network.get_file(url) def get_url(self, meth, args=None, query=None): tmpl = self.get_template(meth) url = expand_template(tmpl, args or {}) url = self.mirror.rstrip('/') + url if query is not None: url = url + '?' + urlencode(query) return url def get_template(self, meth): return self.get_index()[meth] _api_index = None def get_index(self): if self._api_index is None: url = self.mirror.rstrip('/') + '/index.json' try: with network.get_file(url) as f: self._api_index = load_json(f) except ResourceNotFound: raise NetworkError("API index not found at '%s'" % url) return self._api_index pgxnclient-1.3/pgxnclient/archive.py000066400000000000000000000054051354122750200177030ustar00rootroot00000000000000""" pgxnclient -- archives handling """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client import os from pgxnclient.i18n import _ from pgxnclient.utils import load_jsons from pgxnclient.errors import PgxnClientException def from_spec(spec): """Return an `Archive` instance to handle the file requested by *spec* """ assert spec.is_file() return from_file(spec.filename) def from_file(filename): """Return an `Archive` instance to handle the file *filename* """ from pgxnclient.zip import ZipArchive from pgxnclient.tar import TarArchive for cls in (ZipArchive, TarArchive): a = cls(filename) if a.can_open(): return a raise PgxnClientException( _("can't open archive '%s': file type not recognized") % filename ) class Archive(object): """Base class to handle archives.""" def __init__(self, filename): self.filename = filename def can_open(self): """Return `!True` if the `!filename` can be opened by the obect.""" raise NotImplementedError def open(self): """Open the archive for usage. Raise PgxnClientException if the archive can't be open. """ raise NotImplementedError def close(self): """Close the archive after usage.""" raise NotImplementedError def list_files(self): """Return an iterable with the list of file names in the archive.""" raise NotImplementedError def read(self, fn): """Return a file's data from the archive.""" raise NotImplementedError def unpack(self, destdir): raise NotImplementedError def get_meta(self): filename = self.filename self.open() try: # Return the first file with the expected name for fn in self.list_files(): if fn.endswith('META.json'): return load_jsons(self.read(fn).decode('utf8')) else: raise PgxnClientException( _("file 'META.json' not found in archive '%s'") % filename ) finally: self.close() def _find_work_directory(self, destdir): """ Choose the directory where to work. Because we are mostly a wrapper for pgxs, let's look for a makefile. The tar should contain a single base directory, so return the first dir we found containing a Makefile, alternatively just return the unpacked dir """ for dir in os.listdir(destdir): for fn in ('Makefile', 'makefile', 'GNUmakefile', 'configure'): if os.path.exists(os.path.join(destdir, dir, fn)): return os.path.join(destdir, dir) return destdir pgxnclient-1.3/pgxnclient/cli.py000066400000000000000000000066421354122750200170350ustar00rootroot00000000000000""" pgxnclient -- command line entry point """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client import os import sys from pgxnclient import find_script from pgxnclient.i18n import _ from pgxnclient.utils import emit from pgxnclient.errors import PgxnException, UserAbort from pgxnclient.commands import get_option_parser, load_commands, run_command def main(argv=None): """ The program main function. The function is still relatively self contained: it can be called with arguments and raises whatever exception, so it's the best entry point for whole system testing. """ if argv is None: argv = sys.argv[1:] load_commands() parser = get_option_parser() opt = parser.parse_args(argv) run_command(opt, parser) def script(): """ Execute the program as a script. Set up logging, invoke main() using the user-provided arguments and handle any exception raised. """ # Setup logging import logging logging.basicConfig( format="%(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", stream=sys.stderr, ) logger = logging.getLogger() # Dispatch to the command according to the script name script = sys.argv[0] args = sys.argv[1:] if os.path.basename(script).startswith('pgxn-'): args.insert(0, os.path.basename(script)[5:]) # for help print sys.argv[0] = os.path.join(os.path.dirname(script), 'pgxn') # Execute the script try: main(args) # Different ways to fail except UserAbort as e: # The user replied "no" to some question logger.info("%s", e) sys.exit(1) except PgxnException as e: # An regular error from the program logger.error("%s", e) sys.exit(1) except SystemExit as e: # Usually the arg parser bailing out. if isinstance(getattr(e, 'code', None), int): sys.exit(e.code) else: sys.exit(1) except Exception: logger.exception(_("unexpected error")) sys.exit(1) except BaseException: # ctrl-c sys.exit(1) def command_dispatch(argv=None): """ Entry point for a script to dispatch commands to external scripts. Upon invocation of a command ``pgxn cmd --arg``, locate pgxn-cmd and execute it with --arg arguments. """ if argv is None: argv = sys.argv[1:] # Assume the first arg after the option is the command to run for icmd, cmd in enumerate(argv): if not cmd.startswith('-'): argv = [_get_exec(cmd)] + argv[:icmd] + argv[icmd + 1 :] break else: # No command specified: dispatch to the pgxnclient script # to print basic help, main command etc. argv = [ os.path.join(os.path.dirname(sys.argv[0]), 'pgxnclient') ] + argv if not os.access(argv[0], os.X_OK): # This is our friend setuptools' job: the script have lost the # executable flag. We assume the script is a Python one and run it # through the current executable. argv.insert(0, sys.executable) os.execv(argv[0], argv) def _get_exec(cmd): fn = find_script('pgxn-' + cmd) if not fn: emit( "pgxn: unknown command: '%s'. See 'pgxn --help'" % cmd, file=sys.stderr, ) sys.exit(2) return fn if __name__ == '__main__': script() pgxnclient-1.3/pgxnclient/commands/000077500000000000000000000000001354122750200175055ustar00rootroot00000000000000pgxnclient-1.3/pgxnclient/commands/__init__.py000066400000000000000000000604561354122750200216310ustar00rootroot00000000000000""" pgxnclient -- commands package This module contains base classes and functions to implement and deal with commands. Concrete commands implementations are available in other package modules. """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client import os import sys import shlex import logging import argparse from subprocess import Popen, PIPE import six from pgxnclient.utils import load_json, find_executable from pgxnclient import __version__ from pgxnclient import network from pgxnclient import Spec, SemVer from pgxnclient import archive from pgxnclient.api import Api from pgxnclient.i18n import _, gettext from pgxnclient.errors import ( BadSpecError, NotFound, PgxnClientException, ProcessError, ResourceNotFound, UserAbort, ) from pgxnclient.utils.temp import temp_dir logger = logging.getLogger('pgxnclient.commands') def get_option_parser(): """ Return an option parser populated with the available commands. The parser is populated with all the options defined by the implemented commands. Only commands defining a ``name`` attribute are added. The function relies on the `Command` subclasses being already created: call `load_commands()` before calling this function. """ parser = argparse.ArgumentParser( # usage = _("%(prog)s [global options] COMMAND [command options]"), description=_( "Interact with the PostgreSQL Extension Network (PGXN)." ), add_help=False, ) parser.add_argument( "--version", action='version', version="%%(prog)s %s" % __version__, help=_("print the version number and exit"), ) # Drop the conflicting -h argument parser.add_argument( "--help", action='help', default=argparse.SUPPRESS, help=_('show this help message and exit'), ) subparsers = parser.add_subparsers( title=_("available commands"), metavar='COMMAND', help=_( "the command to execute." " The complete list is available using `pgxn help --all`." " Builtin commands are:" ), ) clss = [cls for cls in CommandType.subclasses if cls.name] clss.sort(key=lambda c: c.name) for cls in clss: cls.customize_parser(parser, subparsers) return parser def load_commands(): """ Load all the commands known by the program. Currently commands are read from modules into the `pgxnclient.commands` package. Importing the package causes the `Command` classes to be created: they register themselves thanks to the `CommandType` metaclass. """ pkgdir = os.path.dirname(__file__) for fn in os.listdir(pkgdir): if fn.startswith('_'): continue modname = __name__ + '.' + os.path.splitext(fn)[0] # skip already imported modules if modname in sys.modules: continue try: __import__(modname) except Exception as e: logger.warning( _("error importing commands module %s: %s - %s"), modname, e.__class__.__name__, e, ) def run_command(opts, parser): """Run the command specified by options parsed on the command line.""" # setup the logging logging.getLogger().setLevel( opts.verbose and logging.DEBUG or logging.INFO ) return opts.cmd(opts, parser=parser).run() class CommandType(type): """ Metaclass for the Command class. This metaclass allows self-registration of the commands: any Command subclass is automatically added to the `subclasses` list. """ subclasses = [] def __new__(cls, name, bases, dct): rv = type.__new__(cls, name, bases, dct) CommandType.subclasses.append(rv) return rv def __init__(cls, name, bases, dct): super(CommandType, cls).__init__(name, bases, dct) class Command(six.with_metaclass(CommandType, object)): """ Base class to implement client commands. Provide the argument parsing framework and API dispatch. Commands should subclass this class and possibly other mixin classes, set a value for the `name` and `description` arguments and implement the `run()` method. If command line parser customization is required, `customize_parser()` should be extended. """ name = None description = None def __init__(self, opts, parser=None): """Initialize a new Command. The parser will be specified if the class has been initialized by that parser itself, so run() can expect it being not None. """ self.opts = opts self.parser = parser self._api = None @classmethod def customize_parser(self, parser, subparsers, **kwargs): """Customise the option parser. :param parser: the option parser to be customized :param subparsers: the action object where to register a command subparser :return: the new subparser created Subclasses should extend this method in order to add new options or a subparser implementing a new command. Be careful in calling the superclass' `customize_parser()` via `super()` in order to call all the mixins methods. Also note that the method must be a classmethod. """ return self.__make_subparser(parser, subparsers, **kwargs) def run(self): """The actions to take when the command is invoked.""" raise NotImplementedError @classmethod def __make_subparser( self, parser, subparsers, description=None, epilog=None ): """Create a new subparser with help populated.""" subp = subparsers.add_parser( self.name, help=gettext(self.description), description=description or gettext(self.description), add_help=False, epilog=epilog, ) subp.set_defaults(cmd=self) # Drop the conflicting -h argument subp.add_argument( "--help", action='help', default=argparse.SUPPRESS, help=_('show this help message and exit'), ) glb = subp.add_argument_group(_("global options")) glb.add_argument( "--mirror", metavar="URL", default='https://api.pgxn.org/', help=_("the mirror to interact with [default: %(default)s]"), ) glb.add_argument( "--verbose", action='store_true', help=_("print more information") ) glb.add_argument( "--yes", action='store_true', help=_("assume affirmative answer to all questions"), ) return subp @property def api(self): """Return an `Api` instance to communicate with PGXN. Use the value provided with ``--mirror`` to decide where to connect. """ if self._api is None: self._api = Api(mirror=self.opts.mirror) return self._api def confirm(self, prompt): """Prompt an user confirmation. Raise `UserAbort` if the user replies "no". The method is no-op if the ``--yes`` option is specified. """ if self.opts.yes: return True while 1: ans = six.input(_("%s [y/N] ") % prompt) if _('no').startswith(ans.lower()): raise UserAbort(_("operation interrupted on user request")) elif _('yes').startswith(ans.lower()): return True else: prompt = _("Please answer yes or no") def popen(self, cmd, *args, **kwargs): """ Excecute subprocess.Popen. Commands should use this method instead of importing subprocess.Popen: this allows replacement with a mock in the test suite. """ logger.debug("running command: %s", cmd) try: return Popen(cmd, *args, **kwargs) except OSError as e: if not isinstance(cmd, six.string_types): cmd = ' '.join(cmd) msg = _("%s running command: %s") % (e, cmd) raise ProcessError(msg) class WithSpec(Command): """Mixin to implement commands taking a package specification. This class adds a positional argument SPEC to the parser and related options. """ @classmethod def customize_parser( self, parser, subparsers, with_status=True, epilog=None, **kwargs ): """ Add the SPEC related options to the parser. If *with_status* is true, options ``--stable``, ``--testing``, ``--unstable`` are also handled. """ epilog = ( _( """ SPEC can either specify just a name or contain required versions indications, for instance 'pkgname=1.0', or 'pkgname>=2.1'. """ ) + (epilog or "") ) subp = super(WithSpec, self).customize_parser( parser, subparsers, epilog=epilog, **kwargs ) subp.add_argument( 'spec', metavar='SPEC', help=_("name and optional version of the package"), ) if with_status: g = subp.add_mutually_exclusive_group(required=False) g.add_argument( '--stable', dest='status', action='store_const', const=Spec.STABLE, default=Spec.STABLE, help=_("only accept stable distributions [default]"), ) g.add_argument( '--testing', dest='status', action='store_const', const=Spec.TESTING, help=_("accept testing distributions too"), ) g.add_argument( '--unstable', dest='status', action='store_const', const=Spec.UNSTABLE, help=_("accept unstable distributions too"), ) return subp def get_spec(self, _can_be_local=False, _can_be_url=False): """ Return the package specification requested. Return a `Spec` instance. """ spec = self.opts.spec try: spec = Spec.parse(spec) except (ValueError, BadSpecError) as e: self.parser.error(_("cannot parse package '%s': %s") % (spec, e)) if not _can_be_local and spec.is_local(): 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): """ Return the best version an user may want for a distribution. Return a `SemVer` instance. Raise `ResourceNotFound` if no version is found with the provided specification and options. """ drels = data['releases'] # Get the maximum version for each release status satisfying the spec vers = [None] * len(Spec.STATUS) for n, d in drels.items(): vs = list(filter(spec.accepted, [SemVer(r['version']) for r in d])) if vs: vers[Spec.STATUS[n]] = max(vs) return self._get_best_version(vers, spec, quiet) def get_best_version_from_ext(self, data, spec): """ Return the best distribution version from an extension's data """ # Get the maximum version for each release status satisfying the spec vers = [[] for i in range(len(Spec.STATUS))] vmap = {} # ext_version -> (dist_name, dist_version) for ev, dists in data.get('versions', {}).items(): ev = SemVer(ev) if not spec.accepted(ev): continue for dist in dists: dv = SemVer(dist['version']) ds = dist.get('status', 'stable') vers[Spec.STATUS[ds]].append(ev) vmap[ev] = (dist['dist'], dv) # for each rel status only take the max one. for i in range(len(vers)): vers[i] = vers[i] and max(vers[i]) or None ev = self._get_best_version(vers, spec, quiet=False) return vmap[ev] def _get_best_version(self, vers, spec, quiet): # Is there any result at the desired release status? want = [ v for lvl, v in enumerate(vers) if lvl >= self.opts.status and v is not None ] if want: ver = max(want) if not quiet: logger.info(_("best version: %s %s"), spec.name, ver) return ver # Not found: is there any hint we can give? if self.opts.status > Spec.TESTING and vers[Spec.TESTING]: hint = (vers[Spec.TESTING], _('testing')) elif self.opts.status > Spec.UNSTABLE and vers[Spec.UNSTABLE]: hint = (vers[Spec.UNSTABLE], _('unstable')) else: hint = None msg = _("no suitable version found for %s") % spec if hint: msg += _(" but there is version %s at level %s") % hint raise ResourceNotFound(msg) def get_meta(self, spec): """ Return the content of the ``META.json`` file for *spec*. Return the object obtained parsing the JSON. """ if spec.is_name(): # Get the metadata from the API try: data = self.api.dist(spec.name) except NotFound: # Distro not found: maybe it's an extension? ext = self.api.ext(spec.name) name, ver = self.get_best_version_from_ext(ext, spec) return self.api.meta(name, ver) else: ver = self.get_best_version(data, spec) return self.api.meta(spec.name, ver) elif spec.is_dir(): # Get the metadata from a directory fn = os.path.join(spec.dirname, 'META.json') logger.debug("reading %s", fn) if not os.path.exists(fn): raise PgxnClientException( _("file 'META.json' not found in '%s'") % spec.dirname ) with open(fn) as f: return load_json(f) elif spec.is_file(): arc = archive.from_spec(spec) return arc.get_meta() elif spec.is_url(): with network.get_file(spec.url) as fin: with temp_dir() as dir: fn = network.download(fin, dir) arc = archive.from_file(fn) return arc.get_meta() else: assert False class WithSpecLocal(WithSpec): """ Mixin to implement commands that can also refer to a local file or dir. """ @classmethod def customize_parser(self, parser, subparsers, epilog=None, **kwargs): epilog = ( _( """ SPEC may also be a local zip file or unpacked directory, but in this case it should contain at least a '%s', for instance '.%spkgname.zip'. """ ) % (os.sep, os.sep) + (epilog or "") ) subp = super(WithSpecLocal, self).customize_parser( parser, subparsers, epilog=epilog, **kwargs ) return subp 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): """ Mixin to implement commands that should query :program:`pg_config`. """ @classmethod def customize_parser(self, parser, subparsers, **kwargs): """ Add the ``--pg_config`` option to the options parser. """ subp = super(WithPgConfig, self).customize_parser( parser, subparsers, **kwargs ) subp.add_argument( '--pg_config', metavar="PROG", default='pg_config', help=_( "the pg_config executable to find the database" " [default: %(default)s]" ), ) return subp def call_pg_config(self, what, _cache={}): """ Call :program:`pg_config` and return its output. """ if what in _cache: return _cache[what] logger.debug("running pg_config --%s", what) cmdline = [self.get_pg_config(), "--%s" % what] p = self.popen(cmdline, stdout=PIPE) out, err = p.communicate() if p.returncode: raise ProcessError( _("command returned %s: %s") % (p.returncode, cmdline) ) out = out.rstrip().decode('utf-8') rv = _cache[what] = out return rv def get_pg_config(self): """ Return the absolute path of the pg_config binary. """ pg_config = self.opts.pg_config if os.path.split(pg_config)[0]: pg_config = os.path.abspath(pg_config) else: pg_config = find_executable(pg_config) if not pg_config: raise PgxnClientException(_("pg_config executable not found")) return pg_config class WithMake(WithPgConfig): """ Mixin to implement commands that should invoke :program:`make`. """ @classmethod def customize_parser(self, parser, subparsers, **kwargs): """ Add the ``--make`` option to the options parser. """ subp = super(WithMake, self).customize_parser( parser, subparsers, **kwargs ) subp.add_argument( '--make', metavar="PROG", default=self._find_default_make(), help=_( "the 'make' executable to use to build the extension " "[default: %(default)s]" ), ) return subp def run_make(self, cmd, dir, env=None, sudo=None): """Invoke make with the selected command. :param cmd: the make target or list of options to pass make :param dir: the direcrory to run the command into :param env: variables to add to the make environment :param sudo: if set, use the provided command/arg to elevate privileges """ # check if the directory contains a makefile for fn in ('GNUmakefile', 'makefile', 'Makefile'): if os.path.exists(os.path.join(dir, fn)): break else: raise PgxnClientException( _("no Makefile found in the extension root") ) cmdline = [] if sudo: cmdline.extend(shlex.split(sudo)) cmdline.extend( [self.get_make(), 'PG_CONFIG=%s' % self.get_pg_config()] ) if isinstance(cmd, six.string_types): cmdline.append(cmd) else: # a list cmdline.extend(cmd) logger.debug(_("running: %s"), cmdline) p = self.popen(cmdline, cwd=dir, shell=False, env=env, close_fds=True) p.communicate() if p.returncode: raise ProcessError( _("command returned %s: %s") % (p.returncode, ' '.join(cmdline)) ) def get_make(self, _cache=[]): """ Return the path of the make binary. """ # the cache is not for performance but to return a consistent value # even if the cwd is changed if _cache: return _cache[0] make = self.opts.make if os.path.split(make)[0]: # At least a relative dir specified. if not os.path.exists(make): raise PgxnClientException( _("make executable not found: %s") % make ) # Convert to abs path to be robust in case the dir is changed. make = os.path.abspath(make) else: # we don't find make here and convert to abs path because it's a # security hole: make may be run under sudo and in this case we # don't want root to execute a make hacked in an user local dir if not find_executable(make): raise PgxnClientException( _("make executable not found: %s") % make ) _cache.append(make) return make @classmethod def _find_default_make(self): for make in ('gmake', 'make'): path = find_executable(make) if path: return make # if nothing was found, fall back on 'gmake'. If it was missing we # will give an error when attempting to use it return 'gmake' class WithSudo(object): """ Mixin to implement commands that may invoke sudo. """ @classmethod def customize_parser(self, parser, subparsers, **kwargs): subp = super(WithSudo, self).customize_parser( parser, subparsers, **kwargs ) g = subp.add_mutually_exclusive_group() g.add_argument( '--sudo', metavar="PROG", const='sudo', nargs="?", help=_( "run PROG to elevate privileges when required" " [default: %(const)s]" ), ) g.add_argument( '--nosudo', dest='sudo', action='store_false', help=_( "never elevate privileges " "(no more needed: for backward compatibility)" ), ) return subp class WithDatabase(object): """ Mixin to implement commands that should communicate to a database. """ @classmethod def customize_parser(self, parser, subparsers, epilog=None, **kwargs): """ Add the options related to database connections. """ epilog = ( _( """ The default database connection options depend on the value of environment variables PGDATABASE, PGHOST, PGPORT, PGUSER. """ ) + (epilog or "") ) subp = super(WithDatabase, self).customize_parser( parser, subparsers, epilog=epilog, **kwargs ) g = subp.add_argument_group(_("database connections options")) g.add_argument( '-d', '--dbname', metavar="DBNAME", help=_("database name to install into"), ) g.add_argument( '-h', '--host', metavar="HOST", help=_("database server host or socket directory"), ) g.add_argument( '-p', '--port', metavar="PORT", type=int, help=_("database server port"), ) g.add_argument( '-U', '--username', metavar="NAME", help=_("database user name") ) return subp def get_psql_options(self): """ Return the cmdline options to connect to the specified database. """ rv = [] if self.opts.dbname: rv.extend(['--dbname', self.opts.dbname]) if self.opts.host: rv.extend(['--host', self.opts.host]) if self.opts.port: rv.extend(['--port', str(self.opts.port)]) if self.opts.username: rv.extend(['--username', self.opts.username]) return rv def get_psql_env(self): """ Return a dict with env variables to connect to the specified db. """ rv = {} if self.opts.dbname: rv['PGDATABASE'] = self.opts.dbname if self.opts.host: rv['PGHOST'] = self.opts.host if self.opts.port: rv['PGPORT'] = str(self.opts.port) if self.opts.username: rv['PGUSER'] = self.opts.username return rv pgxnclient-1.3/pgxnclient/commands/help.py000066400000000000000000000043571354122750200210200ustar00rootroot00000000000000""" pgxnclient -- help commands implementation """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client import os from pgxnclient import get_scripts_dirs, get_public_scripts_dir from pgxnclient.i18n import _, N_ from pgxnclient.utils import emit from pgxnclient.commands import Command 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()) pgxnclient-1.3/pgxnclient/commands/info.py000066400000000000000000000205501354122750200210140ustar00rootroot00000000000000""" pgxnclient -- informative commands implementation """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client import re import logging import textwrap import xml.sax.saxutils as saxutils import six from pgxnclient import SemVer from pgxnclient.i18n import _, N_ from pgxnclient.utils import emit from pgxnclient.errors import NotFound, ResourceNotFound from pgxnclient.commands import Command, WithSpec logger = logging.getLogger('pgxnclient.commands') 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 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('', '') excerpt = excerpt.replace('', '*') excerpt = excerpt.replace('', '*') 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 pgxnclient-1.3/pgxnclient/commands/install.py000066400000000000000000000451161354122750200215340ustar00rootroot00000000000000""" pgxnclient -- installation/loading commands implementation """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client import os import re import shutil import difflib import logging import tempfile from subprocess import PIPE import six from pgxnclient import SemVer from pgxnclient import archive from pgxnclient import network from pgxnclient.i18n import _, N_ from pgxnclient.utils import sha1 from pgxnclient.errors import ( BadChecksum, PgxnClientException, InsufficientPrivileges, ) from pgxnclient.commands import Command, WithDatabase, WithMake, WithPgConfig from pgxnclient.commands import WithSpecUrl, WithSpecLocal, WithSudo from pgxnclient.utils.temp import temp_dir from pgxnclient.utils.strings import Identifier logger = logging.getLogger('pgxnclient.commands') 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 InstallUninstall(WithMake, WithSpecUrl, WithSpecLocal, Command): """ Base class to implement the ``install`` and ``uninstall`` commands. """ def run(self): with temp_dir() as dir: return self._run(dir) def _run(self, dir): spec = self.get_spec() if spec.is_dir(): pdir = os.path.abspath(spec.dirname) elif spec.is_file(): pdir = archive.from_file(spec.filename).unpack(dir) elif not spec.is_local(): self.opts.target = dir fn = Download(self.opts).run() pdir = archive.from_file(fn).unpack(dir) else: assert False self.maybe_run_configure(pdir) self._inun(pdir) def _inun(self, pdir): """Run the specific command, implemented in the subclass.""" raise NotImplementedError def maybe_run_configure(self, dir): fn = os.path.join(dir, 'configure') logger.debug("checking '%s'", fn) if not os.path.exists(fn): return logger.info(_("running configure")) p = self.popen(fn, cwd=dir) p.communicate() if p.returncode: raise PgxnClientException( _("configure failed with return code %s") % p.returncode ) class SudoInstallUninstall(WithSudo, InstallUninstall): """ Installation commands base class supporting sudo operations. """ def run(self): if not self.is_libdir_writable() and not self.opts.sudo: dir = self.call_pg_config('libdir') raise InsufficientPrivileges( _( "PostgreSQL library directory (%s) not writable: " "you should run the program as superuser, or specify " "a 'sudo' program" ) % dir ) return super(SudoInstallUninstall, self).run() def get_sudo_prog(self): if self.is_libdir_writable(): return None # not needed return self.opts.sudo def is_libdir_writable(self): """ Check if the Postgres installation directory is writable. If it is, we will assume that sudo is not required to install/uninstall the library, so the sudo program will not be invoked or its specification will not be required. """ dir = self.call_pg_config('libdir') logger.debug("testing if %s is writable", dir) try: f = tempfile.TemporaryFile(prefix="pgxn-", suffix=".test", dir=dir) f.write(b'test') f.close() except (IOError, OSError): rv = False else: rv = True return rv 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 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 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 LoadUnload( WithPgConfig, WithDatabase, WithSpecUrl, WithSpecLocal, Command ): """ Base class to implement the ``load`` and ``unload`` commands. """ @classmethod def customize_parser(self, parser, subparsers, **kwargs): subp = super(LoadUnload, self).customize_parser( parser, subparsers, **kwargs ) subp.add_argument( '--schema', metavar="SCHEMA", type=Identifier.parse_arg, help=_("use SCHEMA instead of the default schema"), ) subp.add_argument( 'extensions', metavar='EXT', nargs='*', help=_("only specified extensions [default: all]"), ) return subp def get_pg_version(self): """Return the version of the selected database.""" data = self.call_psql('SHOW server_version_num') pgver = self.parse_pg_version(data) logger.debug("PostgreSQL version: %d.%d.%d", *pgver) return pgver def parse_pg_version(self, data): try: return (int(data[:-4]), int(data[-4:-2]), int(data[-2:])) except Exception: raise PgxnClientException( "cannot parse version number from '%s'" % data ) def is_extension(self, name): fn = os.path.join( self.call_pg_config('sharedir'), "extension", name + ".control" ) logger.debug("checking if exists %s", fn) return os.path.exists(fn) def call_psql(self, command): cmdline = [self.find_psql()] cmdline.extend(self.get_psql_options()) if command is not None: cmdline.append('-tAX') # tuple only, unaligned, ignore psqlrc cmdline.extend(['-c', command]) logger.debug("calling %s", cmdline) p = self.popen(cmdline, stdout=PIPE) out, err = p.communicate() if p.returncode: raise PgxnClientException( "psql returned %s running command" % (p.returncode) ) return out.decode('utf-8') def load_sql(self, filename=None, data=None): cmdline = [self.find_psql()] cmdline.extend(self.get_psql_options()) # load via pipe to enable psql commands in the file if not data: logger.debug("loading sql from %s", filename) with open(filename, 'r') as fin: p = self.popen(cmdline, stdin=fin) p.communicate() else: if len(data) > 105: tdata = data[:100] + "..." else: tdata = data logger.debug('running sql command: "%s"', tdata) p = self.popen(cmdline, stdin=PIPE) # for Python 3: just assume default encoding will do if isinstance(data, six.text_type): data = data.encode() p.communicate(data) if p.returncode: raise PgxnClientException( "psql returned %s loading extension" % (p.returncode) ) def find_psql(self): return self.call_pg_config('bindir') + '/psql' def find_sql_file(self, name, sqlfile): # In the extension the sql can be specified with a directory, # butit gets flattened into the target dir by the Makefile sqlfile = os.path.basename(sqlfile) sharedir = self.call_pg_config('sharedir') # TODO: we only check in contrib and in : actually it may be # somewhere else - only the makefile knows! tries = [ name + '/' + sqlfile, sqlfile.rsplit('.', 1)[0] + '/' + sqlfile, 'contrib/' + sqlfile, ] tried = set() for fn in tries: if fn in tried: continue tried.add(fn) fn = sharedir + '/' + fn logger.debug("checking sql file in %s" % fn) if os.path.exists(fn): return fn else: raise PgxnClientException( "cannot find sql file for extension '%s': '%s'" % (name, sqlfile) ) def patch_for_schema(self, fn): """ Patch a sql file to set the schema where the commands are executed. If no schema has been requested, return the data unchanged. Else, ask for confirmation and return the data for a patched file. The schema is only useful for PG < 9.1: for proper PG extensions there is no need to patch the sql. """ schema = self.opts.schema f = open(fn) try: data = f.read() finally: f.close() if not schema: return data self._check_schema_exists(schema) re_path = re.compile(r'SET\s+search_path\s*(?:=|to)\s*([^;]+);', re.I) m = re_path.search(data) if m is None: newdata = ("SET search_path = %s;\n\n" % schema) + data else: newdata = re_path.sub("SET search_path = %s;" % schema, data) diff = ''.join( difflib.unified_diff( [r + '\n' for r in data.splitlines()], [r + '\n' for r in newdata.splitlines()], fn, fn + ".schema", ) ) msg = _( """ In order to operate in the schema %s, the following changes will be performed:\n\n%s\n\nDo you want to continue?""" ) self.confirm(msg % (schema, diff)) return newdata def _register_loaded(self, fn): if not hasattr(self, '_loaded'): self._loaded = [] self._loaded.append(fn) def _is_loaded(self, fn): return hasattr(self, '_loaded') and fn in self._loaded def _check_schema_exists(self, schema): cmdline = [self.find_psql()] cmdline.extend(self.get_psql_options()) cmdline.extend(['-c', 'SET search_path=%s' % schema]) p = self.popen(cmdline, stdin=PIPE, stdout=PIPE, stderr=PIPE) p.communicate() if p.returncode: raise PgxnClientException("schema %s does not exist" % schema) def _get_extensions(self): """ Return a list of pairs (name, sql file) to be loaded/unloaded. Items are in loading order. """ spec = self.get_spec() dist = self.get_meta(spec) if 'provides' not in dist: # No 'provides' specified: assume a single extension named # after the distribution. This is automatically done by PGXN, # but we should do ourselves to deal with local META files # not mangled by the PGXN upload script yet. name = dist['name'] for ext in self.opts.extensions: if ext != name: raise PgxnClientException( "can't find extension '%s' in the distribution '%s'" % (name, spec) ) return [(name, None)] rv = [] if not self.opts.extensions: # All the extensions, in the order specified # (assume we got an orddict from json) for name, data in dist['provides'].items(): rv.append((name, data.get('file'))) else: # Only the specified extensions for name in self.opts.extensions: try: data = dist['provides'][name] except KeyError: raise PgxnClientException( "can't find extension '%s' in the distribution '%s'" % (name, spec) ) rv.append((name, data.get('file'))) return rv 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 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) pgxnclient-1.3/pgxnclient/errors.py000066400000000000000000000025351354122750200175770ustar00rootroot00000000000000""" pgxnclient -- package exceptions These exceptions can be used to signal expected problems and to exit in a controlled way from the program. """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client class PgxnException(Exception): """Base class for the exceptions known in the pgxn package.""" class PgxnClientException(PgxnException): """Base class for the exceptions raised by the pgxnclient package.""" class UserAbort(PgxnClientException): """The user requested to stop the operation.""" class BadSpecError(PgxnClientException): """A bad package specification.""" class ProcessError(PgxnClientException): """An error raised calling an external program.""" class InsufficientPrivileges(PgxnClientException): """Operation will fail because the user is too lame.""" class NotFound(PgxnException): """Something requested by the user not found on PGXN""" class NetworkError(PgxnClientException): """An error from the other side of the wire.""" class BadChecksum(PgxnClientException): """A downloaded file is not what expected.""" class ResourceNotFound(NetworkError): """Resource not found on the server.""" class BadRequestError(Exception): """Bad request from our side. This exception is a basic one because it should be rased upon an error on our side. """ pgxnclient-1.3/pgxnclient/i18n.py000066400000000000000000000004721354122750200170400ustar00rootroot00000000000000""" pgxnclient -- internationalization support """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client def gettext(msg): # TODO: real l10n return msg _ = gettext def N_(msg): """Designate a string to be found by gettext but not to be translated.""" return msg pgxnclient-1.3/pgxnclient/libexec/000077500000000000000000000000001354122750200173175ustar00rootroot00000000000000pgxnclient-1.3/pgxnclient/libexec/README000066400000000000000000000013521354122750200202000ustar00rootroot00000000000000This directory contains the PGXN Client builtin commands. If you want to extend the client with your own command, please use the directory returned by ``pgxn help --libexec``. See the documentation for further information about how to create new commands. Note that setuptools doesn't do a perfect job and replaces the links with the script content, dropping the executable flag. If you are packaging pgxnclient for a distribution, you may use soft/hard links instead. The location of this directory may also be changed if your distribution policies prefer a better location (e.g. ``/usr/lib/pgxnclient/libexec``...): in this case change the `LIBEXECDIR` constant in ``pgxnclient/__init__.py`` with the absolute path of the scripts directory. pgxnclient-1.3/pgxnclient/libexec/pgxn-check000077500000000000000000000003031354122750200212700ustar00rootroot00000000000000#!/usr/bin/env python """ pgxnclient -- command line interface """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client from pgxnclient.cli import script script() pgxnclient-1.3/pgxnclient/libexec/pgxn-download000077500000000000000000000003031354122750200220220ustar00rootroot00000000000000#!/usr/bin/env python """ pgxnclient -- command line interface """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client from pgxnclient.cli import script script() pgxnclient-1.3/pgxnclient/libexec/pgxn-help000077500000000000000000000003031354122750200211430ustar00rootroot00000000000000#!/usr/bin/env python """ pgxnclient -- command line interface """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client from pgxnclient.cli import script script() pgxnclient-1.3/pgxnclient/libexec/pgxn-info000077500000000000000000000003031354122750200211460ustar00rootroot00000000000000#!/usr/bin/env python """ pgxnclient -- command line interface """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client from pgxnclient.cli import script script() pgxnclient-1.3/pgxnclient/libexec/pgxn-install000077500000000000000000000003031354122750200216610ustar00rootroot00000000000000#!/usr/bin/env python """ pgxnclient -- command line interface """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client from pgxnclient.cli import script script() pgxnclient-1.3/pgxnclient/libexec/pgxn-load000077500000000000000000000003031354122750200211320ustar00rootroot00000000000000#!/usr/bin/env python """ pgxnclient -- command line interface """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client from pgxnclient.cli import script script() pgxnclient-1.3/pgxnclient/libexec/pgxn-mirror000077500000000000000000000003031354122750200215250ustar00rootroot00000000000000#!/usr/bin/env python """ pgxnclient -- command line interface """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client from pgxnclient.cli import script script() pgxnclient-1.3/pgxnclient/libexec/pgxn-search000077500000000000000000000003031354122750200214600ustar00rootroot00000000000000#!/usr/bin/env python """ pgxnclient -- command line interface """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client from pgxnclient.cli import script script() pgxnclient-1.3/pgxnclient/libexec/pgxn-uninstall000077500000000000000000000003031354122750200222240ustar00rootroot00000000000000#!/usr/bin/env python """ pgxnclient -- command line interface """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client from pgxnclient.cli import script script() pgxnclient-1.3/pgxnclient/libexec/pgxn-unload000077500000000000000000000003031354122750200214750ustar00rootroot00000000000000#!/usr/bin/env python """ pgxnclient -- command line interface """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client from pgxnclient.cli import script script() pgxnclient-1.3/pgxnclient/network.py000066400000000000000000000056041354122750200177540ustar00rootroot00000000000000""" pgxnclient -- network interaction """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client import os from six.moves.urllib.request import build_opener from six.moves.urllib.error import HTTPError, URLError from six.moves.urllib.parse import urlsplit from itertools import count from contextlib import closing from pgxnclient import __version__ from pgxnclient.i18n import _ from pgxnclient.errors import ( PgxnClientException, NetworkError, ResourceNotFound, BadRequestError, ) import logging logger = logging.getLogger('pgxnclient.network') def get_file(url): opener = build_opener() opener.addheaders = [('User-agent', 'pgxnclient/%s' % __version__)] logger.debug('opening url: %s', url) try: return closing(opener.open(url)) except HTTPError as e: if e.code == 404: raise ResourceNotFound(_("resource not found: '%s'") % e.url) elif e.code == 400: raise BadRequestError(_("bad request on '%s'") % e.url) elif e.code == 500: raise NetworkError(_("server error")) elif e.code == 503: raise NetworkError(_("service unavailable")) else: raise NetworkError( _("unexpected response %d for '%s'") % (e.code, e.url) ) except URLError as e: raise NetworkError(_("network error: %s") % e.reason) def get_local_file_name(target, url): """Return a good name for a local file. If *target* is a dir, make a name out of the url. Otherwise return target itself. Always return an absolute path. """ if os.path.isdir(target): basename = urlsplit(url)[2].rsplit('/', 1)[-1] fn = os.path.join(target, basename) else: fn = target return os.path.abspath(fn) def download(f, fn, rename=True): """Download a file locally. :param f: open file to read :param fn: name of the file to write. If a dir, save into it. :param rename: if true and a file *fn* exist, rename the downloaded file adding a prefix ``-1``, ``-2``... before the extension. Return the name of the file saved. """ if os.path.isdir(fn): fn = get_local_file_name(fn, f.url) if rename: if os.path.exists(fn): base, ext = os.path.splitext(fn) for i in count(1): logger.debug(_("file %s exists"), fn) fn = "%s-%d%s" % (base, i, ext) if not os.path.exists(fn): break logger.info(_("saving %s"), fn) try: fout = open(fn, "wb") except Exception as e: raise PgxnClientException( _("cannot open target file: %s: %s") % (e.__class__.__name__, e) ) try: while 1: data = f.read(8192) if not data: break fout.write(data) finally: fout.close() return fn pgxnclient-1.3/pgxnclient/spec.py000066400000000000000000000064731354122750200172220ustar00rootroot00000000000000""" pgxnclient -- specification object """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client import os import re from six.moves.urllib.parse import unquote_plus import operator as _op from pgxnclient.i18n import _ from pgxnclient.errors import BadSpecError, ResourceNotFound from pgxnclient.utils.semver import SemVer from pgxnclient.utils.strings import Term class Spec(object): """A name together with a range of versions.""" # Available release statuses. # Order matters. UNSTABLE = 0 TESTING = 1 STABLE = 2 STATUS = {'unstable': UNSTABLE, 'testing': TESTING, 'stable': STABLE} def __init__( self, name=None, op=None, ver=None, dirname=None, filename=None, url=None, ): self.name = name and name.lower() self.op = op self.ver = ver # 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 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 self.url or "???" if self.op is None: return name else: return "%s%s%s" % (name, self.op, self.ver) @classmethod def parse(self, spec): """Parse a spec string into a populated Spec instance. Raise BadSpecError if couldn't parse. """ # check if it's a network resource if spec.startswith('http://') or spec.startswith('https://'): return Spec(url=spec) # check if it's a local resource if spec.startswith('file://'): try_file = unquote_plus(spec[len('file://') :]) elif os.sep in spec: try_file = spec else: try_file = None if try_file: # This is a local thing, let's see what if os.path.isdir(try_file): return Spec(dirname=try_file) elif os.path.exists(try_file): return Spec(filename=try_file) else: raise ResourceNotFound(_("cannot find '%s'") % try_file) # so we think it's a PGXN spec # split operator/version and name m = re.match(r'(.+?)(?:(==|=|>=|>|<=|<)(.*))?$', spec) if m is None: raise BadSpecError( _("bad format for version specification: '%s'"), spec ) name = Term(m.group(1)) op = m.group(2) if op == '=': op = '==' if op is not None: ver = SemVer.clean(m.group(3)) else: ver = None return Spec(name, op, ver) def accepted( self, version, _map={ '==': _op.eq, '<=': _op.le, '<': _op.lt, '>=': _op.ge, '>': _op.gt, }, ): """Return True if the given version is accepted in the spec.""" if self.op is None: return True return _map[self.op](version, self.ver) pgxnclient-1.3/pgxnclient/tar.py000066400000000000000000000034661354122750200170550ustar00rootroot00000000000000""" pgxnclient -- tar file utilities """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client import os import tarfile from pgxnclient.i18n import _ from pgxnclient.errors import PgxnClientException from pgxnclient.archive import Archive import logging logger = logging.getLogger('pgxnclient.tar') class TarArchive(Archive): """Handle .tar archives""" _file = None def can_open(self): return tarfile.is_tarfile(self.filename) def open(self): assert not self._file, "archive already open" try: self._file = tarfile.open(self.filename, 'r') except Exception as e: raise PgxnClientException( _("cannot open archive '%s': %s") % (self.filename, e) ) def close(self): if self._file is not None: self._file.close() self._file = None def list_files(self): assert self._file, "archive not open" return self._file.getnames() def read(self, fn): assert self._file, "archive not open" return self._file.extractfile(fn).read() def unpack(self, destdir): tarname = self.filename logger.info(_("unpacking: %s"), tarname) destdir = os.path.abspath(destdir) self.open() try: for fn in self.list_files(): fname = os.path.abspath(os.path.join(destdir, fn)) if not fname.startswith(destdir): raise PgxnClientException( _("archive file '%s' trying to escape!") % fname ) self._file.extractall(path=destdir) finally: self.close() return self._find_work_directory(destdir) def unpack(filename, destdir): return TarArchive(filename).unpack(destdir) pgxnclient-1.3/pgxnclient/utils/000077500000000000000000000000001354122750200170445ustar00rootroot00000000000000pgxnclient-1.3/pgxnclient/utils/__init__.py000066400000000000000000000032141354122750200211550ustar00rootroot00000000000000""" pgxnclient -- misc utilities package """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client from __future__ import print_function __all__ = ['emit', 'load_json', 'load_jsons', 'sha1', 'find_executable'] import os import sys import json from collections import OrderedDict # Import the sha1 object without warnings from hashlib import sha1 import six def emit(s=b'', file=None): """ Print a string Easy yes? No. Because if the string is unicode and we are piping stdout into something we end up with sys.stdout.encoding = None and Python trying to use ascii to encode, barfing on the first accented letter (hello Jan). """ if file is None: file = sys.stdout enc = file.encoding or 'ascii' if isinstance(s, six.text_type): s = s.encode(enc, 'replace') # OTOH, printing bytes on Py3 to stdout/stderr will barf as well... # It's facepalms all the way down. if hasattr(file, 'buffer'): file = file.buffer file.write(s) file.write(b'\n') def load_json(f): data = f.read() if not isinstance(data, six.text_type): data = data.decode('utf-8') return load_jsons(data) def load_jsons(data): return json.loads(data, object_pairs_hook=OrderedDict) def find_executable(name): """ Find executable by ``name`` by inspecting PATH environment variable, return ``None`` if nothing found. """ for dir in os.environ.get('PATH', '').split(os.pathsep): if not dir: continue fn = os.path.abspath(os.path.join(dir, name)) if os.path.exists(fn): return os.path.abspath(fn) pgxnclient-1.3/pgxnclient/utils/semver.py000066400000000000000000000111561354122750200207230ustar00rootroot00000000000000""" SemVer -- (not quite) semantic version specification http://semver.org/ IMPORTANT: don't trust this implementation. And don't trust SemVer AT ALL. We have a bloody mess because the specification changed after being published and after several extension had been uploaded with a version number that suddenly had become no more valid. https://github.com/mojombo/semver.org/issues/49 My plea for forking the spec and keep our schema has been ignored. So this module only tries to make sure people can use PGXN, not to be conform to an half-aborted specification. End of rant. This implementation is conform to the SemVer 0.3.0 implementation by David Wheeler (http://pgxn.org/dist/semver/0.3.0/) and passes all its unit test. Note that it is slightly non conform to the original specification, as the trailing part should be compared in ascii order while our comparison is not case sensitive. David has already stated that the meaning is independent on the case (http://blog.pgxn.org/post/4948135198/case-insensitivity) and I'm fine with that: the important thing is that the client and the server understand each other. """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client import re import operator from pgxnclient.i18n import _ class SemVer(str): """A string representing a semantic version number. Non valid version numbers raise ValueError. """ def __new__(cls, value): self = str.__new__(cls, value) self.tuple = SemVer.parse(value) return self @property def major(self): return self.tuple[0] @property def minor(self): return self.tuple[1] @property def patch(self): return self.tuple[2] @property def trail(self): return self.tuple[3] def __repr__(self): return "%s(%r)" % (self.__class__.__name__, str(self)) def __eq__(self, other): if isinstance(other, SemVer): return ( self.tuple[:3] == other.tuple[:3] and self.tuple[3].lower() == other.tuple[3].lower() ) elif isinstance(other, str): return self == SemVer(other) else: return NotImplemented def __ne__(self, other): return not self == other def __hash__(self): return hash(self.tuple[:3] + (self.tuple[3].lower(),)) def _ltgt(self, other, op): if isinstance(other, SemVer): t1 = self.tuple[:3] t2 = other.tuple[:3] if t1 != t2: return op(t1, t2) s1 = self.tuple[3].lower() s2 = other.tuple[3].lower() if s1 == s2: return False if s1 and s2: return op(s1, s2) return op(bool(s2), bool(s1)) elif isinstance(other, str): return op(self, SemVer(other)) else: return NotImplemented def __lt__(self, other, op=operator.lt): return self._ltgt(other, operator.lt) def __gt__(self, other): return self._ltgt(other, operator.gt) def __ge__(self, other): return not self < other def __le__(self, other): return not other < self @classmethod def parse(self, s): """ Split a valid version number in components (major, minor, patch, trail). """ m = re_semver.match(s) if m is None: raise ValueError(_("bad version number: '%s'") % s) maj, min, patch, trail = m.groups() if not patch: patch = 0 if not trail: trail = '' return (int(maj), int(min), int(patch), trail) @classmethod def clean(self, s): """ Convert an invalid but still recognizable version number into a SemVer. """ m = re_clean.match(s.strip()) if m is None: raise ValueError(_("bad version number: '%s' - can't clean") % s) maj, min, patch, trail = m.groups() maj = maj and int(maj) or 0 min = min and int(min) or 0 patch = patch and int(patch) or 0 trail = trail and '-' + trail.strip() or '' return "%d.%d.%d%s" % (maj, min, patch, trail) re_semver = re.compile( r""" ^ (0|[1-9][0-9]*) \. (0|[1-9][0-9]*) \. (0|[1-9][0-9]*) (?: -? # should be mandatory, but see rant above ([a-z][a-z0-9-]*) )? $ """, re.IGNORECASE | re.VERBOSE, ) re_clean = re.compile( r""" ^ ([0-9]+)? \.? ([0-9]+)? \.? ([0-9]+)? (?: -? \s* ([a-z][a-z0-9-]*) )? $ """, re.IGNORECASE | re.VERBOSE, ) pgxnclient-1.3/pgxnclient/utils/strings.py000066400000000000000000000055331354122750200211150ustar00rootroot00000000000000""" Strings -- implementation of a few specific string subclasses. """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client import re from pgxnclient.i18n import _ from argparse import ArgumentTypeError class CIStr(str): """ A case preserving string with non case-sensitive comparison. """ def __eq__(self, other): if isinstance(other, CIStr): return self.lower() == other.lower() else: return NotImplemented def __ne__(self, other): return not self == other def __lt__(self, other): if isinstance(other, CIStr): return self.lower() < other.lower() else: return NotImplemented def __gt__(self, other): return other < self def __le__(self, other): return not other < self def __ge__(self, other): return not self < other class Label(CIStr): """A string following the rules in RFC 1034. Labels can then be used as host names in domains. https://tools.ietf.org/html/rfc1034 "The labels must follow the rules for ARPANET host names. They must start with a letter, end with a letter or digit, and have as interior characters only letters, digits, and hyphen. There are also some restrictions on the length. Labels must be 63 characters or less." """ def __new__(cls, value): if not Label._re_chk.match(value): raise ValueError(_("bad label: '%s'") % value) return CIStr.__new__(cls, value) _re_chk = re.compile(r'^[a-z]([-a-z0-9]{0,61}[a-z0-9])?$', re.IGNORECASE) class Term(CIStr): r""" A Term is a subtype of String that must be at least two characters long contain no slash (/), backslash (\), control, or space characters. See https://pgxn.org/spec#Term """ def __new__(cls, value): if not Term._re_chk.match(value) or min(map(ord, value)) < 32: raise ValueError(_("not a valid term term: '%s'") % value) return CIStr.__new__(cls, value) _re_chk = re.compile(r'^[^\s/\\]{2,}$') class Identifier(CIStr): """ A string modeling a PostgreSQL identifier. """ def __new__(cls, value): if not value: raise ValueError("PostgreSQL identifiers cannot be blank") if not Identifier._re_chk.match(value): value = '"%s"' % value.replace('"', '""') # TODO: identifier are actually case sensitive if quoted return CIStr.__new__(cls, value) _re_chk = re.compile(r'^[a-z_][a-z0-9_\$]*$', re.IGNORECASE) @classmethod def parse_arg(self, s): """ Parse an Identifier from a command line argument. """ try: return Identifier(s) except ValueError as e: # shouldn't happen anymore as we quote invalid identifiers raise ArgumentTypeError(e) pgxnclient-1.3/pgxnclient/utils/temp.py000066400000000000000000000005461354122750200203700ustar00rootroot00000000000000""" pgxnclient -- temp files utilities """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client import shutil import tempfile import contextlib @contextlib.contextmanager def temp_dir(): """Context manager to create a temp dir and delete after usage.""" dir = tempfile.mkdtemp() yield dir shutil.rmtree(dir) pgxnclient-1.3/pgxnclient/utils/uri.py000077500000000000000000000213331354122750200202220ustar00rootroot00000000000000#!/usr/bin/env python """ Simple implementation of URI-Templates (http://bitworking.org/projects/URI-Templates/). Some bits are inspired by or based on: * Joe Gregorio's example implementation (http://code.google.com/p/uri-templates/) * Addressable (http://addressable.rubyforge.org/) Simple usage:: >>> from pgxnclient.utils import uri >>> args = {'foo': 'it worked'} >>> uri.expand_template("http://example.com/{foo}", args) 'http://example.com/it%20worked' >>> args = {'a':'foo', 'b':'bar', 'a_b':'baz'} >>> uri.expand_template("http://example.org/{a}{b}/{a_b}", args) 'http://example.org/foobar/baz' You can also use keyword arguments for a more pythonic style:: >>> uri.expand_template("http://example.org/?q={a}", a="foo") 'http://example.org/?q=foo' """ import re import six from six.moves.urllib.parse import quote __all__ = ["expand_template", "TemplateSyntaxError"] class TemplateSyntaxError(Exception): pass _template_pattern = re.compile(r"{([^}]+)}") def expand_template(template, values={}, **kwargs): """Expand a URI template.""" values = values.copy() values.update(kwargs) values = percent_encode(values) return _template_pattern.sub(lambda m: _handle_match(m, values), template) def _handle_match(match, values): op, arg, variables = parse_expansion(match.group(1)) if op: try: return getattr(_operators, op)(variables, arg, values) except AttributeError: raise TemplateSyntaxError("Unexpected operator: %r" % op) else: assert len(variables) == 1 key, default = list(variables.items())[0] return values.get(key, default) # # Parse an expansion # Adapted directly from the spec (Appendix A); extra validation has been added # to make it pass all the tests. # _varname_pattern = re.compile(r"^[A-Za-z0-9]\w*$") def parse_expansion(expansion): """ Parse an expansion -- the part inside {curlybraces} -- into its component parts. Returns a tuple of (operator, argument, variabledict). For example:: >>> parse_expansion("-join|&|a,b,c=1")[0:2] ('join', '&') >>> sorted(parse_expansion("-join|&|a,b,c=1")[2].items()) [('a', None), ('b', None), ('c', '1')] >>> parse_expansion("c=1") (None, None, {'c': '1'}) """ if "|" in expansion: (op, arg, vars_) = expansion.split("|") op = op[1:] else: (op, arg, vars_) = (None, None, expansion) vars_ = vars_.split(",") variables = {} for var in vars_: if "=" in var: (varname, vardefault) = var.split("=") if not vardefault: raise TemplateSyntaxError("Invalid variable: %r" % var) else: (varname, vardefault) = (var, None) if not _varname_pattern.match(varname): raise TemplateSyntaxError("Invalid variable: %r" % varname) variables[varname] = vardefault return (op, arg, variables) # # Encode an entire dictionary of values # def percent_encode(values): rv = {} for k, v in values.items(): if isinstance(v, six.string_types): rv[k] = quote(v) else: rv[k] = [quote(s) for s in v] return rv # # Operators; see Section 3.3. # Shoved into a class just so we have an ad hoc namespace. # class _operators(object): @staticmethod def opt(variables, arg, values): for k in variables.keys(): v = values.get(k, None) if v is None or ( not isinstance(v, six.string_types) and len(v) == 0 ): continue else: return arg return "" @staticmethod def neg(variables, arg, values): if _operators.opt(variables, arg, values): return "" else: return arg @staticmethod def listjoin(variables, arg, values): k = list(variables.keys())[0] return arg.join(values.get(k, [])) @staticmethod def join(variables, arg, values): return arg.join( [ "%s=%s" % (k, values.get(k, default)) for k, default in variables.items() if values.get(k, default) is not None ] ) @staticmethod def prefix(variables, arg, values): k, default = list(variables.items())[0] v = values.get(k, default) if v is not None and len(v) > 0: return arg + v else: return "" @staticmethod def append(variables, arg, values): k, default = list(variables.items())[0] v = values.get(k, default) if v is not None and len(v) > 0: return v + arg else: return "" # # A bunch more tests that don't rightly fit in docstrings elsewhere # Taken from Joe Gregorio's template_parser.py. # _test_pre = """ >>> expand_template('{foo}', {}) '' >>> expand_template('{foo}', {'foo': 'barney'}) 'barney' >>> expand_template('{foo=wilma}', {}) 'wilma' >>> expand_template('{foo=wilma}', {'foo': 'barney'}) 'barney' >>> expand_template('{-prefix|&|foo}', {}) '' >>> expand_template('{-prefix|&|foo=wilma}', {}) '&wilma' >>> expand_template('{-prefix||foo=wilma}', {}) 'wilma' >>> expand_template('{-prefix|&|foo=wilma}', {'foo': 'barney'}) '&barney' >>> expand_template('{-append|/|foo}', {}) '' >>> expand_template('{-append|#|foo=wilma}', {}) 'wilma#' >>> expand_template('{-append|&?|foo=wilma}', {'foo': 'barney'}) 'barney&?' >>> expand_template('{-join|/|foo}', {}) '' >>> expand_template('{-join|/|foo,bar}', {}) '' >>> expand_template('{-join|&|q,num}', {}) '' >>> expand_template('{-join|#|foo=wilma}', {}) 'foo=wilma' >>> expand_template('{-join|#|foo=wilma,bar}', {}) 'foo=wilma' >>> expand_template('{-join|&?|foo=wilma}', {'foo': 'barney'}) 'foo=barney' >>> expand_template('{-listjoin|/|foo}', {}) '' >>> expand_template('{-listjoin|/|foo}', {'foo': ['a', 'b']}) 'a/b' >>> expand_template('{-listjoin||foo}', {'foo': ['a', 'b']}) 'ab' >>> expand_template('{-listjoin|/|foo}', {'foo': ['a']}) 'a' >>> expand_template('{-listjoin|/|foo}', {'foo': []}) '' >>> expand_template('{-opt|&|foo}', {}) '' >>> expand_template('{-opt|&|foo}', {'foo': 'fred'}) '&' >>> expand_template('{-opt|&|foo}', {'foo': []}) '' >>> expand_template('{-opt|&|foo}', {'foo': ['a']}) '&' >>> expand_template('{-opt|&|foo,bar}', {'foo': ['a']}) '&' >>> expand_template('{-opt|&|foo,bar}', {'bar': 'a'}) '&' >>> expand_template('{-opt|&|foo,bar}', {}) '' >>> expand_template('{-neg|&|foo}', {}) '&' >>> expand_template('{-neg|&|foo}', {'foo': 'fred'}) '' >>> expand_template('{-neg|&|foo}', {'foo': []}) '&' >>> expand_template('{-neg|&|foo}', {'foo': ['a']}) '' >>> expand_template('{-neg|&|foo,bar}', {'bar': 'a'}) '' >>> expand_template('{-neg|&|foo,bar}', {'bar': []}) '&' >>> expand_template('{foo}', {'foo': ' '}) '%20' >>> expand_template('{-listjoin|&|foo}', {'foo': ['&', '&', '|', '_']}) '%26&%26&%7C&_' # Extra hoops to deal with unpredictable dict ordering >>> expand_template('{-join|#|foo=wilma,bar=barney}', {}) in \ ('bar=barney#foo=wilma', 'foo=wilma#bar=barney') True """ _syntax_errors = """ >>> expand_template("{fred=}") # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TemplateSyntaxError: Invalid variable: 'fred=' >>> expand_template("{f:}") # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TemplateSyntaxError: Invalid variable: 'f:' >>> expand_template("{f<}") # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TemplateSyntaxError: Invalid variable: 'f<' >>> expand_template("{<:}") # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TemplateSyntaxError: Invalid variable: '<:' >>> expand_template("{<:fred,barney}") # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TemplateSyntaxError: Invalid variable: '<:fred' >>> expand_template("{>:}") # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TemplateSyntaxError: Invalid variable: '>:' >>> expand_template("{>:fred,barney}") # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TemplateSyntaxError: Invalid variable: '>:fred' """ __test__ = {"test_pre": _test_pre, "syntax_errors": _syntax_errors} if __name__ == '__main__': import doctest doctest.testmod() pgxnclient-1.3/pgxnclient/zip.py000066400000000000000000000055651354122750200170730ustar00rootroot00000000000000""" pgxnclient -- zip file utilities """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client import os import stat import zipfile from pgxnclient.i18n import _ from pgxnclient.errors import PgxnClientException from pgxnclient.archive import Archive import logging logger = logging.getLogger('pgxnclient.zip') class ZipArchive(Archive): """Handle .zip archives""" _file = None def can_open(self): return zipfile.is_zipfile(self.filename) def open(self): assert not self._file, "archive already open" try: self._file = zipfile.ZipFile(self.filename, 'r') except Exception as e: raise PgxnClientException( _("cannot open archive '%s': %s") % (self.filename, e) ) def close(self): if self._file is not None: self._file.close() self._file = None def list_files(self): assert self._file, "archive not open" return self._file.namelist() def read(self, fn): assert self._file, "archive not open" return self._file.read(fn) def unpack(self, destdir): zipname = self.filename logger.info(_("unpacking: %s"), zipname) destdir = os.path.abspath(destdir) self.open() try: for fn in self.list_files(): fname = os.path.abspath(os.path.join(destdir, fn)) if not fname.startswith(destdir): raise PgxnClientException( _("archive file '%s' trying to escape!") % fname ) # Looks like checking for a trailing / is the only way to # tell if the file is a directory. if fn.endswith('/'): os.makedirs(fname) continue # The directory is not always explicitly present in the archive if not os.path.exists(os.path.dirname(fname)): os.makedirs(os.path.dirname(fname)) # Copy the file content logger.debug(_("saving: %s"), fname) fout = open(fname, "wb") try: data = self.read(fn) # In order to restore the executable bit, I haven't find # anything that looks like an executable flag in the zipinfo, # so look at the hashbangs... isexec = data[:2] == b'#!' fout.write(data) finally: fout.close() if isexec: os.chmod( fname, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC ) finally: self.close() return self._find_work_directory(destdir) def unpack(filename, destdir): return ZipArchive(filename).unpack(destdir) pgxnclient-1.3/pyproject.toml000066400000000000000000000002021354122750200164370ustar00rootroot00000000000000[tool.black] line-length=79 skip-string-normalization=true exclude = ''' /( env | \.git | \.eggs | buld )/ ''' pgxnclient-1.3/setup.cfg000066400000000000000000000000261354122750200153500ustar00rootroot00000000000000[aliases] test=pytest pgxnclient-1.3/setup.py000066400000000000000000000072671354122750200152570ustar00rootroot00000000000000#!/usr/bin/env python """ pgxnclient -- setup script """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client import os import sys from setuptools import setup, find_packages from setuptools.command.build_py import build_py here = os.path.dirname(__file__) # Grab the version without importing the module # or we will get import errors on install if prerequisites are still missing with open(os.path.join(here, 'pgxnclient', '__init__.py')) as f: for line in f: if line.startswith('__version__ ='): version = line.split("'")[1] break else: raise ValueError('cannot find __version__ in the pgxnclient module') # Read the description from the readme with open(os.path.join(here, 'README.rst')) as f: long_description = f.read() # External dependencies, depending on the Python version requires = ['six'] setup_requires = ['pytest-runner'] tests_require = ['mock', 'pytest'] if sys.version_info < (2, 7): raise ValueError("PGXN client requires at least Python 2.7") if (3,) < sys.version_info < (3, 4): raise ValueError("PGXN client requires at least Python 3.4") classifiers = """ Development Status :: 5 - Production/Stable Environment :: Console Intended Audience :: Developers Intended Audience :: System Administrators License :: OSI Approved :: BSD License Operating System :: POSIX Programming Language :: Python :: 2 Programming Language :: Python :: 3 Topic :: Database """ class CustomBuildPy(build_py): def run(self): build_py.run(self) self.fix_libexec_hashbangs() def fix_libexec_hashbangs(self): """Replace the hashbangs of the scripts in libexec.""" for package, src_dir, build_dir, filenames in self.data_files: if package != 'pgxnclient': continue for filename in filenames: if not filename.startswith('libexec/'): continue self.fix_script_hashbang(os.path.join(build_dir, filename)) def fix_script_hashbang(self, filename): """Replace the hashbangs of a script in libexec.""" if not os.path.exists(filename): return with open(filename) as f: data = f.read() if not data.startswith('#!'): return lines = data.splitlines() if 'python' not in lines[0]: return lines[0] = '#!%s' % sys.executable with open(filename, 'w') as f: for line in lines: f.write(line) f.write('\n') setup( name='pgxnclient', description=( 'A command line tool to interact with the PostgreSQL Extension Network.' ), long_description=long_description, author='Daniele Varrazzo', author_email='daniele.varrazzo@gmail.com', url='https://github.com/pgxn/pgxnclient', project_urls={ 'Source': 'https://github.com/pgxn/pgxnclient', 'Documentation': 'https://pgxn.github.io/pgxnclient/', 'Discussion group': 'https://groups.google.com/group/pgxn-users/', }, license='BSD', # NOTE: keep consistent with docs/install.txt python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*', packages=find_packages(), package_data={'pgxnclient': ['libexec/*']}, entry_points={ 'console_scripts': [ 'pgxn = pgxnclient.cli:command_dispatch', 'pgxnclient = pgxnclient.cli:script', ] }, classifiers=[x for x in classifiers.split('\n') if x], zip_safe=False, # because we dynamically look for commands install_requires=requires, setup_requires=setup_requires, tests_require=tests_require, version=version, cmdclass={'build_py': CustomBuildPy}, ) pgxnclient-1.3/tests/000077500000000000000000000000001354122750200146735ustar00rootroot00000000000000pgxnclient-1.3/tests/__init__.py000066400000000000000000000011251354122750200170030ustar00rootroot00000000000000""" pgxnclient -- test suite package The test suite can be run via setup.py test. But you better use "make check" in order to correctly set up the pythonpath. """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client import unittest # fix unittest maintainers stubborness: see Python issue #9424 if unittest.TestCase.assert_ is not unittest.TestCase.assertTrue: # Vaffanculo, Wolf unittest.TestCase.assert_ = unittest.TestCase.assertTrue unittest.TestCase.assertEquals = unittest.TestCase.assertEqual if __name__ == '__main__': unittest.main() pgxnclient-1.3/tests/test_archives.py000066400000000000000000000036351354122750200201170ustar00rootroot00000000000000import unittest from pgxnclient import tar from pgxnclient import zip from pgxnclient import archive from pgxnclient.errors import PgxnClientException from .testutils import get_test_filename class TestArchive(unittest.TestCase): def test_from_file_zip(self): fn = get_test_filename('foobar-0.42.1.zip') a = archive.from_file(fn) self.assert_(isinstance(a, zip.ZipArchive)) self.assertEqual(a.filename, fn) def test_from_file_tar(self): fn = get_test_filename('foobar-0.42.1.tar.gz') a = archive.from_file(fn) self.assert_(isinstance(a, tar.TarArchive)) self.assertEqual(a.filename, fn) def test_from_file_unknown(self): fn = get_test_filename('META-manyext.json') self.assertRaises(PgxnClientException, archive.from_file, fn) class TestZipArchive(unittest.TestCase): def test_can_open(self): fn = get_test_filename('foobar-0.42.1.zip') a = zip.ZipArchive(fn) self.assert_(a.can_open()) a.open() a.close() def test_can_open_noext(self): fn = get_test_filename('zip.ext') a = zip.ZipArchive(fn) self.assert_(a.can_open()) a.open() a.close() def test_cannot_open(self): fn = get_test_filename('foobar-0.42.1.tar.gz') a = zip.ZipArchive(fn) self.assert_(not a.can_open()) class TestTarArchive(unittest.TestCase): def test_can_open(self): fn = get_test_filename('foobar-0.42.1.tar.gz') a = tar.TarArchive(fn) self.assert_(a.can_open()) a.open() a.close() def test_can_open_noext(self): fn = get_test_filename('tar.ext') a = tar.TarArchive(fn) self.assert_(a.can_open()) a.open() a.close() def test_cannot_open(self): fn = get_test_filename('foobar-0.42.1.zip') a = tar.TarArchive(fn) self.assert_(not a.can_open()) pgxnclient-1.3/tests/test_commands.py000066400000000000000000001051741354122750200201150ustar00rootroot00000000000000import os import shutil import tempfile import unittest from mock import patch, Mock from six.moves.urllib.parse import quote from pgxnclient.tar import TarArchive from pgxnclient.zip import ZipArchive from pgxnclient.errors import ( PgxnClientException, ResourceNotFound, InsufficientPrivileges, ) from .testutils import ifunlink, get_test_filename class FakeFile(object): def __init__(self, *args): self._f = open(*args) self.url = None def __enter__(self): self._f.__enter__() return self def __exit__(self, type, value, traceback): self._f.__exit__(type, value, traceback) def __getattr__(self, attr): return getattr(self._f, attr) def fake_get_file(url, urlmap=None): if urlmap: url = urlmap.get(url, url) fn = get_test_filename(quote(url, safe="")) if not os.path.exists(fn): raise ResourceNotFound(fn) f = FakeFile(fn, 'rb') f.url = url return f def fake_pg_config(**map): def f(what): return map[what] return f class InfoTestCase(unittest.TestCase): def _get_output(self, cmdline): @patch('sys.stdout') @patch('pgxnclient.network.get_file') def do(mock, stdout): stdout.encoding = 'UTF-8' mock.side_effect = fake_get_file from pgxnclient.cli import main main(cmdline) return get_stdout_data(stdout) return do() def test_info(self): output = self._get_output(['info', '--versions', 'foobar']) self.assertEqual( output, b"""\ foobar 0.43.2b1 testing foobar 0.42.1 stable foobar 0.42.0 stable """, ) def test_info_op(self): output = self._get_output(['info', '--versions', 'foobar>0.42.0']) self.assertEqual( output, b"""\ foobar 0.43.2b1 testing foobar 0.42.1 stable """, ) def test_info_empty(self): output = self._get_output(['info', '--versions', 'foobar>=0.43.2']) self.assertEqual(output, b"") def test_info_case_insensitive(self): output = self._get_output(['info', '--versions', 'Foobar']) self.assertEqual( output, b"""\ foobar 0.43.2b1 testing foobar 0.42.1 stable foobar 0.42.0 stable """, ) def test_mirrors_list(self): output = self._get_output(['mirror']) self.assertEqual( output, b"""\ http://pgxn.depesz.com/ http://www.postgres-support.ch/pgxn/ http://pgxn.justatheory.com/ http://pgxn.darkixion.com/ http://mirrors.cat.pdx.edu/pgxn/ http://pgxn.dalibo.org/ http://pgxn.cxsoftware.org/ http://api.pgxn.org/ """, ) def test_mirror_info(self): output = self._get_output(['mirror', 'http://pgxn.justatheory.com/']) self.assertEqual( output, b"""\ uri: http://pgxn.justatheory.com/ frequency: daily location: Portland, OR, USA bandwidth: Cable organization: David E. Wheeler email: justatheory.com|pgxn timezone: America/Los_Angeles src: rsync://master.pgxn.org/pgxn/ rsync: notes: """, # noqa ) @patch('sys.stdout') @patch('pgxnclient.network.get_file') def test_nonascii(self, mock_get, stdout): mock_get.side_effect = fake_get_file from pgxnclient.cli import main stdout.encoding = 'UTF-8' main(['info', 'first_last_agg']) out = max(get_stdout_data(stdout)) if not isinstance(out, int): out = ord(out) assert out > 127 stdout.reset_mock() stdout.encoding = None main(['info', 'first_last_agg']) out = max(get_stdout_data(stdout)) if not isinstance(out, int): out = ord(out) assert out < 127 class CommandTestCase(unittest.TestCase): def test_popen_raises(self): from pgxnclient.commands import Command c = Command([]) self.assertRaises( PgxnClientException, c.popen, "this-script-doesnt-exist" ) class DownloadTestCase(unittest.TestCase): @patch('pgxnclient.network.get_file') def test_download_latest(self, mock): mock.side_effect = fake_get_file fn = 'foobar-0.42.1.zip' self.assert_(not os.path.exists(fn)) from pgxnclient.cli import main try: main(['download', 'foobar']) self.assert_(os.path.exists(fn)) finally: ifunlink(fn) @patch('pgxnclient.network.get_file') def test_download_testing(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', '--testing', 'foobar']) self.assert_(os.path.exists(fn)) finally: 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', 'https://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 fn = 'pg_amqp-0.3.0.zip' self.assert_(not os.path.exists(fn)) from pgxnclient.cli import main try: main(['download', 'amqp']) self.assert_(os.path.exists(fn)) finally: ifunlink(fn) @patch('pgxnclient.network.get_file') def test_download_rename(self, mock): mock.side_effect = fake_get_file fn = 'foobar-0.42.1.zip' fn1 = 'foobar-0.42.1-1.zip' fn2 = 'foobar-0.42.1-2.zip' for tmp in (fn, fn1, fn2): self.assert_(not os.path.exists(tmp)) try: f = open(fn, "w") f.write('test') f.close() from pgxnclient.cli import main main(['download', 'foobar']) self.assert_(os.path.exists(fn1)) self.assert_(not os.path.exists(fn2)) main(['download', 'foobar']) self.assert_(os.path.exists(fn2)) f = open(fn) self.assertEquals(f.read(), 'test') f.close() finally: ifunlink(fn) ifunlink(fn1) ifunlink(fn2) @patch('pgxnclient.network.get_file') def test_download_bad_sha1(self, mock): def fakefake(url): return fake_get_file( url, urlmap={ 'https://api.pgxn.org/dist/foobar/0.42.1/META.json': 'https://api.pgxn.org/dist/foobar/0.42.1/META-badsha1.json' # noqa }, ) mock.side_effect = fakefake fn = 'foobar-0.42.1.zip' self.assert_(not os.path.exists(fn)) try: from pgxnclient.cli import main from pgxnclient.errors import BadChecksum self.assertRaises(BadChecksum, main, ['download', 'foobar']) self.assert_(not os.path.exists(fn)) finally: ifunlink(fn) @patch('pgxnclient.network.get_file') def test_download_case_insensitive(self, mock): mock.side_effect = fake_get_file fn = 'pyrseas-0.4.1.zip' self.assert_(not os.path.exists(fn)) from pgxnclient.cli import main try: main(['download', 'pyrseas']) self.assert_(os.path.exists(fn)) finally: ifunlink(fn) try: main(['download', 'Pyrseas']) self.assert_(os.path.exists(fn)) finally: ifunlink(fn) def test_version(self): from pgxnclient import Spec from pgxnclient.commands.install import Download from pgxnclient.errors import ResourceNotFound opt = Mock() opt.status = Spec.STABLE cmd = Download(opt) for spec, res, data in [ ('foo', '1.2.0', {'stable': ['1.2.0']}), ('foo', '1.2.0', {'stable': ['1.2.0', '1.2.0b']}), ('foo=1.2', '1.2.0', {'stable': ['1.2.0']}), ('foo>=1.1', '1.2.0', {'stable': ['1.1.0', '1.2.0']}), ( 'foo>=1.1', '1.2.0', { 'stable': ['1.1.0', '1.2.0'], 'testing': ['1.3.0'], 'unstable': ['1.4.0'], }, ), ]: spec = Spec.parse(spec) data = { 'releases': dict( [ (k, [{'version': v} for v in vs]) for k, vs in data.items() ] ) } self.assertEqual(res, cmd.get_best_version(data, spec)) for spec, res, data in [ ('foo>=1.3', '1.2.0', {'stable': ['1.2.0']}), ('foo>=1.3', '1.2.0', {'stable': ['1.2.0'], 'testing': ['1.3.0']}), ]: spec = Spec.parse(spec) data = { 'releases': dict( [ (k, [{'version': v} for v in vs]) for k, vs in data.items() ] ) } self.assertRaises( ResourceNotFound, cmd.get_best_version, data, spec ) opt.status = Spec.TESTING for spec, res, data in [ ( 'foo>=1.1', '1.3.0', { 'stable': ['1.1.0', '1.2.0'], 'testing': ['1.3.0'], 'unstable': ['1.4.0'], }, ) ]: spec = Spec.parse(spec) data = { 'releases': dict( [ (k, [{'version': v} for v in vs]) for k, vs in data.items() ] ) } self.assertEqual(res, cmd.get_best_version(data, spec)) opt.status = Spec.UNSTABLE for spec, res, data in [ ( 'foo>=1.1', '1.4.0', { 'stable': ['1.1.0', '1.2.0'], 'testing': ['1.3.0'], 'unstable': ['1.4.0'], }, ) ]: spec = Spec.parse(spec) data = { 'releases': dict( [ (k, [{'version': v} for v in vs]) for k, vs in data.items() ] ) } self.assertEqual(res, cmd.get_best_version(data, spec)) class Assertions(object): make = object() def assertCallArgs(self, pattern, args): if len(pattern) != len(args): self.fail('args and pattern have different lengths') for p, a in zip(pattern, args): if p is self.make: if not a.endswith('make'): self.fail('%s is not a make in %s' % (a, args)) else: if not a == p: self.fail('%s is not a %s in %s' % (a, p, args)) # With mock patching a method seems tricky: looks there's no way to get to # 'self' as the mock method is unbound. TarArchive.unpack_orig = TarArchive.unpack ZipArchive.unpack_orig = ZipArchive.unpack class InstallTestCase(unittest.TestCase, Assertions): def setUp(self): self._p1 = patch('pgxnclient.network.get_file') self.mock_get = self._p1.start() self.mock_get.side_effect = fake_get_file self._p2 = patch('pgxnclient.commands.Popen') self.mock_popen = self._p2.start() self.mock_popen.return_value.returncode = 0 self._p3 = patch('pgxnclient.commands.WithPgConfig.call_pg_config') self.mock_pgconfig = self._p3.start() self.mock_pgconfig.side_effect = fake_pg_config(libdir='/', bindir='/') def tearDown(self): self._p1.stop() self._p2.stop() self._p3.stop() def test_install_latest(self): from pgxnclient.cli import main main(['install', '--sudo', '--', 'foobar']) self.assertEquals(self.mock_popen.call_count, 2) self.assertCallArgs( [self.make], self.mock_popen.call_args_list[0][0][0][:1] ) self.assertCallArgs( ['sudo', self.make], self.mock_popen.call_args_list[1][0][0][:2] ) def test_install_missing_sudo(self): from pgxnclient.cli import main self.assertRaises(InsufficientPrivileges, main, ['install', 'foobar']) def test_install_local(self): self.mock_pgconfig.side_effect = fake_pg_config( libdir=os.environ['HOME'], bindir='/' ) from pgxnclient.cli import main main(['install', 'foobar']) 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_url(self): self.mock_pgconfig.side_effect = fake_pg_config( libdir=os.environ['HOME'], bindir='/' ) from pgxnclient.cli import main main( [ 'install', 'https://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( libdir=os.environ['HOME'], bindir='/' ) from pgxnclient.cli import main self.assertRaises(PgxnClientException, main, ['install', 'foobar']) self.assertEquals(self.mock_popen.call_count, 1) def test_install_bad_sha1(self): def fakefake(url): return fake_get_file( url, urlmap={ 'https://api.pgxn.org/dist/foobar/0.42.1/META.json': 'https://api.pgxn.org/dist/foobar/0.42.1/META-badsha1.json' # noqa }, ) self.mock_get.side_effect = fakefake from pgxnclient.cli import main from pgxnclient.errors import BadChecksum self.assertRaises( BadChecksum, main, ['install', '--sudo', '--', 'foobar'] ) def test_install_nosudo(self): self.mock_pgconfig.side_effect = fake_pg_config( libdir=os.environ['HOME'] ) from pgxnclient.cli import main main(['install', '--nosudo', 'foobar']) 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_sudo(self): from pgxnclient.cli import main main(['install', '--sudo', 'gksudo -d "hello world"', 'foobar']) self.assertEquals(self.mock_popen.call_count, 2) self.assertCallArgs( [self.make], self.mock_popen.call_args_list[0][0][0][:1] ) self.assertCallArgs( ['gksudo', '-d', 'hello world', self.make], self.mock_popen.call_args_list[1][0][0][:4], ) @patch('pgxnclient.tar.TarArchive.unpack') def test_install_local_tar(self, mock_unpack): fn = get_test_filename('foobar-0.42.1.tar.gz') mock_unpack.side_effect = TarArchive(fn).unpack_orig from pgxnclient.cli import main main(['install', '--sudo', '--', fn]) self.assertEquals(self.mock_popen.call_count, 2) self.assertCallArgs( [self.make], self.mock_popen.call_args_list[0][0][0][:1] ) self.assertCallArgs( ['sudo', self.make], self.mock_popen.call_args_list[1][0][0][:2] ) make_cwd = self.mock_popen.call_args_list[1][1]['cwd'] self.assertEquals(mock_unpack.call_count, 1) tmpdir, = mock_unpack.call_args[0] self.assertEqual(make_cwd, os.path.join(tmpdir, 'foobar-0.42.1')) @patch('pgxnclient.zip.ZipArchive.unpack') def test_install_local_zip(self, mock_unpack): fn = get_test_filename('foobar-0.42.1.zip') mock_unpack.side_effect = ZipArchive(fn).unpack_orig from pgxnclient.cli import main main(['install', '--sudo', '--', fn]) self.assertEquals(self.mock_popen.call_count, 2) self.assertCallArgs( [self.make], self.mock_popen.call_args_list[0][0][0][:1] ) self.assertCallArgs( ['sudo', self.make], self.mock_popen.call_args_list[1][0][0][:2] ) make_cwd = self.mock_popen.call_args_list[1][1]['cwd'] self.assertEquals(mock_unpack.call_count, 1) tmpdir, = mock_unpack.call_args[0] self.assertEqual(make_cwd, os.path.join(tmpdir, 'foobar-0.42.1')) def test_install_url_file(self): fn = get_test_filename('foobar-0.42.1.zip') url = 'file://' + os.path.abspath(fn).replace("f", '%%%2x' % ord('f')) from pgxnclient.cli import main main(['install', '--sudo', '--', url]) self.assertEquals(self.mock_popen.call_count, 2) self.assertCallArgs( [self.make], self.mock_popen.call_args_list[0][0][0][:1] ) self.assertCallArgs( ['sudo', self.make], self.mock_popen.call_args_list[1][0][0][:2] ) def test_install_local_dir(self): self.mock_get.side_effect = lambda *args: self.fail('network invoked') tdir = tempfile.mkdtemp() try: from pgxnclient.zip import unpack dir = unpack(get_test_filename('foobar-0.42.1.zip'), tdir) from pgxnclient.cli import main main(['install', '--sudo', '--', dir]) finally: shutil.rmtree(tdir) self.assertEquals(self.mock_popen.call_count, 2) self.assertCallArgs( [self.make], self.mock_popen.call_args_list[0][0][0][:1] ) self.assertCallArgs(dir, self.mock_popen.call_args_list[0][1]['cwd']) self.assertCallArgs( ['sudo', self.make], self.mock_popen.call_args_list[1][0][0][:2] ) self.assertEquals(dir, self.mock_popen.call_args_list[1][1]['cwd']) class CheckTestCase(unittest.TestCase, Assertions): def setUp(self): self._p1 = patch('pgxnclient.network.get_file') self.mock_get = self._p1.start() self.mock_get.side_effect = fake_get_file self._p2 = patch('pgxnclient.commands.Popen') self.mock_popen = self._p2.start() self.mock_popen.return_value.returncode = 0 self._p3 = patch('pgxnclient.commands.WithPgConfig.call_pg_config') self.mock_pgconfig = self._p3.start() self.mock_pgconfig.side_effect = fake_pg_config(libdir='/', bindir='/') def tearDown(self): self._p1.stop() self._p2.stop() self._p3.stop() def test_check_latest(self): from pgxnclient.cli import main main(['check', 'foobar']) 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', 'https://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 from pgxnclient.cli import main self.assertRaises(PgxnClientException, main, ['check', 'foobar']) self.assertEquals(self.mock_popen.call_count, 1) def test_check_diff_moved(self): def create_regression_files(*args, **kwargs): cwd = kwargs['cwd'] open(os.path.join(cwd, 'regression.out'), 'w').close() open(os.path.join(cwd, 'regression.diffs'), 'w').close() return Mock() self.mock_popen.side_effect = create_regression_files self.mock_popen.return_value.returncode = 1 self.assert_( not os.path.exists('regression.out'), "Please remove temp file 'regression.out' from current dir", ) self.assert_( not os.path.exists('regression.diffs'), "Please remove temp file 'regression.diffs' from current dir", ) from pgxnclient.cli import main try: self.assertRaises(PgxnClientException, main, ['check', 'foobar']) self.assertEquals(self.mock_popen.call_count, 1) self.assert_(os.path.exists('regression.out')) self.assert_(os.path.exists('regression.diffs')) finally: ifunlink('regression.out') ifunlink('regression.diffs') def test_check_bad_sha1(self): def fakefake(url): return fake_get_file( url, urlmap={ 'https://api.pgxn.org/dist/foobar/0.42.1/META.json': 'https://api.pgxn.org/dist/foobar/0.42.1/META-badsha1.json' # noqa }, ) self.mock_get.side_effect = fakefake self.mock_popen.return_value.returncode = 1 from pgxnclient.cli import main from pgxnclient.errors import BadChecksum self.assertRaises(BadChecksum, main, ['check', 'foobar']) self.assertEquals(self.mock_popen.call_count, 0) class LoadTestCase(unittest.TestCase): def setUp(self): self._p1 = patch('pgxnclient.commands.Popen') self.mock_popen = self._p1.start() self.mock_popen.return_value.returncode = 0 self.mock_popen.return_value.communicate.return_value = (b'', b'') self._p2 = patch('pgxnclient.commands.install.LoadUnload.is_extension') self.mock_isext = self._p2.start() self.mock_isext.return_value = True self._p3 = patch( 'pgxnclient.commands.install.LoadUnload.get_pg_version' ) self.mock_pgver = self._p3.start() self.mock_pgver.return_value = (9, 1, 0) def tearDown(self): self._p1.stop() self._p2.stop() self._p3.stop() def test_parse_version(self): from pgxnclient.commands.install import Load cmd = Load(None) self.assertEquals((9, 0, 3), cmd.parse_pg_version('90003')) self.assertEquals((9, 1, 0), cmd.parse_pg_version('90100')) @patch('pgxnclient.network.get_file') def test_check_psql_options(self, mock_get): mock_get.side_effect = fake_get_file from pgxnclient.cli import main main(['load', '--yes', '--dbname', 'dbdb', 'foobar']) args = self.mock_popen.call_args[0][0] self.assertEqual('dbdb', args[args.index('--dbname') + 1]) main(['load', '--yes', '-U', 'meme', 'foobar']) args = self.mock_popen.call_args[0][0] self.assertEqual('meme', args[args.index('--username') + 1]) main(['load', '--yes', '--port', '666', 'foobar']) args = self.mock_popen.call_args[0][0] self.assertEqual('666', args[args.index('--port') + 1]) main(['load', '--yes', '-h', 'somewhere', 'foobar']) args = self.mock_popen.call_args[0][0] self.assertEqual('somewhere', args[args.index('--host') + 1]) @patch('pgxnclient.zip.ZipArchive.unpack') @patch('pgxnclient.network.get_file') def test_load_local_zip(self, mock_get, mock_unpack): mock_get.side_effect = lambda *args: self.fail('network invoked') mock_unpack.side_effect = ZipArchive.unpack_orig from pgxnclient.cli import main main(['load', '--yes', get_test_filename('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], b'CREATE EXTENSION foobar;' ) @patch('pgxnclient.tar.TarArchive.unpack') @patch('pgxnclient.network.get_file') def test_load_local_tar(self, mock_get, mock_unpack): mock_get.side_effect = lambda *args: self.fail('network invoked') mock_unpack.side_effect = TarArchive.unpack_orig from pgxnclient.cli import main main(['load', '--yes', get_test_filename('foobar-0.42.1.tar.gz')]) 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], b'CREATE EXTENSION foobar;' ) @patch('pgxnclient.network.get_file') def test_load_local_dir(self, mock_get): mock_get.side_effect = lambda *args: self.fail('network invoked') tdir = tempfile.mkdtemp() try: from pgxnclient.zip import unpack dir = unpack(get_test_filename('foobar-0.42.1.zip'), tdir) from pgxnclient.cli import main main(['load', '--yes', dir]) finally: shutil.rmtree(tdir) 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], b'CREATE EXTENSION foobar;' ) @patch('pgxnclient.zip.ZipArchive.unpack') @patch('pgxnclient.network.get_file') def test_load_zip_url(self, mock_get, mock_unpack): mock_get.side_effect = fake_get_file mock_unpack.side_effect = ZipArchive.unpack_orig from pgxnclient.cli import main main( [ 'load', '--yes', 'https://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], b'CREATE EXTENSION foobar;' ) @patch('pgxnclient.tar.TarArchive.unpack') @patch('pgxnclient.network.get_file') def test_load_tar_url(self, mock_get, mock_unpack): mock_get.side_effect = fake_get_file mock_unpack.side_effect = TarArchive.unpack_orig from pgxnclient.cli import main main(['load', '--yes', 'https://example.org/foobar-0.42.1.tar.gz']) 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], b'CREATE EXTENSION foobar;' ) def test_load_extensions_order(self): tdir = tempfile.mkdtemp() try: from pgxnclient.zip import unpack dir = unpack(get_test_filename('foobar-0.42.1.zip'), tdir) shutil.copyfile( get_test_filename('META-manyext.json'), os.path.join(dir, 'META.json'), ) from pgxnclient.cli import main main(['load', '--yes', dir]) finally: shutil.rmtree(tdir) self.assertEquals(self.mock_popen.call_count, 4) self.assert_('psql' in self.mock_popen.call_args[0][0][0]) communicate = self.mock_popen.return_value.communicate self.assertEquals( communicate.call_args_list[0][0][0], b'CREATE EXTENSION foo;' ) self.assertEquals( communicate.call_args_list[1][0][0], b'CREATE EXTENSION bar;' ) self.assertEquals( communicate.call_args_list[2][0][0], b'CREATE EXTENSION baz;' ) self.assertEquals( communicate.call_args_list[3][0][0], b'CREATE EXTENSION qux;' ) def test_unload_extensions_order(self): tdir = tempfile.mkdtemp() try: from pgxnclient.zip import unpack dir = unpack(get_test_filename('foobar-0.42.1.zip'), tdir) shutil.copyfile( get_test_filename('META-manyext.json'), os.path.join(dir, 'META.json'), ) from pgxnclient.cli import main main(['unload', '--yes', dir]) finally: shutil.rmtree(tdir) self.assertEquals(self.mock_popen.call_count, 4) self.assert_('psql' in self.mock_popen.call_args[0][0][0]) communicate = self.mock_popen.return_value.communicate self.assertEquals( communicate.call_args_list[0][0][0], b'DROP EXTENSION qux;' ) self.assertEquals( communicate.call_args_list[1][0][0], b'DROP EXTENSION baz;' ) self.assertEquals( communicate.call_args_list[2][0][0], b'DROP EXTENSION bar;' ) self.assertEquals( communicate.call_args_list[3][0][0], b'DROP EXTENSION foo;' ) def test_load_list(self): tdir = tempfile.mkdtemp() try: from pgxnclient.zip import unpack dir = unpack(get_test_filename('foobar-0.42.1.zip'), tdir) shutil.copyfile( get_test_filename('META-manyext.json'), os.path.join(dir, 'META.json'), ) from pgxnclient.cli import main main(['load', '--yes', dir, 'baz', 'foo']) finally: shutil.rmtree(tdir) self.assertEquals(self.mock_popen.call_count, 2) self.assert_('psql' in self.mock_popen.call_args[0][0][0]) communicate = self.mock_popen.return_value.communicate self.assertEquals( communicate.call_args_list[0][0][0], b'CREATE EXTENSION baz;' ) self.assertEquals( communicate.call_args_list[1][0][0], b'CREATE EXTENSION foo;' ) def test_unload_list(self): tdir = tempfile.mkdtemp() try: from pgxnclient.zip import unpack dir = unpack(get_test_filename('foobar-0.42.1.zip'), tdir) shutil.copyfile( get_test_filename('META-manyext.json'), os.path.join(dir, 'META.json'), ) from pgxnclient.cli import main main(['unload', '--yes', dir, 'baz', 'foo']) finally: shutil.rmtree(tdir) self.assertEquals(self.mock_popen.call_count, 2) self.assert_('psql' in self.mock_popen.call_args[0][0][0]) communicate = self.mock_popen.return_value.communicate self.assertEquals( communicate.call_args_list[0][0][0], b'DROP EXTENSION baz;' ) self.assertEquals( communicate.call_args_list[1][0][0], b'DROP EXTENSION foo;' ) def test_load_missing(self): tdir = tempfile.mkdtemp() try: from pgxnclient.zip import unpack dir = unpack(get_test_filename('foobar-0.42.1.zip'), tdir) shutil.copyfile( get_test_filename('META-manyext.json'), os.path.join(dir, 'META.json'), ) from pgxnclient.cli import main self.assertRaises( PgxnClientException, main, ['load', '--yes', dir, 'foo', 'ach'] ) finally: shutil.rmtree(tdir) self.assertEquals(self.mock_popen.call_count, 0) def test_unload_missing(self): tdir = tempfile.mkdtemp() try: from pgxnclient.zip import unpack dir = unpack(get_test_filename('foobar-0.42.1.zip'), tdir) shutil.copyfile( get_test_filename('META-manyext.json'), os.path.join(dir, 'META.json'), ) from pgxnclient.cli import main self.assertRaises( PgxnClientException, main, ['unload', '--yes', dir, 'foo', 'ach'], ) finally: shutil.rmtree(tdir) self.assertEquals(self.mock_popen.call_count, 0) def test_missing_meta_dir(self): # issue #19 tdir = tempfile.mkdtemp() try: from pgxnclient.zip import unpack dir = unpack(get_test_filename('foobar-0.42.1.zip'), tdir) os.unlink(os.path.join(dir, 'META.json')) from pgxnclient.cli import main self.assertRaises(PgxnClientException, main, ['load', dir]) finally: shutil.rmtree(tdir) class SearchTestCase(unittest.TestCase): @patch('sys.stdout') @patch('pgxnclient.network.get_file') def test_search_quoting(self, mock_get, stdout): stdout.encoding = 'UTF-8' mock_get.side_effect = fake_get_file from pgxnclient.cli import main main(['search', '--docs', 'foo bar', 'baz']) @patch('sys.stdout') @patch('pgxnclient.network.get_file') def test_nonascii(self, mock_get, stdout): mock_get.side_effect = fake_get_file from pgxnclient.cli import main stdout.encoding = 'UTF-8' main(['search', 'oracle']) out = max(get_stdout_data(stdout)) if not isinstance(out, int): out = ord(out) assert out > 127 stdout.reset_mock() stdout.encoding = None main(['search', 'oracle']) out = max(get_stdout_data(stdout)) if not isinstance(out, int): out = ord(out) assert out < 127 class HelpTestCase(unittest.TestCase): @patch('sys.stdout') def test_libexec(self, stdout): stdout.encoding = 'UTF-8' from pgxnclient.cli import main main(['help', '--libexec']) out = get_stdout_data(stdout) assert out.strip() assert out.count(b'\n') == 1 def get_stdout_data(mock): # TODO: reorganize tests to be less verbose and do this kind of cruft # with fixtures and pytest-foo calls = mock.write.call_args_list or mock.buffer.write.call_args_list return b''.join([a[0] for a, k in calls]) if __name__ == '__main__': unittest.main() pgxnclient-1.3/tests/test_label.py000066400000000000000000000051521354122750200173660ustar00rootroot00000000000000import unittest from pgxnclient import Label, Term, Identifier class LabelTestCase(unittest.TestCase): def test_ok(self): for s in [ 'd', 'a1234', 'abcd1234-5432XYZ', 'a12345678901234567890123456789012345678901234567890123456789012', ]: self.assertEqual(Label(s), s) self.assertEqual(Label(s), Label(s)) self.assert_(Label(s) <= Label(s)) self.assert_(Label(s) >= Label(s)) def test_bad(self): def ar(s): try: Label(s) except ValueError: pass else: self.fail("ValueError not raised: '%s'" % s) for s in [ '', ' a', 'a ', '1a', '-a', 'a-', 'a_b', 'a123456789012345678901234567890123456789012345678901234567890123', ]: ar(s) def test_compare(self): self.assertEqual(Label('a'), Label('A')) self.assertNotEqual(str(Label('a')), str(Label('A'))) # preserving def test_order(self): self.assert_(Label('a') < Label('B') < Label('c')) self.assert_(Label('A') < Label('b') < Label('C')) self.assert_(Label('a') <= Label('B') <= Label('c')) self.assert_(Label('A') <= Label('b') <= Label('C')) self.assert_(Label('c') > Label('B') > Label('a')) self.assert_(Label('C') > Label('b') > Label('A')) self.assert_(Label('c') >= Label('B') >= Label('a')) self.assert_(Label('C') >= Label('b') >= Label('A')) class TermTestCase(unittest.TestCase): def test_ok(self): for s in ['aa' 'adfkjh"()']: self.assertEqual(Term(s), s) self.assertEqual(Term(s), Term(s)) self.assert_(Term(s) <= Term(s)) self.assert_(Term(s) >= Term(s)) def test_bad(self): def ar(s): try: Term(s) except ValueError: pass else: self.fail("ValueError not raised: '%s'" % s) for s in ['a', 'aa ', 'a/a', 'a\\a', 'a\ta', 'aa\x01']: ar(s) class TestIdentifier(unittest.TestCase): def test_nonblank(self): self.assertRaises(ValueError, Identifier, "") def test_unquoted(self): for s in ['x', 'xxxxx', 'abcxyz_0189', 'ABCXYZ_0189']: self.assertEqual(Identifier(s), s) def test_quoted(self): for s, q in [('x-y', '"x-y"'), (' ', '" "'), ('x"y', '"x""y"')]: self.assertEqual(Identifier(s), q) if __name__ == '__main__': unittest.main() pgxnclient-1.3/tests/test_semver.py000066400000000000000000000107761354122750200176200ustar00rootroot00000000000000from __future__ import print_function import unittest from pgxnclient import SemVer # Tests based on # https://github.com/theory/pg-semver/blob/master/test/sql/base.sql class SemVerTestCase(unittest.TestCase): def test_ok(self): for s in [ '1.2.2', '0.2.2', '1.2.2', '0.0.0', '0.1.999', '9999.9999999.823823', '1.0.0beta1', # no more valid according to semver '1.0.0beta2', # no more valid according to semver '1.0.0-beta1', '1.0.0-beta2', '1.0.0', '20110204.0.0', ]: self.assertEqual(SemVer(s), s) def test_bad(self): def ar(s): try: SemVer(s) except ValueError: pass else: self.fail("ValueError not raised: '%s'" % s) for s in [ '1.2', '1.2.02', '1.2.2-', '1.2.3b#5', '03.3.3', 'v1.2.2', '1.3b', '1.4b.0', '1v', '1v.2.2v', '1.2.4b.5', ]: ar(s) def test_eq(self): for s1, s2 in [ ('1.2.2', '1.2.2'), ('1.2.23', '1.2.23'), ('0.0.0', '0.0.0'), ('999.888.7777', '999.888.7777'), ('0.1.2-beta3', '0.1.2-beta3'), ('1.0.0-rc-1', '1.0.0-RC-1'), ]: self.assertEqual(SemVer(s1), SemVer(s2)) self.assertEqual(hash(SemVer(s1)), hash(SemVer(s2))) self.assert_( SemVer(s1) <= SemVer(s2), "%s <= %s failed" % (s1, s2) ) self.assert_( SemVer(s1) >= SemVer(s2), "%s >= %s failed" % (s1, s2) ) def test_ne(self): for s1, s2 in [ ('1.2.2', '1.2.3'), ('0.0.1', '1.0.0'), ('1.0.1', '1.1.0'), ('1.1.1', '1.1.0'), ('1.2.3-b', '1.2.3'), ('1.2.3', '1.2.3-b'), ('1.2.3-a', '1.2.3-b'), ('1.2.3-aaaaaaa1', '1.2.3-aaaaaaa2'), ]: self.assertNotEqual(SemVer(s1), SemVer(s2)) self.assertNotEqual(hash(SemVer(s1)), hash(SemVer(s2))) def test_dis(self): for s1, s2 in [ ('2.2.2', '1.1.1'), ('2.2.2', '2.1.1'), ('2.2.2', '2.2.1'), ('2.2.2-b', '2.2.1'), ('2.2.2', '2.2.2-b'), ('2.2.2-c', '2.2.2-b'), ('2.2.2-rc-2', '2.2.2-RC-1'), ('0.9.10', '0.9.9'), ]: self.assert_( SemVer(s1) >= SemVer(s2), "%s >= %s failed" % (s1, s2) ) self.assert_(SemVer(s1) > SemVer(s2), "%s > %s failed" % (s1, s2)) self.assert_( SemVer(s2) <= SemVer(s1), "%s <= %s failed" % (s2, s1) ) self.assert_(SemVer(s2) < SemVer(s1), "%s < %s failed" % (s2, s1)) def test_clean(self): for s1, s2 in [ ('1.2.2', '1.2.2'), ('01.2.2', '1.2.2'), ('1.02.2', '1.2.2'), ('1.2.02', '1.2.2'), ('1.2.02b', '1.2.2-b'), ('1.2.02beta-3 ', '1.2.2-beta-3'), ('1.02.02rc1', '1.2.2-rc1'), ('1.0', '1.0.0'), ('1', '1.0.0'), ('.0.02', '0.0.2'), ('1..02', '1.0.2'), ('1..', '1.0.0'), ('1.1', '1.1.0'), ('1.2.b1', '1.2.0-b1'), ('9.0beta4', '9.0.0-beta4'), # PostgreSQL format. ('9b', '9.0.0-b'), ('rc1', '0.0.0-rc1'), ('', '0.0.0'), ('..2', '0.0.2'), ('1.2.3 a', '1.2.3-a'), ('..2 b', '0.0.2-b'), (' 012.2.2', '12.2.2'), ('20110204', '20110204.0.0'), ]: try: self.assertEqual(SemVer.clean(s1), SemVer(s2)) except Exception: print(s1, s2) raise def test_cant_clean(self): def ar(s): try: SemVer.clean(s) except ValueError: pass else: self.fail("ValueError not raised: '%s'" % s) for s in [ '1.2.0 beta 4', '1.2.2-', '1.2.3b#5', 'v1.2.2', '1.4b.0', '1v.2.2v', '1.2.4b.5', '1.2.3.4', '1.2.3 4', '1.2000000000000000.3.4', ]: ar(s) if __name__ == '__main__': unittest.main() pgxnclient-1.3/tests/test_spec.py000066400000000000000000000007151354122750200172410ustar00rootroot00000000000000import unittest from pgxnclient import Spec class SpecTestCase(unittest.TestCase): def test_str(self): self.assertEqual(str(Spec('foo')), 'foo') self.assertEqual(str(Spec('foo>2.0')), 'foo>2.0') self.assertEqual(str(Spec('foo>2.0')), 'foo>2.0') self.assertEqual(str(Spec(dirname='/foo')), '/foo') self.assertEqual(str(Spec(dirname='/foo/foo.zip')), '/foo/foo.zip') if __name__ == '__main__': unittest.main() pgxnclient-1.3/tests/testdata/000077500000000000000000000000001354122750200165045ustar00rootroot00000000000000pgxnclient-1.3/tests/testdata/META-manyext.json000066400000000000000000000023371354122750200216150ustar00rootroot00000000000000{ "name": "foobar", "abstract": "A mock extension", "description": "This library doesn't exist.", "version": "0.42.1", "maintainer": [ "Daniele Varrazzo " ], "license": "postgresql", "provides": { "foo": { "abstract": "A non existing extension", "file": "sql/foo.sql", "version": "0.42.1" }, "bar": { "abstract": "A non existing extension", "file": "sql/bar.sql", "version": "0.42.1" }, "baz": { "abstract": "A non existing extension", "file": "sql/baz.sql", "version": "0.42.1" }, "qux": { "abstract": "A non existing extension", "file": "sql/qux.sql", "version": "0.42.1" } }, "resources": { "bugtracker": { "web": "http://github.com/piro/foobar/issues/" }, "repository": { "url": "git://github.com/piro/foobar.git", "web": "http://github.com/piro/foobar/", "type": "git" } }, "generated_by": "Daniele Varrazzo", "meta-spec": { "version": "1.0.0", "url": "http://pgxn.org/meta/spec.txt" }, "tags": [ "foo", "bar", "testing" ] } pgxnclient-1.3/tests/testdata/download.py000077500000000000000000000010511354122750200206650ustar00rootroot00000000000000#!/usr/bin/env python """Download an URL and save it with tne name as the urlquoted url. The files downloaded are used by the test suite. """ import os import sys from six.moves.urllib.parse import quote from six.moves.urllib.request import urlopen if __name__ == '__main__': url = sys.argv[1] fn = os.path.join(os.path.dirname(__file__), quote(url, safe='')) f = open(fn, "wb") try: try: f.write(urlopen(url).read()) finally: f.close() except Exception: os.unlink(fn) raise pgxnclient-1.3/tests/testdata/foobar-0.42.1.tar.gz000066400000000000000000000120061354122750200216220ustar00rootroot00000000000000z;Pfoobar-0.42.1.tarV|z'p`[^Z^iF^/ʍZ:o3N̯mJBBi5Rp;UR IO# NA Ѯà/$a s, %P|BO3z@iަ4Rr0@ct t[D8Rӹhub'fBjjlտ7jWt;V`v*cX%gAOжB>|:; c+ǝʪ02BN%5n6E] O8=-nYv2F̀?KΊѧJxc>m7K,cBgGn-X]#9Ʈk".ֈPO`' `&LBR*d `m݄0 yZ].A*7F WTaQ*`fa!D,a~$Y3qoj!0eș#@B@8=t )`baKRE #3Hf~TOttLb6>a,L.ܒf pRCsF,)HmAԦ{Tq —:V'yUؙ@Ic>zk9x>OisR1´($u/yQ~'υa6% 0?PSA6 , ZS{Š3fD0x saSa}kh0U~q$dYyV֭ *Nk91Lw'N3+K1 i)ІUWƂu V.G]`94`C[")(P wh[c*Ŕ GY)2ƅ3&fVȋ},STk?Q֨7!Wy?\y_>#}m>ҧ?-GyLD)Fzhtzg% N t,q:E [7^dyt翍zph?[ORloHo+j.Q^!:(\ފ-5[J>(vhE#SMvӮ%X?=Jvsl90seB@y  h:vuv {g߂YcƔh - 1x?s 6~1฽l v aL\U taokEBs h "ŏPWHU,ଵHR+2 [4{X2FWgHGsg]Hu/w-Z;Hh}{r;0򧐻H 5oݩ{BA^3G4Ozy;دog)ϘkakԆeYs,lhÞ֘&b&Ǧt4]c^:(WRT}tgFxNeVsctO?ss:K$*ȓt 1?GI6mb3GX ^F`j@ ԉnXmF5lK[ce)n;ɊbTdDx\^C =*x݉)%'H<"!p@MC$oKU"^UZ^)I+J-VQ ͡,+b" Ws)*}H+ Y+>Ok^jc4ͭlk/^{I]|΅N2IKrOE]5&˜n2v.o!ҀNz4r%ywyI*.uON:74^4u3}ŗB^V$i4+ d11G5j$ 4B^A_W:ѭ 4HABL!#:0eE]].B@Aؠli_O5bu>.T??u#߳~\C矎C;QA')ϵ7]^cW SZk}o# PKW_;-cC&}DYQoTDzȺS_}Eo@%z@ )),K=zH! ^#n}j`p{׉O=ހH'7QU}qo:uj$BV9b_]Gx!h϶1ͯٔ7ocLKB^1 =c8u WhD䧇Z_ R$8Oa:҇yIGr=4O5R{q} C,{c<ߎ>SX<5eЅ,JT2{ROL==dKVd%+YJVd%+YJVېxpgxnclient-1.3/tests/testdata/foobar-0.42.1.zip000066400000000000000000000221271354122750200212240ustar00rootroot00000000000000PK >foobar-0.42.1/UT ԵMԵMux PK>՛钩Vfoobar-0.42.1/MakefileUT ԵMԵMux ]O06o4.luQ0fw2 0(jwus.&ryӏsԣ;A GtQGqLL^-\`Qo nж.&cdsI$- WO*ywN # &Ox%E' _A>OV;5L ̵7E:|RL Tݵ֦j"F4ĔI`Æxv~ͬ{,Λ ?fmJm؆7o/-)V dsI( kok;?P ː+qPK >foobar-0.42.1/sql/UT ӵMӵMux PK҅>VYfoobar-0.42.1/sql/foobar.sqlUT MӵMux OK@O&K=TX4$nO%RBEM"4HiagF㡲ֶ֕iʝmKyt-!TVGH{ܿO)W2&46,ҽك=,Bd N d2!;1fyUXt3X/Y\Cf1&'ޯ z&VsM ϒ1]ZYn?PK҅>L$ņ&foobar-0.42.1/sql/uninstall_foobar.sqlUT MӵMux AN0E9_rE(5 VWS6,P@7K͟^#;O l] aF~qSq$-1'txHvB-ML; &R1l(foobar-0.42.1/sql/foobar--unpackaged.sqlUT MӵMux s q Rpq S(H,RptqQ pqq sh$U(VhZGxvb3!9)ّVd}.}ۊ?PK ҅>foobar-0.42.1/test/UT MbӵMux PK ҅>foobar-0.42.1/test/sql/UT MbӵMux PK҅>*@foobar-0.42.1/test/sql/base.sqlUT MӵMux Ok@)概jo 6Rj/6TLٻ5#ޛ rI|8eymI{$+KB@=1' !]*#5lEj%JԥʾM8?堹ki"lzkc c۫,e!R~(zxF (b (dP,ZӪ\}Qn+3m$V9!^P0d.E Q< JDv4bOs2LUG(t0¼Zx]z\fTnG\c$dG7PK ҅>foobar-0.42.1/test/expected/UT MbӵMux PK҅>0k$foobar-0.42.1/test/expected/base.outUT MӵMux Sj@WM6tvKBB[R<J3ͪS={oF,bTO*Cd˷i|aWhG\yPrB]o n9%9pfe A&զrp{<ʓ8M޹[tEoMsM,`ab^ ~ڸBR9oьI)1< &Brsn9HIfoobar-0.42.1/doc/UT ӵMӵMux PK҅>Й3&dfoobar-0.42.1/doc/foobar.mdUT MӵMux Wn}ﯨD#G"daJ(;3Mlw~{NUo%ؤkNQiS&ؠκG)G|/9磧5PQ.^m_y˱NΝft:^S$[^'{~));V *Eʄ&ZWCa5@x7T:j[l= =g&mG@[*Bƞ ^`H ))'̰.fn 8=MKc)vk@#kBc);v !`k"&sJfȩFÐmMt0aoS%򅯁+&!̂. r$9@ALL3pv]MjQ/D$iQ&jC$J1&wscG( j(T`*UzOU|C;,p8DK[VkEPg tz;6yj¨O#V3m+r0ȭ}F]NŸp\OP!uT45(ϥA2riP.miFř47׌H}T>lZ}mI!ݑ OM#U-uF#=w9]?.<^ oK _~>^??]n/nrjf'zN$˻XK~s;^VmSOtxWjKtwxدJ8twCd`ʎe|sw'x$qy?ߎsXv?1 fr>_=Og[$!]k$ߚxRD48GҜS )o$mvwy૨)inF5bd@5v"=?b%M?kc*z##hEq*@S1=ig< mxk6nphEp9.8w^m=|+[R.o*;Н.]RZlaj7Ýo/+gt4/]WKpӸF( #.h(xN7| dx9(;G .`(+U9LՃK0׵NeLX8$ \b햠w(mzFZV1I@ۈ}4 Vr7i ƙB%j;+>v^OC D~%AV^foobar-0.42.1/README.mdUT MӵMux WMo8W94- MO9t4&$vQ,bZ%6d ]Ù7oL=?agㇱJy ܚ \pL%_ZJ'?Aɰ(VV"F\h- NjkJS%J%ߴ&07цł)١i?Wѭ[Ɍ;׺SKU!B%2$nx: WsmU*8iDMqFiyFZFO [/ސZt|'^@7٪[r>ۋutݐc@۔N6{ X;>FBwP"b@a@q?м/b#Q3opR[4+\D4hѥcOMܩfP ^wŚ6| } cydN0ռAMjH*(ʙjŮ+}yy$w+*J嬉N5 )cO¼Cf^ W٘ $}d#MV(RƢ%I٧ #9Tϩ O*7c/W38VbyEc;ِy ;HV'GMӌ) X@甎&LGn7MڹuB#Q S[JW(i`t?"  7rl8ҼٴфDoLwN$ : 3*YIEwh.v+ ~/ESj4(Y;߇`5 GLe0(QIGǀ ] ? K!4++|?ZB5tS-]|1 Q4bg!Ju>~,0qfoobar-0.42.1/foobar.controlUT ӵMӵMux E;0ާX $w!Z N1X^YG ͎H`% m$]([c%lݽH܋ 8\ɻ*/G㊍%NQdbP66pPK҅>3W foobar-0.42.1/ChangesUT MӵMux eS0S.{Y@*e+(J2$qI6a`˞7o{P#P ag) ke^믟@PA ({I4 ZtycA-i,ASI J ;Q is n՘Y@C2 [Vɤ`([ 4kLh.Y'  ZNIm0>$`3-'8C:̧o|X-ͱ<{Vs!PFGEiUp5ft4i *n!ժ*( I8jwREv [Kt|$o{b}T⢶@y)*!}%xcqljm ghNI}xfoobar-0.42.1/META.jsonUT յM/յMux SAN0V.\i !xrŵޔU:qҴqdz3~ˌ![BE V&hk vbt`.[VZ Ӆ s~w 6Hx:zPDP{ދβ{}VԮ.mE{/RiI TW8o7~OiejcM@u~PU7f"&JiρU'j!} d/Osp(BtAfoobar-0.42.1/UTԵMux PK>՛钩VHfoobar-0.42.1/MakefileUTԵMux PK >AAfoobar-0.42.1/sql/UTӵMux PK҅>VYfoobar-0.42.1/sql/foobar.sqlUTMux PK҅>L$ņ&foobar-0.42.1/sql/uninstall_foobar.sqlUTMux PK҅> &R1l(foobar-0.42.1/sql/foobar--unpackaged.sqlUTMux PK ҅>Afoobar-0.42.1/test/UTMux PK ҅>A,foobar-0.42.1/test/sql/UTMux PK҅>*@}foobar-0.42.1/test/sql/base.sqlUTMux PK ҅>Afoobar-0.42.1/test/expected/UTMux PK҅>0k$Vfoobar-0.42.1/test/expected/base.outUTMux PK g>A foobar-0.42.1/doc/UTӵMux PK҅>Й3&dk foobar-0.42.1/doc/foobar.mdUTMux PK҅>AV^foobar-0.42.1/README.mdUTMux PK>qfoobar-0.42.1/foobar.controlUTӵMux PK҅>3W foobar-0.42.1/ChangesUTMux PK@>hNI}xfoobar-0.42.1/META.jsonUTյMux PK[pgxnclient-1.3/tests/testdata/https%3A%2F%2Fapi.pgxn.org%2Fdist%2Ffirst_last_agg%2F0.1.4%2FMETA.json000066400000000000000000000031301354122750200317250ustar00rootroot00000000000000{ "abstract": "Provides first() and last() aggregate functions.", "date": "2014-06-10T11:08:09Z", "docs": { "README": { "title": "README" }, "doc/first_last_agg": { "title": "first-last-agg 0.1.4" } }, "license": "postgresql", "maintainer": [ "Jan Urbański " ], "name": "first_last_agg", "prereqs": { "build": { "requires": { "PostgreSQL": "9.0.0" } } }, "provides": { "first_last_agg": { "docfile": "doc/first_last_agg.md", "docpath": "doc/first_last_agg", "file": "sql/first_last_agg.sql", "version": "0.1.4" } }, "release_status": "stable", "releases": { "stable": [ { "date": "2014-06-10T11:08:09Z", "version": "0.1.4" } ], "testing": [ { "date": "2011-05-26T10:28:37Z", "version": "0.1.2" } ] }, "resources": { "bugtracker": { "web": "http://github.com/wulczer/first_last_agg/issues/" }, "repository": { "type": "git", "url": "git://github.com/wulczer/first_last_agg.git", "web": "http://github.com/wulczer/first_last_agg/" } }, "sha1": "797a48bd3ea1a080a024d15e9384932702f1e0cf", "special_files": [ "README.md", "META.json", "Makefile", "first_last_agg.control" ], "tags": [ "first aggregate", "last aggregate", "aggregate function" ], "user": "wulczer", "version": "0.1.4" } pgxnclient-1.3/tests/testdata/https%3A%2F%2Fapi.pgxn.org%2Fdist%2Ffirst_last_agg.json000066400000000000000000000031301354122750200302030ustar00rootroot00000000000000{ "abstract": "Provides first() and last() aggregate functions.", "date": "2014-06-10T11:08:09Z", "docs": { "README": { "title": "README" }, "doc/first_last_agg": { "title": "first-last-agg 0.1.4" } }, "license": "postgresql", "maintainer": [ "Jan Urbański " ], "name": "first_last_agg", "prereqs": { "build": { "requires": { "PostgreSQL": "9.0.0" } } }, "provides": { "first_last_agg": { "docfile": "doc/first_last_agg.md", "docpath": "doc/first_last_agg", "file": "sql/first_last_agg.sql", "version": "0.1.4" } }, "release_status": "stable", "releases": { "stable": [ { "date": "2014-06-10T11:08:09Z", "version": "0.1.4" } ], "testing": [ { "date": "2011-05-26T10:28:37Z", "version": "0.1.2" } ] }, "resources": { "bugtracker": { "web": "http://github.com/wulczer/first_last_agg/issues/" }, "repository": { "type": "git", "url": "git://github.com/wulczer/first_last_agg.git", "web": "http://github.com/wulczer/first_last_agg/" } }, "sha1": "797a48bd3ea1a080a024d15e9384932702f1e0cf", "special_files": [ "README.md", "META.json", "Makefile", "first_last_agg.control" ], "tags": [ "first aggregate", "last aggregate", "aggregate function" ], "user": "wulczer", "version": "0.1.4" } https%3A%2F%2Fapi.pgxn.org%2Fdist%2Ffoobar%2F0.42.1%2FMETA-badsha1.json000066400000000000000000000031341354122750200315150ustar00rootroot00000000000000pgxnclient-1.3/tests/testdata{ "abstract": "A mock distribution", "date": "2011-04-20T23:47:22Z", "description": "This library doesn't exist.", "docs": { "README": { "title": "foobar 0.42.1" }, "doc/pair": { "abstract": "Abstract of an abstract project", "title": "foobar 0.42.1" } }, "license": "postgresql", "maintainer": [ "Daniele Varrazzo " ], "name": "foobar", "provides": { "foobar": { "abstract": "A non existing extension", "docfile": "doc/foobar.md", "docpath": "doc/foobar", "file": "sql/foobar.sql", "version": "0.42.1" } }, "release_status": "stable", "releases": { "stable": [ { "date": "2010-10-29T22:44:42Z", "version": "0.42.1" }, { "date": "2010-10-19T03:59:54Z", "version": "0.42.0" } ], "testing": [ { "date": "2011-04-20T23:47:22Z", "version": "0.43.2b1" } ] }, "resources": { "bugtracker": { "web": "http://github.com/piro/foobar/issues/" }, "repository": { "type": "git", "url": "git://github.com/piro/foobar.git", "web": "http://github.com/piro/foobar/" } }, "sha1": "8888888888888888888888888888888888888888", "special_files": [ "Changes", "README.md", "META.json", "Makefile", "foobar.control" ], "tags": [ "foo", "bar", "testing" ], "user": "piro", "version": "0.42.1" } pgxnclient-1.3/tests/testdata/https%3A%2F%2Fapi.pgxn.org%2Fdist%2Ffoobar%2F0.42.1%2FMETA.json000066400000000000000000000031341354122750200302730ustar00rootroot00000000000000{ "abstract": "A mock distribution", "date": "2011-04-20T23:47:22Z", "description": "This library doesn't exist.", "docs": { "README": { "title": "foobar 0.42.1" }, "doc/pair": { "abstract": "Abstract of an abstract project", "title": "foobar 0.42.1" } }, "license": "postgresql", "maintainer": [ "Daniele Varrazzo " ], "name": "foobar", "provides": { "foobar": { "abstract": "A non existing extension", "docfile": "doc/foobar.md", "docpath": "doc/foobar", "file": "sql/foobar.sql", "version": "0.42.1" } }, "release_status": "stable", "releases": { "stable": [ { "date": "2010-10-29T22:44:42Z", "version": "0.42.1" }, { "date": "2010-10-19T03:59:54Z", "version": "0.42.0" } ], "testing": [ { "date": "2011-04-20T23:47:22Z", "version": "0.43.2b1" } ] }, "resources": { "bugtracker": { "web": "http://github.com/piro/foobar/issues/" }, "repository": { "type": "git", "url": "git://github.com/piro/foobar.git", "web": "http://github.com/piro/foobar/" } }, "sha1": "6ae083946254210f6bfc9c5b2cae538bbaf59142", "special_files": [ "Changes", "README.md", "META.json", "Makefile", "foobar.control" ], "tags": [ "foo", "bar", "testing" ], "user": "piro", "version": "0.42.1" } https%3A%2F%2Fapi.pgxn.org%2Fdist%2Ffoobar%2F0.42.1%2Ffoobar-0.42.1.zip000077700000000000000000000000001354122750200340152foobar-0.42.1.zipustar00rootroot00000000000000pgxnclient-1.3/tests/testdatapgxnclient-1.3/tests/testdata/https%3A%2F%2Fapi.pgxn.org%2Fdist%2Ffoobar%2F0.43.2b1%2FMETA.json000066400000000000000000000031401354122750200305150ustar00rootroot00000000000000{ "abstract": "A mock distribution", "date": "2011-04-20T23:47:22Z", "description": "This library doesn't exist.", "docs": { "README": { "title": "foobar 0.42.1" }, "doc/pair": { "abstract": "Abstract of an abstract project", "title": "foobar 0.42.1" } }, "license": "postgresql", "maintainer": [ "Daniele Varrazzo " ], "name": "foobar", "provides": { "foobar": { "abstract": "A non existing extension", "docfile": "doc/foobar.md", "docpath": "doc/foobar", "file": "sql/foobar.sql", "version": "0.43.2b1" } }, "release_status": "stable", "releases": { "stable": [ { "date": "2010-10-29T22:44:42Z", "version": "0.42.1" }, { "date": "2010-10-19T03:59:54Z", "version": "0.42.0" } ], "testing": [ { "date": "2011-04-20T23:47:22Z", "version": "0.43.2b1" } ] }, "resources": { "bugtracker": { "web": "http://github.com/piro/foobar/issues/" }, "repository": { "type": "git", "url": "git://github.com/piro/foobar.git", "web": "http://github.com/piro/foobar/" } }, "sha1": "6ae083946254210f6bfc9c5b2cae538bbaf59142", "special_files": [ "Changes", "README.md", "META.json", "Makefile", "foobar.control" ], "tags": [ "foo", "bar", "testing" ], "user": "piro", "version": "0.43.2b1" } https%3A%2F%2Fapi.pgxn.org%2Fdist%2Ffoobar%2F0.43.2b1%2Ffoobar-0.43.2b1.zip000077700000000000000000000000001354122750200344672foobar-0.42.1.zipustar00rootroot00000000000000pgxnclient-1.3/tests/testdatapgxnclient-1.3/tests/testdata/https%3A%2F%2Fapi.pgxn.org%2Fdist%2Ffoobar.json000066400000000000000000000031341354122750200264670ustar00rootroot00000000000000{ "abstract": "A mock distribution", "date": "2011-04-20T23:47:22Z", "description": "This library doesn't exist.", "docs": { "README": { "title": "foobar 0.42.1" }, "doc/pair": { "abstract": "Abstract of an abstract project", "title": "foobar 0.42.1" } }, "license": "postgresql", "maintainer": [ "Daniele Varrazzo " ], "name": "foobar", "provides": { "foobar": { "abstract": "A non existing extension", "docfile": "doc/foobar.md", "docpath": "doc/foobar", "file": "sql/foobar.sql", "version": "0.42.1" } }, "release_status": "stable", "releases": { "stable": [ { "date": "2010-10-29T22:44:42Z", "version": "0.42.1" }, { "date": "2010-10-19T03:59:54Z", "version": "0.42.0" } ], "testing": [ { "date": "2011-04-20T23:47:22Z", "version": "0.43.2b1" } ] }, "resources": { "bugtracker": { "web": "http://github.com/piro/foobar/issues/" }, "repository": { "type": "git", "url": "git://github.com/piro/foobar.git", "web": "http://github.com/piro/foobar/" } }, "sha1": "6ae083946254210f6bfc9c5b2cae538bbaf59142", "special_files": [ "Changes", "README.md", "META.json", "Makefile", "foobar.control" ], "tags": [ "foo", "bar", "testing" ], "user": "piro", "version": "0.42.1" } pgxnclient-1.3/tests/testdata/https%3A%2F%2Fapi.pgxn.org%2Fdist%2Fpg_amqp%2F0.3.0%2FMETA.json000066400000000000000000000023441354122750200303650ustar00rootroot00000000000000{ "abstract": "AMQP protocol support for PostgreSQL", "date": "2011-05-19T21:47:12Z", "description": "The pg_amqp package provides the ability for postgres statements to directly publish messages to an AMQP broker.", "docs": { "README": { "title": "pg_amqp 0.3.0" }, "doc/amqp": { "abstract": "AMQP protocol support for PostgreSQL", "title": "amqp 0.3.0" } }, "license": [ "bsd", "mozilla_1_0" ], "maintainer": "Theo Schlossnagle ", "name": "pg_amqp", "provides": { "amqp": { "abstract": "AMQP protocol support for PostgreSQL", "docpath": "doc/amqp", "file": "sql/amqp.sql", "version": "0.3.0" } }, "release_status": "stable", "releases": { "stable": [ { "date": "2011-05-19T21:47:12Z", "version": "0.3.0" }, { "date": "2011-05-19T15:23:10Z", "version": "0.2.0" } ] }, "sha1": "6ae083946254210f6bfc9c5b2cae538bbaf59142", "special_files": [ "README.md", "META.json", "Makefile" ], "tags": [ "amqp" ], "user": "postwait", "version": "0.3.0" } https%3A%2F%2Fapi.pgxn.org%2Fdist%2Fpg_amqp%2F0.3.0%2Fpg_amqp-0.3.0.zip000077700000000000000000000000001354122750200341752foobar-0.42.1.zipustar00rootroot00000000000000pgxnclient-1.3/tests/testdatapgxnclient-1.3/tests/testdata/https%3A%2F%2Fapi.pgxn.org%2Fdist%2Fpyrseas%2F0.4.1%2FMETA.json000066400000000000000000000053671354122750200304410ustar00rootroot00000000000000{ "abstract": "Framework and utilities to upgrade and maintain databases", "date": "2011-10-27T16:43:33Z", "description": "Pyrseas provides a framework and utilities to upgrade and maintain a PostgreSQL database. Its utilities output a database schema in YAML format suitable for committing to a version control system and read this format to generate SQL to sync to another database. Supports PostgreSQL 8.4, 9.0 and 9.1.", "docs": { "README": { "title": "README" }, "docs/dbtoyaml": { "abstract": "Output PostgreSQL schemas in YAML format", "title": "dbtoyaml" }, "docs/yamltodb": { "abstract": "Generate SQL to sync a database with a YAML schema spec", "title": "yamltodb" } }, "license": "bsd", "maintainer": "Joe Abbate ", "name": "Pyrseas", "prereqs": { "runtime": { "requires": { "PostgreSQL": "8.4.0" } } }, "provides": { "dbtoyaml": { "abstract": "Output PostgreSQL schemas in YAML format", "docfile": "docs/dbtoyaml.rst", "docpath": "docs/dbtoyaml", "file": "pyrseas/dbtoyaml.py", "version": "0.4.1" }, "yamltodb": { "abstract": "Generate SQL to sync a database with a YAML schema spec", "docfile": "docs/yamltodb.rst", "docpath": "docs/yamltodb", "file": "pyrseas/yamltodb.py", "version": "0.4.1" } }, "release_status": "stable", "releases": { "stable": [ { "date": "2011-10-27T16:43:33Z", "version": "0.4.1" }, { "date": "2011-09-27T01:35:22Z", "version": "0.4.0" }, { "date": "2011-08-26T17:15:44Z", "version": "0.3.1" }, { "date": "2011-06-30T15:31:05Z", "version": "0.3.0" }, { "date": "2011-06-07T20:41:39Z", "version": "0.2.1" }, { "date": "2011-05-19T18:43:06Z", "version": "0.2.0" } ] }, "resources": { "bugtracker": { "web": "https://github.com/jmafc/Pyrseas/issues" }, "homepage": "http://pyrseas.org/", "repository": { "type": "git", "url": "git://github.com/jmafc/Pyrseas.git", "web": "https://github.com/jmafc/Pyrseas" } }, "sha1": "6ae083946254210f6bfc9c5b2cae538bbaf59142", "special_files": [ "README", "META.json", "Makefile", "MANIFEST.in" ], "tags": [ "version control", "yaml", "database version control", "schema versioning" ], "user": "jma", "version": "0.4.1" } https%3A%2F%2Fapi.pgxn.org%2Fdist%2Fpyrseas%2F0.4.1%2Fpyrseas-0.4.1.zip000077700000000000000000000000001354122750200343052foobar-0.42.1.zipustar00rootroot00000000000000pgxnclient-1.3/tests/testdatapgxnclient-1.3/tests/testdata/https%3A%2F%2Fapi.pgxn.org%2Fdist%2Fpyrseas.json000066400000000000000000000053671354122750200267170ustar00rootroot00000000000000{ "abstract": "Framework and utilities to upgrade and maintain databases", "date": "2011-10-27T16:43:33Z", "description": "Pyrseas provides a framework and utilities to upgrade and maintain a PostgreSQL database. Its utilities output a database schema in YAML format suitable for committing to a version control system and read this format to generate SQL to sync to another database. Supports PostgreSQL 8.4, 9.0 and 9.1.", "docs": { "README": { "title": "README" }, "docs/dbtoyaml": { "abstract": "Output PostgreSQL schemas in YAML format", "title": "dbtoyaml" }, "docs/yamltodb": { "abstract": "Generate SQL to sync a database with a YAML schema spec", "title": "yamltodb" } }, "license": "bsd", "maintainer": "Joe Abbate ", "name": "Pyrseas", "prereqs": { "runtime": { "requires": { "PostgreSQL": "8.4.0" } } }, "provides": { "dbtoyaml": { "abstract": "Output PostgreSQL schemas in YAML format", "docfile": "docs/dbtoyaml.rst", "docpath": "docs/dbtoyaml", "file": "pyrseas/dbtoyaml.py", "version": "0.4.1" }, "yamltodb": { "abstract": "Generate SQL to sync a database with a YAML schema spec", "docfile": "docs/yamltodb.rst", "docpath": "docs/yamltodb", "file": "pyrseas/yamltodb.py", "version": "0.4.1" } }, "release_status": "stable", "releases": { "stable": [ { "date": "2011-10-27T16:43:33Z", "version": "0.4.1" }, { "date": "2011-09-27T01:35:22Z", "version": "0.4.0" }, { "date": "2011-08-26T17:15:44Z", "version": "0.3.1" }, { "date": "2011-06-30T15:31:05Z", "version": "0.3.0" }, { "date": "2011-06-07T20:41:39Z", "version": "0.2.1" }, { "date": "2011-05-19T18:43:06Z", "version": "0.2.0" } ] }, "resources": { "bugtracker": { "web": "https://github.com/jmafc/Pyrseas/issues" }, "homepage": "http://pyrseas.org/", "repository": { "type": "git", "url": "git://github.com/jmafc/Pyrseas.git", "web": "https://github.com/jmafc/Pyrseas" } }, "sha1": "11f5085f99811cc1d78ac97d2dfef42c90aeda08", "special_files": [ "README", "META.json", "Makefile", "MANIFEST.in" ], "tags": [ "version control", "yaml", "database version control", "schema versioning" ], "user": "jma", "version": "0.4.1" } pgxnclient-1.3/tests/testdata/https%3A%2F%2Fapi.pgxn.org%2Fextension%2Famqp.json000066400000000000000000000006721354122750200272320ustar00rootroot00000000000000{ "extension": "amqp", "latest": "stable", "stable": { "abstract": "AMQP protocol support for PostgreSQL", "dist": "pg_amqp", "docpath": "doc/amqp", "sha1": "4c7112a28584ecd2ac9607cf62274a0ec9d586cf", "version": "0.3.0" }, "versions": { "0.3.0": [ { "date": "2011-05-19T21:47:12Z", "dist": "pg_amqp", "version": "0.3.0" } ] } } pgxnclient-1.3/tests/testdata/https%3A%2F%2Fapi.pgxn.org%2Findex.json000066400000000000000000000011001354122750200251740ustar00rootroot00000000000000{ "dist": "/dist/{dist}.json", "download": "/dist/{dist}/{version}/{dist}-{version}.zip", "extension": "/extension/{extension}.json", "htmldoc": "/dist/{dist}/{version}/{+docpath}.html", "meta": "/dist/{dist}/{version}/META.json", "mirrors": "/meta/mirrors.json", "readme": "/dist/{dist}/{version}/README.txt", "search": "/search/{in}/", "source": "/src/{dist}/{dist}-{version}/", "spec": "/meta/spec.{format}", "stats": "/stats/{stats}.json", "tag": "/tag/{tag}.json", "user": "/user/{user}.json", "userlist": "/users/{char}.json" } pgxnclient-1.3/tests/testdata/https%3A%2F%2Fapi.pgxn.org%2Fmeta%2Fmirrors.json000066400000000000000000000055111354122750200267000ustar00rootroot00000000000000[ { "uri": "http://pgxn.depesz.com/", "frequency": "every 6 hours", "location": "Nürnberg, Germany", "organization": "depesz Software Hubert Lubaczewski", "timezone": "CEST", "email": "depesz.com|web_pgxn", "bandwidth": "100Mbps", "src": "rsync://master.pgxn.org/pgxn/", "rsync": "", "notes": "access via http only" }, { "uri": "http://www.postgres-support.ch/pgxn/", "frequency": "hourly", "location": "Basel, Switzerland, Europe", "organization": "micro systems", "timezone": "CEST", "email": "msys.ch|marc", "bandwidth": "10Mbps", "src": "rsync://master.pgxn.org/pgxn", "rsync": "", "notes": "" }, { "uri": "http://pgxn.justatheory.com/", "frequency": "daily", "location": "Portland, OR, USA", "organization": "David E. Wheeler", "timezone": "America/Los_Angeles", "email": "justatheory.com|pgxn", "bandwidth": "Cable", "src": "rsync://master.pgxn.org/pgxn/", "rsync": "", "notes": "" }, { "uri": "http://pgxn.darkixion.com/", "frequency": "hourly", "location": "London, UK", "organization": "Thom Brown", "timezone": "Europe/London", "email": "darkixion.com|pgxn", "bandwidth": "1Gbps", "src": "rsync://master.pgxn.org/pgxn", "rsync": "rsync://pgxn.darkixion.com/pgxn", "notes": "" }, { "uri": "http://mirrors.cat.pdx.edu/pgxn/", "frequency": "hourly", "location": "Portland, OR, USA", "organization": "PSU Computer Action Team", "timezone": "America/Los_Angeles", "email": "cat.pdx.edu|support", "bandwidth": "60Mbsec", "src": "rsync://master.pgxn.org/pgxn", "rsync": "rsync://mirrors.cat.pdx.edu/pgxn", "notes": "I2 and IPv6" }, { "uri": "http://pgxn.dalibo.org/", "frequency": "hourly", "location": "Marseille, France", "organization": "DALIBO SARL", "timezone": "CEST", "email": "dalibo.com|contact", "bandwidth": "100Mbps", "src": "rsync://master.pgxn.org/pgxn/", "rsync": "", "notes": "" }, { "uri": "http://pgxn.cxsoftware.org/", "frequency": "hourly", "location": "Seattle, WA, USA", "organization": "CxNet", "timezone": "America/Los_Angeles", "email": "cxnet.cl|cristobal", "bandwidth": "100Mbps", "src": "rsync://master.pgxn.org/pgxn/", "rsync": "", "notes": "" }, { "uri": "http://api.pgxn.org/", "frequency": "hourly", "location": "Portland, OR, USA", "organization": "PGXN", "timezone": "America/Los_Angeles", "email": "pgexperts.com|pgxn", "bandwidth": "10MBps", "src": "rsync://master.pgxn.org/pgxn", "rsync": "", "notes": "API server." } ] https%3A%2F%2Fapi.pgxn.org%2Fsearch%2Fdocs%2F%3Fq%3D%2522foo%2Bbar%2522%2Bbaz000066400000000000000000000021721354122750200320430ustar00rootroot00000000000000pgxnclient-1.3/tests/testdata{"hits":[{"excerpt":"… SELECT pair('foo', 'bar'); pair ------------ (foo,bar) % SELECT 'foo' ~> 'bar'; pair ------------ (foo,bar) Description This library contains a single PostgreSQL extension, a key/value pair data…","date":"2011-05-12T18:55:30Z","version":"0.1.3","dist":"pair","score":"0.245","user_name":"David E. Wheeler","user":"theory","title":"pair 0.1.2","abstract":"A key/value pair data type","docpath":"doc/pair"},{"excerpt":"… labels ) SELECT enum_has_labels( 'myschema', 'someenum', ARRAY['foo', 'bar'], 'Enum someenum should have labels foo, bar' ); This function tests that an enum consists of an expected list of labels.","date":"2011-02-02T03:25:17Z","version":"0.25.0","dist":"pgTAP","score":"0.010","user_name":"David E. Wheeler","user":"theory","title":"pgTAP 0.25.0","abstract":"","docpath":"doc/pgtap"}],"count":2,"query":"\"foo bar\" baz","limit":50,"offset":0}pgxnclient-1.3/tests/testdata/https%3A%2F%2Fapi.pgxn.org%2Fsearch%2Fdocs%2F%3Fq%3Doracle000066400000000000000000000161011354122750200277750ustar00rootroot00000000000000{"count":16,"limit":50,"offset":0,"hits":[{"user_name":"Pavel Stěhule","dist":"orafce","docpath":"doc/sql_migration/sql_migration00","date":"2018-12-23T06:35:11Z","score":"0.332","version":"3.7.2","abstract":"","excerpt":"Appendix A Correspondence with Oracle Databases Explains the correspondence between PostgreSQL and Oracle databases. Version Edition 1.0: February 2017","title":"Oracle to PostgreSQL Migration Guide","user":"okbob"},{"date":"2018-12-23T06:35:11Z","user_name":"Pavel Stěhule","dist":"orafce","docpath":"doc/sql_migration/sql_migration02","title":"sql_migration02","user":"okbob","score":"0.016","excerpt":"… migration when NULL is returned by string concatenation (||) in Oracle databases. Oracle database PostgreSQLSELECT 'NAME:' || name FROM staff_table; SELECT 'NAME:' || NVL( name, '' ) FROM…","version":"3.7.2","abstract":""},{"docpath":"doc/db2_fdw","user_name":"Wolfgang Brandl","dist":"db2_fdw","date":"2018-08-29T07:47:38Z","excerpt":"Description Based on the foreign data wrapper of postgreSQL to oracle, the db2_fdw was built. Main difference are the data types. They are mainly not standardized.","version":"2.0.1","abstract":"PostgreSQL Data Wrappper to DB2 databases","score":"0.015","user":"brandlw","title":"db2_fdw"},{"score":"0.015","abstract":"","version":"3.7.2","excerpt":"… below shows migration when RESPECT NULLS is specified in the Oracle database. LEAD migration example (when RESPECT NULLS is specified) Oracle database PostgreSQLSELECT staff_id, name, job, LEAD(…","title":"sql_migration03","user":"okbob","dist":"orafce","user_name":"Pavel Stěhule","docpath":"doc/sql_migration/sql_migration03","date":"2018-12-23T06:35:11Z"},{"excerpt":"System privileges |GRANT statement features of Oracle databases|Migratability|Remarks| |:---|:---:|:---| | Granting of system privileges | MR | PUBLIC cannot be specified for a grantee.","version":"3.7.2","abstract":"","score":"0.015","user":"okbob","title":"sql_migration04","docpath":"doc/sql_migration/sql_migration04","dist":"orafce","user_name":"Pavel Stěhule","date":"2018-12-23T06:35:11Z"},{"docpath":"doc/sql_migration/sql_migration07","dist":"orafce","user_name":"Pavel Stěhule","date":"2018-12-23T06:35:11Z","version":"3.7.2","abstract":"","excerpt":"Correspondence between Oracle database hints and pg_hint_plan |Hint (Oracle database)|Hint (PostgreSQL)| |:---|:---| |FIRST_ROWS hint |Rows | |FULL hint |Seqscan | |INDEX hint |IndexScan | |LEADING…","score":"0.015","user":"okbob","title":"sql_migration07"},{"date":"2018-12-23T06:35:11Z","user_name":"Pavel Stěhule","dist":"orafce","docpath":"doc/sql_migration/sql_migration05","title":"sql_migration05","user":"okbob","score":"0.014","version":"3.7.2","excerpt":"Functional differences Oracle database Predefined exceptions can be used. PostgreSQL Predefined exceptions cannot be used. Use PostgreSQL error codes instead.Migration procedure Use the following…","abstract":""},{"date":"2018-12-23T06:35:11Z","dist":"orafce","user_name":"Pavel Stěhule","docpath":"doc/orafce_documentation/Orafce_Documentation_02","title":"Orafce_Documentation_02","user":"okbob","score":"0.013","abstract":"","version":"3.7.2","excerpt":"Each feature compatible with Oracle databases is defined in the oracle schema. It is recommended to set search_path in postgresql.conf. In this case, it will be effective for each instance."},{"date":"2018-12-23T06:35:11Z","docpath":"doc/sql_migration/sql_migration06","dist":"orafce","user_name":"Pavel Stěhule","user":"okbob","title":"sql_migration06","version":"3.7.2","excerpt":"Statements such as SET can be used to change oracle.nls_date_format.Migration procedure Use the following procedure to perform migration: Search for the keywords TO_DATE and TO_CHAR, and check where…","abstract":"","score":"0.013"},{"score":"0.012","version":"3.7.2","excerpt":"The table below lists features compatible with Oracle databases. 1.1 Features compatible with Oracle databases Data type |Item|Overview| |:---|:---| |VARCHAR2|Variable-length character data type|…","abstract":"","title":"Orafce Documentation","user":"okbob","dist":"orafce","user_name":"Pavel Stěhule","docpath":"doc/orafce_documentation/Orafce_Documentation_01","date":"2018-12-23T06:35:11Z"},{"docpath":"doc/first_last_agg","user_name":"Jan Urbański","dist":"first_last_agg","date":"2014-06-10T11:08:09Z","excerpt":"… which do you get and for emulating a similar functionality from Oracle. Author Jan Urbański, based on code from the PostgreSQL wiki, specifically on the SQL definitions of first and last taken from…","version":"0.1.4","abstract":"","score":"0.011","user":"wulczer","title":"first-last-agg 0.1.4"},{"title":"sql_migration01","user":"okbob","score":"0.008","abstract":"","version":"3.7.2","excerpt":"Note In this migration guide, the migration source Oracle database and the migration target PostgreSQL are targeted to be the versions listed below.","date":"2018-12-23T06:35:11Z","user_name":"Pavel Stěhule","dist":"orafce","docpath":"doc/sql_migration/sql_migration01"},{"title":"Orafce_Documentation_05","user":"okbob","score":"0.006","version":"3.7.2","abstract":"","excerpt":"If omitted, the format specified in the oracle.nls_date_format variable is used. If the oracle.nls_date_format variable has not been set, the existing date/time input interpretation is used.","date":"2018-12-23T06:35:11Z","user_name":"Pavel Stěhule","dist":"orafce","docpath":"doc/orafce_documentation/Orafce_Documentation_05"},{"score":"0.002","version":"3.7.2","abstract":"","excerpt":"… BEGIN g_Date := DBMS_PIPE.UNPACK_MESSAGE_DATE(); ~~~ Note If the "oracle" schema is set in search_path, the DATE type of orafce will be used, so for receiving data, use UNPACK_MESSAGE_TIMESTAMP.","title":"Orafce_Documentation_06","user":"okbob","user_name":"Pavel Stěhule","dist":"orafce","docpath":"doc/orafce_documentation/Orafce_Documentation_06","date":"2018-12-23T06:35:11Z"},{"excerpt":"There are also extensions to existed DBMSes, such as "Oracle In-Memory Option". This plug-in tries to provide such functionality for PostgreSQL.","version":"0.1.6","abstract":"","score":"0.002","user":"knizhnik","title":"In-Memory Columnar Store (IMCS)","docpath":"user_guide","dist":"imcs","user_name":"Konstantin Knizhnik","date":"2014-07-10T07:40:34Z"},{"score":"0.001","version":"0.99.0","abstract":"Unit testing for PostgreSQL","excerpt":"Examples: SELECT fdw_privs_are( 'oracle', 'fred', ARRAY['USAGE'], 'Fred should be granted USAGE on fdw "oracle"' ); SELECT fdw_privs_are( 'log_csv', ARRAY['USAGE'] ); If the role is granted…","title":"pgTAP 0.99.0","user":"theory","user_name":"David E. Wheeler","dist":"pgTAP","docpath":"doc/pgtap","date":"2018-09-16T20:58:07Z"}],"query":"oracle"}pgxnclient-1.3/tests/testdata/https%3A%2F%2Fexample.org%2Ffoobar-0.42.1.tar.gz000077700000000000000000000000001354122750200313422foobar-0.42.1.tar.gzustar00rootroot00000000000000pgxnclient-1.3/tests/testdata/tar.ext000077700000000000000000000000001354122750200231262foobar-0.42.1.tar.gzustar00rootroot00000000000000pgxnclient-1.3/tests/testdata/zip.ext000077700000000000000000000000001354122750200225372foobar-0.42.1.zipustar00rootroot00000000000000pgxnclient-1.3/tests/testutils.py000066400000000000000000000006321354122750200173060ustar00rootroot00000000000000""" pgxnclient -- unit test utilities """ # Copyright (C) 2011-2019 Daniele Varrazzo # This file is part of the PGXN client import os def ifunlink(fn): """Delete a file if exists.""" if os.path.exists(fn): os.unlink(fn) def get_test_filename(*parts): """Return the complete file name for a testing file. """ return os.path.join(os.path.dirname(__file__), 'testdata', *parts) pgxnclient-1.3/tox.ini000066400000000000000000000001731354122750200150450ustar00rootroot00000000000000[flake8] ignore = W503,E203 max-line-length = 85 exclude = env,build,.eggs [pytest] addopts = --verbose --doctest-modules