github-bot-api 0.5.1__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. github-bot-api-0.5.1/LICENSE.txt → github_bot_api-0.6.0/LICENSE +2 -2
  2. github_bot_api-0.6.0/PKG-INFO +52 -0
  3. github_bot_api-0.6.0/README.md +24 -0
  4. github_bot_api-0.6.0/pyproject.toml +82 -0
  5. github_bot_api-0.6.0/src/github_bot_api/__init__.py +8 -0
  6. github_bot_api-0.6.0/src/github_bot_api/app.py +188 -0
  7. github_bot_api-0.6.0/src/github_bot_api/app_test.py +16 -0
  8. github_bot_api-0.6.0/src/github_bot_api/event.py +95 -0
  9. github_bot_api-0.6.0/src/github_bot_api/flask.py +61 -0
  10. github_bot_api-0.6.0/src/github_bot_api/signature.py +47 -0
  11. github_bot_api-0.6.0/src/github_bot_api/tests/test_import.py +2 -0
  12. github_bot_api-0.6.0/src/github_bot_api/token.py +149 -0
  13. github_bot_api-0.6.0/src/github_bot_api/utils/functions.py +29 -0
  14. github_bot_api-0.6.0/src/github_bot_api/utils/mime.py +24 -0
  15. github_bot_api-0.6.0/src/github_bot_api/utils/types.py +12 -0
  16. github_bot_api-0.6.0/src/github_bot_api/webhook.py +72 -0
  17. github-bot-api-0.5.1/MANIFEST.in +0 -6
  18. github-bot-api-0.5.1/PKG-INFO +0 -82
  19. github-bot-api-0.5.1/README.md +0 -69
  20. github-bot-api-0.5.1/package.yml +0 -29
  21. github-bot-api-0.5.1/setup.cfg +0 -4
  22. github-bot-api-0.5.1/setup.py +0 -58
  23. github-bot-api-0.5.1/src/github_bot_api/__init__.py +0 -9
  24. github-bot-api-0.5.1/src/github_bot_api/app.py +0 -213
  25. github-bot-api-0.5.1/src/github_bot_api/event.py +0 -95
  26. github-bot-api-0.5.1/src/github_bot_api/flask.py +0 -60
  27. github-bot-api-0.5.1/src/github_bot_api/signature.py +0 -48
  28. github-bot-api-0.5.1/src/github_bot_api/token.py +0 -147
  29. github-bot-api-0.5.1/src/github_bot_api/utils/mime.py +0 -25
  30. github-bot-api-0.5.1/src/github_bot_api/utils/types.py +0 -13
  31. github-bot-api-0.5.1/src/github_bot_api/webhook.py +0 -72
  32. github-bot-api-0.5.1/src/github_bot_api.egg-info/PKG-INFO +0 -82
  33. github-bot-api-0.5.1/src/github_bot_api.egg-info/SOURCES.txt +0 -22
  34. github-bot-api-0.5.1/src/github_bot_api.egg-info/dependency_links.txt +0 -1
  35. github-bot-api-0.5.1/src/github_bot_api.egg-info/not-zip-safe +0 -1
  36. github-bot-api-0.5.1/src/github_bot_api.egg-info/requires.txt +0 -12
  37. github-bot-api-0.5.1/src/github_bot_api.egg-info/top_level.txt +0 -1
  38. {github-bot-api-0.5.1 → github_bot_api-0.6.0}/src/github_bot_api/py.typed +0 -0
  39. {github-bot-api-0.5.1 → github_bot_api-0.6.0}/src/github_bot_api/utils/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
- The MIT License
1
+ MIT License
2
2
 
