├── .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 |
--------------------------------------------------------------------------------