{ "info": { "author": "", "author_email": "", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", "Framework :: Django :: 1.11", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6" ], "description": "# django-proctor\n\n\ndjango-proctor allows you to use [Proctor](https://github.com/indeedeng/proctor) (an A/B testing framework) from Django by using [Proctor Pipet](https://github.com/indeedeng/proctor-pipet), which exposes Proctor as a simple REST API.\n\nProctor allows you to place your users into randomly-assigned test groups for A/B testing. It can also be used for feature toggles and gradual rollouts of new features.\n\nUsing Proctor group assignments from Django templates is extremely easy by checking the bucket's name:\n\n```htmldjango\n{% if proc.buttoncolortst.group == 'blue' %}\n\n{% else %}\n\n{% endif %}\n```\n\nAnd you can also use Proctor groups in Python code:\n\n```py\nif request.proc.newfeaturerollout.group == 'active':\n foo()\nelse:\n bar()\n```\n\nIf you need to use Proctor groups in a cron job or some service without a request, you can do an Account test:\n```py\nfrom proctor.identify import proc_by_accountid\n\naccountid = 999999\nif proc_by_accountid(accountid).newfeaturerollout.group == \"active\":\n foo()\nelse:\n bar()\n\n```\n\n## Configuration\n\nBefore using django-proctor, you need to set up [Proctor Pipet](https://github.com/indeedeng/proctor-pipet). This is the REST API that django-proctor communicates with to obtain test group assignments.\n\nYou'll also need a test matrix, which defines all Proctor tests and their current allocations. [Proctor Webapp](https://github.com/indeedeng/proctor-webapp) provides a way to view and modify the test matrix.\n\n### Requirements\n\nTo use django-proctor, just install it with pip:\n\n```bash\n$ pip install django-proctor\n```\n\nOr add it to your `requirements.txt` file (preferably with the current version).\n\n### Views\n\nThere are a set of private views that are available for testing and debugging. Enabling these views requires some extra configuration.\n\n#### Installed Apps\n\nAdd `proctor` to your project's list of installed apps.\n\n```py\nINSTALLED_APPS += (\n ...\n proctor,\n ...\n)\n```\n#### Urls\n\nImport the proctor urls into a private space in your project.\n\n```py\nurlpatterns = [\n ...\n url(r'^private/', include('proctor.urls'))\n ...\n]\n```\n\n##### ShowTestMatrix\n\nThe ShowTestMatrix view allows you to see the entire test matrix for your specific PROCTOR_TESTS. This view is simply the json version of the test matrix filtered to your tests.\n\n\nUsing the above url pattern example, the test matrix would be available at `http:///private/proctor/show` and `http:///private/showTestMatrix`. The latter is for backwards compatibility with other projects.\n\n##### Force Groups\n\nThe Force Groups view allows you to see what the current group assignments are for your session and identification and force yourself into a specific group for any test.\n\n**NOTE**: This template does come with a default base template, but it can be overidden. To override the default base template, you must have a base template to extend and the name of that template file should be set in PROCTOR_BASE_TEMPLATE.\n\nUsing the above url pattern example, the force groups page would be available at `http:///private/proctor/force`.\n\n### Middleware\n\ndjango-proctor does most of its processing in a middleware. This runs before your views and adds `proc` to the `request` object, which lets you access test group assignments.\n\nYou must subclass `proctor.middleware.BaseProctorMiddleware` and override several functions to provide django-proctor with information it needs to handle group assignments.\n\n```py\nclass MyProctorMiddleware(proctor.middleware.BaseProctorMiddleware):\n ...\n```\n\n#### get_identifiers()\n\nIdentifiers are strings that identify users of your site uniquely. These can include tracking cookies and account ids. Proctor uses this information to keep users in the same test groups across requests.\n\nThis returns a dict of identifier source keys (see Pipet configuration) to their values.\n\nIf a user lacks a certain identifier, don't include it in the dict. Proctor will skip any tests using that identifier. However, make sure you always return at least one identifier like a tracking cookie.\n\nYou **must** override this method.\n\nThis method is always run after any previous middleware.\n\n```py\ndef get_identifiers(self, request):\n ids = {'USER': request.COOKIES.get('tracking')}\n\n if request.user.is_authenticated():\n ids['acctid'] = request.user.id\n\n return ids\n```\n\n#### get_context()\n\nContext variables are properties about the user that are used in Proctor test rules to selectively enable tests or change the allocations of a test depending on whether an expression is true. This can be used to run a test only on Firefox, or you can run a test at 50% for US users and 10% for everyone else.\n\nThis returns a dict of context variable source keys (see Pipet configuration) to their values, which are converted by Pipet to their final types before rule expressions are evaluated.\n\nIf you don't override this method, django-proctor uses no context variables.\n\nIf the Pipet configuration doesn't have a default value for a context variable, it must be included on every API request. If that is the case, make sure that context variable appears in this return value.\n\nThis method is always run after any previous middleware.\n\n```py\ndef get_context(self, request):\n return {\"ua\": request.META.get('HTTP_USER_AGENT', ''),\n \"loggedIn\": request.user.is_authenticated(),\n \"country\": geo.country(request.get_host()),\n }\n```\n\n#### is_privileged()\n\nReturns a bool indicating whether the request is allowed to use the `prforceGroups` query parameter to force themselves into Proctor groups.\n\nIf you don't override this method, it returns False, which effectively disables force groups.\n\nUse something like IP address or admin account to determine whether the user can force themselves into test groups.\n\nThis method **may** run without hitting any of the previous middleware. You must assume anything that may have been done in a middleware, like adding a `user` attribute to `request`, may not have happened. Otherwise, you might get exceptions in strange conditions like redirects.\n\n```py\ndef is_privileged(self, request):\n return (request.get_host().startswith('127.0.0.1') or\n request.get_host().startswith('192.168.') or\n (hasattr(request, 'user') and request.user.is_staff)\n )\n\n```\n\n#### get_http()\n\nReturns an instance of `requests.Session` (or equivalent) that will be used when making HTTP requests to the API.\n\nIf you don't override this method, it returns None, which will cause the api to use the `requests` module.\n\n### settings.py\n\nYou must set several things in your `settings.py` for django-proctor to work properly:\n\n#### MIDDLEWARE_CLASSES\n\nAdd the middleware you created to `MIDDLEWARE_CLASSES`. Make sure you place it after any middleware it depends on like `AuthenticationMiddleware`.\n\n#### TEMPLATE_CONTEXT_PROCESSORS\n\nAdd `proctor.context_processors.proc` to `TEMPLATE_CONTEXT_PROCESSORS`. This makes the `proc` object available in all of your Django templates.\n\n#### PROCTOR_API_ROOT\n\nSet `PROCTOR_API_ROOT` to the URL at which Proctor Pipet is running.\n\nInclude `http://` or `https://`. Do not include a trailing slash.\n\nIf you want production to use a different test matrix than your staging server and your developer machines, then you may want to use a different Pipet instance depending on environment.\n\n```py\nPROCTOR_API_ROOT = \"http://pipet.example.com\"\n```\n\n#### PROCTOR_TESTS\n\n`PROCTOR_TESTS` is a tuple or list of the Proctor tests that your Django project intends to use.\n\nAdd tests to this tuple before implementing them in your templates and code, and remove tests from this tuple after removing their implementations.\n\nAll tests listed here are guaranteed to exist on the `proc` object.\n\nThe tests listed here will also be in `str(proc)` (for logging test groups) if they have non-negative group values.\n\n```py\nPROCTOR_TESTS = (\n 'buttoncolortst',\n 'newfeaturerollout',\n)\n```\n\nIf you're only using one test, make sure you include a comma in the tuple. Otherwise, Python interprets it as just a string.\n\n```py\nPROCTOR_TESTS = ('buttoncolortst',)\n```\n\n#### PROCTOR_BASE_TEMPLATE\n\nSet `PROCTOR_BASE_TEMPLATE` to the name of the base html template being used in your project.\n\n```py\nPROCTOR_BASE_TEMPLATE = \"base.html\"\n```\n\n#### PROCTOR_CACHE_METHOD\n\nYou can optionally have django-proctor cache group assignments from the Proctor Pipet REST API. Ordinarily, every HTTP request that hits Django will trigger the Proctor middleware to make an HTTP request to Pipet. You can use caching to avoid this extra cost since group assignments typically stay the same.\n\nIf the cache has no useful value (like when a new user visits your site), then django-proctor falls back to making an HTTP request to Proctor Pipet.\n\nIf `PROCTOR_CACHE_METHOD` is missing or None, django-proctor will not do any caching.\n\nIf `PROCTOR_CACHE_METHOD` is `'cache'`, django-proctor uses [Django's cache framework](https://docs.djangoproject.com/en/dev/topics/cache/) for caching group assignments. See `PROCTOR_CACHE_NAME`.\n\nIf `PROCTOR_CACHE_METHOD` is `'session'`, django-proctor caches group assignments in the `request.session` dict. This is a decent option if all of your HTTP requests get or set [Django's session object](https://docs.djangoproject.com/en/dev/topics/http/sessions/) anyway.\n\n##### Cache Invalidation\n\ndjango-proctor's cache invalidation is fairly smart and will not use the cache if some property of the user's request has changed, like the identifiers, context variables, or the `prforceGroups` parameter. The cache will also be ignored if you change a setting like `PROCTOR_API_ROOT` or `PROCTOR_TESTS`.\n\nThis means that if a user logs in, or a user changes their useragent, or you add a new test to `PROCTOR_TESTS`, the cached value will be skipped. You don't have to worry about outdated values.\n\n**TODO: Explain matrix version detection and caching after that invalid cache issue is resolved. Current cache implementation does not work properly with multiple processes.**\n\n#### PROCTOR_CACHE_NAME\n\nThis setting is only meaningful if `PROCTOR_CACHE_METHOD` is `'cache'`.\n\n`PROCTOR_CACHE_NAME` is the name of a cache in `CACHES` that django-proctor will use.\n\nIf `PROCTOR_CACHE_NAME` is missing or None, django-proctor uses the `default` cache.\n\n#### PROCTOR_LAZY\n\nIf `PROCTOR_LAZY` is `True`, then the `proc` object lazily loads its groups. Proctor group assignments are only retrieved from either the cache or the Proctor Pipet REST API on first access of the `proc` object.\n\nThis means that HTTP requests to your Django server that never use the `proc` object don't incur the cost of getting group assignments.\n\nWhen measuring performance, remember that this option may move the timing of a cache access and REST API request to an unexpected place (like during template rendering).\n\nIf `PROCTOR_LAZY` is missing or `False`, lazy loading will not be used.\n\n## Usage\n\nThe Proctor middleware adds a `proc` object to `request`, which allows you to easily use Proctor group assignments from any view.\n\nGroup assignments can be accessed using the dot operator on `proc`. Every test listed in `PROCTOR_TESTS` is guaranteed to exist as an attribute on `proc`.\n\n```py\nprint request.proc.buttoncolortst\n# -> GroupAssignment(group=u'blue', value=1, payload=u'#2B60DE')\n\nprint request.proc.testnotinsettings\n# throws AttributeError\n```\n\nEach group assignment has three attributes:\n\n* **group**: the assigned test group name (str)\n* **value**: the assigned bucket value (int)\n * -1 typically means inactive, and 0 typically means control.\n* **payload**: the assigned test group payload value\n * Used to change test-specific values from Proctor instead of in code.\n * Is `None` if the test has no payload.\n * The payload type can be a str, long, double, or a list of one of those.\n\nIf Proctor did not give an assignment for a test, then that test is unassigned. In that case: group, value, and payload are all `None`.\n\n```py\nprint request.proc.buttoncolortst\n# -> GroupAssignment(group=None, value=None, payload=None)\n```\n\nThis can happen if an eligibility rule was not met, if there was no matching identifier for the test type, if the test was in `PROCTOR_TESTS` but not in the test matrix, or if django-proctor could not connect to Pipet (or got back an HTTP error) and set all assignments to unassigned by default.\n\n### Switching\n\nYou can use Proctor group assignments to implement different behavior on your site based on the user's assigned test group.\n\nSuppose we have a test called \"algorithmtst\" in our test matrix with four test groups: `'inactive'`, `'control'`, `'bogo'`, and `'quick'`:\n\n```py\nif request.proc.algorithmtst.group == 'bogo':\n bogosort()\nelif request.proc.algorithmtst.group == 'quick':\n quicksort()\nelse:\n # 'control', 'inactive', and None (all default to our old sorting algorithm)\n # Because this covers None (unassigned), this will also be used in case of error.\n oldsort()\n```\n\nUsually your `'control'`, `'inactive'`, and `None` groups will have the same behavior, which is doing whatever your site did before you added this test or feature. It's convenient to have the else branch cover all of these groups.\n\nEnsure that your branches always cover the case that group is `None`. This ensures that if your Proctor Pipet instance goes down or starts returning an HTTP error due to some misconfiguration, your site will simply fall back to default behavior.\n\n### Templates\n\nProctor can be used from Django templates as well if you properly set up `TEMPLATE_CONTEXT_PROCESSORS`.\n\nYour templates have the `proc` object in their context, allowing you to switch behavior based on Proctor groups:\n\n```htmldjango\n{% if proc.buttoncolortst.group == 'blue' %}\n\n{% elif proc.buttoncolortst.group == 'green' %}\n\n{% else %}\n\n{% endif %}\n```\n\n```htmldjango\n{% if proc.newfeaturerollout.group == 'active' %}\n\n{% endif %}\n```\n\n### Payloads\n\nPayloads allow you to specify test-specific values in the test matrix instead of in your code. This allows you to try many different variations without touching Django or even redeploying your application.\n\nHere is an example for button text on a call to action:\n\n```htmldjango\n\n```\n\nRemember that payload can be `None` in many cases, including if Proctor Pipet goes down. Include a [default_if_none](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#default-if-none) filter to ensure rational default behavior if this happens.\n\nPayloads can also be used from your views:\n\n```py\nalgorithm_constants = request.proc.algoconsttst.payload\nif algorithm_constants is None:\n algorithm_constants = [2, 3, 42]\n\nsearch_ranking(algorithm_constants)\n```\n\n#### Payload Arrays\n\n`payload` is `None` when a test group is unassigned, so any attribute accesses are still an error.\n\nIn Python code, make sure you check for `None` before accessing attributes on the payload.\n\nIn Django template output tags, invalid attribute accesses are interpreted as the `TEMPLATE_STRING_IF_INVALID` setting, which is the blank string by default. You can use the [default](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#default) template tag to cover these instances, but be aware that this will also match on empty arrays and other falsey values:\n\n```htmldjango\n

{{ proc.headertexttst.payload.0|default:\"Default Title\" }}

\n

{{ proc.headertexttst.payload.1|default:\"Default description text.\" }}

\n```\n\nSee [How invalid variables are handled](https://docs.djangoproject.com/en/dev/ref/templates/api/#invalid-template-variables) in the Django template documentation for more details.\n\n### JavaScript\n\nIf you want to use Proctor test group assignments from browser-side JavaScript, you'll have to provide the values you want to use to your JavaScript through Django's templating language.\n\nA simple way to do this is to define global JavaScript variables in a script tag in your HTML template with the values your code will use:\n\n```htmldjango\n\n```\n\nFor strings, wrap the template output tag in quotes. Use the [escapejs](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#escapejs) filter so that special characters like quotes and angle brackets are correctly placed into your JavaScript.\n\nSome people place script tags like this in a template block like \"js\" so that these special values appear in a consistent place alongside other script includes.\n\nYou can use these global variables in your JavaScript static files to implement your tests:\n\n```js\n$(function() {\n if (buttoncolortstgroup === \"blue\") {\n $(\".buttonone\").css(\"background\", \"#00f\");\n } else if (buttoncolortstgroup === \"green\") {\n $(\".buttonone\").css(\"background\", \"#0f0\");\n } else {\n $(\".buttonone\").css(\"background\", \"#888\");\n }\n\n if (usenewfeature) {\n $(\".buttontwo\").show();\n }\n\n $(\".buttonthree\").text(buttontext);\n});\n```\n\nThis is just one way of accessing Proctor test groups from the browser. Use whatever makes the most sense for your project.\n\nAnother way is templating your JavaScript directly by placing your code in HTML and mixing your Django template tags with JavaScript code. You could even template your .js files instead of serving them statically. However, these two alternatives can be messy and are not best practices.\n\nIf your application is complex enough, you could even consider making a Django view that returns some test groups or payloads and have your JavaScript make an AJAX request to get them.\n\n### Logging\n\nTo compare metrics between two different test groups, you can log each request's assigned test groups in addition to any metrics you want to track.\n\ndjango-proctor provides a simple comma-separated representation of all the Proctor test groups that the user is in for logging purposes:\n\n```py\nprint str(request.proc)\n# -> \"buttoncolortst1,countryalgotst0,newfeaturerollout0\"\n```\n\nThis output only includes non-negative test groups, as -1 typically means inactive groups that should not be logged.\n\nThe `proc` object also has a method to obtain the list of test groups before joining with a comma:\n\n```py\nprint request.proc.get_group_string_list()\n# -> ['buttoncolortst1', 'countryalgotst0', 'newfeaturerollout0']\n```\n\n### prforceGroups\n\nTo test the implementation of your test group behavior, privileged users can attach a `prforceGroups` query parameter to their site's URL to force themselves into certain test groups:\n\n```\nhttp://django.example.com/?prforceGroups=buttoncolortst2,countryalgotst0\n```\n\nThe format is simply the test name followed by the bucket value (not the name), with all test groups separated by commas.\n\nThe value of `prforceGroups` is set as a session cookie. Your browser will be forced into those groups until your browser is closed. You can also set an empty prforceGroups to clear the cookie:\n\n```\nhttp://django.example.com/?prforceGroups=\n```\n\nThe tests and bucket values specified in `prforceGroups` must exist in the Proctor test matrix.\n\n## Using Proctor from Other Python Frameworks\n\ndjango-proctor was designed primarily for Django as that is the framework that we (the Indeed Labs team) primarily use.\n\nHowever, these modules would be usable in other Python frameworks with some minor modifications:\n\n`api`, `cache*`, `groups`, `identify`, `lazy`\n\ncache unfortunately has some Django mixed in for some of its subclasses. It imports django.core.cache, it uses Django in subclasses, and the abstract Cacher interface takes `request` as a parameter (because `SessionCacher` needs it, but it can safely be None for all other subclasses).\n\nAlso, identify.py imports from django settings for similar reasons when looking up account details.\n\nIf this is a significant problem for you, ask us to split this into two packages: one for Python, and one for Django that has the former as a dependency. Or contribute a solution that splits the packages up.\n\nWhen implementing Proctor in other frameworks, use `middleware.py` to see how we implemented this for Django. We handle providing context variables and identifiers through subclassing. Other implementations could register functions (through decorators or otherwise) to provide these details. Also, note how the prforceGroups query parameter and cookie is handled.\n\n\n## Testing\n\nThis project uses [`tox` for executing tests](https://tox.readthedocs.io/en/latest/). To run tests\nlocally, cd into your project and run\n\n $ tox\n\nUnderneath the hood, `tox` is just running\n[`pytest` for test discovery and execution](https://docs.pytest.org/en/latest/). Test arguments can\nbe passed through into `pytest` by adding `--` after your `tox` command. For example, you can\nisolate a test file or test method using the following:\n\n $ tox -- proctor/tests/test_identify.py\n $ tox -- proctor/tests/test_identify.py::TestIdentifyGroups::test_requested_group_resolved\n\nwhere `pytest` uses a double colon as a test class/method/function separator\n\nBy default, `pytest` captures output, which prevents debugging with breakpoints. If you need to\ndebug the tests, you can run either of the following:\n\n $ tox -- --capture=no\n $ tox -- -s\n\nYou can then add a break point to a test by adding the following to your python code:\n\n```python\nimport pdb; pdb.set_trace()\n```\n\n\n## See Also\n\n* [Proctor Pipet](https://github.com/indeedeng/proctor-pipet)\n\n* [Proctor Documentation](http://indeedeng.github.io/proctor/)\n\n* [Proctor Github Repo](https://github.com/indeedeng/proctor)\n\n* [Proctor Webapp](https://github.com/indeedeng/proctor-webapp) for editing the test matrix.\n\n* [indeedeng-proctor-users](https://groups.google.com/forum/#!forum/indeedeng-proctor-users) for questions and comments.\n\n* [Proctor Blog Post](http://engineering.indeed.com/blog/2014/06/proctor-a-b-testing-framework/) on the Indeed Engineering blog.\n\n* [Proctor Tech Talk](http://engineering.indeed.com/talks/managing-experiments-behavior-dynamically-proctor/)\n\n\n", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/indeedeng/django-proctor", "keywords": "", "license": "Apache", "maintainer": "", "maintainer_email": "", "name": "django-proctor", "package_url": "https://pypi.org/project/django-proctor/", "platform": "", "project_url": "https://pypi.org/project/django-proctor/", "project_urls": { "Homepage": "https://github.com/indeedeng/django-proctor" }, "release_url": "https://pypi.org/project/django-proctor/1.0.4/", "requires_dist": [ "Django", "ndg-httpsclient", "pyOpenSSL", "requests" ], "requires_python": "", "summary": "Django library for interacting with the Proctor A/B testing framework", "version": "1.0.4" }, "last_serial": 5368494, "releases": { "0.0.8": [ { "comment_text": "", "digests": { "md5": "fc855083ce68c2b9e35fd90a530547bf", "sha256": "7908e0e7b1b80e587b90ea00a2c144bdaf8517053a32417996229d3dd962193b" }, "downloads": -1, "filename": "django_proctor-0.0.8-py2-none-any.whl", "has_sig": false, "md5_digest": "fc855083ce68c2b9e35fd90a530547bf", "packagetype": "bdist_wheel", "python_version": "py2", "requires_python": null, "size": 1106, "upload_time": "2018-07-26T15:33:43", "url": "https://files.pythonhosted.org/packages/ed/34/a5160122c1c35a1c73562d8beafd8611183c82e0ff26ce9b24aa8b495e0e/django_proctor-0.0.8-py2-none-any.whl" }, { "comment_text": "", "digests": { "md5": "d267d820e652ecb6799e96acb9551ab9", "sha256": "75d4cb0283c845a07e8cfbd633a09ce118b31e7a882312e6818ffc3bfa122052" }, "downloads": -1, "filename": "django-proctor-0.0.8.tar.gz", "has_sig": false, "md5_digest": "d267d820e652ecb6799e96acb9551ab9", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 696, "upload_time": "2018-07-26T15:33:44", "url": "https://files.pythonhosted.org/packages/65/bf/8f511cc212a56150c23de3494c4686ee9e7cb42ff4e35e223c7d431229e1/django-proctor-0.0.8.tar.gz" } ], "1.0.3": [ { "comment_text": "", "digests": { "md5": "6c0d807109392a1ddd4e846c5ffd3c61", "sha256": "281a6b1dc72e7b92ccdcb13f4d8968df26dc7af15c8db97967636498dfe65cd8" }, "downloads": -1, "filename": "django_proctor-1.0.3-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "6c0d807109392a1ddd4e846c5ffd3c61", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 38917, "upload_time": "2019-06-06T17:00:53", "url": "https://files.pythonhosted.org/packages/55/6b/78add5f8cff114424a007fee99a3cff93fa89a49c52aed217e0ed53b2e48/django_proctor-1.0.3-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "83259e7dd8457d3e40589cf6c3354a8c", "sha256": "aec82871c634f7cf7785ed3158d2fbefee345d4bd436e74de69abed80b0d583b" }, "downloads": -1, "filename": "django-proctor-1.0.3.tar.gz", "has_sig": false, "md5_digest": "83259e7dd8457d3e40589cf6c3354a8c", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 34262, "upload_time": "2019-06-06T17:00:54", "url": "https://files.pythonhosted.org/packages/df/a3/cc96e2d544b02067b84af7645d70c391a228932ac2b09600f12a99e9043e/django-proctor-1.0.3.tar.gz" } ], "1.0.3rc1": [ { "comment_text": "", "digests": { "md5": "336d8e6c13a16e82a305db01c79aa05b", "sha256": "ff900282d561fb56cf8b90c5a73dadc9ce52831f7d0f83dc33bcdd0cb4b40fb5" }, "downloads": -1, "filename": "django_proctor-1.0.3rc1-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "336d8e6c13a16e82a305db01c79aa05b", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 38955, "upload_time": "2019-06-06T16:46:52", "url": "https://files.pythonhosted.org/packages/02/ae/faac7906a825fc0ea49dc3cebf427375d5957c34a807884aaa2c80ee5dec/django_proctor-1.0.3rc1-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "c84c56e5833722519d8884c1ea86ea88", "sha256": "a0d2e1744054e070d5775a39913b257d5c906e9a15d28d3110de12582aafa3f5" }, "downloads": -1, "filename": "django-proctor-1.0.3rc1.tar.gz", "has_sig": false, "md5_digest": "c84c56e5833722519d8884c1ea86ea88", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 34264, "upload_time": "2019-06-06T16:46:54", "url": "https://files.pythonhosted.org/packages/91/7e/cb69e32e594716da607fad7ceffae812e5181d45189b6b4b84b7c852bbc5/django-proctor-1.0.3rc1.tar.gz" } ], "1.0.4": [ { "comment_text": "", "digests": { "md5": "91c11949cc464a80dc425c40582bfeb5", "sha256": "1413d313b2174ea782b62cf4a7588358c3036d487770257c97768f2649e9a2d9" }, "downloads": -1, "filename": "django_proctor-1.0.4-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "91c11949cc464a80dc425c40582bfeb5", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 38947, "upload_time": "2019-06-06T18:33:33", "url": "https://files.pythonhosted.org/packages/8e/d8/36cdb5b670033d854f6b898184452dc57c56bfa855e26089769fa091d85c/django_proctor-1.0.4-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "4af56628cf0bf47266a7bffa9823b1c7", "sha256": "3e8f649acbfef6a6e46cdfeafc00aea064f21ca44978a8dc60988c6d0ab6105f" }, "downloads": -1, "filename": "django-proctor-1.0.4.tar.gz", "has_sig": false, "md5_digest": "4af56628cf0bf47266a7bffa9823b1c7", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 34272, "upload_time": "2019-06-06T18:33:35", "url": "https://files.pythonhosted.org/packages/1c/16/1debec3cbdda322486e1775fff9c80dbafd9dbb54c9cb16185ff590cd9b1/django-proctor-1.0.4.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "91c11949cc464a80dc425c40582bfeb5", "sha256": "1413d313b2174ea782b62cf4a7588358c3036d487770257c97768f2649e9a2d9" }, "downloads": -1, "filename": "django_proctor-1.0.4-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "91c11949cc464a80dc425c40582bfeb5", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 38947, "upload_time": "2019-06-06T18:33:33", "url": "https://files.pythonhosted.org/packages/8e/d8/36cdb5b670033d854f6b898184452dc57c56bfa855e26089769fa091d85c/django_proctor-1.0.4-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "4af56628cf0bf47266a7bffa9823b1c7", "sha256": "3e8f649acbfef6a6e46cdfeafc00aea064f21ca44978a8dc60988c6d0ab6105f" }, "downloads": -1, "filename": "django-proctor-1.0.4.tar.gz", "has_sig": false, "md5_digest": "4af56628cf0bf47266a7bffa9823b1c7", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 34272, "upload_time": "2019-06-06T18:33:35", "url": "https://files.pythonhosted.org/packages/1c/16/1debec3cbdda322486e1775fff9c80dbafd9dbb54c9cb16185ff590cd9b1/django-proctor-1.0.4.tar.gz" } ] }