purl-1.5/0000755000076500000240000000000013441274412012555 5ustar davidstaff00000000000000purl-1.5/PKG-INFO0000644000076500000240000002176413441274412013664 0ustar davidstaff00000000000000Metadata-Version: 1.1 Name: purl Version: 1.5 Summary: An immutable URL class for easy URL-building and manipulation Home-page: https://github.com/codeinthehole/purl Author: David Winterbottom Author-email: david.winterbottom@gmail.com License: MIT Description: ================================ purl - A simple Python URL class ================================ A simple, immutable URL class with a clean API for interrogation and manipulation. Supports Pythons 2.7, 3.3, 3.4, 3.5, 3.6 and pypy. Also supports template URLs as per `RFC 6570`_ Contents: .. contents:: :local: :depth: 1 .. image:: https://secure.travis-ci.org/codeinthehole/purl.png :target: https://travis-ci.org/codeinthehole/purl .. image:: https://img.shields.io/pypi/v/purl.svg :target: https://crate.io/packages/purl/ .. _`RFC 6570`: http://tools.ietf.org/html/rfc6570 Docs ---- http://purl.readthedocs.org/en/latest/ Install ------- From PyPI (stable):: $ pip install purl From Github (unstable):: $ pip install git+git://github.com/codeinthehole/purl.git#egg=purl Use --- Construct: .. code:: python >>> from purl import URL # String constructor >>> from_str = URL('https://www.google.com/search?q=testing') # Keyword constructor >>> from_kwargs = URL(scheme='https', host='www.google.com', path='/search', query='q=testing') # Combine >>> from_combo = URL('https://www.google.com').path('search').query_param('q', 'testing') URL objects are immutable - all mutator methods return a new instance. Interrogate: .. code:: python >>> u = URL('https://www.google.com/search?q=testing') >>> u.scheme() 'https' >>> u.host() 'www.google.com' >>> u.domain() 'www.google.com' >>> u.username() >>> u.password() >>> u.netloc() 'www.google.com' >>> u.port() >>> u.path() '/search' >>> u.query() 'q=testing' >>> u.fragment() '' >>> u.path_segment(0) 'search' >>> u.path_segments() ('search',) >>> u.query_param('q') 'testing' >>> u.query_param('q', as_list=True) ['testing'] >>> u.query_param('lang', default='GB') 'GB' >>> u.query_params() {'q': ['testing']} >>> u.has_query_param('q') True >>> u.has_query_params(('q', 'r')) False >>> u.subdomains() ['www', 'google', 'com'] >>> u.subdomain(0) 'www' Note that each accessor method is overloaded to be a mutator method too, similar to the jQuery API. Eg: .. code:: python >>> u = URL.from_string('https://github.com/codeinthehole') # Access >>> u.path_segment(0) 'codeinthehole' # Mutate (creates a new instance) >>> new_url = u.path_segment(0, 'tangentlabs') >>> new_url is u False >>> new_url.path_segment(0) 'tangentlabs' Hence, you can build a URL up in steps: .. code:: python >>> u = URL().scheme('http').domain('www.example.com').path('/some/path').query_param('q', 'search term') >>> u.as_string() 'http://www.example.com/some/path?q=search+term' Along with the above overloaded methods, there is also a ``add_path_segment`` method for adding a segment at the end of the current path: .. code:: python >>> new_url = u.add_path_segment('here') >>> new_url.as_string() 'http://www.example.com/some/path/here?q=search+term' Couple of other things: * Since the URL class is immutable it can be used as a key in a dictionary * It can be pickled and restored * It supports equality operations * It supports equality operations URL templates can be used either via a ``Template`` class: .. code:: python >>> from purl import Template >>> tpl = Template("http://example.com{/list*}") >>> url = tpl.expand({'list': ['red', 'green', 'blue']}) >>> url.as_string() 'http://example.com/red/green/blue' or the ``expand`` function: .. code:: python >>> from purl import expand >>> expand(u"{/list*}", {'list': ['red', 'green', 'blue']}) '/red/green/blue' A wide variety of expansions are possible - refer to the RFC_ for more details. .. _RFC: http://tools.ietf.org/html/rfc6570 Changelog --------- v1.5 - 2019-03-10 ~~~~~~~~~~~~~~~~~ * Allow `@` in passwords. v1.4 - 2018-03-11 ~~~~~~~~~~~~~~~~~ * Allow usernames and passwords to be removed from URLs. v1.3.1 ~~~~~~ * Ensure paths always have a leading slash. v1.3 ~~~~ * Allow absolute URLs to be converted into relative. v1.2 ~~~~ * Support password-less URLs. * Allow slashes to be passed as path segments. v1.1 ~~~~ * Support setting username and password via mutator methods v1.0.3 ~~~~~~ * Handle some unicode compatibility edge-cases v1.0.2 ~~~~~~ * Fix template expansion bug with no matching variables being passed in. This ensures ``purl.Template`` works correctly with the URLs returned from the Github API. v1.0.1 ~~~~~~ * Fix bug with special characters in paths not being escaped. v1.0 ~~~~ * Slight tidy up. Document support for PyPy and Python 3.4. v0.8 ~~~~ * Support for RFC 6570 URI templates v0.7 ~~~~ * All internal strings are unicode. * Support for unicode chars in path, fragment, query, auth added. v0.6 ~~~~ * Added ``append_query_param`` method * Added ``remove_query_param`` method v0.5 ~~~~ * Added support for Python 3.2/3.3 (thanks @pmcnr and @mitchellrj) v0.4.1 ~~~~~~ * Added API docs * Added to readthedocs.org v0.4 ~~~~ * Modified constructor to accept full URL string as first arg * Added ``add_path_segment`` method v0.3.2 ~~~~~~ * Fixed bug port number in string when using from_string constructor v0.3.1 ~~~~~~ * Fixed bug with passing lists to query param setter methods v0.3 ~~~~ * Added support for comparison and equality * Added support for pickling * Added ``__slots__`` so instances can be used as keys within dictionaries Contribute ---------- Clone, create a virtualenv then install purl and the packages required for testing:: $ git clone git@github.com:codeinthehole/purl.git $ cd purl $ mkvirtualenv purl # requires virtualenvwrapper (purl) $ make Ensure tests pass using:: (purl) $ ./runtests.sh or:: $ tox Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: Libraries :: Python Modules purl-1.5/LICENSE0000644000076500000240000000206212644265650013572 0ustar davidstaff00000000000000Copyright (C) 2012 purl authors (see AUTHORS file) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.purl-1.5/purl.egg-info/0000755000076500000240000000000013441274412015231 5ustar davidstaff00000000000000purl-1.5/purl.egg-info/PKG-INFO0000644000076500000240000002176413441274412016340 0ustar davidstaff00000000000000Metadata-Version: 1.1 Name: purl Version: 1.5 Summary: An immutable URL class for easy URL-building and manipulation Home-page: https://github.com/codeinthehole/purl Author: David Winterbottom Author-email: david.winterbottom@gmail.com License: MIT Description: ================================ purl - A simple Python URL class ================================ A simple, immutable URL class with a clean API for interrogation and manipulation. Supports Pythons 2.7, 3.3, 3.4, 3.5, 3.6 and pypy. Also supports template URLs as per `RFC 6570`_ Contents: .. contents:: :local: :depth: 1 .. image:: https://secure.travis-ci.org/codeinthehole/purl.png :target: https://travis-ci.org/codeinthehole/purl .. image:: https://img.shields.io/pypi/v/purl.svg :target: https://crate.io/packages/purl/ .. _`RFC 6570`: http://tools.ietf.org/html/rfc6570 Docs ---- http://purl.readthedocs.org/en/latest/ Install ------- From PyPI (stable):: $ pip install purl From Github (unstable):: $ pip install git+git://github.com/codeinthehole/purl.git#egg=purl Use --- Construct: .. code:: python >>> from purl import URL # String constructor >>> from_str = URL('https://www.google.com/search?q=testing') # Keyword constructor >>> from_kwargs = URL(scheme='https', host='www.google.com', path='/search', query='q=testing') # Combine >>> from_combo = URL('https://www.google.com').path('search').query_param('q', 'testing') URL objects are immutable - all mutator methods return a new instance. Interrogate: .. code:: python >>> u = URL('https://www.google.com/search?q=testing') >>> u.scheme() 'https' >>> u.host() 'www.google.com' >>> u.domain() 'www.google.com' >>> u.username() >>> u.password() >>> u.netloc() 'www.google.com' >>> u.port() >>> u.path() '/search' >>> u.query() 'q=testing' >>> u.fragment() '' >>> u.path_segment(0) 'search' >>> u.path_segments() ('search',) >>> u.query_param('q') 'testing' >>> u.query_param('q', as_list=True) ['testing'] >>> u.query_param('lang', default='GB') 'GB' >>> u.query_params() {'q': ['testing']} >>> u.has_query_param('q') True >>> u.has_query_params(('q', 'r')) False >>> u.subdomains() ['www', 'google', 'com'] >>> u.subdomain(0) 'www' Note that each accessor method is overloaded to be a mutator method too, similar to the jQuery API. Eg: .. code:: python >>> u = URL.from_string('https://github.com/codeinthehole') # Access >>> u.path_segment(0) 'codeinthehole' # Mutate (creates a new instance) >>> new_url = u.path_segment(0, 'tangentlabs') >>> new_url is u False >>> new_url.path_segment(0) 'tangentlabs' Hence, you can build a URL up in steps: .. code:: python >>> u = URL().scheme('http').domain('www.example.com').path('/some/path').query_param('q', 'search term') >>> u.as_string() 'http://www.example.com/some/path?q=search+term' Along with the above overloaded methods, there is also a ``add_path_segment`` method for adding a segment at the end of the current path: .. code:: python >>> new_url = u.add_path_segment('here') >>> new_url.as_string() 'http://www.example.com/some/path/here?q=search+term' Couple of other things: * Since the URL class is immutable it can be used as a key in a dictionary * It can be pickled and restored * It supports equality operations * It supports equality operations URL templates can be used either via a ``Template`` class: .. code:: python >>> from purl import Template >>> tpl = Template("http://example.com{/list*}") >>> url = tpl.expand({'list': ['red', 'green', 'blue']}) >>> url.as_string() 'http://example.com/red/green/blue' or the ``expand`` function: .. code:: python >>> from purl import expand >>> expand(u"{/list*}", {'list': ['red', 'green', 'blue']}) '/red/green/blue' A wide variety of expansions are possible - refer to the RFC_ for more details. .. _RFC: http://tools.ietf.org/html/rfc6570 Changelog --------- v1.5 - 2019-03-10 ~~~~~~~~~~~~~~~~~ * Allow `@` in passwords. v1.4 - 2018-03-11 ~~~~~~~~~~~~~~~~~ * Allow usernames and passwords to be removed from URLs. v1.3.1 ~~~~~~ * Ensure paths always have a leading slash. v1.3 ~~~~ * Allow absolute URLs to be converted into relative. v1.2 ~~~~ * Support password-less URLs. * Allow slashes to be passed as path segments. v1.1 ~~~~ * Support setting username and password via mutator methods v1.0.3 ~~~~~~ * Handle some unicode compatibility edge-cases v1.0.2 ~~~~~~ * Fix template expansion bug with no matching variables being passed in. This ensures ``purl.Template`` works correctly with the URLs returned from the Github API. v1.0.1 ~~~~~~ * Fix bug with special characters in paths not being escaped. v1.0 ~~~~ * Slight tidy up. Document support for PyPy and Python 3.4. v0.8 ~~~~ * Support for RFC 6570 URI templates v0.7 ~~~~ * All internal strings are unicode. * Support for unicode chars in path, fragment, query, auth added. v0.6 ~~~~ * Added ``append_query_param`` method * Added ``remove_query_param`` method v0.5 ~~~~ * Added support for Python 3.2/3.3 (thanks @pmcnr and @mitchellrj) v0.4.1 ~~~~~~ * Added API docs * Added to readthedocs.org v0.4 ~~~~ * Modified constructor to accept full URL string as first arg * Added ``add_path_segment`` method v0.3.2 ~~~~~~ * Fixed bug port number in string when using from_string constructor v0.3.1 ~~~~~~ * Fixed bug with passing lists to query param setter methods v0.3 ~~~~ * Added support for comparison and equality * Added support for pickling * Added ``__slots__`` so instances can be used as keys within dictionaries Contribute ---------- Clone, create a virtualenv then install purl and the packages required for testing:: $ git clone git@github.com:codeinthehole/purl.git $ cd purl $ mkvirtualenv purl # requires virtualenvwrapper (purl) $ make Ensure tests pass using:: (purl) $ ./runtests.sh or:: $ tox Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: Libraries :: Python Modules purl-1.5/purl.egg-info/SOURCES.txt0000644000076500000240000000035213441274412017115 0ustar davidstaff00000000000000LICENSE MANIFEST.in README.rst setup.cfg setup.py purl/__init__.py purl/template.py purl/url.py purl.egg-info/PKG-INFO purl.egg-info/SOURCES.txt purl.egg-info/dependency_links.txt purl.egg-info/requires.txt purl.egg-info/top_level.txtpurl-1.5/purl.egg-info/requires.txt0000644000076500000240000000000413441274412017623 0ustar davidstaff00000000000000six purl-1.5/purl.egg-info/top_level.txt0000644000076500000240000000000513441274412017756 0ustar davidstaff00000000000000purl purl-1.5/purl.egg-info/dependency_links.txt0000644000076500000240000000000113441274412021277 0ustar davidstaff00000000000000 purl-1.5/MANIFEST.in0000644000076500000240000000002612644265650014321 0ustar davidstaff00000000000000include *.rst LICENSE purl-1.5/setup.py0000755000076500000240000000234513441274162014300 0ustar davidstaff00000000000000#!/usr/bin/env python from setuptools import setup, find_packages setup( name='purl', version='1.5', description=( "An immutable URL class for easy URL-building and manipulation"), long_description=open('README.rst').read(), url='https://github.com/codeinthehole/purl', license='MIT', author="David Winterbottom", author_email="david.winterbottom@gmail.com", packages=find_packages(exclude=['tests']), install_requires=['six'], include_package_data=True, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 2', 'Programming Language :: Python', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries :: Python Modules', ], ) purl-1.5/setup.cfg0000644000076500000240000000007513441274412014400 0ustar davidstaff00000000000000[wheel] universal = 1 [egg_info] tag_build = tag_date = 0 purl-1.5/README.rst0000644000076500000240000001335713441274262014260 0ustar davidstaff00000000000000================================ purl - A simple Python URL class ================================ A simple, immutable URL class with a clean API for interrogation and manipulation. Supports Pythons 2.7, 3.3, 3.4, 3.5, 3.6 and pypy. Also supports template URLs as per `RFC 6570`_ Contents: .. contents:: :local: :depth: 1 .. image:: https://secure.travis-ci.org/codeinthehole/purl.png :target: https://travis-ci.org/codeinthehole/purl .. image:: https://img.shields.io/pypi/v/purl.svg :target: https://crate.io/packages/purl/ .. _`RFC 6570`: http://tools.ietf.org/html/rfc6570 Docs ---- http://purl.readthedocs.org/en/latest/ Install ------- From PyPI (stable):: $ pip install purl From Github (unstable):: $ pip install git+git://github.com/codeinthehole/purl.git#egg=purl Use --- Construct: .. code:: python >>> from purl import URL # String constructor >>> from_str = URL('https://www.google.com/search?q=testing') # Keyword constructor >>> from_kwargs = URL(scheme='https', host='www.google.com', path='/search', query='q=testing') # Combine >>> from_combo = URL('https://www.google.com').path('search').query_param('q', 'testing') URL objects are immutable - all mutator methods return a new instance. Interrogate: .. code:: python >>> u = URL('https://www.google.com/search?q=testing') >>> u.scheme() 'https' >>> u.host() 'www.google.com' >>> u.domain() 'www.google.com' >>> u.username() >>> u.password() >>> u.netloc() 'www.google.com' >>> u.port() >>> u.path() '/search' >>> u.query() 'q=testing' >>> u.fragment() '' >>> u.path_segment(0) 'search' >>> u.path_segments() ('search',) >>> u.query_param('q') 'testing' >>> u.query_param('q', as_list=True) ['testing'] >>> u.query_param('lang', default='GB') 'GB' >>> u.query_params() {'q': ['testing']} >>> u.has_query_param('q') True >>> u.has_query_params(('q', 'r')) False >>> u.subdomains() ['www', 'google', 'com'] >>> u.subdomain(0) 'www' Note that each accessor method is overloaded to be a mutator method too, similar to the jQuery API. Eg: .. code:: python >>> u = URL.from_string('https://github.com/codeinthehole') # Access >>> u.path_segment(0) 'codeinthehole' # Mutate (creates a new instance) >>> new_url = u.path_segment(0, 'tangentlabs') >>> new_url is u False >>> new_url.path_segment(0) 'tangentlabs' Hence, you can build a URL up in steps: .. code:: python >>> u = URL().scheme('http').domain('www.example.com').path('/some/path').query_param('q', 'search term') >>> u.as_string() 'http://www.example.com/some/path?q=search+term' Along with the above overloaded methods, there is also a ``add_path_segment`` method for adding a segment at the end of the current path: .. code:: python >>> new_url = u.add_path_segment('here') >>> new_url.as_string() 'http://www.example.com/some/path/here?q=search+term' Couple of other things: * Since the URL class is immutable it can be used as a key in a dictionary * It can be pickled and restored * It supports equality operations * It supports equality operations URL templates can be used either via a ``Template`` class: .. code:: python >>> from purl import Template >>> tpl = Template("http://example.com{/list*}") >>> url = tpl.expand({'list': ['red', 'green', 'blue']}) >>> url.as_string() 'http://example.com/red/green/blue' or the ``expand`` function: .. code:: python >>> from purl import expand >>> expand(u"{/list*}", {'list': ['red', 'green', 'blue']}) '/red/green/blue' A wide variety of expansions are possible - refer to the RFC_ for more details. .. _RFC: http://tools.ietf.org/html/rfc6570 Changelog --------- v1.5 - 2019-03-10 ~~~~~~~~~~~~~~~~~ * Allow `@` in passwords. v1.4 - 2018-03-11 ~~~~~~~~~~~~~~~~~ * Allow usernames and passwords to be removed from URLs. v1.3.1 ~~~~~~ * Ensure paths always have a leading slash. v1.3 ~~~~ * Allow absolute URLs to be converted into relative. v1.2 ~~~~ * Support password-less URLs. * Allow slashes to be passed as path segments. v1.1 ~~~~ * Support setting username and password via mutator methods v1.0.3 ~~~~~~ * Handle some unicode compatibility edge-cases v1.0.2 ~~~~~~ * Fix template expansion bug with no matching variables being passed in. This ensures ``purl.Template`` works correctly with the URLs returned from the Github API. v1.0.1 ~~~~~~ * Fix bug with special characters in paths not being escaped. v1.0 ~~~~ * Slight tidy up. Document support for PyPy and Python 3.4. v0.8 ~~~~ * Support for RFC 6570 URI templates v0.7 ~~~~ * All internal strings are unicode. * Support for unicode chars in path, fragment, query, auth added. v0.6 ~~~~ * Added ``append_query_param`` method * Added ``remove_query_param`` method v0.5 ~~~~ * Added support for Python 3.2/3.3 (thanks @pmcnr and @mitchellrj) v0.4.1 ~~~~~~ * Added API docs * Added to readthedocs.org v0.4 ~~~~ * Modified constructor to accept full URL string as first arg * Added ``add_path_segment`` method v0.3.2 ~~~~~~ * Fixed bug port number in string when using from_string constructor v0.3.1 ~~~~~~ * Fixed bug with passing lists to query param setter methods v0.3 ~~~~ * Added support for comparison and equality * Added support for pickling * Added ``__slots__`` so instances can be used as keys within dictionaries Contribute ---------- Clone, create a virtualenv then install purl and the packages required for testing:: $ git clone git@github.com:codeinthehole/purl.git $ cd purl $ mkvirtualenv purl # requires virtualenvwrapper (purl) $ make Ensure tests pass using:: (purl) $ ./runtests.sh or:: $ tox purl-1.5/purl/0000755000076500000240000000000013441274412013537 5ustar davidstaff00000000000000purl-1.5/purl/__init__.py0000644000076500000240000000016513035441343015650 0ustar davidstaff00000000000000from .url import URL # noqa from .template import expand, Template # noqa __all__ = ['URL', 'expand', 'Template'] purl-1.5/purl/url.py0000644000076500000240000003710313441272231014714 0ustar davidstaff00000000000000from __future__ import unicode_literals try: from urllib.parse import parse_qs, urlencode, urlparse, quote, unquote except ImportError: from urllib import urlencode, quote, unquote from urlparse import parse_qs, urlparse from collections import namedtuple import six # To minimise memory consumption, we use a namedtuple to store all instance # variables, as well as using the __slots__ attribute. _URLTuple = namedtuple( "_URLTuple", "host username password scheme port path query fragment") # Encoding helpers def to_unicode(string): """ Ensure a passed string is unicode """ if isinstance(string, six.binary_type): return string.decode('utf8') if isinstance(string, six.text_type): return string if six.PY2: return unicode(string) return str(string) def to_utf8(string): """ Encode a string as a UTF8 bytestring. This function could be passed a bytestring or unicode string so must distinguish between the two. """ if isinstance(string, six.text_type): return string.encode('utf8') if isinstance(string, six.binary_type): return string return str(string) def dict_to_unicode(raw_dict): """ Ensure all keys and values in a dict are unicode. The passed dict is assumed to have lists for all values. """ decoded = {} for key, value in raw_dict.items(): decoded[to_unicode(key)] = map( to_unicode, value) return decoded def unicode_quote(string, safe='/'): if string is None: return None return quote(to_utf8(string), to_utf8(safe)) def unicode_quote_path_segment(string): if string is None: return None return quote(to_utf8(string), safe=to_utf8("")) def unicode_unquote(string): if string is None: return None if six.PY3: return unquote(string) return to_unicode(unquote(to_utf8(string))) def unicode_urlencode(query, doseq=True): """ Custom wrapper around urlencode to support unicode Python urlencode doesn't handle unicode well so we need to convert to bytestrings before using it: http://stackoverflow.com/questions/6480723/urllib-urlencode-doesnt-like-unicode-values-how-about-this-workaround """ pairs = [] for key, value in query.items(): if isinstance(value, list): value = list(map(to_utf8, value)) else: value = to_utf8(value) pairs.append((to_utf8(key), value)) encoded_query = dict(pairs) xx = urlencode(encoded_query, doseq) return xx def parse(url_str): """ Extract all parts from a URL string and return them as a dictionary """ url_str = to_unicode(url_str) result = urlparse(url_str) netloc_parts = result.netloc.rsplit('@', 1) if len(netloc_parts) == 1: username = password = None host = netloc_parts[0] else: user_and_pass = netloc_parts[0].split(':') if len(user_and_pass) == 2: username, password = user_and_pass elif len(user_and_pass) == 1: username = user_and_pass[0] password = None host = netloc_parts[1] if host and ':' in host: host = host.split(':')[0] return {'host': host, 'username': username, 'password': password, 'scheme': result.scheme, 'port': result.port, 'path': result.path, 'query': result.query, 'fragment': result.fragment} class URL(object): """ The constructor can be used in two ways: 1. Pass a URL string:: >>> URL('http://www.google.com/search?q=testing').as_string() 'http://www.google.com/search?q=testing' 2. Pass keyword arguments:: >>> URL(host='www.google.com', path='/search', query='q=testing').as_string() 'http://www.google.com/search?q=testing' If you pass both a URL string and keyword args, then the values of keyword args take precedence. """ __slots__ = ("_tuple",) def __init__(self, url_str=None, host=None, username=None, password=None, scheme=None, port=None, path=None, query=None, fragment=None): if url_str is not None: params = parse(url_str) else: # Defaults params = {'scheme': 'http', 'username': None, 'password': None, 'host': None, 'port': None, 'path': '/', 'query': None, 'fragment': None} # Ensure path starts with a slash if path and not path.startswith("/"): path = "/%s" % path # Kwargs override the url_str for var in 'host username password scheme port path query fragment'.split(): if locals()[var] is not None: params[var] = locals()[var] # Store the various components in %-encoded form self._tuple = _URLTuple(params['host'], unicode_quote(params['username']), unicode_quote(params['password']), params['scheme'], params['port'], params['path'], params['query'], unicode_quote(params['fragment'])) def __eq__(self, other): return self._tuple == other._tuple def __ne__(self, other): return self._tuple != other._tuple def __getstate__(self): return tuple(self._tuple) def __setstate__(self, state): self._tuple = _URLTuple(*state) def __hash__(self): return hash(self._tuple) def __repr__(self): return str(self._tuple) def __unicode__(self): url = self._tuple parts = ["%s://" % url.scheme if url.scheme else '', self.netloc(), url.path, '?%s' % url.query if url.query else '', '#%s' % url.fragment if url.fragment else ''] if not url.host: return ''.join(parts[2:]) return ''.join(parts) __str__ = as_string = __unicode__ # Accessors / Mutators # These use the jQuery overloading style whereby they become mutators if # extra args are passed def netloc(self): """ Return the netloc """ url = self._tuple if url.username and url.password: netloc = '%s:%s@%s' % (url.username, url.password, url.host) elif url.username and not url.password: netloc = '%s@%s' % (url.username, url.host) else: netloc = url.host if url.port: netloc = '%s:%s' % (netloc, url.port) return netloc def host(self, value=None): """ Return the host :param string value: new host string """ if value is not None: return URL._mutate(self, host=value) return self._tuple.host domain = host def username(self, value=None): """ Return or set the username :param string value: the new username to use :returns: string or new :class:`URL` instance """ if value is not None: return URL._mutate(self, username=value) return unicode_unquote(self._tuple.username) def password(self, value=None): """ Return or set the password :param string value: the new password to use :returns: string or new :class:`URL` instance """ if value is not None: return URL._mutate(self, password=value) return unicode_unquote(self._tuple.password) def subdomains(self, value=None): """ Returns a list of subdomains or set the subdomains and returns a new :class:`URL` instance. :param list value: a list of subdomains """ if value is not None: return URL._mutate(self, host='.'.join(value)) return self.host().split('.') def subdomain(self, index, value=None): """ Return a subdomain or set a new value and return a new :class:`URL` instance. :param integer index: 0-indexed subdomain :param string value: New subdomain """ if value is not None: subdomains = self.subdomains() subdomains[index] = value return URL._mutate(self, host='.'.join(subdomains)) return self.subdomains()[index] def scheme(self, value=None): """ Return or set the scheme. :param string value: the new scheme to use :returns: string or new :class:`URL` instance """ if value is not None: return URL._mutate(self, scheme=value) return self._tuple.scheme def path(self, value=None): """ Return or set the path :param string value: the new path to use :returns: string or new :class:`URL` instance """ if value is not None: if not value.startswith('/'): value = '/' + value encoded_value = unicode_quote(value) return URL._mutate(self, path=encoded_value) return self._tuple.path def query(self, value=None): """ Return or set the query string :param string value: the new query string to use :returns: string or new :class:`URL` instance """ if value is not None: return URL._mutate(self, query=value) return self._tuple.query def port(self, value=None): """ Return or set the port :param string value: the new port to use :returns: string or new :class:`URL` instance """ if value is not None: return URL._mutate(self, port=value) return self._tuple.port def fragment(self, value=None): """ Return or set the fragment (hash) :param string value: the new fragment to use :returns: string or new :class:`URL` instance """ if value is not None: return URL._mutate(self, fragment=value) return unicode_unquote(self._tuple.fragment) def relative(self): """ Return a relative URL object (eg strip the protocol and host) :returns: new :class:`URL` instance """ return URL._mutate(self, scheme=None, host=None) # ==== # Path # ==== def path_segment(self, index, value=None, default=None): """ Return the path segment at the given index :param integer index: :param string value: the new segment value :param string default: the default value to return if no path segment exists with the given index """ if value is not None: segments = list(self.path_segments()) segments[index] = unicode_quote_path_segment(value) new_path = '/' + '/'.join(segments) if self._tuple.path.endswith('/'): new_path += '/' return URL._mutate(self, path=new_path) try: return self.path_segments()[index] except IndexError: return default def path_segments(self, value=None): """ Return the path segments :param list value: the new path segments to use """ if value is not None: encoded_values = map(unicode_quote_path_segment, value) new_path = '/' + '/'.join(encoded_values) return URL._mutate(self, path=new_path) parts = self._tuple.path.split('/') segments = parts[1:] if self._tuple.path.endswith('/'): segments.pop() segments = map(unicode_unquote, segments) return tuple(segments) def add_path_segment(self, value): """ Add a new path segment to the end of the current string :param string value: the new path segment to use Example:: >>> u = URL('http://example.com/foo/') >>> u.add_path_segment('bar').as_string() 'http://example.com/foo/bar' """ segments = self.path_segments() + (to_unicode(value),) return self.path_segments(segments) # ============ # Query params # ============ def has_query_param(self, key): """ Test if a given query parameter is present :param string key: key to test for """ return self.query_param(key) is not None def has_query_params(self, keys): """ Test if a given set of query parameters are present :param list keys: keys to test for """ return all([self.has_query_param(k) for k in keys]) def query_param(self, key, value=None, default=None, as_list=False): """ Return or set a query parameter for the given key The value can be a list. :param string key: key to look for :param string default: value to return if ``key`` isn't found :param boolean as_list: whether to return the values as a list :param string value: the new query parameter to use """ parse_result = self.query_params() if value is not None: # Need to ensure all strings are unicode if isinstance(value, (list, tuple)): value = list(map(to_unicode, value)) else: value = to_unicode(value) parse_result[to_unicode(key)] = value return URL._mutate( self, query=unicode_urlencode(parse_result, doseq=True)) try: result = parse_result[key] except KeyError: return default if as_list: return result return result[0] if len(result) == 1 else result def append_query_param(self, key, value): """ Append a query parameter :param string key: The query param key :param string value: The new value """ values = self.query_param(key, as_list=True, default=[]) values.append(value) return self.query_param(key, values) def query_params(self, value=None): """ Return or set a dictionary of query params :param dict value: new dictionary of values """ if value is not None: return URL._mutate(self, query=unicode_urlencode(value, doseq=True)) query = '' if self._tuple.query is None else self._tuple.query # In Python 2.6, urlparse needs a bytestring so we encode and then # decode the result. if not six.PY3: result = parse_qs(to_utf8(query), True) return dict_to_unicode(result) return parse_qs(query, True) def remove_query_param(self, key, value=None): """ Remove a query param from a URL Set the value parameter if removing from a list. :param string key: The key to delete :param string value: The value of the param to delete (of more than one) """ parse_result = self.query_params() if value is not None: index = parse_result[key].index(value) del parse_result[key][index] else: del parse_result[key] return URL._mutate(self, query=unicode_urlencode(parse_result, doseq=True)) # ======= # Helpers # ======= @classmethod def _mutate(cls, url, **kwargs): args = url._tuple._asdict() args.update(kwargs) return cls(**args) @classmethod def from_string(cls, url_str): """ Factory method to create a new instance based on a passed string This method is deprecated now """ return cls(url_str) purl-1.5/purl/template.py0000644000076500000240000001351712644265650015743 0ustar davidstaff00000000000000import re import functools try: from urllib.parse import quote except ImportError: # Python 2 from urllib import quote from . import url __all__ = ['Template', 'expand'] patterns = re.compile("{([^\}]+)}") class Template(object): def __init__(self, url_str): self._base = url_str def __str__(self): return 'Template: %s' % self._base def expand(self, variables=None): return url.URL(expand(self._base, variables)) def expand(template, variables=None): """ Expand a URL template string using the passed variables """ if variables is None: variables = {} return patterns.sub(functools.partial(_replace, variables), template) # Utils def _flatten(container): """ _flatten a sequence of sequences into a single list """ _flattened = [] for sequence in container: _flattened.extend(sequence) return _flattened # Format functions # ---------------- # These are responsible for formatting the (key, value) pair into a string def _format_pair_no_equals(explode, separator, escape, key, value): """ Format a key, value pair but don't include the equals sign when there is no value """ if not value: return key return _format_pair(explode, separator, escape, key, value) def _format_pair_with_equals(explode, separator, escape, key, value): """ Format a key, value pair including the equals sign when there is no value """ if not value: return key + '=' return _format_pair(explode, separator, escape, key, value) def _format_pair(explode, separator, escape, key, value): if isinstance(value, (list, tuple)): join_char = "," if explode: join_char = separator try: dict(value) except: # Scalar container if explode: items = ["%s=%s" % (key, escape(v)) for v in value] return join_char.join(items) else: escaped_value = join_char.join(map(escape, value)) else: # Tuple container if explode: items = ["%s=%s" % (k, escape(v)) for (k, v) in value] return join_char.join(items) else: items = _flatten(value) escaped_value = join_char.join(map(escape, items)) else: escaped_value = escape(value) return '%s=%s' % (key, escaped_value) def _format_default(explode, separator, escape, key, value): if isinstance(value, (list, tuple)): join_char = "," if explode: join_char = separator try: dict(value) except: # Scalar container escaped_value = join_char.join(map(escape, value)) else: # Tuple container if explode: items = ["%s=%s" % (k, escape(v)) for (k, v) in value] escaped_value = join_char.join(items) else: items = _flatten(value) escaped_value = join_char.join(map(escape, items)) else: escaped_value = escape(value) return escaped_value # Modifer functions # ----------------- # These are responsible for modifying the variable before formatting _identity = lambda x: x def _truncate(string, num_chars): return string[:num_chars] # Splitting functions # ------------------- # These are responsible for splitting a string into a sequence of (key, # modifier) tuples def _split_basic(string): """ Split a string into a list of tuples of the form (key, modifier_fn, explode) where modifier_fn is a function that applies the appropriate modification to the variable. """ tuples = [] for word in string.split(','): # Attempt to split on colon parts = word.split(':', 2) key, modifier_fn, explode = parts[0], _identity, False if len(parts) > 1: modifier_fn = functools.partial( _truncate, num_chars=int(parts[1])) if word[len(word) - 1] == '*': key = word[:len(word) - 1] explode = True tuples.append((key, modifier_fn, explode)) return tuples def _split_operator(string): return _split_basic(string[1:]) # Escaping functions # ------------------ def _escape_all(value): return url.unicode_quote(value, safe="") def _escape_reserved(value): return url.unicode_quote(value, safe="/!,.;") # Operator map # ------------ # A mapping of: # operator -> (prefix, separator, split_fn, escape_fn, format_fn) operator_map = { '+': ('', ',', _split_operator, _escape_reserved, _format_default), '#': ('#', ',', _split_operator, _escape_reserved, _format_default), '.': ('.', '.', _split_operator, _escape_all, _format_default), '/': ('/', '/', _split_operator, _escape_all, _format_default), ';': (';', ';', _split_operator, _escape_all, _format_pair_no_equals), '?': ('?', '&', _split_operator, _escape_all, _format_pair_with_equals), '&': ('&', '&', _split_operator, _escape_all, _format_pair_with_equals), } defaults = ('', ',', _split_basic, _escape_all, _format_default) def _replace(variables, match): """ Return the appropriate replacement for `match` using the passed variables """ expression = match.group(1) # Look-up chars and functions for the specified operator (prefix_char, separator_char, split_fn, escape_fn, format_fn) = operator_map.get(expression[0], defaults) replacements = [] for key, modify_fn, explode in split_fn(expression): if key in variables: variable = modify_fn(variables[key]) replacement = format_fn( explode, separator_char, escape_fn, key, variable) replacements.append(replacement) if not replacements: return '' return prefix_char + separator_char.join(replacements)