├── .editorconfig ├── .gitattributes ├── .gitignore ├── AUTHORS ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── _ext │ ├── __init__.py │ └── django_models.py ├── api │ ├── exceptions.rst │ ├── index.rst │ ├── models.rst │ ├── recorder.rst │ └── views.rst ├── changes.rst ├── conf.py ├── contributing.rst ├── credits.rst ├── index.rst ├── installation.rst ├── introduction.rst └── license.rst ├── example ├── manage.py └── mysite │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── httpproxy ├── __init__.py ├── _version.py ├── admin.py ├── exceptions.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20160121_1940.py │ └── __init__.py ├── models.py ├── recorder.py └── views.py ├── requirements.txt ├── setup.py └── versioneer.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 4 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | 15 | [*.md] 16 | # Markdown uses trailing whitespace for manual linebreaks 17 | trim_trailing_whitespace = false 18 | 19 | [*.rst] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | httpproxy/_version.py export-subst 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info/ 3 | *.sqlite3 4 | .DS_Store 5 | docs/_build/ 6 | build/ 7 | dist/ 8 | MANIFEST 9 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | -*- restructuredtext -*- 2 | 3 | The Django HTTP Proxy is developed and maintained by Yuri van der Meer. 4 | 5 | Many thanks to Will Larson for providing the inspiration and original source 6 | code for this app: 7 | 8 | http://lethain.com/entry/2008/sep/30/suffer-less-by-using-django-dev-server-as-a-proxy/ 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include README 3 | recursive-include docs * 4 | recursive-exclude docs/_build *.* 5 | recursive-include requirements * 6 | include versioneer.py 7 | include httpproxy/_version.py 8 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django HTTP Proxy 2 | ================= 3 | 4 | Django HTTP Proxy provides simple HTTP proxy functionality for the Django web 5 | development framework. 6 | 7 | Features 8 | -------- 9 | 10 | * Configure one or more HTTP proxies in your Django project. 11 | * Avoid typical cross-domain issues while developing an Ajax application based 12 | on live data from another server. 13 | * Record responses and play them back at a later time: 14 | * Use "live" data, even when you are developing offline 15 | * Speedy responses instead of having to wait for a remote server 16 | * Manually edit recorded responses via the Django admin interface 17 | 18 | Documentation can be found on 19 | `Read the Docs `_. 20 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " pickle to make pickle files" 22 | @echo " json to make JSON files" 23 | @echo " htmlhelp to make HTML files and a HTML help project" 24 | @echo " qthelp to make HTML files and a qthelp project" 25 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 26 | @echo " changes to make an overview of all changed/added/deprecated items" 27 | @echo " linkcheck to check all external links for integrity" 28 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 29 | 30 | clean: 31 | -rm -rf $(BUILDDIR)/* 32 | 33 | html: 34 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 35 | @echo 36 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 37 | 38 | dirhtml: 39 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 40 | @echo 41 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 42 | 43 | pickle: 44 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 45 | @echo 46 | @echo "Build finished; now you can process the pickle files." 47 | 48 | json: 49 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 50 | @echo 51 | @echo "Build finished; now you can process the JSON files." 52 | 53 | htmlhelp: 54 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 55 | @echo 56 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 57 | ".hhp project file in $(BUILDDIR)/htmlhelp." 58 | 59 | qthelp: 60 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 61 | @echo 62 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 63 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 64 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DjangoHTTPProxy.qhcp" 65 | @echo "To view the help file:" 66 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DjangoHTTPProxy.qhc" 67 | 68 | latex: 69 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 70 | @echo 71 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 72 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 73 | "run these through (pdf)latex." 74 | 75 | changes: 76 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 77 | @echo 78 | @echo "The overview file is in $(BUILDDIR)/changes." 79 | 80 | linkcheck: 81 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 82 | @echo 83 | @echo "Link check complete; look for any errors in the above output " \ 84 | "or in $(BUILDDIR)/linkcheck/output.txt." 85 | 86 | doctest: 87 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 88 | @echo "Testing of doctests in the sources finished, look at the " \ 89 | "results in $(BUILDDIR)/doctest/output.txt." 90 | -------------------------------------------------------------------------------- /docs/_ext/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yvandermeer/django-http-proxy/0bf66c22bf1bd4e614de6dac5a28484c155a38f1/docs/_ext/__init__.py -------------------------------------------------------------------------------- /docs/_ext/django_models.py: -------------------------------------------------------------------------------- 1 | # https://djangosnippets.org/snippets/2533/ 2 | import inspect 3 | 4 | from django.utils.html import strip_tags 5 | from django.utils.encoding import force_unicode 6 | 7 | 8 | def process_docstring(app, what, name, obj, options, lines): 9 | # This causes import errors if left outside the function 10 | from django.db import models 11 | 12 | # Only look at objects that inherit from Django's base model class 13 | if inspect.isclass(obj) and issubclass(obj, models.Model): 14 | # Grab the field list from the meta class 15 | fields = obj._meta.fields 16 | 17 | for field in fields: 18 | # Decode and strip any html out of the field's help text 19 | help_text = strip_tags(force_unicode(field.help_text)) 20 | 21 | # Decode and capitalize the verbose name, for use if there isn't 22 | # any help text 23 | verbose_name = force_unicode(field.verbose_name).capitalize() 24 | 25 | if help_text: 26 | # Add the model field to the end of the docstring as a param 27 | # using the help text as the description 28 | lines.append(u':param %s: %s' % (field.attname, help_text)) 29 | else: 30 | # Add the model field to the end of the docstring as a param 31 | # using the verbose name as the description 32 | lines.append(u':param %s: %s' % (field.attname, verbose_name)) 33 | 34 | # Add the field's type to the docstring 35 | if isinstance(field, models.ForeignKey): 36 | to = field.rel.to 37 | lines.append(u':type %s: %s to :class:`~%s.%s`' % (field.attname, type(field).__name__, to.__module__, to.__name__)) 38 | else: 39 | lines.append(u':type %s: %s' % (field.attname, type(field).__name__)) 40 | 41 | # Return the extended docstring 42 | return lines 43 | -------------------------------------------------------------------------------- /docs/api/exceptions.rst: -------------------------------------------------------------------------------- 1 | :mod:`httpproxy.exceptions` 2 | =========================== 3 | 4 | .. automodule:: httpproxy.exceptions 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/api/index.rst: -------------------------------------------------------------------------------- 1 | API documentation 2 | ================= 3 | 4 | .. automodule:: httpproxy 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | views 10 | models 11 | recorder 12 | exceptions 13 | -------------------------------------------------------------------------------- /docs/api/models.rst: -------------------------------------------------------------------------------- 1 | :mod:`httpproxy.models` 2 | ======================= 3 | 4 | .. automodule:: httpproxy.models 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/recorder.rst: -------------------------------------------------------------------------------- 1 | :mod:`httpproxy.recorder` 2 | ========================= 3 | 4 | .. automodule:: httpproxy.recorder 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/views.rst: -------------------------------------------------------------------------------- 1 | :mod:`httpproxy.views` 2 | ====================== 3 | 4 | .. autoclass:: httpproxy.views.HttpProxy 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/changes.rst: -------------------------------------------------------------------------------- 1 | Changes 2 | ------- 3 | 4 | 0.4.3 (2016-02-10) 5 | ~~~~~~~~~~~~~~~~~~ 6 | 7 | * Merged `pull request `_ from `Ihor Kucher `_ (thanks!): add support of post requests & update headers setting 8 | 9 | 0.4.2 (2016-02-10) 10 | ~~~~~~~~~~~~~~~~~~ 11 | 12 | * Merged `pull request `_ from `Don Naegely `_ (thanks!): Generate missing migrations 13 | 14 | 0.4.1 15 | ~~~~~ 16 | 17 | * Python 3 compatibility (requires Django >= 1.6) 18 | * Fix duplicated forward slashes in urls 19 | 20 | 0.4 21 | ~~~ 22 | 23 | * Migration from Bitbucket (Mercurial) to Github 24 | * Refactored main view using Django class-based views (see :class:`~httpproxy.views.HttpProxy`) 25 | * Removed basic authentication support (`PROXY_USER` and `PROXY_PASSWORD`); may be added back later on. 26 | * Finally merged back Django 1.6 fixes by `Petr Dlouhý `_ (thanks!) 27 | * Merged pull request from `Garrett Seward `_ (thanks!) 28 | * Added Django 1.7 compatibility 29 | * Added database migrations (Django 1.7 and higher only) 30 | * Updated and improvement the documentation (including :doc:`API documentation `) 31 | * Added an ``example`` project for reference 32 | * Using urllib2 instead of httplib2 33 | * Using setuptools instead of distutils 34 | * Using versioneer2 for package versioning 35 | * Removed some unused imports and did some further code cleanup 36 | 37 | 0.3.2 38 | ~~~~~ 39 | 40 | * Limited display of request querystring in admin screen to 50 characters 41 | 42 | 0.3.1 43 | ~~~~~ 44 | 45 | * Fixed 250 character limitation for querystring in Recorded Request 46 | (`issue #2 `_) 47 | * Added new Request Parameter model; requires ``./manage.py reset httpproxy && ./manage.py syncdb`` 48 | 49 | 0.3 50 | ~~~ 51 | 52 | * Fixed Python 2.5 support by removing use of ``__package__`` 53 | * Implemented request path "normalization", fixing record and playback if the 54 | proxy is URL-configured anywhere other than directly in the root. 55 | * Added experimental ``PROXY_REWRITE_RESPONSES`` settings to fix paths to 56 | resources (images, javascript, etc) on the same domain if ``httproxy`` is 57 | not configured at the root. 58 | 59 | 0.2.2 60 | ~~~~~ 61 | 62 | * Removed print statement I accidentally left behind. 63 | 64 | 0.2.1 65 | ~~~~~ 66 | 67 | * Fixed `issue #1 `_; 68 | Unsupported content types are now silently ignored. 69 | * Added ``PROXY_IGNORE_UNSUPPORTED`` setting to control the behavior for 70 | handling unsupported responses. 71 | 72 | 0.2 73 | ~~~ 74 | 75 | * Added recording and playback functionality 76 | * Improved handling of ``httpproxy``-specific settings 77 | * Started using Sphinx for documentation 78 | 79 | 0.1 80 | ~~~ 81 | 82 | * Initial release 83 | * Basic HTTP proxy functionality based on `a blog post by Will Larson `_ 84 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import sys 4 | from datetime import datetime 5 | from os.path import abspath, dirname, join 6 | 7 | 8 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 9 | 10 | sys.path.append(abspath(join(dirname(__file__), '_ext'))) 11 | 12 | # -- General configuration ----------------------------------------------------- 13 | 14 | extensions = [ 15 | 'sphinx.ext.autodoc', 16 | ] 17 | 18 | from django_models import process_docstring 19 | def setup(app): 20 | app.connect('autodoc-process-docstring', process_docstring) 21 | 22 | autodoc_member_order = 'bysource' 23 | 24 | # Bootstrap Django for autodoc 25 | import django 26 | from django.conf import settings 27 | settings.configure() 28 | django.setup() 29 | 30 | templates_path = ['_templates'] 31 | 32 | source_suffix = '.rst' 33 | 34 | master_doc = 'index' 35 | 36 | project = u'Django HTTP Proxy' 37 | copyright = u'2009-{}, Yuri van der Meer'.format(datetime.today().year) 38 | 39 | exclude_trees = ['_build'] 40 | 41 | pygments_style = 'sphinx' 42 | 43 | 44 | # -- Options for HTML output --------------------------------------------------- 45 | 46 | if not on_rtd: # only import and set the theme if we're building docs locally 47 | import sphinx_rtd_theme 48 | html_theme = 'sphinx_rtd_theme' 49 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 50 | 51 | version = release = __import__('httpproxy').__version__ 52 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | .. note:: 5 | 6 | This project has recently moved from `Bitbucket `_ to 7 | `Github `_. 8 | 9 | .. _bitbucket: https://bitbucket.org/yvandermeer/django-http-proxy 10 | .. _github: https://github.com/yvandermeer/django-http-proxy 11 | 12 | If you have any contributions, feel free to 13 | `fork Django HTTP Proxy `_. 14 | 15 | 16 | Development setup 17 | ----------------- 18 | 19 | To set up the project for local development:: 20 | 21 | $ git clone https://github.com/yvandermeer/django-http-proxy.git 22 | $ mkvirtualenv django-http-proxy 23 | $ pip install -r requirements.txt 24 | $ python example/manage.py syncdb 25 | $ python example/manage.py runserver 26 | 27 | Finally, point your browser to http://127.0.0.1:8000/python/ and you should see 28 | something that resembles what you see on http://www.python.org/. 29 | 30 | 31 | Building the documentation 32 | -------------------------- 33 | 34 | Documention is provided in Sphinx format in the `docs` subdirectory. To 35 | build the HTML version of the documentation yourself:: 36 | 37 | $ cd docs 38 | $ make html 39 | -------------------------------------------------------------------------------- /docs/credits.rst: -------------------------------------------------------------------------------- 1 | Credits 2 | ======= 3 | 4 | Django HTTP Proxy was created by Yuri van der Meer, inspired by `a blog post 5 | by Will Larson `_. 6 | 7 | Contributions were made by `Petr Dlouhý `_ and 8 | `Garrett Seward `_. (thanks!) 9 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Django HTTP Proxy 2 | ================= 3 | 4 | Django HTTP Proxy provides simple HTTP proxy functionality for the Django web 5 | development framework. 6 | 7 | Contents 8 | -------- 9 | 10 | .. toctree:: 11 | :maxdepth: 1 12 | 13 | introduction 14 | installation 15 | api/index 16 | changes 17 | credits 18 | contributing 19 | license 20 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation & configuration 2 | ============================ 3 | 4 | Requirements 5 | ------------ 6 | 7 | Django HTTP Proxy should be compatible with any Python 2.x version from 2.5 and 8 | up and a relatively recent version of Django. It is compatible with Django 1.7. 9 | 10 | 11 | Installation 12 | ------------ 13 | 14 | The easiest way to install the latest version of Django HTTP Proxy is using 15 | `pip `_:: 16 | 17 | $ pip install django-http-proxy 18 | 19 | Alternatively, you can manually download the package from the `Python Package Index `_ or from the `Github repository `_. 20 | 21 | .. _github: https://github.com/yvandermeer/django-http-proxy 22 | 23 | 24 | Next, you need to add "httpproxy" to the ``INSTALLED_APPS`` list 25 | in your Django settings module (typically ``settings.py``):: 26 | 27 | INSTALLED_APPS = ( 28 | ... 29 | 'httpproxy', 30 | ) 31 | 32 | Finally, install the database tables:: 33 | 34 | $ python manage.py syncdb 35 | 36 | .. note:: 37 | 38 | If you are only interested in using Django HTTP Proxy as a live proxy and 39 | don't care about recording/playing back requests and responses, simply 40 | do not add it to your ``INSTALLED_APPS`` and no database tables will be created. 41 | 42 | 43 | Configuration 44 | ------------- 45 | 46 | The core of Django HTTP Proxy is a class-based Django view, 47 | :class:`httpproxy.views.HttpProxy`. 48 | 49 | To use Django HTTP Proxy, you create an entry in your ``urls.py`` that forwards 50 | requests to the :class:`~httpproxy.views.HttpProxy` view class, e.g.:: 51 | 52 | from httpproxy.views import HttpProxy 53 | 54 | urlpatterns += patterns('', 55 | (r'^proxy/(?P.*)$', 56 | HttpProxy.as_view(base_url='http://www.python.org/')), 57 | ) 58 | 59 | Given the above url config, request matching ``/proxy/`` will be 60 | handled by the configured :class:`~httpproxy.views.HttpProxy` view instance and 61 | forwarded to ``http://www.python.org/``. 62 | 63 | .. note:: 64 | 65 | Older versions of Django HTTP Proxy only supported a single proxy per Django 66 | project, which had to be configured using a Django setting:: 67 | 68 | PROXY_BASE_URL = 'http://www.python.org/' 69 | 70 | Naturally, you can easily replicate this behavior using the new class-based 71 | view syntax:: 72 | 73 | from django.conf import settings 74 | from httpproxy.views import HttpProxy 75 | 76 | urlpatterns += patterns('', 77 | (r'^proxy/(?P.*)$', 78 | HttpProxy.as_view(base_url=settings.PROXY_BASE_URL)), 79 | ) 80 | -------------------------------------------------------------------------------- /docs/introduction.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | Django HTTP Proxy provides simple HTTP proxy functionality for the Django web 5 | development framework. It allows you make requests to an external server by 6 | requesting them from the main server running your Django application. In 7 | addition, it allows you to record the responses to those requests and play them 8 | back at any time. 9 | 10 | One possible use for this application (actually, the reason it was developed) 11 | is to allow for easy development of Ajax applications against a live server 12 | environment: 13 | 14 | * Avoid typical cross-domain issues while developing an Ajax application based 15 | on live data from another server. 16 | * Record responses and play them back at a later time: 17 | * Use "live" data, even when you are developing offline 18 | * Speedy responses instead of having to wait for a remote server 19 | * Manually edit recorded responses via the Django admin interface 20 | 21 | Combined with the standard `Django development server `_, 22 | you have a useful toolkit for developing HTML5/Ajax applications. 23 | 24 | Django HTTP Proxy is licensed under an `MIT-style permissive license `_ and 25 | `maintained on Github `_. 26 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | Copyright (c) 2009-2015 Yuri van der Meer 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /example/mysite/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yvandermeer/django-http-proxy/0bf66c22bf1bd4e614de6dac5a28484c155a38f1/example/mysite/__init__.py -------------------------------------------------------------------------------- /example/mysite/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for mysite project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.7/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.7/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 14 | 15 | 16 | # Quick-start development settings - unsuitable for production 17 | # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ 18 | 19 | # SECURITY WARNING: keep the secret key used in production secret! 20 | SECRET_KEY = 'x3uh1b2lgjtrdmb!2pt8t)4%h#)$#*!^*hst_m55a+4em$uxh)' 21 | 22 | # SECURITY WARNING: don't run with debug turned on in production! 23 | DEBUG = True 24 | 25 | TEMPLATE_DEBUG = True 26 | 27 | ALLOWED_HOSTS = [] 28 | 29 | 30 | # Application definition 31 | 32 | INSTALLED_APPS = ( 33 | 'django.contrib.admin', 34 | 'django.contrib.auth', 35 | 'django.contrib.contenttypes', 36 | 'django.contrib.sessions', 37 | 'django.contrib.messages', 38 | 'django.contrib.staticfiles', 39 | 'httpproxy', 40 | ) 41 | 42 | MIDDLEWARE_CLASSES = ( 43 | 'django.contrib.sessions.middleware.SessionMiddleware', 44 | 'django.middleware.common.CommonMiddleware', 45 | 'django.middleware.csrf.CsrfViewMiddleware', 46 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 47 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 48 | 'django.contrib.messages.middleware.MessageMiddleware', 49 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 50 | ) 51 | 52 | ROOT_URLCONF = 'mysite.urls' 53 | 54 | WSGI_APPLICATION = 'mysite.wsgi.application' 55 | 56 | 57 | # Database 58 | # https://docs.djangoproject.com/en/1.7/ref/settings/#databases 59 | 60 | DATABASES = { 61 | 'default': { 62 | 'ENGINE': 'django.db.backends.sqlite3', 63 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 64 | } 65 | } 66 | 67 | # Internationalization 68 | # https://docs.djangoproject.com/en/1.7/topics/i18n/ 69 | 70 | LANGUAGE_CODE = 'en-us' 71 | 72 | TIME_ZONE = 'UTC' 73 | 74 | USE_I18N = True 75 | 76 | USE_L10N = True 77 | 78 | USE_TZ = True 79 | 80 | 81 | # Static files (CSS, JavaScript, Images) 82 | # https://docs.djangoproject.com/en/1.7/howto/static-files/ 83 | 84 | STATIC_URL = '/static/' 85 | -------------------------------------------------------------------------------- /example/mysite/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | from django.contrib import admin 3 | 4 | from httpproxy.views import HttpProxy 5 | 6 | 7 | urlpatterns = patterns('', 8 | url(r'^python/(?P.*)$', 9 | HttpProxy.as_view(base_url='http://python.org/', rewrite=True)), 10 | 11 | url(r'^admin/', include(admin.site.urls)), 12 | ) 13 | -------------------------------------------------------------------------------- /example/mysite/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for mysite project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | application = get_wsgi_application() 15 | -------------------------------------------------------------------------------- /httpproxy/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django HTTP Proxy - A simple HTTP proxy for the Django framework. 3 | """ 4 | from ._version import get_versions 5 | __version__ = get_versions()['version'] 6 | del get_versions 7 | -------------------------------------------------------------------------------- /httpproxy/_version.py: -------------------------------------------------------------------------------- 1 | 2 | # This file helps to compute a version number in source trees obtained from 3 | # git-archive tarball (such as those provided by githubs download-from-tag 4 | # feature). Distribution tarballs (built by setup.py sdist) and build 5 | # directories (produced by setup.py build) will contain a much shorter file 6 | # that just contains the computed version number. 7 | 8 | # This file is released into the public domain. Generated by 9 | # versioneer2-(0.1.10) (https://github.com/ryanpdwyer/versioneer2) 10 | 11 | # these strings will be replaced by git during git-archive 12 | git_refnames = " (HEAD -> develop)" 13 | git_full = "0bf66c22bf1bd4e614de6dac5a28484c155a38f1" 14 | 15 | # these strings are filled in when 'setup.py versioneer' creates _version.py 16 | tag_prefix = "" 17 | parentdir_prefix = "" 18 | versionfile_source = "httpproxy/_version.py" 19 | 20 | import os 21 | import sys 22 | import re 23 | import subprocess 24 | import errno 25 | import io 26 | 27 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): 28 | assert isinstance(commands, list) 29 | p = None 30 | for c in commands: 31 | try: 32 | # remember shell=False, so use git.cmd on windows, not just git 33 | p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, 34 | stderr=(subprocess.PIPE if hide_stderr 35 | else None)) 36 | break 37 | except EnvironmentError: 38 | e = sys.exc_info()[1] 39 | if e.errno == errno.ENOENT: 40 | continue 41 | if verbose: 42 | print("unable to run %s" % args[0]) 43 | print(e) 44 | return None 45 | else: 46 | if verbose: 47 | print("unable to find command, tried %s" % (commands,)) 48 | return None 49 | stdout = p.communicate()[0].strip() 50 | if sys.version >= '3': 51 | stdout = stdout.decode() 52 | if p.returncode != 0: 53 | if verbose: 54 | print("unable to run %s (error)" % args[0]) 55 | return None 56 | return stdout 57 | 58 | 59 | def versions_from_parentdir(parentdir_prefix, root, verbose=False): 60 | # Source tarballs conventionally unpack into a directory that includes 61 | # both the project name and a version string. 62 | dirname = os.path.basename(root) 63 | if not dirname.startswith(parentdir_prefix): 64 | if verbose: 65 | print("guessing rootdir is '%s', but '%s' doesn't start with prefix '%s'" % 66 | (root, dirname, parentdir_prefix)) 67 | return None 68 | return {"version": dirname[len(parentdir_prefix):].replace("_", "+").strip(".egg"), "full": ""} 69 | 70 | def git_get_keywords(versionfile_abs): 71 | # the code embedded in _version.py can just fetch the value of these 72 | # keywords. When used from setup.py, we don't want to import _version.py, 73 | # so we do it with a regexp instead. This function is not used from 74 | # _version.py. 75 | keywords = {} 76 | try: 77 | f = io.open(versionfile_abs, "r", encoding='utf-8') 78 | for line in f.readlines(): 79 | if line.strip().startswith("git_refnames ="): 80 | mo = re.search(r'=\s*"(.*)"', line) 81 | if mo: 82 | keywords["refnames"] = mo.group(1) 83 | if line.strip().startswith("git_full ="): 84 | mo = re.search(r'=\s*"(.*)"', line) 85 | if mo: 86 | keywords["full"] = mo.group(1) 87 | f.close() 88 | except EnvironmentError: 89 | pass 90 | return keywords 91 | 92 | def git_versions_from_keywords(keywords, tag_prefix, verbose=False): 93 | if not keywords: 94 | return {} # keyword-finding function failed to find keywords 95 | refnames = keywords["refnames"].strip() 96 | if refnames.startswith("$Format"): 97 | if verbose: 98 | print("keywords are unexpanded, not using") 99 | return {} # unexpanded, so not in an unpacked git-archive tarball 100 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 101 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 102 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 103 | TAG = "tag: " 104 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 105 | if not tags: 106 | # Either we're using git < 1.8.3, or there really are no tags. We use 107 | # a heuristic: assume all version tags have a digit. The old git %d 108 | # expansion behaves like git log --decorate=short and strips out the 109 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 110 | # between branches and tags. By ignoring refnames without digits, we 111 | # filter out many common branch names like "release" and 112 | # "stabilization", as well as "HEAD" and "master". 113 | tags = set([r for r in refs if re.search(r'\d', r)]) 114 | if verbose: 115 | print("discarding '%s', no digits" % ",".join(refs-tags)) 116 | if verbose: 117 | print("likely tags: %s" % ",".join(sorted(tags))) 118 | for ref in sorted(tags): 119 | # sorting will prefer e.g. "2.0" over "2.0rc1" 120 | if ref.startswith(tag_prefix): 121 | r = ref[len(tag_prefix):] 122 | if verbose: 123 | print("picking %s" % r) 124 | return { "version": r, 125 | "full": keywords["full"].strip() } 126 | # no suitable tags, so we use the full revision id 127 | if verbose: 128 | print("no suitable tags, using full revision id") 129 | return { "version": keywords["full"].strip(), 130 | "full": keywords["full"].strip() } 131 | 132 | 133 | def git_versions_from_vcs(tag_prefix, root, verbose=False): 134 | # this runs 'git' from the root of the source tree. This only gets called 135 | # if the git-archive 'subst' keywords were *not* expanded, and 136 | # _version.py hasn't already been rewritten with a short version string, 137 | # meaning we're inside a checked out source tree. 138 | 139 | if not os.path.exists(os.path.join(root, ".git")): 140 | if verbose: 141 | print("no .git in %s" % root) 142 | return {} 143 | 144 | GITS = ["git"] 145 | if sys.platform == "win32": 146 | GITS = ["git.cmd", "git.exe"] 147 | stdout = run_command(GITS, ["describe", "--tags", "--dirty", "--always"], 148 | cwd=root) 149 | if stdout is None: 150 | return {} 151 | if not stdout.startswith(tag_prefix): 152 | if verbose: 153 | print("tag '%s' doesn't start with prefix '%s'" % (stdout, tag_prefix)) 154 | return {} 155 | tag = stdout[len(tag_prefix):] 156 | stdout = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 157 | if stdout is None: 158 | return {} 159 | full = stdout.strip() 160 | if tag.endswith("-dirty"): 161 | full += "-dirty" 162 | return {"version": tag, "full": full} 163 | 164 | 165 | def get_versions(default={"version": "unknown", "full": ""}, verbose=False): 166 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 167 | # __file__, we can work backwards from there to the root. Some 168 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 169 | # case we can only use expanded keywords. 170 | 171 | keywords = { "refnames": git_refnames, "full": git_full } 172 | ver = git_versions_from_keywords(keywords, tag_prefix, verbose) 173 | if ver: 174 | return ver 175 | 176 | try: 177 | root = os.path.abspath(__file__) 178 | # versionfile_source is the relative path from the top of the source 179 | # tree (where the .git directory might live) to this file. Invert 180 | # this to find the root from __file__. 181 | for i in range(len(versionfile_source.split('/'))): 182 | root = os.path.dirname(root) 183 | except NameError: 184 | return default 185 | 186 | return (git_versions_from_vcs(tag_prefix, root, verbose) 187 | or versions_from_parentdir(parentdir_prefix, root, verbose) 188 | or default) 189 | -------------------------------------------------------------------------------- /httpproxy/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from httpproxy.models import Request, RequestParameter, Response 4 | 5 | 6 | class ResponseInline(admin.StackedInline): 7 | model = Response 8 | 9 | 10 | class RequestParameterInline(admin.TabularInline): 11 | model = RequestParameter 12 | extra = 1 13 | 14 | 15 | class RequestAdmin(admin.ModelAdmin): 16 | list_display = ('method', 'domain', 'port', 'path', 'querystring_display', 'date') 17 | list_filter = ('method', 'domain', 'port') 18 | inlines = (RequestParameterInline, ResponseInline) 19 | 20 | 21 | class ResponseAdmin(admin.ModelAdmin): 22 | list_display = ('request_domain', 'request_path', 'request_querystring', 'status', 'content_type') 23 | list_filter = ('status', 'content_type') 24 | 25 | 26 | admin.site.register(Request, RequestAdmin) 27 | admin.site.register(Response, ResponseAdmin) 28 | -------------------------------------------------------------------------------- /httpproxy/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Some generic exceptions that can occur in Django HTTP Proxy. 3 | """ 4 | from django.http import Http404 5 | 6 | 7 | class ResponseUnsupported(Exception): 8 | """ 9 | Raised by :meth:`httpproxy.recorder.ProxyRecorder.record` when it cannot 10 | a response (e.g. because it has an unsupported content type). 11 | """ 12 | pass 13 | 14 | 15 | class RequestNotRecorded(Http404): 16 | """ 17 | Raised by :meth:`httpproxy.recorder.ProxyRecorder.playback` when a request 18 | is made for a URL that has not previously recorded yet. 19 | """ 20 | pass 21 | -------------------------------------------------------------------------------- /httpproxy/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='Request', 15 | fields=[ 16 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 17 | ('method', models.CharField(max_length=20, verbose_name='method')), 18 | ('domain', models.CharField(max_length=100, verbose_name='domain')), 19 | ('port', models.PositiveSmallIntegerField(default=80)), 20 | ('path', models.CharField(max_length=250, verbose_name='path')), 21 | ('date', models.DateTimeField(auto_now=True)), 22 | ('querykey', models.CharField(verbose_name='query key', max_length=255, editable=False)), 23 | ], 24 | options={ 25 | 'get_latest_by': 'date', 26 | 'verbose_name': 'request', 27 | 'verbose_name_plural': 'requests', 28 | }, 29 | bases=(models.Model,), 30 | ), 31 | migrations.CreateModel( 32 | name='RequestParameter', 33 | fields=[ 34 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 35 | ('type', models.CharField(default=b'G', max_length=1, choices=[(b'G', b'GET'), (b'P', b'POST')])), 36 | ('order', models.PositiveSmallIntegerField(default=1)), 37 | ('name', models.CharField(max_length=100, verbose_name='naam')), 38 | ('value', models.CharField(max_length=250, null=True, verbose_name='value', blank=True)), 39 | ('request', models.ForeignKey(related_name='parameters', verbose_name='request', to='httpproxy.Request')), 40 | ], 41 | options={ 42 | 'ordering': ('order',), 43 | 'verbose_name': 'request parameter', 44 | 'verbose_name_plural': 'request parameters', 45 | }, 46 | bases=(models.Model,), 47 | ), 48 | migrations.CreateModel( 49 | name='Response', 50 | fields=[ 51 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 52 | ('status', models.PositiveSmallIntegerField(default=200)), 53 | ('content_type', models.CharField(max_length=200, verbose_name='inhoudstype')), 54 | ('content', models.TextField(verbose_name='inhoud')), 55 | ('request', models.OneToOneField(verbose_name='request', to='httpproxy.Request')), 56 | ], 57 | options={ 58 | 'verbose_name': 'response', 59 | 'verbose_name_plural': 'responses', 60 | }, 61 | bases=(models.Model,), 62 | ), 63 | migrations.AlterUniqueTogether( 64 | name='request', 65 | unique_together=set([('method', 'domain', 'port', 'path', 'querykey')]), 66 | ), 67 | ] 68 | -------------------------------------------------------------------------------- /httpproxy/migrations/0002_auto_20160121_1940.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('httpproxy', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='requestparameter', 16 | name='name', 17 | field=models.CharField(max_length=100, verbose_name='name'), 18 | ), 19 | migrations.AlterField( 20 | model_name='response', 21 | name='content', 22 | field=models.TextField(verbose_name='content'), 23 | ), 24 | migrations.AlterField( 25 | model_name='response', 26 | name='content_type', 27 | field=models.CharField(max_length=200, verbose_name='content type'), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /httpproxy/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yvandermeer/django-http-proxy/0bf66c22bf1bd4e614de6dac5a28484c155a38f1/httpproxy/migrations/__init__.py -------------------------------------------------------------------------------- /httpproxy/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.six.moves import urllib 3 | from django.utils.translation import ugettext as _ 4 | 5 | 6 | class Request(models.Model): 7 | """ 8 | An HTTP request recorded in the database. 9 | 10 | Used by the :class:`~httpproxy.recorder.ProxyRecorder` to record all 11 | identifying aspects of an HTTP request for matching later on when playing 12 | back the response. 13 | 14 | Request parameters are recorded separately, see 15 | :class:`~httpproxy.models.RequestParameter`. 16 | """ 17 | method = models.CharField(_('method'), max_length=20) 18 | domain = models.CharField(_('domain'), max_length=100) 19 | port = models.PositiveSmallIntegerField(default=80) 20 | path = models.CharField(_('path'), max_length=250) 21 | date = models.DateTimeField(auto_now=True) 22 | querykey = models.CharField(_('query key'), max_length=255, editable=False) 23 | 24 | @property 25 | def querystring(self): 26 | """ 27 | The URL-encoded set of request parameters. 28 | """ 29 | return self.parameters.urlencode() 30 | 31 | def querystring_display(self): 32 | maxlength = 50 33 | if len(self.querystring) > maxlength: 34 | return '%s [...]' % self.querystring[:50] 35 | else: 36 | return self.querystring 37 | querystring_display.short_description = 'querystring' 38 | 39 | def __unicode__(self): 40 | output = u'%s %s:%d%s' % \ 41 | (self.method, self.domain, self.port, self.path) 42 | if self.querystring: 43 | output += '?%s' % self.querystring 44 | return output[:50] # TODO add elipsed if truncating 45 | 46 | class Meta: 47 | verbose_name = _('request') 48 | verbose_name_plural = _('requests') 49 | unique_together = ('method', 'domain', 'port', 'path', 'querykey') 50 | get_latest_by = 'date' 51 | 52 | 53 | class RequestParameterManager(models.Manager): 54 | 55 | def urlencode(self): 56 | output = [] 57 | for param in self.values('name', 'value'): 58 | output.extend([urllib.parse.urlencode({param['name']: param['value']})]) 59 | return '&'.join(output) 60 | 61 | 62 | class RequestParameter(models.Model): 63 | """ 64 | A single HTTP request parameter for a :class:`~httpproxy.models.Request` 65 | object. 66 | """ 67 | REQUEST_TYPES = ( 68 | ('G', 'GET'), 69 | ('P', 'POST'), 70 | ) 71 | request = models.ForeignKey(Request, verbose_name=_('request'), related_name='parameters') 72 | type = models.CharField(max_length=1, choices=REQUEST_TYPES, default='G') 73 | order = models.PositiveSmallIntegerField(default=1) 74 | name = models.CharField(_('name'), max_length=100) 75 | value = models.CharField(_('value'), max_length=250, null=True, blank=True) 76 | objects = RequestParameterManager() 77 | 78 | def __unicode__(self): 79 | return u'%d %s=%s' % (self.pk, self.name, self.value) 80 | 81 | class Meta: 82 | ordering = ('order',) 83 | verbose_name = _('request parameter') 84 | verbose_name_plural = _('request parameters') 85 | 86 | 87 | class Response(models.Model): 88 | """ 89 | The response that was recorded in response to the corresponding 90 | :class:`~httpproxy.models.Request`. 91 | """ 92 | request = models.OneToOneField(Request, verbose_name=_('request')) 93 | status = models.PositiveSmallIntegerField(default=200) 94 | content_type = models.CharField(_('content type'), max_length=200) 95 | content = models.TextField(_('content')) 96 | 97 | @property 98 | def request_domain(self): 99 | return self.request.domain 100 | 101 | @property 102 | def request_path(self): 103 | return self.request.path 104 | 105 | @property 106 | def request_querystring(self): 107 | return self.request.querystring 108 | 109 | def __unicode__(self): 110 | return u'Response to %s (%d)' % (self.request, self.status) 111 | 112 | class Meta: 113 | verbose_name = _('response') 114 | verbose_name_plural = _('responses') 115 | -------------------------------------------------------------------------------- /httpproxy/recorder.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | 4 | from django.conf import settings 5 | from django.http import HttpResponse 6 | try: 7 | from hashlib import md5 as md5_constructor 8 | except ImportError: 9 | from django.utils.hashcompat import md5_constructor 10 | 11 | from httpproxy.exceptions import RequestNotRecorded, ResponseUnsupported 12 | from httpproxy.models import Request, Response 13 | 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | class ProxyRecorder(object): 18 | """ 19 | Facilitates recording and playback of Django HTTP requests and responses. 20 | """ 21 | 22 | response_types_supported = ( 23 | 'application/javascript', 24 | 'application/xml', 25 | 'text/css', 26 | 'text/html', 27 | 'text/javascript', 28 | 'text/plain', 29 | 'text/xml', 30 | ) 31 | 32 | def __init__(self, domain, port): 33 | super(ProxyRecorder, self).__init__() 34 | self.domain, self.port = domain, port 35 | 36 | def record(self, request, response): 37 | """ 38 | Attempts to record the request and the corresponding response. 39 | 40 | .. note:: 41 | The recording functionality of the Django HTTP Proxy is currently 42 | limited to plain text content types. The default behavior is to 43 | *ignore* any unsupported content types when 44 | :attr:`httpproxy.views.HttpProxy.mode` is set to ``record`` – 45 | responses with unsupported content types are not recorded and will 46 | be ignored silently. To prevent this, put this in your Django 47 | settings module:: 48 | 49 | PROXY_IGNORE_UNSUPPORTED = False 50 | 51 | Doing so will raise a 52 | :class:`~httpproxy.exceptions.ResponseUnsupported` exception if any 53 | unsupported response type is encountered in "record" mode. 54 | """ 55 | if self.response_supported(response): 56 | recorded_request = self.record_request(request) 57 | self.record_response(recorded_request, response) 58 | elif not getattr(settings, 'PROXY_IGNORE_UNSUPPORTED', True): 59 | raise ResponseUnsupported('Response of type "%s" could not be recorded.' % response['Content-Type']) 60 | 61 | def record_request(self, request): 62 | """ 63 | Saves the provided :class:`~httpproxy.models.Request`, including its 64 | :class:`~httpproxy.models.RequestParameter` objects. 65 | """ 66 | logger.info('Recording: GET "%s"' % self._request_string(request)) 67 | 68 | recorded_request, created = Request.objects.get_or_create( 69 | method=request.method, domain=self.domain, port=self.port, 70 | path=request.path, querykey=self._get_query_key(request)) 71 | 72 | self.record_request_parameters(request, recorded_request) 73 | 74 | # Update the timestamp on the existing recorded request 75 | if not created: 76 | recorded_request.save() 77 | 78 | return recorded_request 79 | 80 | def record_request_parameters(self, request, recorded_request): 81 | """ 82 | Records the :class:`~httpproxy.models.RequestParameter` objects for the 83 | recorded :class:`~httpproxy.models.Request`. 84 | 85 | The order field is set to reflect the order in which the QueryDict 86 | returns the GET parameters. 87 | """ 88 | recorded_request.parameters.get_query_set().delete() 89 | position = 1 90 | for name, values_list in request.GET.lists(): 91 | for value in values_list: 92 | recorded_request.parameters.create(order=position, name=name, 93 | value=value) 94 | position += 1 95 | 96 | def record_response(self, recorded_request, response): 97 | """ 98 | Records a :class:`~httpproxy.models.Response` so it can be replayed at a 99 | later stage. 100 | 101 | The recorded response is linked to a previously recorded 102 | :class:`~httpproxy.models.Request` and its request parameters to allow 103 | for reverse-finding the recorded response given the recorded request 104 | object. 105 | """ 106 | # Delete the previously recorded response, if any 107 | try: 108 | recorded_request.response.delete() 109 | except Response.DoesNotExist: 110 | pass 111 | 112 | # Extract the encoding from the response 113 | content_type = response['Content-Type'] 114 | encoding = content_type.partition('charset=')[-1] or 'utf-8' 115 | 116 | # Record the new response 117 | Response.objects.create(request=recorded_request, 118 | status=response.status_code, content_type=content_type, 119 | content=response.content.decode(encoding)) 120 | 121 | def playback(self, request): 122 | """ 123 | Returns a previously recorded response based on the provided request. 124 | """ 125 | try: 126 | matching_request = Request.objects.filter(method=request.method, 127 | domain=self.domain, port=self.port, path=request.path, 128 | querykey=self._get_query_key(request)).latest() 129 | except Request.DoesNotExist: 130 | raise RequestNotRecorded('The request made has not been ' \ 131 | 'recorded yet. Please run httpproxy in "record" mode ' \ 132 | 'first.') 133 | 134 | logger.info('Playback: GET "%s"' % self._request_string(request)) 135 | response = matching_request.response # TODO handle "no response" situation 136 | encoding = self._get_encoding(response.content_type) 137 | 138 | return HttpResponse(response.content.encode(encoding), 139 | status=response.status, content_type=response.content_type) 140 | 141 | def response_supported(self, response): 142 | return response['Content-Type'].partition(';')[0] \ 143 | in self.response_types_supported 144 | 145 | def _get_encoding(self, content_type): 146 | """ 147 | Extracts the character encoding from an HTTP Content-Type header. 148 | """ 149 | return content_type.partition('charset=')[-1] or 'utf-8' 150 | 151 | def _request_string(self, request): 152 | """ 153 | Helper for getting a string representation of a request. 154 | """ 155 | return '%(domain)s:%(port)d%(path)s' % { 156 | 'domain': self.domain, 157 | 'port': self.port, 158 | 'path': request.get_full_path() 159 | } 160 | 161 | def _get_query_key(self, request): 162 | """ 163 | Returns an MD5 has of the request's query parameters. 164 | """ 165 | querystring = request.GET.urlencode() 166 | return md5_constructor(querystring).hexdigest() 167 | -------------------------------------------------------------------------------- /httpproxy/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | import re 4 | 5 | from django.http import HttpResponse 6 | from django.utils.six.moves import urllib 7 | from django.views.generic import View 8 | 9 | from httpproxy.recorder import ProxyRecorder 10 | 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | REWRITE_REGEX = re.compile(r'((?:src|action|href)=["\'])/(?!\/)') 16 | 17 | class HttpProxy(View): 18 | """ 19 | Class-based view to configure Django HTTP Proxy for a URL pattern. 20 | 21 | In its most basic usage:: 22 | 23 | from httpproxy.views import HttpProxy 24 | 25 | urlpatterns += patterns('', 26 | (r'^my-proxy/(?P.*)$', 27 | HttpProxy.as_view(base_url='http://python.org/')), 28 | ) 29 | 30 | Using the above configuration (and assuming your Django project is server by 31 | the Django development server on port 8000), a request to 32 | ``http://localhost:8000/my-proxy/index.html`` is proxied to 33 | ``http://python.org/index.html``. 34 | """ 35 | 36 | base_url = None 37 | """ 38 | The base URL that the proxy should forward requests to. 39 | """ 40 | 41 | mode = None 42 | """ 43 | The mode that the proxy should run in. Available modes are ``record`` and 44 | ``play``. If no mode is defined (``None`` – the default), this means the proxy 45 | will work as a "standard" HTTP proxy. 46 | 47 | If the mode is set to ``record``, all requests will be forwarded to the remote 48 | server, but both the requests and responses will be recorded to the database 49 | for playback at a later stage. 50 | 51 | If the mode is set to ``play``, no requests will be forwarded to the remote 52 | server. 53 | 54 | In ``play`` mode, if the response (to the request being made) was previously 55 | recorded, the recorded response will be served. Otherwise, a custom 56 | ``Http404`` exception will be raised 57 | (:class:`~httpproxy.exceptions.RequestNotRecorded`). 58 | """ 59 | 60 | rewrite = False 61 | """ 62 | If you configure the HttpProxy view on any non-root URL, the proxied 63 | responses may still contain references to resources as if they were served 64 | at the root. By setting this attribute to ``True``, the response will be 65 | :meth:`rewritten ` to try to 66 | fix the paths. 67 | """ 68 | 69 | _msg = 'Response body: \n%s' 70 | 71 | def dispatch(self, request, url, *args, **kwargs): 72 | self.url = url 73 | self.original_request_path = request.path 74 | request = self.normalize_request(request) 75 | if self.mode == 'play': 76 | response = self.play(request) 77 | # TODO: avoid repetition, flow of logic could be improved 78 | if self.rewrite: 79 | response = self.rewrite_response(request, response) 80 | return response 81 | 82 | response = super(HttpProxy, self).dispatch(request, *args, **kwargs) 83 | if self.mode == 'record': 84 | self.record(response) 85 | if self.rewrite: 86 | response = self.rewrite_response(request, response) 87 | return response 88 | 89 | def normalize_request(self, request): 90 | """ 91 | Updates all path-related info in the original request object with the 92 | url given to the proxy. 93 | 94 | This way, any further processing of the proxy'd request can just ignore 95 | the url given to the proxy and use request.path safely instead. 96 | """ 97 | if not self.url.startswith('/'): 98 | self.url = '/' + self.url 99 | request.path = self.url 100 | request.path_info = self.url 101 | request.META['PATH_INFO'] = self.url 102 | return request 103 | 104 | def rewrite_response(self, request, response): 105 | """ 106 | Rewrites the response to fix references to resources loaded from HTML 107 | files (images, etc.). 108 | 109 | .. note:: 110 | The rewrite logic uses a fairly simple regular expression to look for 111 | "src", "href" and "action" attributes with a value starting with "/" 112 | – your results may vary. 113 | """ 114 | proxy_root = self.original_request_path.rsplit(request.path, 1)[0] 115 | response.content = REWRITE_REGEX.sub(r'\1{}/'.format(proxy_root), 116 | response.content) 117 | return response 118 | 119 | def play(self, request): 120 | """ 121 | Plays back the response to a request, based on a previously recorded 122 | request/response. 123 | 124 | Delegates to :class:`~httpproxy.recorder.ProxyRecorder`. 125 | """ 126 | return self.get_recorder().playback(request) 127 | 128 | def record(self, response): 129 | """ 130 | Records the request being made and its response. 131 | 132 | Delegates to :class:`~httpproxy.recorder.ProxyRecorder`. 133 | """ 134 | self.get_recorder().record(self.request, response) 135 | 136 | def get_recorder(self): 137 | url = urllib.parse(self.base_url) 138 | return ProxyRecorder(domain=url.hostname, port=(url.port or 80)) 139 | 140 | def get(self, *args, **kwargs): 141 | return self.get_response() 142 | 143 | def post(self, request, *args, **kwargs): 144 | headers = {} 145 | if request.META.get('CONTENT_TYPE'): 146 | headers['Content-type'] = request.META.get('CONTENT_TYPE') 147 | return self.get_response(body=request.body, headers=headers) 148 | 149 | def get_full_url(self, url): 150 | """ 151 | Constructs the full URL to be requested. 152 | """ 153 | param_str = self.request.GET.urlencode() 154 | request_url = u'%s%s' % (self.base_url, url) 155 | request_url += '?%s' % param_str if param_str else '' 156 | return request_url 157 | 158 | def create_request(self, url, body=None, headers={}): 159 | request = urllib.request.Request(url, body, headers) 160 | logger.info('%s %s' % (request.get_method(), request.get_full_url())) 161 | return request 162 | 163 | def get_response(self, body=None, headers={}): 164 | request_url = self.get_full_url(self.url) 165 | request = self.create_request(request_url, body=body, headers=headers) 166 | response = urllib.request.urlopen(request) 167 | try: 168 | response_body = response.read() 169 | status = response.getcode() 170 | logger.debug(self._msg % response_body) 171 | except urllib.error.HTTPError as e: 172 | response_body = e.read() 173 | logger.error(self._msg % response_body) 174 | status = e.code 175 | return HttpResponse(response_body, status=status, 176 | content_type=response.headers['content-type']) 177 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx-rtd-theme==0.1.6 2 | Sphinx~=1.2.3 3 | versioneer2==0.1.10 4 | wheel~=0.24.0 5 | 6 | -e . 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import versioneer 3 | 4 | 5 | packages = find_packages() 6 | 7 | versioneer.VCS = 'git' 8 | versioneer.versionfile_source = '{0}/_version.py'.format(packages[0]) 9 | versioneer.tag_prefix = '' 10 | versioneer.parentdir_prefix = '' 11 | 12 | setup( 13 | name='django-http-proxy', 14 | version=versioneer.get_version(), 15 | cmdclass=versioneer.get_cmdclass(), 16 | description='A simple HTTP proxy for the Django framework.', 17 | author='Yuri van der Meer', 18 | author_email='contact@yvandermeer.net', 19 | url='http://django-http-proxy.readthedocs.org/', 20 | packages=find_packages(), 21 | install_requires=[ 22 | 'Django>=1.6', 23 | ], 24 | classifiers=[ 25 | 'Development Status :: 3 - Alpha', 26 | 'Environment :: Console', 27 | 'Environment :: Web Environment', 28 | 'Framework :: Django', 29 | 'Intended Audience :: Developers', 30 | 'Intended Audience :: System Administrators', 31 | 'License :: OSI Approved :: MIT License', 32 | 'Operating System :: MacOS :: MacOS X', 33 | 'Operating System :: Microsoft', 34 | 'Operating System :: POSIX', 35 | 'Programming Language :: Python', 36 | 'Topic :: Internet :: WWW/HTTP', 37 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 38 | ], 39 | ) 40 | -------------------------------------------------------------------------------- /versioneer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | """ 4 | Versioneer 2 5 | ============ 6 | 7 | * Like a rocketeer, but for versions! 8 | * Based on https://github.com/warner/python-versioneer 9 | * Brian Warner 10 | * License: Public Domain 11 | * Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, and pypy 12 | * Edited by Ryan Dwyer 13 | 14 | This is a tool for managing a recorded version number in python projects. 15 | The goal is to remove the tedious and error-prone "update the embedded version 16 | string" step from your release process. Making a new 17 | release should be as easy as recording a new tag in your version-control system, 18 | and maybe making new tarballs. 19 | 20 | ## Cookiecutter 21 | 22 | * If you got this file using cookiecutter, the manual steps listed below should 23 | all be done. 24 | * Run `git tag 1.0` (for example), and register and upload to PyPI as usual. 25 | 26 | ## Manual Install 27 | 28 | * Copy this file to beside your setup.py 29 | 30 | * Add the following to your setup.py: 31 | 32 | import imp 33 | fp, pathname, description = imp.find_module('versioneer') 34 | try: 35 | versioneer = imp.load_module('versioneer', fp, pathname, description) 36 | finally: 37 | if fp: fp.close() 38 | 39 | versioneer.VCS = 'git' 40 | versioneer.versionfile_source = 'src/myproject/_version.py' 41 | versioneer.versionfile_build = 'myproject/_version.py' 42 | versioneer.tag_prefix = '' # tags are like 1.2.0 43 | versioneer.parentdir_prefix = 'myproject-' # dirname like 'myproject-1.2.0' 44 | 45 | * Add the following arguments to the setup() call in your setup.py: 46 | 47 | version=versioneer.get_version(), 48 | cmdclass=versioneer.get_cmdclass(), 49 | 50 | * Now run `python setup.py versioneer`, which will create `_version.py`, and will 51 | modify your `__init__.py` (if one exists next to `_version.py`) to define 52 | `__version__` (by calling a function from `_version.py`). It will also 53 | modify your `MANIFEST.in` to include both `versioneer.py` and the generated 54 | `_version.py` in sdist tarballs. 55 | 56 | * Commit these changes to your VCS. To make sure you won't forget, 57 | `setup.py versioneer` will mark everything it touched for addition. 58 | 59 | If you distribute your project through PyPI, then the release process should 60 | boil down to two steps: 61 | 62 | * 1: git tag 1.0 63 | * 2: python setup.py register sdist upload 64 | 65 | ## Version Identifiers 66 | 67 | Source trees come from a variety of places: 68 | 69 | * a version-control system checkout (mostly used by developers) 70 | * a nightly tarball, produced by build automation 71 | * a snapshot tarball, produced by a web-based VCS browser, like github's 72 | "tarball from tag" feature 73 | * a release tarball, produced by "setup.py sdist", distributed through PyPI 74 | 75 | Within each source tree, the version identifier (either a string or a number, 76 | this tool is format-agnostic) can come from a variety of places: 77 | 78 | * ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows 79 | about recent "tags" and an absolute revision-id 80 | * the name of the directory into which the tarball was unpacked 81 | * an expanded VCS keyword ($Id$, etc) 82 | * a `_version.py` created by some earlier build step 83 | 84 | For released software, the version identifier is closely related to a VCS 85 | tag. Some projects use tag names that include more than just the version 86 | string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool 87 | needs to strip the tag prefix to extract the version identifier. For 88 | unreleased software (between tags), the version identifier should provide 89 | enough information to help developers recreate the same tree, while also 90 | giving them an idea of roughly how old the tree is (after version 1.2, before 91 | version 1.3). Many VCS systems can report a description that captures this, 92 | for example 'git describe --tags --dirty --always' reports things like 93 | "0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the 94 | 0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has 95 | uncommitted changes. 96 | 97 | The version identifier is used for multiple purposes: 98 | 99 | * to allow the module to self-identify its version: `myproject.__version__` 100 | * to choose a name and prefix for a 'setup.py sdist' tarball 101 | 102 | ## Theory of Operation 103 | 104 | Versioneer works by adding a special `_version.py` file into your source 105 | tree, where your `__init__.py` can import it. This `_version.py` knows how to 106 | dynamically ask the VCS tool for version information at import time. However, 107 | when you use "setup.py build" or "setup.py sdist", `_version.py` in the new 108 | copy is replaced by a small static file that contains just the generated 109 | version data. 110 | 111 | `_version.py` also contains `$Revision$` markers, and the installation 112 | process marks `_version.py` to have this marker rewritten with a tag name 113 | during the "git archive" command. As a result, generated tarballs will 114 | contain enough information to get the proper version. 115 | 116 | 117 | ## Detailed Installation Instructions 118 | 119 | First, decide on values for the following configuration variables: 120 | 121 | * `VCS`: the version control system you use. Currently accepts "git". 122 | 123 | * `versionfile_source`: 124 | 125 | A project-relative pathname into which the generated version strings should 126 | be written. This is usually a `_version.py` next to your project's main 127 | `__init__.py` file, so it can be imported at runtime. If your project uses 128 | `src/myproject/__init__.py`, this should be `src/myproject/_version.py`. 129 | This file should be checked in to your VCS as usual: the copy created below 130 | by `setup.py versioneer` will include code that parses expanded VCS 131 | keywords in generated tarballs. The 'build' and 'sdist' commands will 132 | replace it with a copy that has just the calculated version string. 133 | 134 | This must be set even if your project does not have any modules (and will 135 | therefore never import `_version.py`), since "setup.py sdist" -based trees 136 | still need somewhere to record the pre-calculated version strings. Anywhere 137 | in the source tree should do. If there is a `__init__.py` next to your 138 | `_version.py`, the `setup.py versioneer` command (described below) will 139 | append some `__version__`-setting assignments, if they aren't already 140 | present. 141 | 142 | * `versionfile_build`: 143 | 144 | Like `versionfile_source`, but relative to the build directory instead of 145 | the source directory. These will differ when your setup.py uses 146 | 'package_dir='. If you have `package_dir={'myproject': 'src/myproject'}`, 147 | then you will probably have `versionfile_build='myproject/_version.py'` and 148 | `versionfile_source='src/myproject/_version.py'`. 149 | 150 | If this is set to None, then `setup.py build` will not attempt to rewrite 151 | any `_version.py` in the built tree. If your project does not have any 152 | libraries (e.g. if it only builds a script), then you should use 153 | `versionfile_build = None` and override `distutils.command.build_scripts` 154 | to explicitly insert a copy of `versioneer.get_version()` into your 155 | generated script. 156 | 157 | * `tag_prefix`: 158 | 159 | a string, like 'PROJECTNAME-', which appears at the start of all VCS tags. 160 | If your tags look like 'myproject-1.2.0', then you should use 161 | tag_prefix='myproject-'. If you use unprefixed tags like '1.2.0', this 162 | should be an empty string. 163 | 164 | * `parentdir_prefix`: 165 | 166 | a string, frequently the same as tag_prefix, which appears at the start of 167 | all unpacked tarball filenames. If your tarball unpacks into 168 | 'myproject-1.2.0', this should be 'myproject-'. 169 | 170 | This tool provides one script, named `versioneer-installer`. That script does 171 | one thing: write a copy of `versioneer.py` into the current directory. 172 | 173 | To versioneer-enable your project: 174 | 175 | * 1: Run `versioneer-installer` to copy `versioneer.py` into the top of your 176 | source tree. 177 | 178 | * 2: add the following lines to the top of your `setup.py`, with the 179 | configuration values you decided earlier: 180 | 181 | import imp 182 | fp, pathname, description = imp.find_module('versioneer') 183 | try: 184 | versioneer = imp.load_module('versioneer', fp, pathname, description) 185 | finally: 186 | if fp: fp.close() 187 | 188 | versioneer.VCS = 'git' 189 | versioneer.versionfile_source = 'src/myproject/_version.py' 190 | versioneer.versionfile_build = 'myproject/_version.py' 191 | versioneer.tag_prefix = '' # tags are like 1.2.0 192 | versioneer.parentdir_prefix = 'myproject-' # dirname like 'myproject-1.2.0' 193 | 194 | * 3: add the following arguments to the setup() call in your setup.py: 195 | 196 | version=versioneer.get_version(), 197 | cmdclass=versioneer.get_cmdclass(), 198 | 199 | * 4: now run `setup.py versioneer`, which will create `_version.py`, and will 200 | modify your `__init__.py` (if one exists next to `_version.py`) to define 201 | `__version__` (by calling a function from `_version.py`). It will also 202 | modify your `MANIFEST.in` to include both `versioneer.py` and the generated 203 | `_version.py` in sdist tarballs. 204 | 205 | * 5: commit these changes to your VCS. To make sure you won't forget, 206 | `setup.py versioneer` will mark everything it touched for addition. 207 | 208 | ## Post-Installation Usage 209 | 210 | Once established, all uses of your tree from a VCS checkout should get the 211 | current version string. All generated tarballs should include an embedded 212 | version string (so users who unpack them will not need a VCS tool installed). 213 | 214 | If you distribute your project through PyPI, then the release process should 215 | boil down to two steps: 216 | 217 | * 1: git tag 1.0 218 | * 2: python setup.py register sdist upload 219 | 220 | If you distribute it through github (i.e. users use github to generate 221 | tarballs with `git archive`), the process is: 222 | 223 | * 1: git tag 1.0 224 | * 2: git push; git push --tags 225 | 226 | Currently, all version strings must be based upon a tag. Versioneer will 227 | report "unknown" until your tree has at least one tag in its history. This 228 | restriction will be fixed eventually (see issue #12). 229 | 230 | ## Version-String Flavors 231 | 232 | Code which uses Versioneer can learn about its version string at runtime by 233 | importing `_version` from your main `__init__.py` file and running the 234 | `get_versions()` function. From the "outside" (e.g. in `setup.py`), you can 235 | import the top-level `versioneer.py` and run `get_versions()`. 236 | 237 | Both functions return a dictionary with different keys for different flavors 238 | of the version string: 239 | 240 | * `['version']`: condensed tag+distance+shortid+dirty identifier. 241 | This is based on the output of `git describe --tags --dirty --always` but 242 | strips the tag_prefix. For example "0.11.post0.dev2+g1076c97-dirty" indicates 243 | that the tree is like the "1076c97" commit but has uncommitted changes 244 | ("-dirty"), and that this commit is two revisions (".dev2") beyond the "0.11" 245 | tag. For released software (exactly equal to a known tag), 246 | the identifier will only contain the stripped tag, e.g. "0.11". 247 | This version string is always fully PEP440 compliant. 248 | 249 | * `['full']`: detailed revision identifier. For Git, this is the full SHA1 250 | commit id, followed by "-dirty" if the tree contains uncommitted changes, 251 | e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac-dirty". 252 | 253 | Some variants are more useful than others. Including `full` in a bug report 254 | should allow developers to reconstruct the exact code being tested (or 255 | indicate the presence of local changes that should be shared with the 256 | developers). `version` is suitable for display in an "about" box or a CLI 257 | `--version` output: it can be easily compared against release notes and lists 258 | of bugs fixed in various releases. 259 | 260 | The `setup.py versioneer` command adds the following text to your 261 | `__init__.py` to place a basic version in `YOURPROJECT.__version__`: 262 | 263 | from ._version import get_versions 264 | __version__ = get_versions()['version'] 265 | del get_versions 266 | 267 | ## Updating Versioneer2 268 | 269 | To upgrade your project to a new release of Versioneer, do the following: 270 | 271 | * install the new versioneer2 (`pip install -U versioneer2` or equivalent) 272 | * re-run `versioneer2installer` in your source tree to replace your copy of 273 | `versioneer.py` 274 | * edit `setup.py`, if necessary, to include any new configuration settings 275 | indicated by the release notes 276 | * re-run `setup.py versioneer` to replace `SRC/_version.py` 277 | * commit any changed files 278 | 279 | """ 280 | 281 | import os 282 | import sys 283 | import re 284 | import subprocess 285 | import errno 286 | import string 287 | import io 288 | 289 | from distutils.core import Command 290 | from distutils.command.sdist import sdist as _sdist 291 | from distutils.command.build import build as _build 292 | 293 | __version__ = '0.1.10' 294 | 295 | # these configuration settings will be overridden by setup.py after it 296 | # imports us 297 | versionfile_source = None 298 | versionfile_build = None 299 | tag_prefix = None 300 | parentdir_prefix = None 301 | VCS = None 302 | 303 | # This is hard-coded for now. 304 | release_type_string = "post0.dev" 305 | 306 | # these dictionaries contain VCS-specific tools 307 | LONG_VERSION_PY = {} 308 | 309 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): 310 | assert isinstance(commands, list) 311 | p = None 312 | for c in commands: 313 | try: 314 | # remember shell=False, so use git.cmd on windows, not just git 315 | p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, 316 | stderr=(subprocess.PIPE if hide_stderr 317 | else None)) 318 | break 319 | except EnvironmentError: 320 | e = sys.exc_info()[1] 321 | if e.errno == errno.ENOENT: 322 | continue 323 | if verbose: 324 | print("unable to run %s" % args[0]) 325 | print(e) 326 | return None 327 | else: 328 | if verbose: 329 | print("unable to find command, tried %s" % (commands,)) 330 | return None 331 | stdout = p.communicate()[0].strip() 332 | if sys.version >= '3': 333 | stdout = stdout.decode() 334 | if p.returncode != 0: 335 | if verbose: 336 | print("unable to run %s (error)" % args[0]) 337 | return None 338 | return stdout 339 | 340 | LONG_VERSION_PY['git'] = ''' 341 | # This file helps to compute a version number in source trees obtained from 342 | # git-archive tarball (such as those provided by githubs download-from-tag 343 | # feature). Distribution tarballs (built by setup.py sdist) and build 344 | # directories (produced by setup.py build) will contain a much shorter file 345 | # that just contains the computed version number. 346 | 347 | # This file is released into the public domain. Generated by 348 | # versioneer2-(%(__version__)s) (https://github.com/ryanpdwyer/versioneer2) 349 | 350 | # these strings will be replaced by git during git-archive 351 | git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" 352 | git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" 353 | 354 | # these strings are filled in when 'setup.py versioneer' creates _version.py 355 | tag_prefix = "%(TAG_PREFIX)s" 356 | parentdir_prefix = "%(PARENTDIR_PREFIX)s" 357 | versionfile_source = "%(VERSIONFILE_SOURCE)s" 358 | 359 | import os 360 | import sys 361 | import re 362 | import subprocess 363 | import errno 364 | import io 365 | 366 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): 367 | assert isinstance(commands, list) 368 | p = None 369 | for c in commands: 370 | try: 371 | # remember shell=False, so use git.cmd on windows, not just git 372 | p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, 373 | stderr=(subprocess.PIPE if hide_stderr 374 | else None)) 375 | break 376 | except EnvironmentError: 377 | e = sys.exc_info()[1] 378 | if e.errno == errno.ENOENT: 379 | continue 380 | if verbose: 381 | print("unable to run %%s" %% args[0]) 382 | print(e) 383 | return None 384 | else: 385 | if verbose: 386 | print("unable to find command, tried %%s" %% (commands,)) 387 | return None 388 | stdout = p.communicate()[0].strip() 389 | if sys.version >= '3': 390 | stdout = stdout.decode() 391 | if p.returncode != 0: 392 | if verbose: 393 | print("unable to run %%s (error)" %% args[0]) 394 | return None 395 | return stdout 396 | 397 | 398 | def versions_from_parentdir(parentdir_prefix, root, verbose=False): 399 | # Source tarballs conventionally unpack into a directory that includes 400 | # both the project name and a version string. 401 | dirname = os.path.basename(root) 402 | if not dirname.startswith(parentdir_prefix): 403 | if verbose: 404 | print("guessing rootdir is '%%s', but '%%s' doesn't start with prefix '%%s'" %% 405 | (root, dirname, parentdir_prefix)) 406 | return None 407 | return {"version": dirname[len(parentdir_prefix):].replace("_", "+").strip(".egg"), "full": ""} 408 | 409 | def git_get_keywords(versionfile_abs): 410 | # the code embedded in _version.py can just fetch the value of these 411 | # keywords. When used from setup.py, we don't want to import _version.py, 412 | # so we do it with a regexp instead. This function is not used from 413 | # _version.py. 414 | keywords = {} 415 | try: 416 | f = io.open(versionfile_abs, "r", encoding='utf-8') 417 | for line in f.readlines(): 418 | if line.strip().startswith("git_refnames ="): 419 | mo = re.search(r'=\s*"(.*)"', line) 420 | if mo: 421 | keywords["refnames"] = mo.group(1) 422 | if line.strip().startswith("git_full ="): 423 | mo = re.search(r'=\s*"(.*)"', line) 424 | if mo: 425 | keywords["full"] = mo.group(1) 426 | f.close() 427 | except EnvironmentError: 428 | pass 429 | return keywords 430 | 431 | def git_versions_from_keywords(keywords, tag_prefix, verbose=False): 432 | if not keywords: 433 | return {} # keyword-finding function failed to find keywords 434 | refnames = keywords["refnames"].strip() 435 | if refnames.startswith("$Format"): 436 | if verbose: 437 | print("keywords are unexpanded, not using") 438 | return {} # unexpanded, so not in an unpacked git-archive tarball 439 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 440 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 441 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 442 | TAG = "tag: " 443 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 444 | if not tags: 445 | # Either we're using git < 1.8.3, or there really are no tags. We use 446 | # a heuristic: assume all version tags have a digit. The old git %%d 447 | # expansion behaves like git log --decorate=short and strips out the 448 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 449 | # between branches and tags. By ignoring refnames without digits, we 450 | # filter out many common branch names like "release" and 451 | # "stabilization", as well as "HEAD" and "master". 452 | tags = set([r for r in refs if re.search(r'\d', r)]) 453 | if verbose: 454 | print("discarding '%%s', no digits" %% ",".join(refs-tags)) 455 | if verbose: 456 | print("likely tags: %%s" %% ",".join(sorted(tags))) 457 | for ref in sorted(tags): 458 | # sorting will prefer e.g. "2.0" over "2.0rc1" 459 | if ref.startswith(tag_prefix): 460 | r = ref[len(tag_prefix):] 461 | if verbose: 462 | print("picking %%s" %% r) 463 | return { "version": r, 464 | "full": keywords["full"].strip() } 465 | # no suitable tags, so we use the full revision id 466 | if verbose: 467 | print("no suitable tags, using full revision id") 468 | return { "version": keywords["full"].strip(), 469 | "full": keywords["full"].strip() } 470 | 471 | 472 | def git_versions_from_vcs(tag_prefix, root, verbose=False): 473 | # this runs 'git' from the root of the source tree. This only gets called 474 | # if the git-archive 'subst' keywords were *not* expanded, and 475 | # _version.py hasn't already been rewritten with a short version string, 476 | # meaning we're inside a checked out source tree. 477 | 478 | if not os.path.exists(os.path.join(root, ".git")): 479 | if verbose: 480 | print("no .git in %%s" %% root) 481 | return {} 482 | 483 | GITS = ["git"] 484 | if sys.platform == "win32": 485 | GITS = ["git.cmd", "git.exe"] 486 | stdout = run_command(GITS, ["describe", "--tags", "--dirty", "--always"], 487 | cwd=root) 488 | if stdout is None: 489 | return {} 490 | if not stdout.startswith(tag_prefix): 491 | if verbose: 492 | print("tag '%%s' doesn't start with prefix '%%s'" %% (stdout, tag_prefix)) 493 | return {} 494 | tag = stdout[len(tag_prefix):] 495 | stdout = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 496 | if stdout is None: 497 | return {} 498 | full = stdout.strip() 499 | if tag.endswith("-dirty"): 500 | full += "-dirty" 501 | return {"version": tag, "full": full} 502 | 503 | 504 | def get_versions(default={"version": "unknown", "full": ""}, verbose=False): 505 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 506 | # __file__, we can work backwards from there to the root. Some 507 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 508 | # case we can only use expanded keywords. 509 | 510 | keywords = { "refnames": git_refnames, "full": git_full } 511 | ver = git_versions_from_keywords(keywords, tag_prefix, verbose) 512 | if ver: 513 | return ver 514 | 515 | try: 516 | root = os.path.abspath(__file__) 517 | # versionfile_source is the relative path from the top of the source 518 | # tree (where the .git directory might live) to this file. Invert 519 | # this to find the root from __file__. 520 | for i in range(len(versionfile_source.split('/'))): 521 | root = os.path.dirname(root) 522 | except NameError: 523 | return default 524 | 525 | return (git_versions_from_vcs(tag_prefix, root, verbose) 526 | or versions_from_parentdir(parentdir_prefix, root, verbose) 527 | or default) 528 | ''' 529 | 530 | def git_get_keywords(versionfile_abs): 531 | # the code embedded in _version.py can just fetch the value of these 532 | # keywords. When used from setup.py, we don't want to import _version.py, 533 | # so we do it with a regexp instead. This function is not used from 534 | # _version.py. 535 | keywords = {} 536 | try: 537 | f = io.open(versionfile_abs, "r", encoding='utf-8') 538 | for line in f.readlines(): 539 | if line.strip().startswith("git_refnames ="): 540 | mo = re.search(r'=\s*"(.*)"', line) 541 | if mo: 542 | keywords["refnames"] = mo.group(1) 543 | if line.strip().startswith("git_full ="): 544 | mo = re.search(r'=\s*"(.*)"', line) 545 | if mo: 546 | keywords["full"] = mo.group(1) 547 | f.close() 548 | except EnvironmentError: 549 | pass 550 | return keywords 551 | 552 | def git_versions_from_keywords(keywords, tag_prefix, verbose=False): 553 | if not keywords: 554 | return {} # keyword-finding function failed to find keywords 555 | refnames = keywords["refnames"].strip() 556 | if refnames.startswith("$Format"): 557 | if verbose: 558 | print("keywords are unexpanded, not using") 559 | return {} # unexpanded, so not in an unpacked git-archive tarball 560 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 561 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 562 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 563 | TAG = "tag: " 564 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 565 | if not tags: 566 | # Either we're using git < 1.8.3, or there really are no tags. We use 567 | # a heuristic: assume all version tags have a digit. The old git %d 568 | # expansion behaves like git log --decorate=short and strips out the 569 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 570 | # between branches and tags. By ignoring refnames without digits, we 571 | # filter out many common branch names like "release" and 572 | # "stabilization", as well as "HEAD" and "master". 573 | tags = set([r for r in refs if re.search(r'\d', r)]) 574 | if verbose: 575 | print("discarding '%s', no digits" % ",".join(refs-tags)) 576 | if verbose: 577 | print("likely tags: %s" % ",".join(sorted(tags))) 578 | for ref in sorted(tags): 579 | # sorting will prefer e.g. "2.0" over "2.0rc1" 580 | if ref.startswith(tag_prefix): 581 | r = ref[len(tag_prefix):] 582 | if verbose: 583 | print("picking %s" % r) 584 | return { "version": r, 585 | "full": keywords["full"].strip() } 586 | # no suitable tags, so we use the full revision id 587 | if verbose: 588 | print("no suitable tags, using full revision id") 589 | return { "version": keywords["full"].strip(), 590 | "full": keywords["full"].strip() } 591 | 592 | 593 | def git_versions_from_vcs(tag_prefix, root, verbose=False): 594 | # this runs 'git' from the root of the source tree. This only gets called 595 | # if the git-archive 'subst' keywords were *not* expanded, and 596 | # _version.py hasn't already been rewritten with a short version string, 597 | # meaning we're inside a checked out source tree. 598 | 599 | if not os.path.exists(os.path.join(root, ".git")): 600 | if verbose: 601 | print("no .git in %s" % root) 602 | return {} 603 | 604 | GITS = ["git"] 605 | if sys.platform == "win32": 606 | GITS = ["git.cmd", "git.exe"] 607 | stdout = run_command(GITS, ["describe", "--tags", "--dirty", "--always"], 608 | cwd=root) 609 | if stdout is None: 610 | return {} 611 | if not stdout.startswith(tag_prefix): 612 | if verbose: 613 | print("tag '%s' doesn't start with prefix '%s'" % (stdout, tag_prefix)) 614 | return {} 615 | tag = stdout[len(tag_prefix):] 616 | stdout = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 617 | if stdout is None: 618 | return {} 619 | full = stdout.strip() 620 | if tag.endswith("-dirty"): 621 | full += "-dirty" 622 | return {"version": tag, "full": full, '__version__': __version__} 623 | 624 | 625 | def do_vcs_install(manifest_in, versionfile_source, ipy): 626 | GITS = ["git"] 627 | if sys.platform == "win32": 628 | GITS = ["git.cmd", "git.exe"] 629 | files = [manifest_in, versionfile_source] 630 | if ipy: 631 | files.append(ipy) 632 | try: 633 | me = __file__ 634 | if me.endswith(".pyc") or me.endswith(".pyo"): 635 | me = os.path.splitext(me)[0] + ".py" 636 | versioneer_file = os.path.relpath(me) 637 | except NameError: 638 | versioneer_file = "versioneer.py" 639 | files.append(versioneer_file) 640 | present = False 641 | try: 642 | f = io.open(".gitattributes", "r", encoding='utf-8') 643 | for line in f.readlines(): 644 | if line.strip().startswith(versionfile_source): 645 | if "export-subst" in line.strip().split()[1:]: 646 | present = True 647 | f.close() 648 | except EnvironmentError: 649 | pass 650 | if not present: 651 | f = io.open(".gitattributes", "a+", encoding='utf-8') 652 | f.write("%s export-subst\n" % versionfile_source) 653 | f.close() 654 | files.append(".gitattributes") 655 | run_command(GITS, ["add", "--"] + files) 656 | 657 | def versions_from_parentdir(parentdir_prefix, root, verbose=False): 658 | # Source tarballs conventionally unpack into a directory that includes 659 | # both the project name and a version string. 660 | dirname = os.path.basename(root) 661 | if not dirname.startswith(parentdir_prefix): 662 | if verbose: 663 | print("guessing rootdir is '%s', but '%s' doesn't start with prefix '%s'" % 664 | (root, dirname, parentdir_prefix)) 665 | return None 666 | return {"version": dirname[len(parentdir_prefix):].replace("_", "+").strip(".egg"), "full": ""} 667 | 668 | SHORT_VERSION_PY = """ 669 | # This file was generated by 'versioneer.py' (%(__version__)s) from 670 | # revision-control system data, or from the parent directory name of an 671 | # unpacked source archive. Distribution tarballs contain a pre-generated copy 672 | # of this file. 673 | 674 | version_version = '%(version)s' 675 | version_full = '%(full)s' 676 | def get_versions(default={}, verbose=False): 677 | return {'version': version_version, 'full': version_full} 678 | 679 | """ 680 | 681 | DEFAULT = {"version": "unknown", "full": "unknown"} 682 | 683 | def versions_from_file(filename): 684 | versions = {} 685 | try: 686 | with io.open(filename, encoding='utf-8') as f: 687 | for line in f.readlines(): 688 | mo = re.match("version_version = '([^']+)'", line) 689 | if mo: 690 | versions["version"] = mo.group(1) 691 | mo = re.match("version_full = '([^']+)'", line) 692 | if mo: 693 | versions["full"] = mo.group(1) 694 | except EnvironmentError: 695 | return {} 696 | 697 | return versions 698 | 699 | def write_to_version_file(filename, versions): 700 | with io.open(filename, "w", encoding='utf-8') as f: 701 | f.write(SHORT_VERSION_PY % versions) 702 | 703 | print("set %s to '%s'" % (filename, versions["version"])) 704 | 705 | 706 | def get_root(): 707 | try: 708 | return os.path.dirname(os.path.abspath(__file__)) 709 | except NameError: 710 | return os.path.dirname(os.path.abspath(sys.argv[0])) 711 | 712 | def vcs_function(vcs, suffix): 713 | return getattr(sys.modules[__name__], '%s_%s' % (vcs, suffix), None) 714 | 715 | def get_versions(default=DEFAULT, verbose=False): 716 | # returns dict with two keys: 'version' and 'full' 717 | assert versionfile_source is not None, "please set versioneer.versionfile_source" 718 | assert tag_prefix is not None, "please set versioneer.tag_prefix" 719 | assert parentdir_prefix is not None, "please set versioneer.parentdir_prefix" 720 | assert VCS is not None, "please set versioneer.VCS" 721 | 722 | # I am in versioneer.py, which must live at the top of the source tree, 723 | # which we use to compute the root directory. py2exe/bbfreeze/non-CPython 724 | # don't have __file__, in which case we fall back to sys.argv[0] (which 725 | # ought to be the setup.py script). We prefer __file__ since that's more 726 | # robust in cases where setup.py was invoked in some weird way (e.g. pip) 727 | root = get_root() 728 | versionfile_abs = os.path.join(root, versionfile_source) 729 | 730 | # extract version first from _version.py, VCS command (e.g. 'git 731 | # describe'), parentdir. This is meant to work for developers using a 732 | # source checkout, for users of a tarball created by 'setup.py sdist', 733 | # and for users of a tarball/zipball created by 'git archive' or github's 734 | # download-from-tag feature or the equivalent in other VCSes. 735 | 736 | get_keywords_f = vcs_function(VCS, "get_keywords") 737 | versions_from_keywords_f = vcs_function(VCS, "versions_from_keywords") 738 | if get_keywords_f and versions_from_keywords_f: 739 | vcs_keywords = get_keywords_f(versionfile_abs) 740 | ver = versions_from_keywords_f(vcs_keywords, tag_prefix) 741 | if ver: 742 | if verbose: print("got version from expanded keyword %s" % ver) 743 | return rep_by_pep440(ver) 744 | 745 | ver = versions_from_file(versionfile_abs) 746 | if ver: 747 | if verbose: print("got version from file %s %s" % (versionfile_abs,ver)) 748 | return rep_by_pep440(ver) 749 | 750 | versions_from_vcs_f = vcs_function(VCS, "versions_from_vcs") 751 | if versions_from_vcs_f: 752 | ver = versions_from_vcs_f(tag_prefix, root, verbose) 753 | if ver: 754 | if verbose: print("got version from VCS %s" % ver) 755 | return rep_by_pep440(ver) 756 | 757 | ver = versions_from_parentdir(parentdir_prefix, root, verbose) 758 | if ver: 759 | if verbose: print("got version from parentdir %s" % ver) 760 | return rep_by_pep440(ver) 761 | 762 | if verbose: print("got version from default %s" % default) 763 | return default 764 | 765 | def get_version(verbose=False): 766 | return get_versions(verbose=verbose)["version"] 767 | 768 | 769 | def git2pep440(ver_str): 770 | ver_parts = ver_str.split('-') 771 | tag = ver_parts[0] 772 | if len(ver_parts) == 1: 773 | return tag 774 | elif len(ver_parts) == 2: 775 | commits = 0 776 | git_hash = '' 777 | dirty = 'dirty' 778 | elif len(ver_parts) == 3: 779 | commits = ver_parts[1] 780 | git_hash = ver_parts[2] 781 | dirty='' 782 | elif len(ver_parts) == 4: 783 | commits = ver_parts[1] 784 | git_hash = ver_parts[2] 785 | dirty = '.dirty' 786 | else: 787 | raise Warning("git version string could not be parsed.") 788 | return ver_str 789 | 790 | return "{tag}.{release_type_string}{commits}+{git_hash}{dirty}".format( 791 | tag=tag, 792 | release_type_string=release_type_string, 793 | commits=commits, 794 | git_hash=git_hash, 795 | dirty=dirty) 796 | 797 | 798 | def rep_by_pep440(ver): 799 | ver["version"] = git2pep440(ver["version"]) 800 | return ver 801 | 802 | class cmd_version(Command): 803 | description = "report generated version string" 804 | user_options = [] 805 | boolean_options = [] 806 | def initialize_options(self): 807 | pass 808 | def finalize_options(self): 809 | pass 810 | def run(self): 811 | ver = get_version(verbose=True) 812 | print("Version is currently: %s" % ver) 813 | 814 | 815 | class cmd_build(_build): 816 | def run(self): 817 | versions = get_versions(verbose=True) 818 | _build.run(self) 819 | # now locate _version.py in the new build/ directory and replace it 820 | # with an updated value 821 | if versionfile_build: 822 | target_versionfile = os.path.join(self.build_lib, versionfile_build) 823 | print("UPDATING %s" % target_versionfile) 824 | os.unlink(target_versionfile) 825 | with io.open(target_versionfile, "w", encoding='utf-8') as f: 826 | f.write(SHORT_VERSION_PY % versions) 827 | 828 | if 'cx_Freeze' in sys.modules: # cx_freeze enabled? 829 | from cx_Freeze.dist import build_exe as _build_exe 830 | 831 | class cmd_build_exe(_build_exe): 832 | def run(self): 833 | versions = get_versions(verbose=True) 834 | target_versionfile = versionfile_source 835 | print("UPDATING %s" % target_versionfile) 836 | os.unlink(target_versionfile) 837 | with io.open(target_versionfile, "w", encoding='utf-8') as f: 838 | f.write(SHORT_VERSION_PY(version=__version__) % versions) 839 | 840 | _build_exe.run(self) 841 | os.unlink(target_versionfile) 842 | with io.open(versionfile_source, "w", encoding='utf-8') as f: 843 | assert VCS is not None, "please set versioneer.VCS" 844 | LONG = LONG_VERSION_PY[VCS] 845 | f.write(LONG % {"DOLLAR": "$", 846 | "TAG_PREFIX": tag_prefix, 847 | "PARENTDIR_PREFIX": parentdir_prefix, 848 | "VERSIONFILE_SOURCE": versionfile_source, 849 | "__version__": __version__ 850 | }) 851 | 852 | class cmd_sdist(_sdist): 853 | def run(self): 854 | versions = get_versions(verbose=True) 855 | self._versioneer_generated_versions = versions 856 | # unless we update this, the command will keep using the old version 857 | self.distribution.metadata.version = versions["version"] 858 | return _sdist.run(self) 859 | 860 | def make_release_tree(self, base_dir, files): 861 | _sdist.make_release_tree(self, base_dir, files) 862 | # now locate _version.py in the new base_dir directory (remembering 863 | # that it may be a hardlink) and replace it with an updated value 864 | target_versionfile = os.path.join(base_dir, versionfile_source) 865 | print("UPDATING %s" % target_versionfile) 866 | os.unlink(target_versionfile) 867 | with io.open(target_versionfile, "w", encoding='utf-8') as f: 868 | f.write(SHORT_VERSION_PY % self._versioneer_generated_versions) 869 | 870 | INIT_PY_SNIPPET = """ 871 | from ._version import get_versions 872 | __version__ = get_versions()['version'] 873 | del get_versions 874 | """ 875 | 876 | class cmd_update_files(Command): 877 | description = "install/upgrade Versioneer files: __init__.py SRC/_version.py" 878 | user_options = [] 879 | boolean_options = [] 880 | def initialize_options(self): 881 | pass 882 | def finalize_options(self): 883 | pass 884 | def run(self): 885 | print(" creating %s" % versionfile_source) 886 | with io.open(versionfile_source, "w", encoding='utf-8') as f: 887 | assert VCS is not None, "please set versioneer.VCS" 888 | LONG = LONG_VERSION_PY[VCS] 889 | f.write(LONG % {"DOLLAR": "$", 890 | "TAG_PREFIX": tag_prefix, 891 | "PARENTDIR_PREFIX": parentdir_prefix, 892 | "VERSIONFILE_SOURCE": versionfile_source, 893 | "__version__": __version__ 894 | }) 895 | 896 | ipy = os.path.join(os.path.dirname(versionfile_source), "__init__.py") 897 | if os.path.exists(ipy): 898 | try: 899 | with io.open(ipy, "r", encoding='utf-8') as f: 900 | old = f.read() 901 | except EnvironmentError: 902 | old = "" 903 | if INIT_PY_SNIPPET not in old: 904 | print(" appending to %s" % ipy) 905 | with io.open(ipy, "a", encoding='utf-8') as f: 906 | f.write(INIT_PY_SNIPPET) 907 | else: 908 | print(" %s unmodified" % ipy) 909 | else: 910 | print(" %s doesn't exist, ok" % ipy) 911 | ipy = None 912 | 913 | # Make sure both the top-level "versioneer.py" and versionfile_source 914 | # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so 915 | # they'll be copied into source distributions. Pip won't be able to 916 | # install the package without this. 917 | manifest_in = os.path.join(get_root(), "MANIFEST.in") 918 | simple_includes = set() 919 | try: 920 | with io.open(manifest_in, "r", encoding='utf-8') as f: 921 | for line in f: 922 | if line.startswith("include "): 923 | for include in line.split()[1:]: 924 | simple_includes.add(include) 925 | except EnvironmentError: 926 | pass 927 | # That doesn't cover everything MANIFEST.in can do 928 | # (http://docs.python.org/2/distutils/sourcedist.html#commands), so 929 | # it might give some false negatives. Appending redundant 'include' 930 | # lines is safe, though. 931 | if "versioneer.py" not in simple_includes: 932 | print(" appending 'versioneer.py' to MANIFEST.in") 933 | with io.open(manifest_in, "a", encoding='utf-8') as f: 934 | f.write("include versioneer.py\n") 935 | else: 936 | print(" 'versioneer.py' already in MANIFEST.in") 937 | if versionfile_source not in simple_includes: 938 | print(" appending versionfile_source ('%s') to MANIFEST.in" % 939 | versionfile_source) 940 | with io.open(manifest_in, "a", encoding='utf-8') as f: 941 | f.write("include %s\n" % versionfile_source) 942 | else: 943 | print(" versionfile_source already in MANIFEST.in") 944 | 945 | # Make VCS-specific changes. For git, this means creating/changing 946 | # .gitattributes to mark _version.py for export-time keyword 947 | # substitution. 948 | do_vcs_install(manifest_in, versionfile_source, ipy) 949 | 950 | 951 | def get_cmdclass(): 952 | cmds = {'version': cmd_version, 953 | 'versioneer': cmd_update_files, 954 | 'build': cmd_build, 955 | 'sdist': cmd_sdist, 956 | } 957 | if 'cx_Freeze' in sys.modules: # cx_freeze enabled? 958 | cmds['build_exe'] = cmd_build_exe 959 | del cmds['build'] 960 | 961 | return cmds 962 | --------------------------------------------------------------------------------