flask-githubkit 0.1.2__py3-none-any.whl

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.
@@ -0,0 +1,11 @@
1
+ """Flask extension for GitHub OAuth login, built on GitHubKit."""
2
+
3
+ import flask_githubkit.auth
4
+ import flask_githubkit.config
5
+ import flask_githubkit.store
6
+
7
+ GitHub = flask_githubkit.auth.GitHub
8
+ Config = flask_githubkit.config.Config
9
+ Token = flask_githubkit.store.Token
10
+
11
+ __all__ = ["Config", "GitHub", "Token"]
@@ -0,0 +1,233 @@
1
+ """GitHub OAuth web flow for Flask, backed by GitHubKit."""
2
+
3
+ import collections.abc
4
+ import functools
5
+ import typing
6
+ import urllib.parse
7
+
8
+ import flask
9
+ import githubkit
10
+ import githubkit.auth
11
+ import githubkit.exception
12
+ import httpx
13
+ import werkzeug.routing
14
+ import werkzeug.wrappers
15
+
16
+ import flask_githubkit.config
17
+ import flask_githubkit.store
18
+
19
+ AUTHORIZE_URL = "https://github.com/login/oauth/authorize"
20
+ CLIENT_ATTR = "flask_githubkit_client"
21
+ LOGIN_VIEW = "login"
22
+ LOGIN_MARK = "_flask_githubkit_login"
23
+
24
+ ViewResult = (
25
+ werkzeug.wrappers.Response | tuple[werkzeug.wrappers.Response, int]
26
+ )
27
+
28
+
29
+ class GitHub:
30
+ """GitHub OAuth web flow for Flask, backed by GitHubKit.
31
+
32
+ ``login`` starts the flow and ``callback`` wraps the callback view,
33
+ passing it the access token (or ``None`` if the user denied
34
+ access). ``client`` returns one GitHub client per request for the
35
+ signed-in user. ``init_app`` sends users to the endpoint named by
36
+ ``GITHUB_LOGIN_VIEW`` (default ``login``) when their refresh token
37
+ has expired.
38
+
39
+ ``scope`` and ``login_view`` may be given here. When they are not,
40
+ they come from ``GITHUB_SCOPE`` and ``GITHUB_LOGIN_VIEW`` in the
41
+ Flask config. The scope then defaults to ``read:user``. The login
42
+ view defaults to the view decorated with ``@login``, or to
43
+ ``login`` if there is none. Tokens are kept by ``store.Token`` in
44
+ ``flask.session``.
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ app: flask.Flask | None = None,
50
+ *,
51
+ scope: str | None = None,
52
+ login_view: str | None = None,
53
+ ) -> None:
54
+ self.scope = scope
55
+ self.login_view = login_view
56
+ if app is not None:
57
+ self.init_app(app)
58
+
59
+ def init_app(self, app: flask.Flask) -> None:
60
+ app.register_error_handler(
61
+ githubkit.exception.AuthExpiredError, self._expired
62
+ )
63
+
64
+ @typing.overload
65
+ def login(self) -> ViewResult: ...
66
+
67
+ @typing.overload
68
+ def login(
69
+ self, view_func: collections.abc.Callable[..., typing.Any]
70
+ ) -> collections.abc.Callable[..., ViewResult]: ...
71
+
72
+ def login(
73
+ self,
74
+ view_func: collections.abc.Callable[..., typing.Any] | None = None,
75
+ ) -> ViewResult | collections.abc.Callable[..., ViewResult]:
76
+ """Start the OAuth flow.
77
+
78
+ Call ``login()`` inside a view, or use ``@github.login`` as a
79
+ decorator. The decorated function is replaced by one that
80
+ redirects to GitHub, and keeps its name for ``url_for``.
81
+ """
82
+ if view_func is None:
83
+ return self._redirect()
84
+
85
+ @functools.wraps(view_func)
86
+ def wrapper(*args: typing.Any, **kwargs: typing.Any) -> ViewResult:
87
+ return self._redirect()
88
+
89
+ setattr(wrapper, LOGIN_MARK, self)
90
+ return wrapper
91
+
92
+ def _redirect(self) -> ViewResult:
93
+ config = self._config()
94
+ if config is None:
95
+ return self._error("GitHub OAuth is not configured.", 500)
96
+ return flask.redirect(self._url(config))
97
+
98
+ def callback(
99
+ self, view_func: collections.abc.Callable[..., ViewResult]
100
+ ) -> collections.abc.Callable[..., ViewResult]:
101
+ @functools.wraps(view_func)
102
+ def wrapper(*args: typing.Any, **kwargs: typing.Any) -> ViewResult:
103
+ if not self._check_state():
104
+ return self._error("Invalid OAuth state.", 400)
105
+
106
+ if flask.request.args.get("error"):
107
+ return view_func(None, *args, **kwargs)
108
+
109
+ code = flask.request.args.get("code")
110
+ if not code:
111
+ return self._error(
112
+ "GitHub did not return an authorization code.", 400
113
+ )
114
+
115
+ config = self._config()
116
+ if config is None:
117
+ return self._error("GitHub OAuth is not configured.", 500)
118
+
119
+ try:
120
+ strategy = self._exchange(code, config)
121
+ except (githubkit.exception.RequestError, httpx.HTTPError):
122
+ return self._error("Could not reach GitHub.", 502)
123
+ flask_githubkit.store.Token.save(strategy)
124
+ return view_func(strategy.token, *args, **kwargs)
125
+
126
+ return wrapper
127
+
128
+ def client(self) -> "githubkit.GitHub[typing.Any] | None":
129
+ """Return the request's GitHub client, or None if signed out.
130
+
131
+ The client is created once per request, so a token refresh
132
+ happens once and every caller sees the new token. GitHubKit
133
+ refreshes an expired access token itself; the refreshed values
134
+ are saved to the session at the end of the request.
135
+ """
136
+ clients: dict[GitHub, githubkit.GitHub[typing.Any]] | None = getattr(
137
+ flask.g, CLIENT_ATTR, None
138
+ )
139
+ if clients is None:
140
+ clients = {}
141
+ setattr(flask.g, CLIENT_ATTR, clients)
142
+
143
+ cached = clients.get(self)
144
+ if cached is not None:
145
+ return cached
146
+
147
+ config = self._config()
148
+ if config is None:
149
+ return None
150
+ strategy = flask_githubkit.store.Token.load(config)
151
+ if strategy is None:
152
+ return None
153
+
154
+ def _save(
155
+ response: werkzeug.wrappers.Response,
156
+ ) -> werkzeug.wrappers.Response:
157
+ if flask_githubkit.store.Token.exists():
158
+ flask_githubkit.store.Token.save(strategy)
159
+ return response
160
+
161
+ flask.after_this_request(_save)
162
+ github = githubkit.GitHub(auth=strategy)
163
+ clients[self] = github
164
+ return github
165
+
166
+ def logout(self) -> None:
167
+ flask_githubkit.store.Token.clear()
168
+ flask.g.pop(CLIENT_ATTR, None)
169
+
170
+ @staticmethod
171
+ def _config() -> flask_githubkit.config.Config | None:
172
+ return flask_githubkit.config.Config.from_current_app()
173
+
174
+ def _url(self, config: flask_githubkit.config.Config) -> str:
175
+ query = urllib.parse.urlencode(
176
+ {
177
+ "client_id": config.client_id,
178
+ "redirect_uri": config.redirect_uri,
179
+ "scope": self.scope or config.scope,
180
+ "state": flask_githubkit.store.State.new(),
181
+ }
182
+ )
183
+ return f"{AUTHORIZE_URL}?{query}"
184
+
185
+ @staticmethod
186
+ def _check_state() -> bool:
187
+ received = flask.request.args.get("state", "")
188
+ return flask_githubkit.store.State.check(received)
189
+
190
+ @staticmethod
191
+ def _exchange(
192
+ code: str,
193
+ config: flask_githubkit.config.Config,
194
+ ) -> githubkit.auth.OAuthTokenAuthStrategy:
195
+ app = githubkit.auth.OAuthAppAuthStrategy(
196
+ client_id=config.client_id,
197
+ client_secret=config.client_secret,
198
+ )
199
+ return app.as_web_user(
200
+ code, redirect_uri=config.redirect_uri
201
+ ).exchange_token(githubkit.GitHub(auth=app))
202
+
203
+ def _expired(
204
+ self, error: githubkit.exception.AuthExpiredError
205
+ ) -> werkzeug.wrappers.Response:
206
+ self.logout()
207
+ view = (
208
+ self.login_view
209
+ or flask.current_app.config.get("GITHUB_LOGIN_VIEW")
210
+ or self._decorated_endpoint()
211
+ or LOGIN_VIEW
212
+ )
213
+ try:
214
+ return flask.redirect(flask.url_for(view))
215
+ except werkzeug.routing.BuildError as build_error:
216
+ raise RuntimeError(
217
+ f"GITHUB_LOGIN_VIEW {view!r} is not a Flask endpoint. "
218
+ "Use the endpoint name, such as 'auth.login' for a "
219
+ "blueprint named 'auth'."
220
+ ) from build_error
221
+
222
+ def _decorated_endpoint(self) -> str | None:
223
+ """Return the endpoint of the view decorated with ``@login``."""
224
+ for endpoint, view in flask.current_app.view_functions.items():
225
+ if getattr(view, LOGIN_MARK, None) is self:
226
+ return endpoint
227
+ return None
228
+
229
+ @staticmethod
230
+ def _error(
231
+ message: str, status: int
232
+ ) -> tuple[werkzeug.wrappers.Response, int]:
233
+ return flask.jsonify({"message": message}), status
@@ -0,0 +1,47 @@
1
+ """OAuth App settings read from the Flask application config."""
2
+
3
+ import dataclasses
4
+
5
+ import flask
6
+
7
+ SCOPE = "read:user"
8
+
9
+
10
+ @dataclasses.dataclass(frozen=True)
11
+ class Config:
12
+ """OAuth App settings read from the Flask app config.
13
+
14
+ Reads ``GITHUB_CLIENT_ID``, ``GITHUB_CLIENT_SECRET`` and
15
+ ``GITHUB_REDIRECT_URI``. ``SECRET_KEY`` must also be set, because
16
+ the OAuth state and tokens are kept in ``flask.session``.
17
+ ``GITHUB_SCOPE`` is optional: the space-separated OAuth scope to
18
+ request, ``read:user`` by default.
19
+ """
20
+
21
+ client_id: str
22
+ client_secret: str
23
+ redirect_uri: str
24
+ scope: str = SCOPE
25
+
26
+ @staticmethod
27
+ def from_current_app() -> "Config | None":
28
+ app_config = flask.current_app.config
29
+ client_id = app_config.get("GITHUB_CLIENT_ID")
30
+ client_secret = app_config.get("GITHUB_CLIENT_SECRET")
31
+ redirect_uri = app_config.get("GITHUB_REDIRECT_URI")
32
+ scope = app_config.get("GITHUB_SCOPE", SCOPE)
33
+ secret_key = app_config.get("SECRET_KEY")
34
+ if not all(
35
+ isinstance(value, str) and value
36
+ for value in (
37
+ client_id,
38
+ client_secret,
39
+ redirect_uri,
40
+ scope,
41
+ secret_key,
42
+ )
43
+ ):
44
+ return None
45
+ return Config(
46
+ str(client_id), str(client_secret), str(redirect_uri), str(scope)
47
+ )
File without changes
@@ -0,0 +1,92 @@
1
+ """Everything flask_githubkit keeps in the Flask session."""
2
+
3
+ import datetime
4
+ import hmac
5
+ import secrets
6
+
7
+ import flask
8
+ import githubkit.auth
9
+ import githubkit.exception
10
+
11
+ import flask_githubkit.config
12
+
13
+ TOKEN_KEY = "flask_githubkit.token"
14
+ STATE_KEY = "flask_githubkit.state"
15
+ DATETIME_KEYS = ("expire_time", "refresh_token_expire_time")
16
+
17
+
18
+ class Token:
19
+ """Keeps a user's GitHub tokens in ``flask.session``.
20
+
21
+ With a server-side session backend such as Redis, the browser only
22
+ holds the session id. Sessions are marked permanent, so
23
+ ``PERMANENT_SESSION_LIFETIME`` is their expiry and each save resets
24
+ it.
25
+ """
26
+
27
+ @staticmethod
28
+ def save(strategy: githubkit.auth.OAuthTokenAuthStrategy) -> None:
29
+ payload: dict[str, str | None] = {
30
+ "token": strategy.token,
31
+ "refresh_token": strategy.refresh_token,
32
+ }
33
+ for key in DATETIME_KEYS:
34
+ value = getattr(strategy, key)
35
+ payload[key] = value.isoformat() if value else None
36
+
37
+ flask.session.permanent = True
38
+ flask.session[TOKEN_KEY] = payload
39
+
40
+ @staticmethod
41
+ def load(
42
+ config: flask_githubkit.config.Config,
43
+ ) -> githubkit.auth.OAuthTokenAuthStrategy | None:
44
+ payload = flask.session.get(TOKEN_KEY)
45
+ if not isinstance(payload, dict):
46
+ return None
47
+
48
+ try:
49
+ return githubkit.auth.OAuthTokenAuthStrategy(
50
+ client_id=config.client_id,
51
+ client_secret=config.client_secret,
52
+ token=payload.get("token"),
53
+ refresh_token=payload.get("refresh_token"),
54
+ **{
55
+ key: datetime.datetime.fromisoformat(payload[key])
56
+ if payload.get(key)
57
+ else None
58
+ for key in DATETIME_KEYS
59
+ },
60
+ )
61
+ except (
62
+ githubkit.exception.AuthCredentialError,
63
+ TypeError,
64
+ ValueError,
65
+ ):
66
+ Token.clear()
67
+ return None
68
+
69
+ @staticmethod
70
+ def clear() -> None:
71
+ flask.session.pop(TOKEN_KEY, None)
72
+
73
+ @staticmethod
74
+ def exists() -> bool:
75
+ return TOKEN_KEY in flask.session
76
+
77
+
78
+ class State:
79
+ """Keeps the one-time OAuth ``state`` value in ``flask.session``."""
80
+
81
+ @staticmethod
82
+ def new() -> str:
83
+ state = secrets.token_urlsafe(32)
84
+ flask.session[STATE_KEY] = state
85
+ return state
86
+
87
+ @staticmethod
88
+ def check(received: str) -> bool:
89
+ expected = flask.session.pop(STATE_KEY, None)
90
+ return isinstance(expected, str) and hmac.compare_digest(
91
+ expected, received
92
+ )
@@ -0,0 +1,259 @@
1
+ Metadata-Version: 2.4
2
+ Name: flask-githubkit
3
+ Version: 0.1.2
4
+ Summary: Flask extension for GitHub OAuth login, built on GitHubKit. Handles expiring-token refresh and server-side sessions.
5
+ Author: LJFCoding
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/LJFCoding/flask-githubkit
8
+ Project-URL: Repository, https://github.com/LJFCoding/flask-githubkit
9
+ Project-URL: Issues, https://github.com/LJFCoding/flask-githubkit/issues
10
+ Keywords: flask,github,githubkit,oauth,authentication
11
+ Classifier: Framework :: Flask
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Internet :: WWW/HTTP :: Session
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: Flask>=3.0
22
+ Requires-Dist: githubkit<1.0,>=0.16.1
23
+ Requires-Dist: httpx<1.0,>=0.23
24
+ Provides-Extra: test
25
+ Requires-Dist: pytest; extra == "test"
26
+ Requires-Dist: coverage; extra == "test"
27
+ Provides-Extra: lint
28
+ Requires-Dist: ruff; extra == "lint"
29
+ Requires-Dist: mypy; extra == "lint"
30
+ Dynamic: license-file
31
+
32
+ # flask-githubkit
33
+
34
+ A Flask extension for GitHub OAuth login, built on
35
+ [GitHubKit](https://github.com/yanyongyu/githubkit). It handles expiring-token
36
+ refresh and stores tokens in `flask.session`.
37
+
38
+ ## Installation
39
+
40
+ Install the release directly from GitHub:
41
+
42
+ ```console
43
+ pip install "flask-githubkit @ git+https://github.com/LJFCoding/flask-githubkit.git@v0.1.2"
44
+ ```
45
+
46
+ It is not published on PyPI yet, so `pip install flask-githubkit` will not
47
+ find it. It requires Python 3.10+ and installs Flask, GitHubKit and httpx.
48
+
49
+ For local development, install the test dependencies with
50
+ `pip install -e ".[test]"`.
51
+
52
+ ## How it works
53
+
54
+ 1. `github.login()` redirects the user to GitHub with a random `state` value
55
+ that is kept in `flask.session`.
56
+ 2. GitHub redirects back to your callback URL with a `code` and the `state`.
57
+ 3. `@github.callback` checks the `state` (blocking login CSRF), exchanges the
58
+ `code` for tokens through GitHubKit, saves them in `flask.session`, and
59
+ calls your view with the access token.
60
+ 4. In any later request, `github.client()` returns a GitHubKit client for that
61
+ user. GitHubKit refreshes an expired access token itself, and the new
62
+ token values are saved back to the session when the request finishes.
63
+
64
+ The names follow [Flask-GitHub](https://github.com/cenkalti/github-flask), but
65
+ the API is smaller and the HTTP calls are made by
66
+ [GitHubKit](https://github.com/yanyongyu/githubkit), so you get its typed
67
+ `rest` methods instead of raw `get`/`post` helpers. Only synchronous Flask
68
+ apps are supported.
69
+
70
+ ## Quick start
71
+
72
+ Register a GitHub OAuth App and set its callback URL to the value of
73
+ `GITHUB_REDIRECT_URI`. Enable **Expire user authorization tokens** in the
74
+ OAuth App settings if you want GitHub to issue refresh tokens.
75
+
76
+ ```python
77
+ import flask
78
+ import flask_githubkit
79
+
80
+ app = flask.Flask(__name__)
81
+ app.config.update(
82
+ SECRET_KEY="load-this-from-a-secret-manager",
83
+ GITHUB_CLIENT_ID="your-client-id",
84
+ GITHUB_CLIENT_SECRET="load-this-from-a-secret-manager",
85
+ GITHUB_REDIRECT_URI="https://example.com/callback",
86
+ GITHUB_SCOPE="read:user",
87
+ )
88
+
89
+ github = flask_githubkit.GitHub(app)
90
+
91
+
92
+ @app.route("/login")
93
+ @github.login
94
+ def login():
95
+ """Replaced by a redirect to GitHub."""
96
+
97
+
98
+ @app.route("/callback")
99
+ @github.callback
100
+ def callback(access_token):
101
+ if access_token is None:
102
+ return flask.jsonify(
103
+ {"message": "GitHub authorization was denied"}
104
+ ), 401
105
+ return flask.jsonify({"message": "Signed in"})
106
+
107
+
108
+ @app.route("/me")
109
+ def me():
110
+ api = github.client()
111
+ if api is None:
112
+ return flask.redirect(flask.url_for("login"))
113
+ user = api.rest.users.get_authenticated().parsed_data
114
+ return flask.jsonify({"login": user.login})
115
+ ```
116
+
117
+ `callback` validates the OAuth `state`, exchanges the authorization
118
+ code, stores the token data, and calls the decorated view with the access token.
119
+ If the user denies access, the view receives `None`. Protect callback handling
120
+ with the decorator as shown; it is responsible for validating `state`.
121
+
122
+ With an application factory, create the extension once and initialize it for
123
+ each app:
124
+
125
+ ```python
126
+ github = flask_githubkit.GitHub()
127
+
128
+
129
+ def create_app():
130
+ app = flask.Flask(__name__)
131
+ app.config.from_mapping(
132
+ SECRET_KEY="load-this-from-a-secret-manager",
133
+ GITHUB_CLIENT_ID="your-client-id",
134
+ GITHUB_CLIENT_SECRET="load-this-from-a-secret-manager",
135
+ GITHUB_REDIRECT_URI="https://example.com/callback",
136
+ GITHUB_SCOPE="read:user",
137
+ )
138
+ github.init_app(app)
139
+ return app
140
+ ```
141
+
142
+ ## Examples
143
+
144
+ Each file in [examples/](examples) is a complete app. They are checked by the
145
+ test suite, which confirms the login route redirects to GitHub.
146
+
147
+ | File | Shows |
148
+ | --- | --- |
149
+ | [single_file.py](examples/single_file.py) | A module-level app with `GitHub(app)` |
150
+ | [factory.py](examples/factory.py) | An app factory with `init_app` and the optional config keys |
151
+ | [blueprint.py](examples/blueprint.py) | Login routes in a blueprint; the `auth.login` endpoint is found from `@github.login` |
152
+ | [routes.py](examples/routes.py) | A `login_required` decorator and a route that calls the GitHub API |
153
+
154
+ Set `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET` and `SECRET_KEY` in your
155
+ environment, then run one with, for example,
156
+ `flask --app examples/single_file run`.
157
+
158
+ Calling the API goes through GitHubKit, so see its
159
+ [documentation](https://yanyongyu.github.io/githubkit/) for everything
160
+ `client().rest` offers.
161
+
162
+ ## Coming from Flask-GitHub
163
+
164
+ | Flask-GitHub | flask-githubkit |
165
+ | --- | --- |
166
+ | `GitHub(app)` | `GitHub(app)` or `init_app(app)` |
167
+ | `github.authorize()` | `@github.login`, or `github.login()` in a view |
168
+ | `@github.authorized_handler` | `@github.callback` |
169
+ | `@github.access_token_getter` | Not needed: tokens are kept in `flask.session` |
170
+ | `github.get("user")` | `github.client().rest.users.get_authenticated()` |
171
+ | `authorize(scope=..., redirect_uri=...)` | Not supported: use `scope=` in `GitHub(...)` or `GITHUB_SCOPE`, and `GITHUB_REDIRECT_URI` |
172
+
173
+ ## Configuration and API
174
+
175
+ | Flask config key | Required | Purpose |
176
+ | --- | --- | --- |
177
+ | `GITHUB_CLIENT_ID` | Yes | GitHub OAuth App client ID |
178
+ | `GITHUB_CLIENT_SECRET` | Yes | GitHub OAuth App client secret |
179
+ | `GITHUB_REDIRECT_URI` | Yes | Callback URL registered with the OAuth App |
180
+ | `SECRET_KEY` | Yes | Flask session signing key |
181
+ | `GITHUB_SCOPE` | No | Space-separated OAuth scopes to request. Default `read:user`. |
182
+ | `GITHUB_LOGIN_VIEW` | No | Endpoint name users are sent to when their refresh token has expired. Usually not needed: see below. It is an endpoint name such as `auth.login`, not a URL. |
183
+
184
+ `flask_githubkit.GitHub(app=None, *, scope=None, login_view=None)` accepts an
185
+ optional Flask app. Use `init_app(app)` to initialize it later.
186
+
187
+ `scope` and `login_view` are optional. Each one is taken from the constructor
188
+ if you pass it, otherwise from `GITHUB_SCOPE` or `GITHUB_LOGIN_VIEW` in the
189
+ Flask config. If neither is set, the scope is `read:user`. The login view is
190
+ the view you decorated with `@github.login`, with its blueprint prefix
191
+ (`auth.login`), or an endpoint named `login` if there is no decorated view.
192
+ The client ID, client secret, redirect URI and `SECRET_KEY` always come from
193
+ the Flask config.
194
+
195
+ ```python
196
+ github = flask_githubkit.GitHub(
197
+ scope="read:user repo", login_view="auth.login"
198
+ )
199
+ ```
200
+
201
+ - `@github.login` turns a view into a redirect to GitHub that starts the
202
+ OAuth flow. You can also call `github.login()` inside a view and return
203
+ the result, which is useful if you want to run code first. The decorated
204
+ function's body never runs.
205
+ - `@callback` wraps the callback view.
206
+ - `client()` returns the request-cached GitHubKit `GitHub` client, or `None`
207
+ when there is no valid stored token.
208
+ - `logout()` removes the stored GitHub token from the session.
209
+
210
+ The extension registers an `AuthExpiredError` handler that clears the stored
211
+ token and redirects to the login view. GitHubKit also raises
212
+ `AuthExpiredError` when GitHub rejects the authorization code, so a stale or
213
+ reused code sends the user back to login. If GitHub cannot be reached while
214
+ exchanging the code (a timeout, a connection error or a 5xx response), the
215
+ callback returns a `502` JSON error, `Could not reach GitHub.`, and does not
216
+ call your view. If the login view is not a real
217
+ endpoint, the handler raises a `RuntimeError` that says so.
218
+
219
+ `flask_githubkit.Config.from_current_app()` reads the configuration keys above.
220
+ `flask_githubkit.Token` serializes token expiry datetimes as ISO-formatted
221
+ strings in `flask.session`.
222
+
223
+ ## Sessions, token refresh, and limitations
224
+
225
+ The default Flask session is a signed, client-side cookie; it is **not
226
+ encrypted**, so users can read its contents. This package stores OAuth token
227
+ data there, so use a server-side session backend such as Flask-Session with
228
+ Redis in production. The extension only uses `flask.session` and marks the
229
+ session permanent when it saves tokens. Configure your backend and
230
+ `PERMANENT_SESSION_LIFETIME` (for example, 30 days) in your application; sliding
231
+ expiry and TTL behavior depend on that backend and are not provided by this
232
+ package.
233
+
234
+ GitHubKit refreshes expired access tokens when the client is used. The extension
235
+ writes updated token values back to the session after the request. GitHub
236
+ refresh tokens are single-use, so concurrent requests can race while refreshing;
237
+ this package does not provide a distributed lock. Errors raised while your own
238
+ code uses `client()` (for example a network failure during an API call) are not
239
+ caught by the extension.
240
+
241
+ This package supports synchronous Flask apps and GitHub OAuth Apps. It does not
242
+ support GitHub Apps or configure GitHub Enterprise endpoints. Real GitHub code
243
+ exchange and refresh, Redis operation, and Flask-Session TTL behavior require
244
+ integration testing against those services and are not verified by the
245
+ automated tests in this repository.
246
+
247
+ ## Tests
248
+
249
+ ```console
250
+ pip install -e ".[test]"
251
+ pytest
252
+ ```
253
+
254
+ CI also requires 100% line and branch coverage:
255
+
256
+ ```console
257
+ coverage run -m pytest
258
+ coverage report
259
+ ```
@@ -0,0 +1,10 @@
1
+ flask_githubkit/__init__.py,sha256=AZt9expycUWgbxbKHIWtjnLHId-5ML13tepLQAeX3MU,308
2
+ flask_githubkit/auth.py,sha256=fDO-oSCFEywqNQLh_w_11qHtvPwJlhjy2kXGc7iprkE,7919
3
+ flask_githubkit/config.py,sha256=28OOjlIpBjHAMOsisu1sJcl7XRmkUesqAi9LAfrVgiQ,1414
4
+ flask_githubkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ flask_githubkit/store.py,sha256=2PzbYa8-dMD5_vHTZDjxb0jhClWRNVc3qVk84lbnHHw,2645
6
+ flask_githubkit-0.1.2.dist-info/licenses/LICENSE,sha256=M7lCXlVXCyRAtQabbdCdtyn9PNxUSHCYi-fAz3jUmyI,1066
7
+ flask_githubkit-0.1.2.dist-info/METADATA,sha256=demhXsk-LceMVsl2ayaNpKi3ZJfCNUQpJs2rLUt7yyE,10269
8
+ flask_githubkit-0.1.2.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
9
+ flask_githubkit-0.1.2.dist-info/top_level.txt,sha256=lDJ7tJYO7dwMWR59OaihDiG7Hm3WG4MYYUiko-aeHS8,16
10
+ flask_githubkit-0.1.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LJFCoding
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ flask_githubkit