3
- Copyright (c) 2021 Niklas Rosenstein
3
+ Copyright (c) 2022 Niklas Rosenstein
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  this software and associated documentation files (the "Software"), to deal in
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.1
2
+ Name: github-bot-api
3
+ Version: 0.6.0
4
+ Summary: API for creating GitHub bots and webhooks in Python.
5
+ License: MIT
6
+ Author: Niklas Rosenstein
7
+ Author-email: rosensteinniklas@gmail.com
8
+ Requires-Python: >=3.8,<4.0
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Dist: PyGithub (>=1.58,<2.0)
18
+ Requires-Dist: PyJWT (>=2.6.0,<3.0.0)
19
+ Requires-Dist: cryptography (>=42.0.5,<43.0.0)
20
+ Requires-Dist: flask
21
+ Requires-Dist: requests (>=2.28.2,<3.0.0)
22
+ Requires-Dist: urllib3 (>=1.26.15,<2.0.0)
23
+ Project-URL: Bug Tracker, https://github.com/NiklasRosenstein/python-github-bot-api/issues
24
+ Project-URL: Documentation, https://niklasrosenstein.github.io/python-github-bot-api/
25
+ Project-URL: Repository, https://github.com/NiklasRosenstein/python-github-bot-api
26
+ Description-Content-Type: text/markdown
27
+
28
+ <p align="center"><img src="https://i.imgur.com/5SiDsz8.png"></p>
29
+ <h1 align="center">python-github-bot-api</h1>
30
+ <p align="center">
31
+ <a href="https://pypi.org/project/github-bot-api"><img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/github-bot-api"></a></p>
32
+
33
+ [PyGithub]: https://pypi.org/project/PyGithub/
34
+
35
+ A thin Python library for creating GitHub bots and webhooks in Python with [PyGithub].
36
+
37
+ ```python
38
+ from github import Github
39
+ from github_bot_api import GithubApp
40
+ from pathlib import Path
41
+
42
+ app = GithubApp(
43
+ user_agent='my-bot/0.0.0',
44
+ app_id="67890",
45
+ private_key=Path("app-private.key").read_text(),
46
+ )
47
+
48
+ client: Github = app.installation_client(12345)
49
+ ```
50
+
51
+ For more examples, check out the [documentation](https://niklasrosenstein.github.io/python-github-bot-api/).
52
+
@@ -0,0 +1,24 @@
1
+ <p align="center"><img src="https://i.imgur.com/5SiDsz8.png"></p>
2
+ <h1 align="center">python-github-bot-api</h1>
3
+ <p align="center">
4
+ <a href="https://pypi.org/project/github-bot-api"><img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/github-bot-api"></a></p>
5
+
6
+ [PyGithub]: https://pypi.org/project/PyGithub/
7
+
8
+ A thin Python library for creating GitHub bots and webhooks in Python with [PyGithub].
9
+
10
+ ```python
11
+ from github import Github
12
+ from github_bot_api import GithubApp
13
+ from pathlib import Path
14
+
15
+ app = GithubApp(
16
+ user_agent='my-bot/0.0.0',
17
+ app_id="67890",
18
+ private_key=Path("app-private.key").read_text(),
19
+ )
20
+
21
+ client: Github = app.installation_client(12345)
22
+ ```
23
+
24
+ For more examples, check out the [documentation](https://niklasrosenstein.github.io/python-github-bot-api/).
@@ -0,0 +1,82 @@
1
+ [build-system]
2
+ requires = ["poetry-core"]
3
+ build-backend = "poetry.core.masonry.api"
4
+
5
+ [tool.poetry]
6
+ name = "github-bot-api"
7
+ version = "0.6.0"
8
+ description = "API for creating GitHub bots and webhooks in Python."
9
+ authors = ["Niklas Rosenstein <rosensteinniklas@gmail.com>"]
10
+ license = "MIT"
11
+ readme = "README.md"
12
+ packages = [{ include = "github_bot_api", from = "src" }]
13
+ classifiers = [
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.8",
18
+ "Programming Language :: Python :: 3.9",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11"
21
+ ]
22
+ keywords = []
23
+
24
+ [tool.poetry.urls]
25
+ "Bug Tracker" = "https://github.com/NiklasRosenstein/python-github-bot-api/issues"
26
+ Documentation = "https://niklasrosenstein.github.io/python-github-bot-api/"
27
+ Repository = "https://github.com/NiklasRosenstein/python-github-bot-api"
28
+
29
+ [tool.poetry.dependencies]
30
+ python = "^3.8"
31
+ cryptography = "^42.0.5"
32
+ flask = { version = "*", optional = true }
33
+ PyGithub = { version = "^1.58", optional = true }
34
+ PyJWT = "^2.6.0"
35
+ requests = "^2.28.2"
36
+ urllib3 = "^1.26.15"
37
+
38
+ [tool.poetry.dev-dependencies]
39
+ black = "*"
40
+ flake8 = "*"
41
+ isort = "*"
42
+ mypy = "*"
43
+ pytest = "*"
44
+ types-flask = "*"
45
+ types-requests = "*"
46
+
47
+ [tool.slap]
48
+ typed = true
49
+
50
+ [tool.slap.test]
51
+ check = "slap check"
52
+ mypy = "dmypy run src"
53
+ pytest = "pytest -vv"
54
+ black = "black --check src"
55
+ isort = "isort --check-only src"
56
+ flake8 = "flake8 src"
57
+
58
+ [tool.slap.run]
59
+ fmt = "black src && isort src"
60
+
61
+ [tool.mypy]
62
+ python_version = "3.8"
63
+ explicit_package_bases = true
64
+ mypy_path = ["src"]
65
+ namespace_packages = true
66
+ pretty = true
67
+ show_error_codes = true
68
+ show_error_context = true
69
+ # strict = true
70
+ warn_no_return = true
71
+ warn_redundant_casts = true
72
+ warn_unreachable = true
73
+ warn_unused_ignores = true
74
+ check_untyped_defs = true
75
+
76
+ [tool.isort]
77
+ profile = "black"
78
+ line_length = 120
79
+ combine_as_imports = true
80
+
81
+ [tool.black]
82
+ line-length = 120
@@ -0,0 +1,8 @@
1
+ __author__ = "Niklas Rosenstein <nrosenstein@palantir.com>"
2
+ __version__ = "0.6.0"
3
+
4
+ from .app import GithubApp
5
+ from .event import Event, accept_event
6
+ from .webhook import Webhook
7
+
8
+ __all__ = ["GithubApp", "Event", "accept_event", "Webhook"]
@@ -0,0 +1,188 @@
1
+ """
2
+ Registry for GitHub event handlers.
3
+ """
4
+
5
+ import dataclasses
6
+ import logging
7
+ import sys
8
+ import threading
9
+ import typing as t
10
+
11
+ import requests
12
+ import urllib3
13
+
14
+ from . import __version__
15
+ from .token import InstallationTokenSupplier, JwtSupplier, TokenInfo
16
+ from .utils.functions import coalesce
17
+
18
+ T = t.TypeVar("T")
19
+ logger = logging.getLogger(__name__)
20
+ user_agent = f"python/{sys.version.split()[0]} github-bot-api/{__version__}"
21
+
22
+ if t.TYPE_CHECKING:
23
+ import github
24
+
25
+
26
+ @dataclasses.dataclass
27
+ class GithubClientSettings:
28
+ """
29
+ Settings for constructing a #github.Github client object.
30
+ """
31
+
32
+ base_url: t.Optional[str] = None
33
+ user_agent: t.Optional[str] = None
34
+ timeout: t.Optional[int] = None
35
+ per_page: t.Optional[int] = None
36
+ verify: t.Optional[bool] = None
37
+ retry: t.Optional[urllib3.Retry] = None
38
+
39
+ def update(self, other: "GithubClientSettings") -> "GithubClientSettings":
40
+ result = GithubClientSettings()
41
+ for field in dataclasses.fields(self):
42
+ value = getattr(other, field.name)
43
+ if value is None:
44
+ value = getattr(self, field.name)
45
+ setattr(result, field.name, value)
46
+ return result
47
+
48
+ def make_client(self, login_or_token: t.Optional[str] = None, jwt: t.Optional[str] = None) -> "github.Github":
49
+ import github
50
+ import github.Consts
51
+
52
+ return github.Github(
53
+ login_or_token=login_or_token,
54
+ jwt=jwt,
55
+ base_url=self.base_url or github.Consts.DEFAULT_BASE_URL,
56
+ user_agent=self.user_agent or "PyGithub/Python",
57
+ timeout=coalesce(self.timeout, github.Consts.DEFAULT_TIMEOUT),
58
+ per_page=coalesce(self.per_page, github.Consts.DEFAULT_PER_PAGE),
59
+ verify=coalesce(self.verify, True),
60
+ retry=self.retry,
61
+ )
62
+
63
+
64
+ @dataclasses.dataclass
65
+ class GithubApp:
66
+ """
67
+ Represents a GitHub application and all the required details.
68
+ """
69
+
70
+ PUBLIC_GITHUB_V3_API_URL = "https://api.github.com"
71
+
72
+ user_agent: str
73
+ """User agent of the application. This will be respected in #get_user_agent()."""
74
+
75
+ app_id: int
76
+ """GitHub Application ID."""
77
+
78
+ private_key: str
79
+ """RSA private key to sign the JWT with."""
80
+
81
+ v3_api_url: str = PUBLIC_GITHUB_V3_API_URL
82
+ """GitHub API base URL. Defaults to the public GitHub API."""
83
+
84
+ def __post_init__(self):
85
+ self._jwt_supplier = JwtSupplier(self.app_id, self.private_key)
86
+ self._lock = threading.Lock()
87
+ self._installation_tokens: t.Dict[int, InstallationTokenSupplier] = {}
88
+
89
+ def _get_base_github_client_settings(self) -> GithubClientSettings:
90
+ return GithubClientSettings(self.v3_api_url, self.get_user_agent())
91
+
92
+ def get_user_agent(self, installation_id: t.Optional[int] = None) -> str:
93
+ """
94
+ Create a user agent string for the PyGithub client, including the installation if specified.
95
+ """
96
+
97
+ user_agent = f"{self.user_agent} PyGithub/python (app_id={self.app_id}"
98
+ if installation_id:
99
+ user_agent += f", installation_id={installation_id})"
100
+ return user_agent
101
+
102
+ @property
103
+ def jwt(self) -> TokenInfo:
104
+ """
105
+ Returns the JWT for your GitHub application. The JWT is the token to use with GitHub application APIs.
106
+ """
107
+
108
+ return self._jwt_supplier()
109
+
110
+ @property
111
+ def jwt_supplier(self) -> JwtSupplier:
112
+ """
113
+ Returns a new #JwtSupplier that is used for generating JWT tokens for your GitHub application.
114
+ """
115
+
116
+ return JwtSupplier(self.app_id, self.private_key)
117
+
118
+ def app_client(self, settings: t.Union[GithubClientSettings, t.Dict[str, t.Any], None] = None) -> "github.Github":
119
+ """
120
+ Returns a PyGithub client for your GitHub application.
121
+
122
+ Note that the client's token will expire after 10 minutes and you will have to create a new client or update the
123
+ client's token with the value returned by #jwt. It is recommended that you create a new client for each atomic
124
+ operation you perform.
125
+
126
+ This requires you to install `PyGithub>=1.58`.
127
+ """
128
+
129
+ if isinstance(settings, dict):
130
+ settings = GithubClientSettings(**settings)
131
+ elif settings is None:
132
+ settings = GithubClientSettings()
133
+
134
+ settings = self._get_base_github_client_settings().update(settings)
135
+ return settings.make_client(jwt=self.jwt.value)
136
+
137
+ def __requestor(self, auth_header: str, installation_id: int) -> t.Dict[str, str]:
138
+ return requests.post(
139
+ self.v3_api_url.rstrip("/") + f"/app/installations/{installation_id}/access_tokens",
140
+ headers={"Authorization": auth_header, "User-Agent": user_agent},
141
+ ).json()
142
+
143
+ def get_installation_token_supplier(self, installation_id: int) -> InstallationTokenSupplier:
144
+ """
145
+ Create an #InstallationTokenSupplier for your GitHub application to act within the scope of the given
146
+ *installation_id*.
147
+ """
148
+
149
+ with self._lock:
150
+ return self._installation_tokens.setdefault(
151
+ installation_id,
152
+ InstallationTokenSupplier(
153
+ self._jwt_supplier,
154
+ installation_id,
155
+ self.__requestor,
156
+ ),
157
+ )
158
+
159
+ def installation_token(self, installation_id: int) -> TokenInfo:
160
+ """
161
+ A short-hand to retrieve a new installation token for the given *installation_id*.
162
+ """
163
+
164
+ return self.get_installation_token_supplier(installation_id)()
165
+
166
+ def installation_client(
167
+ self,
168
+ installation_id: int,
169
+ settings: t.Union[GithubClientSettings, t.Dict[str, t.Any], None] = None,
170
+ ) -> "github.Github":
171
+ """
172
+ Returns a PyGithub client for your GitHub application to act in the scope of the given *installation_id*.
173
+
174
+ Note that the client's token will expire after 10 minutes and you will have to create a new client or update the
175
+ client's token with the value returned by #jwt. It is recommended that you create a new client for each atomic
176
+ operation you perform.
177
+
178
+ This requires you to install `PyGithub>=1.58`.
179
+ """
180
+
181
+ if isinstance(settings, dict):
182
+ settings = GithubClientSettings(**settings)
183
+ elif settings is None:
184
+ settings = GithubClientSettings()
185
+
186
+ token = self.installation_token(installation_id).value
187
+ settings = self._get_base_github_client_settings().update(settings)
188
+ return settings.make_client(login_or_token=token)
@@ -0,0 +1,16 @@
1
+ from unittest.mock import patch
2
+
3
+ from github_bot_api.app import GithubApp
4
+ from github_bot_api.token import TokenInfo
5
+
6
+
7
+ def test_simple_app():
8
+ """
9
+ Regression test for PyGithub 1.58+ where such code failed on:
10
+
11
+ AttributeError: module 'github.MainClass' has no attribute 'DEFAULT_TIMEOUT'
12
+ """
13
+ token = TokenInfo(1, "Bearer", "token")
14
+ with patch("github_bot_api.token.InstallationTokenSupplier._new_token", return_value=token):
15
+ app = GithubApp("UA/0.0.0", 42, "private_key")
16
+ app.installation_client(1)
@@ -0,0 +1,95 @@
1
+ """
2
+ Abstraction of a GitHub Webhook event.
3
+
4
+ Reference: https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/webhook-events-and-payloads
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ import typing as t
10
+ from dataclasses import dataclass
11
+
12
+ from .signature import check_signature
13
+ from .utils.mime import get_mime_components
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ @dataclass
19
+ class Event:
20
+ """
21
+ Represents a GitHub webhook event.
22
+ """
23
+
24
+ name: str
25
+ """ The name of the event. Could be `pull_request`, for example."""
26
+
27
+ delivery_id: str
28
+ """The delivery ID of the event."""
29
+
30
+ signature: t.Optional[str]
31
+ """The signature of the event. Will only be set if a Webhook-secret is configured on the
32
+ client side (e.g. in [`Webhook.secret`][github_bot_api.webhook.Webhook] / if the *webhook_secret* parameter is
33
+ passed to [`accept_event()`][github_bot_api.event.accept_event])."""
34
+
35
+ user_agent: str
36
+ """The user agent invoking the webhook."""
37
+
38
+ payload: t.Dict[str, t.Any]
39
+ """The event payload."""
40
+
41
+
42
+ def accept_event(
43
+ headers: t.Mapping[str, str],
44
+ raw_body: bytes,
45
+ webhook_secret: t.Optional[str] = None,
46
+ ) -> Event:
47
+ """
48
+ Converts thee HTTP *headers* and the *raw_body* to an #Event object.
49
+
50
+ Args:
51
+ headers: The HTTP headers. Must have `X-Github-Event`, `X-Github-Delivery`, `User-Agent`, `Content-Type`.
52
+ May have `X-Hub-Signature` or `X-Hub-Signature-256`.
53
+ raw_body: The raw request body for the event. This is converted into a JSON payload.
54
+ webhook_secret: If specified, the `X-Hub-Signature` or `X-Hub-Signature-256` headers are used to verify
55
+ the signature of the payload. If not specified, the client does not validate the signature.
56
+ """
57
+
58
+ event_name = headers.get("X-GitHub-Event")
59
+ delivery_id = headers.get("X-GitHub-Delivery")
60
+ signature_1 = headers.get("X-Hub-Signature")
61
+ signature_256 = headers.get("X-Hub-Signature-256")
62
+ user_agent = headers.get("User-Agent")
63
+ content_type = headers.get("Content-Type")
64
+
65
+ if not event_name or not delivery_id or not user_agent or not content_type:
66
+ raise InvalidRequest("missing required headers")
67
+ if webhook_secret is not None and not signature_1 and not signature_256:
68
+ raise InvalidRequest("webhook secret is configured but no signature header was received")
69
+
70
+ mime_type, parameters = get_mime_components(content_type)
71
+ if mime_type != "application/json":
72
+ raise InvalidRequest(f"expected Content-Type: application/json, got {content_type}")
73
+ encoding = dict(parameters).get("encoding", "UTF-8")
74
+
75
+ if webhook_secret is not None:
76
+ if signature_256:
77
+ check_signature(signature_256, raw_body, webhook_secret.encode("ascii"), algo="sha256")
78
+ elif signature_1:
79
+ check_signature(signature_1, raw_body, webhook_secret.encode("ascii"), algo="sha1")
80
+ else:
81
+ raise RuntimeError
82
+
83
+ return Event(
84
+ event_name,
85
+ delivery_id,
86
+ signature_256 or signature_1,
87
+ user_agent,
88
+ json.loads(raw_body.decode(encoding)),
89
+ )
90
+
91
+
92
+ class InvalidRequest(Exception):
93
+ """
94
+ Raised when an invalid request is passed to #accept_event().
95
+ """
@@ -0,0 +1,61 @@
1
+ """
2
+ Flask binding for handling GitHub webhook events.
3
+
4
+ Note that you need to install the `flask` module separately.
5
+
6
+ # Example
7
+
8
+ ```python
9
+ from github_bot_api import Event, Webhook
10
+ from github_bot_api.flask import create_flask_app
11
+
12
+ def on_any_event(event: Event) -> bool:
13
+ print(event)
14
+ return True
15
+
16
+ webhook = Webhook(secret=None)
17
+ webhook.listen('*', on_any_event)
18
+
19
+ import os; os.environ['FLASK_ENV'] = 'development'
20
+ flask_app = create_flask_app(__name__, webhook)
21
+ flask_app.run()
22
+ ```
23
+ """
24
+
25
+ import typing as t
26
+
27
+ import flask
28
+
29
+ from .event import accept_event
30
+ from .webhook import Webhook
31
+
32
+
33
+ def create_event_handler(webhook: Webhook) -> t.Callable[[], t.Tuple[t.Text, int, t.Dict[str, str]]]:
34
+ """
35
+ Creates an event handler flask view that interprets the received HTTP request as a GitHub application
36
+ event and dispatches it via #webhook.dispatch().
37
+ """
38
+
39
+ def event_handler():
40
+ event = accept_event(
41
+ t.cast(t.Mapping[str, str], flask.request.headers), flask.request.get_data(), webhook.secret
42
+ )
43
+ webhook.dispatch(event)
44
+ return "", 202, {}
45
+
46
+ return event_handler
47
+
48
+
49
+ def create_flask_app(
50
+ name: str,
51
+ webhook: Webhook,
52
+ path: str = "/event-handler",
53
+ ) -> flask.Flask:
54
+ """
55
+ Creates a new #flask.Flask application with a `POST` event handler under the given *path* (defaulting
56
+ to `/event-handler`). This is a useful shorthand to attach your #Webhook to an HTTP server.
57
+ """
58
+
59
+ flask_app = flask.Flask(name)
60
+ flask_app.route(path, methods=["POST"])(create_event_handler(webhook))
61
+ return flask_app
@@ -0,0 +1,47 @@
1
+ """
2
+ Helper to check the signature of a GitHub event request.
3
+ """
4
+
5
+ import hmac
6
+
7
+
8
+ def compute_signature(payload: bytes, secret: bytes, algo: str = "sha256") -> str:
9
+ """
10
+ Computes the HMAC signature of *payload* given the specified *secret* and the given hashing *algo*.
11
+
12
+ # Parmeters
13
+ payload: The payload for which the signature should be computed.
14
+ secret: The secret string that is used in conjunction to generate the signature.
15
+ algo: The hash algorithm to use, must be `sha1` or `sha256`.
16
+ """
17
+
18
+ if algo in {"sha1", "sha256"}:
19
+ return f"{algo}={hmac.new(secret, payload, algo).hexdigest()}"
20
+ raise ValueError(f"algo must be {{sha1, sha256}}, got {algo!r}")
21
+
22
+
23
+ def check_signature(sig: str, payload: bytes, secret: bytes, algo: str = "sha256") -> None:
24
+ """
25
+ Compares the provided signature *sig* with the computed signature of the *payload* and
26
+ raises a #SignatureMismatchException if they do not match. This function uses constant-time
27
+ string comparison to prevent timing analysis.
28
+ """
29
+
30
+ computed = compute_signature(payload, secret, algo)
31
+ if not hmac.compare_digest(sig, computed):
32
+ raise SignatureMismatchException(sig, computed)
33
+
34
+
35
+ class SignatureMismatchException(Exception):
36
+ """
37
+ Raised if a signature can not be verified with #check_signature().
38
+ """
39
+
40
+ _MSG = "The provided signature does not match the computed signature of the payload."
41
+
42
+ def __init__(self, provided: str, computed: str) -> None:
43
+ self.provided = provided
44
+ self.computed = computed
45
+
46
+ def __str__(self) -> str:
47
+ return f"{self._MSG}\n provided: {self.provided}\n computed: {self.computed}"
@@ -0,0 +1,2 @@
1
+ def test_import():
2
+ import github_bot_api # noqa: F401