├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── DCO.md ├── LICENSE ├── README.md ├── docs ├── .gitignore ├── Makefile ├── api_reference.rst ├── conf.py ├── index.rst └── references.rst ├── github_webhook ├── __init__.py └── webhook.py ├── setup.py ├── tests ├── __init__.py └── test_webhook.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .tox/* 3 | *.pyc 4 | .coverage* 5 | !.coveragerc 6 | *egg* 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | python: 4 | - "2.7" 5 | - "3.6" 6 | - "3.7" 7 | - "pypy" 8 | - "pypy3" 9 | install: 10 | - pip install tox-travis 11 | script: 12 | - tox 13 | matrix: 14 | include: 15 | - python: "3.7" 16 | script: black --line-length 120 --check github_webhook tests setup.py 17 | install: pip install black 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | If you'd like to help us improve and extend python-github-webhook, then we welcome your 4 | contributions! 5 | 6 | Below you will find some basic steps required to be able to contribute to python-github-webhook. If 7 | you have any questions about this process or any other aspect of contributing to a Bloomberg open 8 | source project, feel free to send an email to open-source@bloomberg.net and we'll get your questions 9 | answered as quickly as we can. 10 | 11 | 12 | ## Contribution Licensing 13 | 14 | Since python-github-webhook is distributed under the terms of the [Apache License, Version 15 | 2.0](http://www.apache.org/licenses/LICENSE-2.0.html), contributions that you make to python-github-webhook 16 | are licensed under the same terms. In order for us to be able to accept your contributions, 17 | we will need explicit confirmation from you that you are able and willing to provide them under 18 | these terms, and the mechanism we use to do this is called a Developer's Certificate of Origin 19 | [DCO](DCO.md). This is very similar to the process used by the Linux(R) kernel, Samba, and many 20 | other major open source projects. 21 | 22 | To participate under these terms, all that you must do is include a line like the following as the 23 | last line of the commit message for each commit in your contribution: 24 | 25 | Signed-Off-By: Random J. Developer 26 | 27 | You must use your real name (sorry, no pseudonyms, and no anonymous contributions). 28 | -------------------------------------------------------------------------------- /DCO.md: -------------------------------------------------------------------------------- 1 | Developer's Certificate of Origin 1.1 2 | 3 | By making a contribution to this project, I certify that: 4 | 5 | 1. The contribution was created in whole or in part by me and I 6 | have the right to submit it under the open source license 7 | indicated in the file; or 8 | 9 | 2. The contribution is based upon previous work that, to the best 10 | of my knowledge, is covered under an appropriate open source 11 | license and I have the right under that license to submit that 12 | work with modifications, whether created in whole or in part 13 | by me, under the same open source license (unless I am 14 | permitted to submit under a different license), as indicated 15 | in the file; or 16 | 17 | 3. The contribution was provided directly to me by some other 18 | person who certified (1), (2) or (3) and I have not modified 19 | it. 20 | 21 | 4. I understand and agree that this project and the contribution 22 | are public and that a record of the contribution (including all 23 | personal information I submit with it, including my sign-off) is 24 | maintained indefinitely and may be redistributed consistent with 25 | this project or the open source license(s) involved. 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2015 Bloomberg Finance L.P. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHub Webhook (micro) Framework 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/github-webhook.svg)][2] 4 | 5 | `python-github-webhook` is a very simple, but powerful, microframework for writing [GitHub 6 | webhooks][1] in Python. It can be used to write webhooks for individual repositories or whole 7 | organisations, and can be used for GitHub.com or GitHub Enterprise installations; in fact, it was 8 | orginally developed for Bloomberg's GHE install. 9 | 10 | ## Getting started 11 | 12 | `python-github-webhook` is designed to be as simple as possible, to make a simple Webhook that 13 | receives push events all it takes is: 14 | 15 | ```py 16 | from github_webhook import Webhook 17 | from flask import Flask 18 | 19 | app = Flask(__name__) # Standard Flask app 20 | webhook = Webhook(app) # Defines '/postreceive' endpoint 21 | 22 | @app.route("/") # Standard Flask endpoint 23 | def hello_world(): 24 | return "Hello, World!" 25 | 26 | @webhook.hook() # Defines a handler for the 'push' event 27 | def on_push(data): 28 | print("Got push with: {0}".format(data)) 29 | 30 | if __name__ == "__main__": 31 | app.run(host="0.0.0.0", port=80) 32 | ``` 33 | 34 | ## License 35 | 36 | The `python-github-webhook` repository is distributed under the Apache License (version 2.0); 37 | see the LICENSE file at the top of the source tree for more information. 38 | 39 | [1]: https://developer.github.com/webhooks/ 40 | [2]: https://pypi.python.org/pypi/github-webhook 41 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | -------------------------------------------------------------------------------- /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 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | @echo " coverage to run coverage check of the documentation (if enabled)" 49 | 50 | .PHONY: clean 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | .PHONY: html 55 | html: 56 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 59 | 60 | .PHONY: dirhtml 61 | dirhtml: 62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 63 | @echo 64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 65 | 66 | .PHONY: singlehtml 67 | singlehtml: 68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 69 | @echo 70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 71 | 72 | .PHONY: pickle 73 | pickle: 74 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 75 | @echo 76 | @echo "Build finished; now you can process the pickle files." 77 | 78 | .PHONY: json 79 | json: 80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 81 | @echo 82 | @echo "Build finished; now you can process the JSON files." 83 | 84 | .PHONY: htmlhelp 85 | htmlhelp: 86 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 87 | @echo 88 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 89 | ".hhp project file in $(BUILDDIR)/htmlhelp." 90 | 91 | .PHONY: qthelp 92 | qthelp: 93 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 94 | @echo 95 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 96 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 97 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-github-webhook.qhcp" 98 | @echo "To view the help file:" 99 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-github-webhook.qhc" 100 | 101 | .PHONY: applehelp 102 | applehelp: 103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 104 | @echo 105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 106 | @echo "N.B. You won't be able to view it unless you put it in" \ 107 | "~/Library/Documentation/Help or install it in your application" \ 108 | "bundle." 109 | 110 | .PHONY: devhelp 111 | devhelp: 112 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 113 | @echo 114 | @echo "Build finished." 115 | @echo "To view the help file:" 116 | @echo "# mkdir -p $$HOME/.local/share/devhelp/python-github-webhook" 117 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-github-webhook" 118 | @echo "# devhelp" 119 | 120 | .PHONY: epub 121 | epub: 122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 123 | @echo 124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 125 | 126 | .PHONY: latex 127 | latex: 128 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 129 | @echo 130 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 131 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 132 | "(use \`make latexpdf' here to do that automatically)." 133 | 134 | .PHONY: latexpdf 135 | latexpdf: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo "Running LaTeX files through pdflatex..." 138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 140 | 141 | .PHONY: latexpdfja 142 | latexpdfja: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through platex and dvipdfmx..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: text 149 | text: 150 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 151 | @echo 152 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 153 | 154 | .PHONY: man 155 | man: 156 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 157 | @echo 158 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 159 | 160 | .PHONY: texinfo 161 | texinfo: 162 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 163 | @echo 164 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 165 | @echo "Run \`make' in that directory to run these through makeinfo" \ 166 | "(use \`make info' here to do that automatically)." 167 | 168 | .PHONY: info 169 | info: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo "Running Texinfo files through makeinfo..." 172 | make -C $(BUILDDIR)/texinfo info 173 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 174 | 175 | .PHONY: gettext 176 | gettext: 177 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 178 | @echo 179 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 180 | 181 | .PHONY: changes 182 | changes: 183 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 184 | @echo 185 | @echo "The overview file is in $(BUILDDIR)/changes." 186 | 187 | .PHONY: linkcheck 188 | linkcheck: 189 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 190 | @echo 191 | @echo "Link check complete; look for any errors in the above output " \ 192 | "or in $(BUILDDIR)/linkcheck/output.txt." 193 | 194 | .PHONY: doctest 195 | doctest: 196 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 197 | @echo "Testing of doctests in the sources finished, look at the " \ 198 | "results in $(BUILDDIR)/doctest/output.txt." 199 | 200 | .PHONY: coverage 201 | coverage: 202 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 203 | @echo "Testing of coverage in the sources finished, look at the " \ 204 | "results in $(BUILDDIR)/coverage/python.txt." 205 | 206 | .PHONY: xml 207 | xml: 208 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 209 | @echo 210 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 211 | 212 | .PHONY: pseudoxml 213 | pseudoxml: 214 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 215 | @echo 216 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 217 | -------------------------------------------------------------------------------- /docs/api_reference.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | .. contents:: Contents 5 | :local: 6 | 7 | .. autoclass:: github_webhook.Webhook 8 | :members: 9 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # python-github-webhook documentation build configuration file, created by 5 | # sphinx-quickstart on Tue Mar 1 19:58:13 2016. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | sys.path.insert(0, os.path.abspath('..')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.autodoc', 34 | ] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix(es) of source filenames. 40 | # You can specify multiple suffix as a list of string: 41 | # source_suffix = ['.rst', '.md'] 42 | source_suffix = '.rst' 43 | 44 | # The encoding of source files. 45 | #source_encoding = 'utf-8-sig' 46 | 47 | # The master toctree document. 48 | master_doc = 'index' 49 | 50 | # General information about the project. 51 | project = 'python-github-webhook' 52 | copyright = '2016, Bloomberg LP' 53 | author = 'Bloomberg LP' 54 | 55 | # The version info for the project you're documenting, acts as replacement for 56 | # |version| and |release|, also used in various other places throughout the 57 | # built documents. 58 | # 59 | # The short X.Y version. 60 | version = '1.0' 61 | # The full version, including alpha/beta/rc tags. 62 | release = '1.0' 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | # 67 | # This is also used if you do content translation via gettext catalogs. 68 | # Usually you set "language" from the command line for these cases. 69 | language = None 70 | 71 | # There are two options for replacing |today|: either, you set today to some 72 | # non-false value, then it is used: 73 | #today = '' 74 | # Else, today_fmt is used as the format for a strftime call. 75 | #today_fmt = '%B %d, %Y' 76 | 77 | # List of patterns, relative to source directory, that match files and 78 | # directories to ignore when looking for source files. 79 | exclude_patterns = ['_build'] 80 | 81 | # The reST default role (used for this markup: `text`) to use for all 82 | # documents. 83 | #default_role = None 84 | 85 | # If true, '()' will be appended to :func: etc. cross-reference text. 86 | #add_function_parentheses = True 87 | 88 | # If true, the current module name will be prepended to all description 89 | # unit titles (such as .. function::). 90 | #add_module_names = True 91 | 92 | # If true, sectionauthor and moduleauthor directives will be shown in the 93 | # output. They are ignored by default. 94 | #show_authors = False 95 | 96 | # The name of the Pygments (syntax highlighting) style to use. 97 | pygments_style = 'sphinx' 98 | 99 | # A list of ignored prefixes for module index sorting. 100 | #modindex_common_prefix = [] 101 | 102 | # If true, keep warnings as "system message" paragraphs in the built documents. 103 | #keep_warnings = False 104 | 105 | # If true, `todo` and `todoList` produce output, else they produce nothing. 106 | todo_include_todos = False 107 | 108 | 109 | # -- Options for HTML output ---------------------------------------------- 110 | 111 | # The theme to use for HTML and HTML Help pages. See the documentation for 112 | # a list of builtin themes. 113 | html_theme = 'classic' 114 | 115 | # Theme options are theme-specific and customize the look and feel of a theme 116 | # further. For a list of options available for each theme, see the 117 | # documentation. 118 | #html_theme_options = {} 119 | 120 | # Add any paths that contain custom themes here, relative to this directory. 121 | #html_theme_path = [] 122 | 123 | # The name for this set of Sphinx documents. If None, it defaults to 124 | # " v documentation". 125 | #html_title = None 126 | 127 | # A shorter title for the navigation bar. Default is the same as html_title. 128 | #html_short_title = None 129 | 130 | # The name of an image file (relative to this directory) to place at the top 131 | # of the sidebar. 132 | #html_logo = None 133 | 134 | # The name of an image file (relative to this directory) to use as a favicon of 135 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 136 | # pixels large. 137 | #html_favicon = None 138 | 139 | # Add any paths that contain custom static files (such as style sheets) here, 140 | # relative to this directory. They are copied after the builtin static files, 141 | # so a file named "default.css" will overwrite the builtin "default.css". 142 | html_static_path = ['_static'] 143 | 144 | # Add any extra paths that contain custom files (such as robots.txt or 145 | # .htaccess) here, relative to this directory. These files are copied 146 | # directly to the root of the documentation. 147 | #html_extra_path = [] 148 | 149 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 150 | # using the given strftime format. 151 | #html_last_updated_fmt = '%b %d, %Y' 152 | 153 | # If true, SmartyPants will be used to convert quotes and dashes to 154 | # typographically correct entities. 155 | #html_use_smartypants = True 156 | 157 | # Custom sidebar templates, maps document names to template names. 158 | html_sidebars = { 159 | '**': ['globaltoc.html', 'relations.html', 'sourcelink.html'] 160 | } 161 | 162 | # Additional templates that should be rendered to pages, maps page names to 163 | # template names. 164 | #html_additional_pages = {} 165 | 166 | # If false, no module index is generated. 167 | #html_domain_indices = True 168 | 169 | # If false, no index is generated. 170 | #html_use_index = True 171 | 172 | # If true, the index is split into individual pages for each letter. 173 | #html_split_index = False 174 | 175 | # If true, links to the reST sources are added to the pages. 176 | #html_show_sourcelink = True 177 | 178 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 179 | #html_show_sphinx = True 180 | 181 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 182 | #html_show_copyright = True 183 | 184 | # If true, an OpenSearch description file will be output, and all pages will 185 | # contain a tag referring to it. The value of this option must be the 186 | # base URL from which the finished HTML is served. 187 | #html_use_opensearch = '' 188 | 189 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 190 | #html_file_suffix = None 191 | 192 | # Language to be used for generating the HTML full-text search index. 193 | # Sphinx supports the following languages: 194 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 195 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' 196 | #html_search_language = 'en' 197 | 198 | # A dictionary with options for the search language support, empty by default. 199 | # Now only 'ja' uses this config value 200 | #html_search_options = {'type': 'default'} 201 | 202 | # The name of a javascript file (relative to the configuration directory) that 203 | # implements a search results scorer. If empty, the default will be used. 204 | #html_search_scorer = 'scorer.js' 205 | 206 | # Output file base name for HTML help builder. 207 | htmlhelp_basename = 'python-github-webhookdoc' 208 | 209 | # -- Options for LaTeX output --------------------------------------------- 210 | 211 | latex_elements = { 212 | # The paper size ('letterpaper' or 'a4paper'). 213 | #'papersize': 'letterpaper', 214 | 215 | # The font size ('10pt', '11pt' or '12pt'). 216 | #'pointsize': '10pt', 217 | 218 | # Additional stuff for the LaTeX preamble. 219 | #'preamble': '', 220 | 221 | # Latex figure (float) alignment 222 | #'figure_align': 'htbp', 223 | } 224 | 225 | # Grouping the document tree into LaTeX files. List of tuples 226 | # (source start file, target name, title, 227 | # author, documentclass [howto, manual, or own class]). 228 | latex_documents = [ 229 | (master_doc, 'python-github-webhook.tex', 'python-github-webhook Documentation', 230 | 'Bloomberg LP', 'manual'), 231 | ] 232 | 233 | # The name of an image file (relative to this directory) to place at the top of 234 | # the title page. 235 | #latex_logo = None 236 | 237 | # For "manual" documents, if this is true, then toplevel headings are parts, 238 | # not chapters. 239 | #latex_use_parts = False 240 | 241 | # If true, show page references after internal links. 242 | #latex_show_pagerefs = False 243 | 244 | # If true, show URL addresses after external links. 245 | #latex_show_urls = False 246 | 247 | # Documents to append as an appendix to all manuals. 248 | #latex_appendices = [] 249 | 250 | # If false, no module index is generated. 251 | #latex_domain_indices = True 252 | 253 | 254 | # -- Options for manual page output --------------------------------------- 255 | 256 | # One entry per manual page. List of tuples 257 | # (source start file, name, description, authors, manual section). 258 | man_pages = [ 259 | (master_doc, 'python-github-webhook', 'python-github-webhook Documentation', 260 | [author], 1) 261 | ] 262 | 263 | # If true, show URL addresses after external links. 264 | #man_show_urls = False 265 | 266 | 267 | # -- Options for Texinfo output ------------------------------------------- 268 | 269 | # Grouping the document tree into Texinfo files. List of tuples 270 | # (source start file, target name, title, author, 271 | # dir menu entry, description, category) 272 | texinfo_documents = [ 273 | (master_doc, 'python-github-webhook', 'python-github-webhook Documentation', 274 | author, 'python-github-webhook', 'One line description of project.', 275 | 'Miscellaneous'), 276 | ] 277 | 278 | # Documents to append as an appendix to all manuals. 279 | #texinfo_appendices = [] 280 | 281 | # If false, no module index is generated. 282 | #texinfo_domain_indices = True 283 | 284 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 285 | #texinfo_show_urls = 'footnote' 286 | 287 | # If true, do not generate a @detailmenu in the "Top" node's menu. 288 | #texinfo_no_detailmenu = False 289 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to python-github-webhook's documentation! 2 | ================================================= 3 | 4 | Very simple, but powerful, microframework for writing Github webhooks in Python. 5 | 6 | .. code-block:: python 7 | 8 | from github_webhook import Webhook 9 | from flask import Flask 10 | 11 | app = Flask(__name__) # Standard Flask app 12 | webhook = Webhook(app) # Defines '/postreceive' endpoint 13 | 14 | @app.route("/") # Standard Flask endpoint 15 | def hello_world(): 16 | return "Hello, World!" 17 | 18 | @webhook.hook() # Defines a handler for the 'push' event 19 | def on_push(data): 20 | print("Got push with: {0}".format(data)) 21 | 22 | if __name__ == "__main__": 23 | app.run(host="0.0.0.0", port=80) 24 | 25 | Contents: 26 | 27 | .. toctree:: 28 | :maxdepth: 1 29 | 30 | api_reference 31 | references 32 | -------------------------------------------------------------------------------- /docs/references.rst: -------------------------------------------------------------------------------- 1 | References 2 | ========== 3 | 4 | These websites should help you along the way to writing your Webhook 5 | 6 | - `Webhooks, Github Developers' Guide to Webhooks `_ 7 | - `The Flask Documentation `_ 8 | -------------------------------------------------------------------------------- /github_webhook/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | github_webhook 3 | ~~~~~~~~~~~~~~ 4 | 5 | Very simple, but powerful, microframework for writing Github webhooks in Python. 6 | 7 | :copyright: (c) 2016 by Bloomberg Finance L.P. 8 | :license: Apache License, Version 2.0 9 | """ 10 | 11 | from github_webhook.webhook import Webhook # noqa 12 | 13 | # ----------------------------------------------------------------------------- 14 | # Copyright 2015 Bloomberg Finance L.P. 15 | # 16 | # Licensed under the Apache License, Version 2.0 (the "License"); 17 | # you may not use this file except in compliance with the License. 18 | # You may obtain a copy of the License at 19 | # 20 | # http://www.apache.org/licenses/LICENSE-2.0 21 | # 22 | # Unless required by applicable law or agreed to in writing, software 23 | # distributed under the License is distributed on an "AS IS" BASIS, 24 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 25 | # See the License for the specific language governing permissions and 26 | # limitations under the License. 27 | # ----------------------------- END-OF-FILE ----------------------------------- 28 | -------------------------------------------------------------------------------- /github_webhook/webhook.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import hashlib 3 | import hmac 4 | import logging 5 | import json 6 | import six 7 | from flask import abort, request 8 | 9 | 10 | class Webhook(object): 11 | """ 12 | Construct a webhook on the given :code:`app`. 13 | 14 | :param app: Flask app that will host the webhook 15 | :param endpoint: the endpoint for the registered URL rule 16 | :param secret: Optional secret, used to authenticate the hook comes from Github 17 | """ 18 | 19 | def __init__(self, app=None, endpoint="/postreceive", secret=None): 20 | self.app = app 21 | self.secret = secret 22 | if app is not None: 23 | self.init_app(app, endpoint, secret) 24 | 25 | def init_app(self, app, endpoint="/postreceive", secret=None): 26 | self._hooks = collections.defaultdict(list) 27 | self._logger = logging.getLogger("webhook") 28 | if secret is not None: 29 | self.secret = secret 30 | app.add_url_rule(rule=endpoint, endpoint=endpoint, view_func=self._postreceive, methods=["POST"]) 31 | 32 | @property 33 | def secret(self): 34 | return self._secret 35 | 36 | @secret.setter 37 | def secret(self, secret): 38 | if secret is not None and not isinstance(secret, six.binary_type): 39 | secret = secret.encode("utf-8") 40 | self._secret = secret 41 | 42 | def hook(self, event_type="push"): 43 | """ 44 | Registers a function as a hook. Multiple hooks can be registered for a given type, but the 45 | order in which they are invoke is unspecified. 46 | 47 | :param event_type: The event type this hook will be invoked for. 48 | """ 49 | 50 | def decorator(func): 51 | self._hooks[event_type].append(func) 52 | return func 53 | 54 | return decorator 55 | 56 | def _get_digest(self): 57 | """Return message digest if a secret key was provided""" 58 | 59 | return hmac.new(self._secret, request.data, hashlib.sha1).hexdigest() if self._secret else None 60 | 61 | def _postreceive(self): 62 | """Callback from Flask""" 63 | 64 | digest = self._get_digest() 65 | 66 | if digest is not None: 67 | sig_parts = _get_header("X-Hub-Signature").split("=", 1) 68 | if not isinstance(digest, six.text_type): 69 | digest = six.text_type(digest) 70 | 71 | if len(sig_parts) < 2 or sig_parts[0] != "sha1" or not hmac.compare_digest(sig_parts[1], digest): 72 | abort(400, "Invalid signature") 73 | 74 | event_type = _get_header("X-Github-Event") 75 | content_type = _get_header("content-type") 76 | data = ( 77 | json.loads(request.form.to_dict(flat=True)["payload"]) 78 | if content_type == "application/x-www-form-urlencoded" 79 | else request.get_json() 80 | ) 81 | 82 | if data is None: 83 | abort(400, "Request body must contain json") 84 | 85 | self._logger.info("%s (%s)", _format_event(event_type, data), _get_header("X-Github-Delivery")) 86 | 87 | for hook in self._hooks.get(event_type, []): 88 | hook(data) 89 | 90 | return "", 204 91 | 92 | 93 | def _get_header(key): 94 | """Return message header""" 95 | 96 | try: 97 | return request.headers[key] 98 | except KeyError: 99 | abort(400, "Missing header: " + key) 100 | 101 | 102 | EVENT_DESCRIPTIONS = { 103 | "commit_comment": "{comment[user][login]} commented on " "{comment[commit_id]} in {repository[full_name]}", 104 | "create": "{sender[login]} created {ref_type} ({ref}) in " "{repository[full_name]}", 105 | "delete": "{sender[login]} deleted {ref_type} ({ref}) in " "{repository[full_name]}", 106 | "deployment": "{sender[login]} deployed {deployment[ref]} to " 107 | "{deployment[environment]} in {repository[full_name]}", 108 | "deployment_status": "deployment of {deployement[ref]} to " 109 | "{deployment[environment]} " 110 | "{deployment_status[state]} in " 111 | "{repository[full_name]}", 112 | "fork": "{forkee[owner][login]} forked {forkee[name]}", 113 | "gollum": "{sender[login]} edited wiki pages in {repository[full_name]}", 114 | "issue_comment": "{sender[login]} commented on issue #{issue[number]} " "in {repository[full_name]}", 115 | "issues": "{sender[login]} {action} issue #{issue[number]} in " "{repository[full_name]}", 116 | "member": "{sender[login]} {action} member {member[login]} in " "{repository[full_name]}", 117 | "membership": "{sender[login]} {action} member {member[login]} to team " "{team[name]} in {repository[full_name]}", 118 | "page_build": "{sender[login]} built pages in {repository[full_name]}", 119 | "ping": "ping from {sender[login]}", 120 | "public": "{sender[login]} publicized {repository[full_name]}", 121 | "pull_request": "{sender[login]} {action} pull #{pull_request[number]} in " "{repository[full_name]}", 122 | "pull_request_review": "{sender[login]} {action} {review[state]} " 123 | "review on pull #{pull_request[number]} in " 124 | "{repository[full_name]}", 125 | "pull_request_review_comment": "{comment[user][login]} {action} comment " 126 | "on pull #{pull_request[number]} in " 127 | "{repository[full_name]}", 128 | "push": "{pusher[name]} pushed {ref} in {repository[full_name]}", 129 | "release": "{release[author][login]} {action} {release[tag_name]} in " "{repository[full_name]}", 130 | "repository": "{sender[login]} {action} repository " "{repository[full_name]}", 131 | "status": "{sender[login]} set {sha} status to {state} in " "{repository[full_name]}", 132 | "team_add": "{sender[login]} added repository {repository[full_name]} to " "team {team[name]}", 133 | "watch": "{sender[login]} {action} watch in repository " "{repository[full_name]}", 134 | } 135 | 136 | 137 | def _format_event(event_type, data): 138 | try: 139 | return EVENT_DESCRIPTIONS[event_type].format(**data) 140 | except KeyError: 141 | return event_type 142 | 143 | 144 | # ----------------------------------------------------------------------------- 145 | # Copyright 2015 Bloomberg Finance L.P. 146 | # 147 | # Licensed under the Apache License, Version 2.0 (the "License"); 148 | # you may not use this file except in compliance with the License. 149 | # You may obtain a copy of the License at 150 | # 151 | # http://www.apache.org/licenses/LICENSE-2.0 152 | # 153 | # Unless required by applicable law or agreed to in writing, software 154 | # distributed under the License is distributed on an "AS IS" BASIS, 155 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 156 | # See the License for the specific language governing permissions and 157 | # limitations under the License. 158 | # ----------------------------- END-OF-FILE ----------------------------------- 159 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="github-webhook", 5 | version="1.0.4", 6 | description="Very simple, but powerful, microframework for writing Github webhooks in Python", 7 | url="https://github.com/bloomberg/python-github-webhook", 8 | author="Alex Chamberlain, Fred Phillips, Daniel Kiss, Daniel Beer", 9 | author_email="achamberlai9@bloomberg.net, fphillips7@bloomberg.net, dkiss1@bloomberg.net, dbeer1@bloomberg.net", 10 | license="Apache 2.0", 11 | packages=["github_webhook"], 12 | install_requires=["flask", "six"], 13 | tests_require=["mock", "pytest"], 14 | classifiers=[ 15 | "Development Status :: 4 - Beta", 16 | "Framework :: Flask", 17 | "Environment :: Web Environment", 18 | "Intended Audience :: Developers", 19 | "Intended Audience :: System Administrators", 20 | "License :: OSI Approved :: Apache Software License", 21 | "Operating System :: MacOS :: MacOS X", 22 | "Operating System :: Microsoft :: Windows", 23 | "Operating System :: POSIX", 24 | "Programming Language :: Python :: 2", 25 | "Programming Language :: Python :: 3", 26 | "Topic :: Software Development :: Version Control", 27 | ], 28 | test_suite="nose.collector", 29 | ) 30 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloomberg/python-github-webhook/61e713c3781e2de6e327554be54095df2d666604/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_webhook.py: -------------------------------------------------------------------------------- 1 | """Tests for github_webhook.webhook""" 2 | 3 | from __future__ import print_function 4 | 5 | import pytest 6 | import werkzeug 7 | import json 8 | 9 | try: 10 | from unittest import mock 11 | except ImportError: 12 | import mock 13 | 14 | from github_webhook.webhook import Webhook 15 | 16 | 17 | @pytest.fixture 18 | def mock_request(): 19 | with mock.patch("github_webhook.webhook.request") as req: 20 | req.headers = {"X-Github-Delivery": ""} 21 | yield req 22 | 23 | 24 | @pytest.fixture 25 | def push_request(mock_request): 26 | mock_request.headers["X-Github-Event"] = "push" 27 | mock_request.headers["content-type"] = "application/json" 28 | yield mock_request 29 | 30 | 31 | @pytest.fixture 32 | def push_request_encoded(mock_request): 33 | mock_request.headers["X-Github-Event"] = "push" 34 | mock_request.headers["content-type"] = "application/x-www-form-urlencoded" 35 | yield mock_request 36 | 37 | 38 | @pytest.fixture 39 | def app(): 40 | yield mock.Mock() 41 | 42 | 43 | @pytest.fixture 44 | def webhook(app): 45 | yield Webhook(app) 46 | 47 | 48 | @pytest.fixture 49 | def handler(webhook): 50 | handler = mock.Mock() 51 | webhook.hook()(handler) 52 | yield handler 53 | 54 | 55 | def test_constructor(): 56 | # GIVEN 57 | app = mock.Mock() 58 | 59 | # WHEN 60 | webhook = Webhook(app) 61 | 62 | # THEN 63 | app.add_url_rule.assert_called_once_with( 64 | endpoint="/postreceive", rule="/postreceive", view_func=webhook._postreceive, methods=["POST"] 65 | ) 66 | 67 | 68 | def test_init_app_flow(): 69 | # GIVEN 70 | app = mock.Mock() 71 | 72 | # WHEN 73 | webhook = Webhook() 74 | webhook.init_app(app) 75 | 76 | # THEN 77 | app.add_url_rule.assert_called_once_with( 78 | endpoint="/postreceive", rule="/postreceive", view_func=webhook._postreceive, methods=["POST"] 79 | ) 80 | 81 | 82 | def test_init_app_flow_should_not_accidentally_override_secrets(): 83 | # GIVEN 84 | app = mock.Mock() 85 | 86 | # WHEN 87 | webhook = Webhook(secret="hello-world-of-secrecy") 88 | webhook.init_app(app) 89 | 90 | # THEN 91 | assert webhook.secret is not None 92 | 93 | 94 | def test_init_app_flow_should_override_secrets(): 95 | # GIVEN 96 | app = mock.Mock() 97 | 98 | # WHEN 99 | webhook = Webhook(secret="hello-world-of-secrecy") 100 | webhook.init_app(app, secret="a-new-world-of-secrecy") 101 | 102 | # THEN 103 | assert webhook.secret == "a-new-world-of-secrecy".encode("utf-8") 104 | 105 | 106 | def test_run_push_hook(webhook, handler, push_request): 107 | # WHEN 108 | webhook._postreceive() 109 | 110 | # THEN 111 | handler.assert_called_once_with(push_request.get_json.return_value) 112 | 113 | 114 | def test_run_push_hook_urlencoded(webhook, handler, push_request_encoded): 115 | github_mock_payload = {"payload": '{"key": "value"}'} 116 | push_request_encoded.form.to_dict.return_value = github_mock_payload 117 | payload = json.loads(github_mock_payload["payload"]) 118 | 119 | # WHEN 120 | webhook._postreceive() 121 | 122 | # THEN 123 | handler.assert_called_once_with(payload) 124 | 125 | 126 | def test_do_not_run_push_hook_on_ping(webhook, handler, mock_request): 127 | # GIVEN 128 | mock_request.headers["X-Github-Event"] = "ping" 129 | mock_request.headers["content-type"] = "application/json" 130 | 131 | # WHEN 132 | webhook._postreceive() 133 | 134 | # THEN 135 | handler.assert_not_called() 136 | 137 | 138 | def test_do_not_run_push_hook_on_ping_urlencoded(webhook, handler, mock_request): 139 | # GIVEN 140 | mock_request.headers["X-Github-Event"] = "ping" 141 | mock_request.headers["content-type"] = "application/x-www-form-urlencoded" 142 | mock_request.form.to_dict.return_value = {"payload": '{"key": "value"}'} 143 | 144 | # WHEN 145 | webhook._postreceive() 146 | 147 | # THEN 148 | handler.assert_not_called() 149 | 150 | 151 | def test_can_handle_zero_events(webhook, push_request): 152 | # WHEN, THEN 153 | webhook._postreceive() # noop 154 | 155 | 156 | @pytest.mark.parametrize("secret", [u"secret", b"secret"]) 157 | @mock.patch("github_webhook.webhook.hmac") 158 | def test_calls_if_signature_is_correct(mock_hmac, app, push_request, secret): 159 | # GIVEN 160 | webhook = Webhook(app, secret=secret) 161 | push_request.headers["X-Hub-Signature"] = "sha1=hash_of_something" 162 | push_request.data = b"something" 163 | handler = mock.Mock() 164 | mock_hmac.compare_digest.return_value = True 165 | 166 | # WHEN 167 | webhook.hook()(handler) 168 | webhook._postreceive() 169 | 170 | # THEN 171 | handler.assert_called_once_with(push_request.get_json.return_value) 172 | 173 | 174 | @mock.patch("github_webhook.webhook.hmac") 175 | def test_does_not_call_if_signature_is_incorrect(mock_hmac, app, push_request): 176 | # GIVEN 177 | webhook = Webhook(app, secret="super_secret") 178 | push_request.headers["X-Hub-Signature"] = "sha1=hash_of_something" 179 | push_request.data = b"something" 180 | handler = mock.Mock() 181 | mock_hmac.compare_digest.return_value = False 182 | 183 | # WHEN, THEN 184 | webhook.hook()(handler) 185 | with pytest.raises(werkzeug.exceptions.BadRequest): 186 | webhook._postreceive() 187 | 188 | 189 | def test_request_has_no_data(webhook, handler, push_request): 190 | # GIVEN 191 | push_request.get_json.return_value = None 192 | 193 | # WHEN, THEN 194 | with pytest.raises(werkzeug.exceptions.BadRequest): 195 | webhook._postreceive() 196 | 197 | 198 | def test_request_had_headers(webhook, handler, mock_request): 199 | # WHEN, THEN 200 | with pytest.raises(werkzeug.exceptions.BadRequest): 201 | webhook._postreceive() 202 | 203 | 204 | # ----------------------------------------------------------------------------- 205 | # Copyright 2015 Bloomberg Finance L.P. 206 | # 207 | # Licensed under the Apache License, Version 2.0 (the "License"); 208 | # you may not use this file except in compliance with the License. 209 | # You may obtain a copy of the License at 210 | # 211 | # http://www.apache.org/licenses/LICENSE-2.0 212 | # 213 | # Unless required by applicable law or agreed to in writing, software 214 | # distributed under the License is distributed on an "AS IS" BASIS, 215 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 216 | # See the License for the specific language governing permissions and 217 | # limitations under the License. 218 | # ----------------------------- END-OF-FILE ----------------------------------- 219 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py36,py37,pypy,pypy3,flake8 3 | 4 | [testenv] 5 | deps = 6 | pytest 7 | pytest-cov 8 | flask 9 | six 10 | py{27,py}: mock 11 | commands = pytest -vl --cov=github_webhook --cov-report term-missing --cov-fail-under 100 12 | 13 | [testenv:flake8] 14 | deps = flake8 15 | commands = flake8 github_webhook 16 | 17 | [flake8] 18 | max-line-length = 100 --------------------------------------------------------------------------------