{"id":30240406,"url":"https://github.com/hypothesis/pyramid-sanity","last_synced_at":"2026-03-15T22:37:29.320Z","repository":{"id":37796989,"uuid":"289340100","full_name":"hypothesis/pyramid-sanity","owner":"hypothesis","description":"Sensible defaults to catch bad behavior","archived":false,"fork":false,"pushed_at":"2025-09-10T08:51:34.000Z","size":98,"stargazers_count":1,"open_issues_count":1,"forks_count":1,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-11-27T19:28:31.525Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-2-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hypothesis.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2020-08-21T18:36:19.000Z","updated_at":"2025-09-10T08:51:35.000Z","dependencies_parsed_at":"2023-02-13T21:35:17.041Z","dependency_job_id":"d3615352-7f92-4527-b6aa-f63a8474149a","html_url":"https://github.com/hypothesis/pyramid-sanity","commit_stats":{"total_commits":42,"total_committers":5,"mean_commits":8.4,"dds":"0.45238095238095233","last_synced_commit":"12e80200133b8b13be01cccda9a38316586cd2f7"},"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/hypothesis/pyramid-sanity","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hypothesis%2Fpyramid-sanity","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hypothesis%2Fpyramid-sanity/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hypothesis%2Fpyramid-sanity/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hypothesis%2Fpyramid-sanity/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hypothesis","download_url":"https://codeload.github.com/hypothesis/pyramid-sanity/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hypothesis%2Fpyramid-sanity/sbom","scorecard":{"id":75490,"data":{"date":"2025-08-11","repo":{"name":"github.com/hypothesis/pyramid-sanity","commit":"2d950b8dac57e0aeecf9bee038ff8dd6e2c30ef7"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":4.5,"checks":[{"name":"Code-Review","score":6,"reason":"Found 3/5 approved changesets -- score normalized to 6","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Maintained","score":1,"reason":"2 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 1","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/ci.yml:1","Warn: no topLevel permission defined: .github/workflows/keepalive.yml:1","Warn: no topLevel permission defined: .github/workflows/pypi.yml:1","Warn: no topLevel permission defined: .github/workflows/slack.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:21: update your workflow using https://app.stepsecurity.io/secureworkflow/hypothesis/pyramid-sanity/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:23: update your workflow using https://app.stepsecurity.io/secureworkflow/hypothesis/pyramid-sanity/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:31: update your workflow using https://app.stepsecurity.io/secureworkflow/hypothesis/pyramid-sanity/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:33: update your workflow using https://app.stepsecurity.io/secureworkflow/hypothesis/pyramid-sanity/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:41: update your workflow using https://app.stepsecurity.io/secureworkflow/hypothesis/pyramid-sanity/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:43: update your workflow using https://app.stepsecurity.io/secureworkflow/hypothesis/pyramid-sanity/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:55: update your workflow using https://app.stepsecurity.io/secureworkflow/hypothesis/pyramid-sanity/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:57: update your workflow using https://app.stepsecurity.io/secureworkflow/hypothesis/pyramid-sanity/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:65: update your workflow using https://app.stepsecurity.io/secureworkflow/hypothesis/pyramid-sanity/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:74: update your workflow using https://app.stepsecurity.io/secureworkflow/hypothesis/pyramid-sanity/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:76: update your workflow using https://app.stepsecurity.io/secureworkflow/hypothesis/pyramid-sanity/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:80: update your workflow using https://app.stepsecurity.io/secureworkflow/hypothesis/pyramid-sanity/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:93: update your workflow using https://app.stepsecurity.io/secureworkflow/hypothesis/pyramid-sanity/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:95: update your workflow using https://app.stepsecurity.io/secureworkflow/hypothesis/pyramid-sanity/ci.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/keepalive.yml:23: update your workflow using https://app.stepsecurity.io/secureworkflow/hypothesis/pyramid-sanity/keepalive.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/pypi.yml:7: update your workflow using https://app.stepsecurity.io/secureworkflow/hypothesis/pyramid-sanity/pypi.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/slack.yml:13: update your workflow using https://app.stepsecurity.io/secureworkflow/hypothesis/pyramid-sanity/slack.yml/main?enable=pin","Warn: pipCommand not pinned by hash: .github/workflows/ci.yml:27","Warn: pipCommand not pinned by hash: .github/workflows/ci.yml:37","Warn: pipCommand not pinned by hash: .github/workflows/ci.yml:47","Warn: pipCommand not pinned by hash: .github/workflows/ci.yml:61","Warn: pipCommand not pinned by hash: .github/workflows/ci.yml:85","Warn: pipCommand not pinned by hash: .github/workflows/ci.yml:99","Info:   0 out of  14 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   3 third-party GitHubAction dependencies pinned","Info:   0 out of   6 pipCommand dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: BSD 2-Clause \"Simplified\" License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 30 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-15T04:42:38.704Z","repository_id":37796989,"created_at":"2025-08-15T04:42:38.704Z","updated_at":"2025-08-15T04:42:38.704Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30553216,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-15T15:03:43.933Z","status":"ssl_error","status_checked_at":"2026-03-15T15:03:37.630Z","response_time":61,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-08-15T04:38:25.152Z","updated_at":"2026-03-15T22:37:29.291Z","avatar_url":"https://github.com/hypothesis.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ca href=\"https://github.com/hypothesis/pyramid-sanity/actions/workflows/ci.yml?query=branch%3Amain\"\u003e\u003cimg src=\"https://img.shields.io/github/actions/workflow/status/hypothesis/pyramid-sanity/ci.yml?branch=main\"\u003e\u003c/a\u003e\n\u003ca href=\"https://pypi.org/project/pyramid-sanity\"\u003e\u003cimg src=\"https://img.shields.io/pypi/v/pyramid-sanity\"\u003e\u003c/a\u003e\n\u003ca\u003e\u003cimg src=\"https://img.shields.io/badge/python-3.12 | 3.11 | 3.10 | 3.9-success\"\u003e\u003c/a\u003e\n\u003ca href=\"https://github.com/hypothesis/pyramid-sanity/blob/main/LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/badge/license-BSD--2--Clause-success\"\u003e\u003c/a\u003e\n\u003ca href=\"https://github.com/hypothesis/cookiecutters/tree/main/pypackage\"\u003e\u003cimg src=\"https://img.shields.io/badge/cookiecutter-pypackage-success\"\u003e\u003c/a\u003e\n\u003ca href=\"https://black.readthedocs.io/en/stable/\"\u003e\u003cimg src=\"https://img.shields.io/badge/code%20style-black-000000\"\u003e\u003c/a\u003e\n\n# pyramid-sanity\n\nSensible defaults to catch bad behavior.\n\n`pyramid-sanity` is a Pyramid extension that catches certain crashes caused by\nbadly formed requests, turning them into `400: Bad Request` responses instead.\n\nIt also prevents apps from returning HTTP redirects with badly encoded locations\nthat can crash WSGI servers.\n\nThe aim is to have sensible defaults to make it easier to write a reliable Pyramid app.\n\nFor details of all the errors and fixes, and how to reproduce them see: [Error details](#error-details).\n\nUsage\n-----\n\n```python\nwith Configurator() as config:\n    config.add_settings({\n        # See the section below for all settings...        \n        \"pyramid_sanity.check_form\": False,\n    })\n    \n    # Add this as near to the end of your config as possible:\n    config.include(\"pyramid_sanity\")\n```\n\nBy default all fixes are enabled. You can disable them individually with settings:\n\n```python\nconfig.add_settings({\n    # Don't check for badly declared forms.\n    \"pyramid_sanity.check_form\": False\n})\n```\n\nYou can set `pyramid_sanity.disable_all` to `True` to disable all of the fixes,\nthen enable only certain fixes one by one:\n\n```python\nconfig.add_settings({\n    # Disable all fixes.\n    \"pyramid_sanity.disable_all\": True,\n\n    # Enable only the badly encoded query params fix.\n    \"pyramid_sanity.check_params\": True\n})\n```\n\nOptions\n-------\n\n| Option | Default | Effect |\n|--------|---------|--------|\n| `pyramid_sanity.disable_all` | `False` | Disable all checks by default\n| `pyramid_sanity.check_form` | `True` | Check for badly declared forms\n| `pyramid_sanity.check_params` | `True` | Check for badly encoded query params\n| `pyramid_sanity.check_path` | `True` | Check for badly encoded URL paths\n| `pyramid_sanity.ascii_safe_redirects` | `True` | Safely encode redirect locations\n\nExceptions\n----------\n\nAll exceptions returned by `pyramid-sanity` are subclasses of\n`pyramid_sanity.exceptions.SanityException`, which is a subclass of\n`pyramid.httpexceptions.HTTPBadRequest`.\n\nThis means all `pyramid-sanity` exceptions trigger `400: Bad Request` responses.\n\nDifferent exception subclasses are returned for different problems, so you can\nregister [custom exception views](https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/views.html#custom-exception-views)\nto handle them if you want:\n\n| Exception                                      | Returned for                    |\n|------------------------------------------------|---------------------------------|\n| `pyramid_sanity.exceptions.InvalidQueryString` | Badly encoded query params      |\n| `pyramid_sanity.exceptions.InvalidFormData`    | Bad form posts                  |\n| `pyramid_sanity.exceptions.InvalidURL`         | Badly encoded URL paths         |\n\nTween ordering\n--------------\n\n`pyramid-sanity` uses a number of Pyramid [tweens](https://docs.pylonsproject.org/projects/pyramid/en/latest/glossary.html#term-tween)\nto do its work. It's important that your app's tween chain has:\n\n * Our tweens that check for errors in the request, first\n * Our tweens that check for errors in the output of your app, last\n\nThe easiest way to achieve this is to include `config.include(\"pyramid_sanity\")`\n**as late as possible** in your config. This uses Pyramid's\n[\"best effort\" implicit tween ordering](https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/hooks.html#suggesting-implicit-tween-ordering)\nto add the tweens and should work as long as your app doesn't add any\nmore tweens, or include any extensions that add tweens, afterwards.\n\nYou can to check the order of tweens in your app with Pyramid's\n[`ptweens` command](https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/commandline.html#displaying-tweens).\nAs long as there are no tweens which access `request.GET` or `request.POST`\nabove the input checking tweens, or generate redirects below output checking\ntweens, you should be fine.\n\nYou can force the order with Pyramid's\n[explicit tween ordering](https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/hooks.html#explicit-tween-ordering)\nif you need to.\n\n### Tweens that raise non-ASCII redirects\n\n`pyramid-sanity` protects against non-ASCII redirects raised by your app's\nviews by safely encoding them, but it can't protect against _other tweens_ that\n_raise_ non-ASCII redirects.\n\nFor example this tween might cause a WSGI server (like Gunicorn) that's serving\nyour app to crash with `UnicodeEncodeError`:\n\n```python\ndef non_ascii_redirecting_tween_factory(handler, registry):\n    def non_ascii_redirecting_tween(request):\n        from pyramid.httpexceptions import HTTPFound\n        raise HTTPFound(location=\"http://example.com/€/☃\")\n    return non_ascii_redirecting_tween\n```\n\nYou'll just have to make sure that your app doesn't have any tweens that do this!\nTweens should encode any redirect locations that they generate,\n[like this](https://github.com/hypothesis/pyramid-sanity/blob/d8492620225ec6be0ba28b3eb49d329ef1e11dc2/src/pyramid_sanity/_egress.py#L22-L30).\n\nError details\n-------------\n\nIf you would like to reproduce the errors an [example app](#addendum-example-application) is given at the end\nof this section. All of the presented `curl` commands work with this app.\n\n### Badly encoded query parameters makes `request.GET` crash\n\n```terminal\ncurl 'http://localhost:6543/foo?q=%FC'\n```\n\n**By default**\n\nWebOb raises `UnicodeDecodeError`. As there is no built-in exception view for\nthis exception the app crashes.\n\n**With `pyramid-sanity`**\n\nA `pyramid_sanity.exceptions.InvalidQueryString` is returned which results in a\n`400: Bad Request` response.\n\nRelated issues:\n\n* https://github.com/Pylons/pyramid/issues/3399\n* https://github.com/Pylons/webob/issues/161\n\n### A badly encoded path can cause a crash\n\n```terminal\ncurl 'http://localhost:6543/%FC'\n```\n\n**By default**\n\nPyramid raises [`pyramid.exceptions.URLDecodeError`](https://docs.pylonsproject.org/projects/pyramid/en/latest/api/exceptions.html#pyramid.exceptions.URLDecodeError).\nAs there is no built-in exception view for this exception the app crashes.\n\n**With `pyramid-sanity`**\n\nA `pyramid_sanity.exceptions.InvalidURL` is returned which results in a\n`400: Bad Request` response.\n\n**Related issues**\n\n* https://github.com/Pylons/pyramid/issues/434\n* https://github.com/Pylons/pyramid/issues/1374\n* https://github.com/Pylons/pyramid/issues/2047\n* https://github.com/Pylons/webob/issues/114\n\n### Bad or missing multipart boundary declarations make `request.POST` crash\n\n```terminal\ncurl --request POST --url http://localhost:6543/foo --header 'content-type: multipart/form-data'\n```\n\n**By default**\n\nWebOb raises an uncaught `ValueError`. As there is no built-in exception view\nfor this exception the app crashes.\n\n**With `pyramid-sanity`**\n\nA `pyramid_sanity.exceptions.InvalidFormData` is returned which results in a\n`400: Bad Request` response.\n\nRelated issues:\n\n* https://github.com/Pylons/pyramid/issues/1258\n\n### Issuing redirects containing a non-ASCII location crashes the WSGI server\n\n```terminal\ncurl http://localhost:6543/redirect\n```\n\n**By default**\n\nThe app will emit the redirect successfully, but the WSGI server running the app\nmay crash. With the example app below `wsgiref.simple_server` raises an\nuncaught `AttributeError`.\n\n**With `pyramid-sanity`**\n\nThe redirect is safely URL encoded.\n\n#### Addendum: Example application\n\n```python\nfrom wsgiref.simple_server import make_server\nfrom pyramid.config import Configurator\nfrom pyramid.response import Response\nfrom pyramid.httpexceptions import HTTPFound\n\n\ndef redirect(request):\n    # Return a redirect to a URL with a non-ASCII character in it.\n    return HTTPFound(location=\"http://example.com/☃\")\n\n\ndef hello_world(request):\n    return Response(f\"Hello World! Query string was: {request.GET}. Form body was: {request.POST}\")\n\n\nif __name__ == \"__main__\":\n    with Configurator() as config:\n        config.add_route(\"redirect\", \"/redirect\")\n        config.add_route(\"hello\", \"/{anything}\")\n        config.add_view(hello_world, route_name=\"hello\")\n        config.add_view(redirect, route_name=\"redirect\")\n        app = config.make_wsgi_app()\n\n    server = make_server(\"0.0.0.0\", 6543, app)\n    server.serve_forever()\n```\n\n## Setting up Your pyramid-sanity Development Environment\n\nFirst you'll need to install:\n\n* [Git](https://git-scm.com/).\n  On Ubuntu: `sudo apt install git`, on macOS: `brew install git`.\n* [GNU Make](https://www.gnu.org/software/make/).\n  This is probably already installed, run `make --version` to check.\n* [pyenv](https://github.com/pyenv/pyenv).\n  Follow the instructions in pyenv's README to install it.\n  The **Homebrew** method works best on macOS.\n  The **Basic GitHub Checkout** method works best on Ubuntu.\n  You _don't_ need to set up pyenv's shell integration (\"shims\"), you can\n  [use pyenv without shims](https://github.com/pyenv/pyenv#using-pyenv-without-shims).\n\nThen to set up your development environment:\n\n```terminal\ngit clone https://github.com/hypothesis/pyramid-sanity.git\ncd pyramid-sanity\nmake help\n```\n\n## Releasing a New Version of the Project\n\n1. First, to get PyPI publishing working you need to go to:\n   \u003chttps://github.com/organizations/hypothesis/settings/secrets/actions/PYPI_TOKEN\u003e\n   and add pyramid-sanity to the `PYPI_TOKEN` secret's selected\n   repositories.\n\n2. Now that the pyramid-sanity project has access to the `PYPI_TOKEN` secret\n   you can release a new version by just [creating a new GitHub release](https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository).\n   Publishing a new GitHub release will automatically trigger\n   [a GitHub Actions workflow](.github/workflows/pypi.yml)\n   that will build the new version of your Python package and upload it to\n   \u003chttps://pypi.org/project/pyramid-sanity\u003e.\n\n## Changing the Project's Python Versions\n\nTo change what versions of Python the project uses:\n\n1. Change the Python versions in the\n   [cookiecutter.json](.cookiecutter/cookiecutter.json) file. For example:\n\n   ```json\n   \"python_versions\": \"3.10.4, 3.9.12\",\n   ```\n\n2. Re-run the cookiecutter template:\n\n   ```terminal\n   make template\n   ```\n\n3. Commit everything to git and send a pull request\n\n## Changing the Project's Python Dependencies\n\nTo change the production dependencies in the `setup.cfg` file:\n\n1. Change the dependencies in the [`.cookiecutter/includes/setuptools/install_requires`](.cookiecutter/includes/setuptools/install_requires) file.\n   If this file doesn't exist yet create it and add some dependencies to it.\n   For example:\n\n   ```\n   pyramid\n   sqlalchemy\n   celery\n   ```\n\n2. Re-run the cookiecutter template:\n\n   ```terminal\n   make template\n   ```\n\n3. Commit everything to git and send a pull request\n\nTo change the project's formatting, linting and test dependencies:\n\n1. Change the dependencies in the [`.cookiecutter/includes/tox/deps`](.cookiecutter/includes/tox/deps) file.\n   If this file doesn't exist yet create it and add some dependencies to it.\n   Use tox's [factor-conditional settings](https://tox.wiki/en/latest/config.html#factors-and-factor-conditional-settings)\n   to limit which environment(s) each dependency is used in.\n   For example:\n\n   ```\n   lint: flake8,\n   format: autopep8,\n   lint,tests: pytest-faker,\n   ```\n\n2. Re-run the cookiecutter template:\n\n   ```terminal\n   make template\n   ```\n\n3. Commit everything to git and send a pull request\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhypothesis%2Fpyramid-sanity","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhypothesis%2Fpyramid-sanity","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhypothesis%2Fpyramid-sanity/lists"}