mongo-oidc-human-callback 0.1.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.
@@ -0,0 +1,30 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ id-token: write
13
+ contents: read
14
+ steps:
15
+ - name: Checkout
16
+ uses: actions/checkout@v4
17
+
18
+ - name: Install uv
19
+ uses: astral-sh/setup-uv@v7
20
+ with:
21
+ enable-cache: true
22
+
23
+ - name: Set up Python
24
+ run: uv python install
25
+
26
+ - name: Build
27
+ run: uv build
28
+
29
+ - name: Publish to PyPI
30
+ run: uv publish
@@ -0,0 +1,17 @@
1
+ # Build
2
+ dist/
3
+ build/
4
+ *.egg-info/
5
+ *.egg
6
+
7
+ # Python
8
+ __pycache__/
9
+ *.py[cod]
10
+ .venv/
11
+ venv/
12
+ .env
13
+
14
+ # IDE
15
+ .idea/
16
+ .vscode/
17
+ *.swp
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Reouven Mimoun
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,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: mongo-oidc-human-callback
3
+ Version: 0.1.0
4
+ Summary: OIDC Human Callback for MongoDB: browser-based OAuth 2.0 Authorization Code Flow with PKCE (Azure AD and other OIDC providers).
5
+ Project-URL: Repository, https://github.com/mreouven/mongo-oidc-human-callback
6
+ Project-URL: Documentation, https://github.com/mreouven/mongo-oidc-human-callback#readme
7
+ Author: Reouven Mimoun
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: atlas,authentication,azure-ad,mongodb,oauth2,oidc,pkce
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Database
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: pymongo>=4.14
23
+ Description-Content-Type: text/markdown
24
+
25
+ # mongo-oidc-human-callback
26
+
27
+ OIDC Human Callback for MongoDB Atlas authentication: OAuth 2.0 Authorization Code flow with PKCE (Azure AD and other OIDC providers).
28
+
29
+ **Single dependency**: `pymongo` (stdlib for the rest: `urllib`, `logging`, `http.server`, etc.).
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install mongo-oidc-human-callback
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ```python
40
+ from pymongo import MongoClient
41
+ from mongo_oidc_human_callback import OIDCHumanCallback
42
+
43
+ # Connect with human callback (browser opens + PKCE)
44
+ client = MongoClient(
45
+ "mongodb+srv://<cluster>.mongodb.net/",
46
+ authMechanism="MONGODB-OIDC",
47
+ authMechanismProperties={"OIDC_CALLBACK": OIDCHumanCallback()},
48
+ )
49
+ # First access: browser opens to sign in (e.g. Azure AD)
50
+ client.admin.command("ping")
51
+ ```
52
+
53
+ ### Callback options
54
+
55
+ ```python
56
+ OIDCHumanCallback(
57
+ redirect_path="redirect", # or "callback" depending on Azure AD config
58
+ port=27097, # local callback server port
59
+ )
60
+ ```
61
+
62
+ ## Features
63
+
64
+ - **PKCE** (Proof Key for Code Exchange)
65
+ - **Token caching** to avoid re-authenticating on each connection
66
+ - **Local HTTP server** to receive the OAuth redirect
67
+ - **Automatic browser opening**
68
+ - **Azure AD compatibility** (endpoints `/oauth2/v2.0/authorize` and `/oauth2/v2.0/token`)
69
+
70
+ ## Azure AD configuration
71
+
72
+ In the Azure portal, for your app registration:
73
+
74
+ 1. **Authentication** → Redirect URI: `http://localhost:27097/redirect` (or the `port`/`redirect_path` you use).
75
+ 2. **API permissions** → Microsoft Graph (or target API): e.g. `openid`, `profile`, `email`.
76
+
77
+ ## Publishing (PyPI)
78
+
79
+ ### Via GitHub Actions
80
+
81
+ The `.github/workflows/publish-pypi.yml` workflow publishes to PyPI:
82
+
83
+ - **On each release**: create a release (tag) on GitHub → the workflow runs.
84
+ - **On demand**: Actions → Publish to PyPI → Run workflow.
85
+
86
+ **PyPI setup (trusted publishing, no secrets):**
87
+
88
+ 1. PyPI → your project → **Publishing** → **Add a new trusted publisher**.
89
+ 2. Enter **exactly** (as on GitHub):
90
+ - **Owner**: `mreouven` (your GitHub username, not display name)
91
+ - **Repository**: `mongo-oidc-human-callback`
92
+ - **Workflow**: `publish-pypi.yml`
93
+ 3. Save. Future workflow runs will publish without `PYPI_API_TOKEN`.
94
+
95
+ If you get `invalid-publisher`, ensure Owner is the GitHub username of the repo (e.g. `mreouven`), not the org or display name.
96
+
97
+ **Alternative (API token):** add the `PYPI_API_TOKEN` secret in the repo (Settings → Secrets) and use a workflow that sets `TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}` if you prefer not to use trusted publishing.
98
+
99
+ ### From your machine
100
+
101
+ ```bash
102
+ pip install build twine
103
+ python -m build
104
+ twine upload dist/*
105
+ ```
106
+
107
+ ## License
108
+
109
+ MIT
@@ -0,0 +1,85 @@
1
+ # mongo-oidc-human-callback
2
+
3
+ OIDC Human Callback for MongoDB Atlas authentication: OAuth 2.0 Authorization Code flow with PKCE (Azure AD and other OIDC providers).
4
+
5
+ **Single dependency**: `pymongo` (stdlib for the rest: `urllib`, `logging`, `http.server`, etc.).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install mongo-oidc-human-callback
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```python
16
+ from pymongo import MongoClient
17
+ from mongo_oidc_human_callback import OIDCHumanCallback
18
+
19
+ # Connect with human callback (browser opens + PKCE)
20
+ client = MongoClient(
21
+ "mongodb+srv://<cluster>.mongodb.net/",
22
+ authMechanism="MONGODB-OIDC",
23
+ authMechanismProperties={"OIDC_CALLBACK": OIDCHumanCallback()},
24
+ )
25
+ # First access: browser opens to sign in (e.g. Azure AD)
26
+ client.admin.command("ping")
27
+ ```
28
+
29
+ ### Callback options
30
+
31
+ ```python
32
+ OIDCHumanCallback(
33
+ redirect_path="redirect", # or "callback" depending on Azure AD config
34
+ port=27097, # local callback server port
35
+ )
36
+ ```
37
+
38
+ ## Features
39
+
40
+ - **PKCE** (Proof Key for Code Exchange)
41
+ - **Token caching** to avoid re-authenticating on each connection
42
+ - **Local HTTP server** to receive the OAuth redirect
43
+ - **Automatic browser opening**
44
+ - **Azure AD compatibility** (endpoints `/oauth2/v2.0/authorize` and `/oauth2/v2.0/token`)
45
+
46
+ ## Azure AD configuration
47
+
48
+ In the Azure portal, for your app registration:
49
+
50
+ 1. **Authentication** → Redirect URI: `http://localhost:27097/redirect` (or the `port`/`redirect_path` you use).
51
+ 2. **API permissions** → Microsoft Graph (or target API): e.g. `openid`, `profile`, `email`.
52
+
53
+ ## Publishing (PyPI)
54
+
55
+ ### Via GitHub Actions
56
+
57
+ The `.github/workflows/publish-pypi.yml` workflow publishes to PyPI:
58
+
59
+ - **On each release**: create a release (tag) on GitHub → the workflow runs.
60
+ - **On demand**: Actions → Publish to PyPI → Run workflow.
61
+
62
+ **PyPI setup (trusted publishing, no secrets):**
63
+
64
+ 1. PyPI → your project → **Publishing** → **Add a new trusted publisher**.
65
+ 2. Enter **exactly** (as on GitHub):
66
+ - **Owner**: `mreouven` (your GitHub username, not display name)
67
+ - **Repository**: `mongo-oidc-human-callback`
68
+ - **Workflow**: `publish-pypi.yml`
69
+ 3. Save. Future workflow runs will publish without `PYPI_API_TOKEN`.
70
+
71
+ If you get `invalid-publisher`, ensure Owner is the GitHub username of the repo (e.g. `mreouven`), not the org or display name.
72
+
73
+ **Alternative (API token):** add the `PYPI_API_TOKEN` secret in the repo (Settings → Secrets) and use a workflow that sets `TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}` if you prefer not to use trusted publishing.
74
+
75
+ ### From your machine
76
+
77
+ ```bash
78
+ pip install build twine
79
+ python -m build
80
+ twine upload dist/*
81
+ ```
82
+
83
+ ## License
84
+
85
+ MIT
@@ -0,0 +1,11 @@
1
+ """
2
+ OIDC Human Callback for MongoDB authentication.
3
+
4
+ Browser-based OAuth 2.0 Authorization Code Flow with PKCE for MongoDB Atlas
5
+ (Azure AD and other OIDC providers).
6
+ """
7
+
8
+ from mongo_oidc_human_callback.callback import OIDCHumanCallback
9
+
10
+ __all__ = ["OIDCHumanCallback"]
11
+ __version__ = "0.1.0"
@@ -0,0 +1,364 @@
1
+ """
2
+ OIDC Human Callback implementation for MongoDB authentication.
3
+
4
+ OAuth 2.0 Authorization Code Flow with PKCE for MongoDB Atlas
5
+ (Azure AD and other OIDC providers).
6
+ """
7
+ import base64
8
+ import hashlib
9
+ import http.server
10
+ import json
11
+ import logging
12
+ import secrets
13
+ import socketserver
14
+ import time
15
+ import urllib.error
16
+ import urllib.parse
17
+ import urllib.request
18
+ import webbrowser
19
+ from threading import Event, Thread
20
+ from typing import Any, Dict, Optional
21
+
22
+ from pymongo.auth_oidc import (
23
+ OIDCCallback,
24
+ OIDCCallbackContext,
25
+ OIDCCallbackResult,
26
+ )
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ # Global cache for OIDC tokens to avoid re-authentication
31
+ _oidc_token_cache: Dict[str, Dict[str, Any]] = {}
32
+
33
+
34
+ class OIDCHumanCallback(OIDCCallback):
35
+ """
36
+ OIDC Human Callback for browser-based authentication with PKCE.
37
+
38
+ Implements OAuth 2.0 Authorization Code Flow with PKCE.
39
+
40
+ Features:
41
+ - PKCE (Proof Key for Code Exchange)
42
+ - Token caching to avoid repeated authentication
43
+ - Local HTTP server for callback handling
44
+ - Browser auto-open
45
+ - Azure AD endpoint compatibility
46
+ """
47
+
48
+ def __init__(self, redirect_path: str = "redirect", port: int = 27097):
49
+ """
50
+ Initialize the callback handler.
51
+
52
+ Args:
53
+ redirect_path: Path for the redirect URI (e.g. "redirect" or "callback").
54
+ port: Port for the local callback server.
55
+ """
56
+ self._token_cache = _oidc_token_cache
57
+ self._redirect_path = redirect_path
58
+ self._port = port
59
+
60
+ def _get_cache_key(self, context: OIDCCallbackContext) -> str:
61
+ """Generate a cache key from the context."""
62
+ if context.idp_info:
63
+ return (
64
+ f"{context.idp_info.issuer}:"
65
+ f"{context.idp_info.clientId}:"
66
+ f"{context.username}"
67
+ )
68
+ return f"default:{context.username}"
69
+
70
+ def _get_cached_token(self, cache_key: str) -> Optional[OIDCCallbackResult]:
71
+ """Return a cached token if valid (with 60s buffer), else None."""
72
+ if cache_key not in self._token_cache:
73
+ return None
74
+
75
+ cached = self._token_cache[cache_key]
76
+ if cached.get("expires_at", 0) > time.time() + 60:
77
+ logger.debug("Using cached OIDC token for %s", cache_key)
78
+ return OIDCCallbackResult(
79
+ access_token=cached["access_token"],
80
+ expires_in_seconds=cached.get("expires_at", 0) - time.time(),
81
+ refresh_token=cached.get("refresh_token"),
82
+ )
83
+
84
+ del self._token_cache[cache_key]
85
+ return None
86
+
87
+ def _cache_token(
88
+ self,
89
+ cache_key: str,
90
+ token: str,
91
+ expires_in: Optional[float],
92
+ refresh_token: Optional[str],
93
+ ) -> None:
94
+ """Store a token with expiration."""
95
+ self._token_cache[cache_key] = {
96
+ "access_token": token,
97
+ "expires_at": time.time() + (expires_in or 3600),
98
+ "refresh_token": refresh_token,
99
+ }
100
+
101
+ def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult:
102
+ """
103
+ Fetch OIDC token via browser (PKCE flow).
104
+
105
+ 1. Generate PKCE challenge
106
+ 2. Start local HTTP server for callback
107
+ 3. Open browser with authorization URL
108
+ 4. Wait for authorization code
109
+ 5. Exchange code for access token
110
+ 6. Cache token for future use
111
+ """
112
+ if not context.idp_info:
113
+ raise RuntimeError(
114
+ "Missing required IdP information in OIDC callback parameters"
115
+ )
116
+ if not context.idp_info.issuer or not context.idp_info.clientId:
117
+ raise RuntimeError("Missing issuer or clientId in IdP information")
118
+
119
+ cache_key = self._get_cache_key(context)
120
+ cached_token = self._get_cached_token(cache_key)
121
+ if cached_token:
122
+ logger.info("Using cached OIDC token (avoiding re-authentication)")
123
+ return cached_token
124
+
125
+ logger.info("No cached token found, starting new authentication flow...")
126
+
127
+ code_verifier = (
128
+ base64.urlsafe_b64encode(secrets.token_bytes(32)).decode("utf-8").rstrip("=")
129
+ )
130
+ code_challenge = (
131
+ base64.urlsafe_b64encode(
132
+ hashlib.sha256(code_verifier.encode("utf-8")).digest()
133
+ )
134
+ .decode("utf-8")
135
+ .rstrip("=")
136
+ )
137
+
138
+ auth_response: Dict[str, Any] = {
139
+ "code": None,
140
+ "error": None,
141
+ "error_description": None,
142
+ "received": Event(),
143
+ }
144
+
145
+ redirect_uri = f"http://localhost:{self._port}/{self._redirect_path}"
146
+
147
+ class OIDCCallbackHandler(http.server.BaseHTTPRequestHandler):
148
+ """HTTP handler for OAuth callback."""
149
+
150
+ def do_GET(self) -> None:
151
+ parsed_path = urllib.parse.urlparse(self.path)
152
+ params = urllib.parse.parse_qs(parsed_path.query)
153
+
154
+ if "code" in params:
155
+ auth_response["code"] = params["code"][0]
156
+ self.send_response(200)
157
+ self.send_header("Content-type", "text/html; charset=utf-8")
158
+ self.end_headers()
159
+ html = _success_html()
160
+ self.wfile.write(html.encode("utf-8"))
161
+ auth_response["received"].set()
162
+ elif "error" in params:
163
+ auth_response["error"] = params["error"][0]
164
+ auth_response["error_description"] = params.get(
165
+ "error_description", [params["error"][0]]
166
+ )[0]
167
+ self.send_response(400)
168
+ self.send_header("Content-type", "text/html; charset=utf-8")
169
+ self.end_headers()
170
+ html = _error_html(
171
+ auth_response["error"],
172
+ auth_response["error_description"],
173
+ )
174
+ self.wfile.write(html.encode("utf-8"))
175
+ auth_response["received"].set()
176
+ else:
177
+ self.send_response(400)
178
+ self.send_header("Content-type", "text/html; charset=utf-8")
179
+ self.end_headers()
180
+ self.wfile.write(_invalid_html().encode("utf-8"))
181
+
182
+ def log_message(self, format: str, *args: Any) -> None:
183
+ pass
184
+
185
+ server: Optional[socketserver.TCPServer] = None
186
+ try:
187
+ server = socketserver.TCPServer(
188
+ ("localhost", self._port), OIDCCallbackHandler
189
+ )
190
+ server_thread = Thread(target=server.serve_forever)
191
+ server_thread.daemon = True
192
+ server_thread.start()
193
+
194
+ logger.info("Local callback server started on port %s", self._port)
195
+
196
+ issuer = context.idp_info.issuer
197
+ client_id = context.idp_info.clientId
198
+ scopes = context.idp_info.requestScopes or []
199
+
200
+ if issuer.endswith("/v2.0"):
201
+ base_issuer = issuer.rsplit("/v2.0", 1)[0]
202
+ auth_url = f"{base_issuer}/oauth2/v2.0/authorize"
203
+ else:
204
+ auth_url = f"{issuer}/authorize"
205
+
206
+ params = {
207
+ "client_id": client_id,
208
+ "response_type": "code",
209
+ "redirect_uri": redirect_uri,
210
+ "response_mode": "query",
211
+ "scope": " ".join(scopes) if scopes else "openid profile email",
212
+ "prompt": "select_account",
213
+ "code_challenge": code_challenge,
214
+ "code_challenge_method": "S256",
215
+ }
216
+ full_auth_url = f"{auth_url}?{urllib.parse.urlencode(params)}"
217
+
218
+ logger.info("Opening browser for authentication...")
219
+ webbrowser.open(full_auth_url)
220
+ logger.info("Waiting for authentication to complete...")
221
+
222
+ if not auth_response["received"].wait(timeout=context.timeout_seconds):
223
+ raise RuntimeError("Authentication timeout - no response received")
224
+
225
+ if auth_response["error"]:
226
+ error_msg = auth_response.get("error_description") or auth_response[
227
+ "error"
228
+ ]
229
+ raise RuntimeError(f"Authentication failed: {error_msg}")
230
+
231
+ if not auth_response["code"]:
232
+ raise RuntimeError("No authorization code received")
233
+
234
+ logger.info("Authorization code received")
235
+
236
+ if issuer.endswith("/v2.0"):
237
+ base_issuer = issuer.rsplit("/v2.0", 1)[0]
238
+ token_url = f"{base_issuer}/oauth2/v2.0/token"
239
+ else:
240
+ token_url = f"{issuer}/token"
241
+
242
+ token_data = urllib.parse.urlencode(
243
+ {
244
+ "grant_type": "authorization_code",
245
+ "code": auth_response["code"],
246
+ "redirect_uri": redirect_uri,
247
+ "client_id": client_id,
248
+ "code_verifier": code_verifier,
249
+ }
250
+ ).encode("utf-8")
251
+
252
+ logger.info("Exchanging authorization code for access token (PKCE)...")
253
+
254
+ req = urllib.request.Request(
255
+ token_url,
256
+ data=token_data,
257
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
258
+ method="POST",
259
+ )
260
+ with urllib.request.urlopen(req, timeout=30) as resp:
261
+ token_response = json.loads(resp.read().decode("utf-8"))
262
+
263
+ access_token = token_response.get("access_token")
264
+ refresh_token = token_response.get("refresh_token")
265
+ expires_in = token_response.get("expires_in")
266
+
267
+ if not access_token:
268
+ raise RuntimeError("No access token in response")
269
+
270
+ logger.info("Access token obtained successfully")
271
+
272
+ self._cache_token(
273
+ cache_key,
274
+ access_token,
275
+ float(expires_in) if expires_in else None,
276
+ refresh_token,
277
+ )
278
+
279
+ return OIDCCallbackResult(
280
+ access_token=access_token,
281
+ expires_in_seconds=float(expires_in) if expires_in else None,
282
+ refresh_token=refresh_token or context.refresh_token,
283
+ )
284
+
285
+ except urllib.error.HTTPError as e:
286
+ body = e.read().decode("utf-8", errors="replace")
287
+ logger.error("Token exchange failed: %s - %s", e.code, body[:200])
288
+ raise RuntimeError(f"Token exchange failed: {e.code} - {body[:200]}") from e
289
+ except urllib.error.URLError as e:
290
+ logger.error("Token exchange request failed: %s", e)
291
+ raise RuntimeError(f"Token exchange failed: {e}") from e
292
+ except json.JSONDecodeError as e:
293
+ logger.error("Failed to parse token response: %s", e)
294
+ raise RuntimeError("Invalid token response format") from e
295
+ except Exception as e:
296
+ logger.error("OIDC authentication failed: %s", e)
297
+ raise
298
+ finally:
299
+ if server:
300
+ server.shutdown()
301
+ server.server_close()
302
+ logger.debug("Callback server closed")
303
+
304
+
305
+ def _success_html() -> str:
306
+ return """
307
+ <html>
308
+ <head>
309
+ <title>Authentication Successful</title>
310
+ <style>
311
+ body { font-family: Arial, sans-serif; text-align: center; padding: 50px; background: #f0f0f0; }
312
+ .container { background: white; padding: 40px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); max-width: 500px; margin: 0 auto; }
313
+ h1 { color: #28a745; }
314
+ .icon { font-size: 64px; margin-bottom: 20px; }
315
+ </style>
316
+ </head>
317
+ <body>
318
+ <div class="container">
319
+ <div class="icon">&#10003;</div>
320
+ <h1>Authentication Successful!</h1>
321
+ <p>You can close this window and return to your application.</p>
322
+ </div>
323
+ </body>
324
+ </html>
325
+ """
326
+
327
+
328
+ def _error_html(error: str, description: str) -> str:
329
+ return f"""
330
+ <html>
331
+ <head>
332
+ <title>Authentication Failed</title>
333
+ <style>
334
+ body {{ font-family: Arial, sans-serif; text-align: center; padding: 50px; background: #f0f0f0; }}
335
+ .container {{ background: white; padding: 40px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); max-width: 500px; margin: 0 auto; }}
336
+ h1 {{ color: #dc3545; }}
337
+ .icon {{ font-size: 64px; margin-bottom: 20px; }}
338
+ .error {{ color: #666; margin-top: 20px; padding: 15px; background: #f8f9fa; border-radius: 5px; }}
339
+ </style>
340
+ </head>
341
+ <body>
342
+ <div class="container">
343
+ <div class="icon">&#10007;</div>
344
+ <h1>Authentication Failed</h1>
345
+ <div class="error">
346
+ <strong>Error:</strong> {error}<br>
347
+ <strong>Description:</strong> {description}
348
+ </div>
349
+ <p style="margin-top: 20px;">Please close this window and try again.</p>
350
+ </div>
351
+ </body>
352
+ </html>
353
+ """
354
+
355
+
356
+ def _invalid_html() -> str:
357
+ return """
358
+ <html>
359
+ <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
360
+ <h1>Invalid Request</h1>
361
+ <p>Missing required parameters.</p>
362
+ </body>
363
+ </html>
364
+ """
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mongo-oidc-human-callback"
7
+ version = "0.1.0"
8
+ description = "OIDC Human Callback for MongoDB: browser-based OAuth 2.0 Authorization Code Flow with PKCE (Azure AD and other OIDC providers)."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "Reouven Mimoun" }
14
+ ]
15
+ keywords = ["mongodb", "oidc", "oauth2", "pkce", "azure-ad", "atlas", "authentication"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: Database",
27
+ ]
28
+ dependencies = [
29
+ "pymongo>=4.14",
30
+ ]
31
+
32
+ [project.urls]
33
+ Repository = "https://github.com/mreouven/mongo-oidc-human-callback"
34
+ Documentation = "https://github.com/mreouven/mongo-oidc-human-callback#readme"
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["mongo_oidc_human_callback"]
@@ -0,0 +1,94 @@
1
+ version = 1
2
+ revision = 1
3
+ requires-python = ">=3.10"
4
+
5
+ [[package]]
6
+ name = "dnspython"
7
+ version = "2.8.0"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251 }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094 },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "mongo-oidc-human-callback"
16
+ version = "0.1.0"
17
+ source = { editable = "." }
18
+ dependencies = [
19
+ { name = "pymongo" },
20
+ ]
21
+
22
+ [package.metadata]
23
+ requires-dist = [{ name = "pymongo", specifier = ">=4.14" }]
24
+
25
+ [[package]]
26
+ name = "pymongo"
27
+ version = "4.16.0"
28
+ source = { registry = "https://pypi.org/simple" }
29
+ dependencies = [
30
+ { name = "dnspython" },
31
+ ]
32
+ sdist = { url = "https://files.pythonhosted.org/packages/65/9c/a4895c4b785fc9865a84a56e14b5bd21ca75aadc3dab79c14187cdca189b/pymongo-4.16.0.tar.gz", hash = "sha256:8ba8405065f6e258a6f872fe62d797a28f383a12178c7153c01ed04e845c600c", size = 2495323 }
33
+ wheels = [
34
+ { url = "https://files.pythonhosted.org/packages/4d/93/c36c0998dd91ad8b5031d2e77a903d5cd705b5ba05ca92bcc8731a2c3a8d/pymongo-4.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ed162b2227f98d5b270ecbe1d53be56c8c81db08a1a8f5f02d89c7bb4d19591d", size = 807993 },
35
+ { url = "https://files.pythonhosted.org/packages/f3/96/d2117d792fa9fedb2f6ccf0608db31f851e8382706d7c3c88c6ac92cc958/pymongo-4.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a9390dce61d705a88218f0d7b54d7e1fa1b421da8129fc7c009e029a9a6b81e", size = 808355 },
36
+ { url = "https://files.pythonhosted.org/packages/ae/2e/e79b7b86c0dd6323d0985c201583c7921d67b842b502aae3f3327cbe3935/pymongo-4.16.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:92a232af9927710de08a6c16a9710cc1b175fb9179c0d946cd4e213b92b2a69a", size = 1182337 },
37
+ { url = "https://files.pythonhosted.org/packages/7b/82/07ec9966381c57d941fddc52637e9c9653e63773be410bd8605f74683084/pymongo-4.16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d79aa147ce86aef03079096d83239580006ffb684eead593917186aee407767", size = 1200928 },
38
+ { url = "https://files.pythonhosted.org/packages/44/15/9d45e3cc6fa428b0a3600b0c1c86b310f28c91251c41493460695ab40b6b/pymongo-4.16.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:19a1c96e7f39c7a59a9cfd4d17920cf9382f6f684faeff4649bf587dc59f8edc", size = 1239418 },
39
+ { url = "https://files.pythonhosted.org/packages/c8/b3/f35ee51e2a3f05f673ad4f5e803ae1284c42f4413e8d121c4958f1af4eb9/pymongo-4.16.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efe020c46ce3c3a89af6baec6569635812129df6fb6cf76d4943af3ba6ee2069", size = 1229045 },
40
+ { url = "https://files.pythonhosted.org/packages/18/2d/1688b88d7c0a5c01da8c703dea831419435d9ce67c6ddbb0ac629c9c72d2/pymongo-4.16.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9dc2c00bed568732b89e211b6adca389053d5e6d2d5a8979e80b813c3ec4d1f9", size = 1196517 },
41
+ { url = "https://files.pythonhosted.org/packages/e6/c6/e89db0f23bd20757b627a5d8c73a609ffd6741887b9004ab229208a79764/pymongo-4.16.0-cp310-cp310-win32.whl", hash = "sha256:5b9c6d689bbe5beb156374508133218610e14f8c81e35bc17d7a14e30ab593e6", size = 794911 },
42
+ { url = "https://files.pythonhosted.org/packages/37/54/e00a5e517153f310a33132375159e42dceb12bee45b51b35aa0df14f1866/pymongo-4.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:2290909275c9b8f637b0a92eb9b89281e18a72922749ebb903403ab6cc7da914", size = 804801 },
43
+ { url = "https://files.pythonhosted.org/packages/e5/0a/2572faf89195a944c99c6d756227019c8c5f4b5658ecc261c303645dfe69/pymongo-4.16.0-cp310-cp310-win_arm64.whl", hash = "sha256:6af1aaa26f0835175d2200e62205b78e7ec3ffa430682e322cc91aaa1a0dbf28", size = 797579 },
44
+ { url = "https://files.pythonhosted.org/packages/e6/3a/907414a763c4270b581ad6d960d0c6221b74a70eda216a1fdd8fa82ba89f/pymongo-4.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6f2077ec24e2f1248f9cac7b9a2dfb894e50cc7939fcebfb1759f99304caabef", size = 862561 },
45
+ { url = "https://files.pythonhosted.org/packages/8c/58/787d8225dd65cb2383c447346ea5e200ecfde89962d531111521e3b53018/pymongo-4.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d4f7ba040f72a9f43a44059872af5a8c8c660aa5d7f90d5344f2ed1c3c02721", size = 862923 },
46
+ { url = "https://files.pythonhosted.org/packages/5d/a7/cc2865aae32bc77ade7b35f957a58df52680d7f8506f93c6edbf458e5738/pymongo-4.16.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8a0f73af1ea56c422b2dcfc0437459148a799ef4231c6aee189d2d4c59d6728f", size = 1426779 },
47
+ { url = "https://files.pythonhosted.org/packages/81/25/3e96eb7998eec05382174da2fefc58d28613f46bbdf821045539d0ed60ab/pymongo-4.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa30cd16ddd2f216d07ba01d9635c873e97ddb041c61cf0847254edc37d1c60e", size = 1454207 },
48
+ { url = "https://files.pythonhosted.org/packages/86/7b/8e817a7df8c5d565d39dd4ca417a5e0ef46cc5cc19aea9405f403fec6449/pymongo-4.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d638b0b1b294d95d0fdc73688a3b61e05cc4188872818cd240d51460ccabcb5", size = 1511654 },
49
+ { url = "https://files.pythonhosted.org/packages/39/7a/50c4d075ccefcd281cdcfccc5494caa5665b096b85e65a5d6afabb80e09e/pymongo-4.16.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:21d02cc10a158daa20cb040985e280e7e439832fc6b7857bff3d53ef6914ad50", size = 1496794 },
50
+ { url = "https://files.pythonhosted.org/packages/0f/cd/ebdc1aaca5deeaf47310c369ef4083e8550e04e7bf7e3752cfb7d95fcdb8/pymongo-4.16.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fbb8d3552c2ad99d9e236003c0b5f96d5f05e29386ba7abae73949bfebc13dd", size = 1448371 },
51
+ { url = "https://files.pythonhosted.org/packages/3d/c9/50fdd78c37f68ea49d590c027c96919fbccfd98f3a4cb39f84f79970bd37/pymongo-4.16.0-cp311-cp311-win32.whl", hash = "sha256:be1099a8295b1a722d03fb7b48be895d30f4301419a583dcf50e9045968a041c", size = 841024 },
52
+ { url = "https://files.pythonhosted.org/packages/4a/dd/a3aa1ade0cf9980744db703570afac70a62c85b432c391dea0577f6da7bb/pymongo-4.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:61567f712bda04c7545a037e3284b4367cad8d29b3dec84b4bf3b2147020a75b", size = 855838 },
53
+ { url = "https://files.pythonhosted.org/packages/bf/10/9ad82593ccb895e8722e4884bad4c5ce5e8ff6683b740d7823a6c2bcfacf/pymongo-4.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:c53338613043038005bf2e41a2fafa08d29cdbc0ce80891b5366c819456c1ae9", size = 845007 },
54
+ { url = "https://files.pythonhosted.org/packages/6a/03/6dd7c53cbde98de469a3e6fb893af896dca644c476beb0f0c6342bcc368b/pymongo-4.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bd4911c40a43a821dfd93038ac824b756b6e703e26e951718522d29f6eb166a8", size = 917619 },
55
+ { url = "https://files.pythonhosted.org/packages/73/e1/328915f2734ea1f355dc9b0e98505ff670f5fab8be5e951d6ed70971c6aa/pymongo-4.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25a6b03a68f9907ea6ec8bc7cf4c58a1b51a18e23394f962a6402f8e46d41211", size = 917364 },
56
+ { url = "https://files.pythonhosted.org/packages/41/fe/4769874dd9812a1bc2880a9785e61eba5340da966af888dd430392790ae0/pymongo-4.16.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:91ac0cb0fe2bf17616c2039dac88d7c9a5088f5cb5829b27c9d250e053664d31", size = 1686901 },
57
+ { url = "https://files.pythonhosted.org/packages/fa/8d/15707b9669fdc517bbc552ac60da7124dafe7ac1552819b51e97ed4038b4/pymongo-4.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf0ec79e8ca7077f455d14d915d629385153b6a11abc0b93283ed73a8013e376", size = 1723034 },
58
+ { url = "https://files.pythonhosted.org/packages/5b/af/3d5d16ff11d447d40c1472da1b366a31c7380d7ea2922a449c7f7f495567/pymongo-4.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2d0082631a7510318befc2b4fdab140481eb4b9dd62d9245e042157085da2a70", size = 1797161 },
59
+ { url = "https://files.pythonhosted.org/packages/fb/04/725ab8664eeec73ec125b5a873448d80f5d8cf2750aaaf804cbc538a50a5/pymongo-4.16.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85dc2f3444c346ea019a371e321ac868a4fab513b7a55fe368f0cc78de8177cc", size = 1780938 },
60
+ { url = "https://files.pythonhosted.org/packages/22/50/dd7e9095e1ca35f93c3c844c92eb6eb0bc491caeb2c9bff3b32fe3c9b18f/pymongo-4.16.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dabbf3c14de75a20cc3c30bf0c6527157224a93dfb605838eabb1a2ee3be008d", size = 1714342 },
61
+ { url = "https://files.pythonhosted.org/packages/03/c9/542776987d5c31ae8e93e92680ea2b6e5a2295f398b25756234cabf38a39/pymongo-4.16.0-cp312-cp312-win32.whl", hash = "sha256:60307bb91e0ab44e560fe3a211087748b2b5f3e31f403baf41f5b7b0a70bd104", size = 887868 },
62
+ { url = "https://files.pythonhosted.org/packages/2e/d4/b4045a7ccc5680fb496d01edf749c7a9367cc8762fbdf7516cf807ef679b/pymongo-4.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:f513b2c6c0d5c491f478422f6b5b5c27ac1af06a54c93ef8631806f7231bd92e", size = 907554 },
63
+ { url = "https://files.pythonhosted.org/packages/60/4c/33f75713d50d5247f2258405142c0318ff32c6f8976171c4fcae87a9dbdf/pymongo-4.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:dfc320f08ea9a7ec5b2403dc4e8150636f0d6150f4b9792faaae539c88e7db3b", size = 892971 },
64
+ { url = "https://files.pythonhosted.org/packages/47/84/148d8b5da8260f4679d6665196ae04ab14ffdf06f5fe670b0ab11942951f/pymongo-4.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d15f060bc6d0964a8bb70aba8f0cb6d11ae99715438f640cff11bbcf172eb0e8", size = 972009 },
65
+ { url = "https://files.pythonhosted.org/packages/1e/5e/9f3a8daf583d0adaaa033a3e3e58194d2282737dc164014ff33c7a081103/pymongo-4.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a19ea46a0fe71248965305a020bc076a163311aefbaa1d83e47d06fa30ac747", size = 971784 },
66
+ { url = "https://files.pythonhosted.org/packages/ad/f2/b6c24361fcde24946198573c0176406bfd5f7b8538335f3d939487055322/pymongo-4.16.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:311d4549d6bf1f8c61d025965aebb5ba29d1481dc6471693ab91610aaffbc0eb", size = 1947174 },
67
+ { url = "https://files.pythonhosted.org/packages/47/1a/8634192f98cf740b3d174e1018dd0350018607d5bd8ac35a666dc49c732b/pymongo-4.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46ffb728d92dd5b09fc034ed91acf5595657c7ca17d4cf3751322cd554153c17", size = 1991727 },
68
+ { url = "https://files.pythonhosted.org/packages/5a/2f/0c47ac84572b28e23028a23a3798a1f725e1c23b0cf1c1424678d16aff42/pymongo-4.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:acda193f440dd88c2023cb00aa8bd7b93a9df59978306d14d87a8b12fe426b05", size = 2082497 },
69
+ { url = "https://files.pythonhosted.org/packages/ba/57/9f46ef9c862b2f0cf5ce798f3541c201c574128d31ded407ba4b3918d7b6/pymongo-4.16.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d9fdb386cf958e6ef6ff537d6149be7edb76c3268cd6833e6c36aa447e4443f", size = 2064947 },
70
+ { url = "https://files.pythonhosted.org/packages/b8/56/5421c0998f38e32288100a07f6cb2f5f9f352522157c901910cb2927e211/pymongo-4.16.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91899dd7fb9a8c50f09c3c1cf0cb73bfbe2737f511f641f19b9650deb61c00ca", size = 1980478 },
71
+ { url = "https://files.pythonhosted.org/packages/92/93/bfc448d025e12313a937d6e1e0101b50cc9751636b4b170e600fe3203063/pymongo-4.16.0-cp313-cp313-win32.whl", hash = "sha256:2cd60cd1e05de7f01927f8e25ca26b3ea2c09de8723241e5d3bcfdc70eaff76b", size = 934672 },
72
+ { url = "https://files.pythonhosted.org/packages/96/10/12710a5e01218d50c3dd165fd72c5ed2699285f77348a3b1a119a191d826/pymongo-4.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3ead8a0050c53eaa55935895d6919d393d0328ec24b2b9115bdbe881aa222673", size = 959237 },
73
+ { url = "https://files.pythonhosted.org/packages/0c/56/d288bcd1d05bc17ec69df1d0b1d67bc710c7c5dbef86033a5a4d2e2b08e6/pymongo-4.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:dbbc5b254c36c37d10abb50e899bc3939bbb7ab1e7c659614409af99bd3e7675", size = 940909 },
74
+ { url = "https://files.pythonhosted.org/packages/30/9e/4d343f8d0512002fce17915a89477b9f916bda1205729e042d8f23acf194/pymongo-4.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:8a254d49a9ffe9d7f888e3c677eed3729b14ce85abb08cd74732cead6ccc3c66", size = 1026634 },
75
+ { url = "https://files.pythonhosted.org/packages/c3/e3/341f88c5535df40c0450fda915f582757bb7d988cdfc92990a5e27c4c324/pymongo-4.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a1bf44e13cf2d44d2ea2e928a8140d5d667304abe1a61c4d55b4906f389fbe64", size = 1026252 },
76
+ { url = "https://files.pythonhosted.org/packages/af/64/9471b22eb98f0a2ca0b8e09393de048502111b2b5b14ab1bd9e39708aab5/pymongo-4.16.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f1c5f1f818b669875d191323a48912d3fcd2e4906410e8297bb09ac50c4d5ccc", size = 2207399 },
77
+ { url = "https://files.pythonhosted.org/packages/87/ac/47c4d50b25a02f21764f140295a2efaa583ee7f17992a5e5fa542b3a690f/pymongo-4.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77cfd37a43a53b02b7bd930457c7994c924ad8bbe8dff91817904bcbf291b371", size = 2260595 },
78
+ { url = "https://files.pythonhosted.org/packages/ee/1b/0ce1ce9dd036417646b2fe6f63b58127acff3cf96eeb630c34ec9cd675ff/pymongo-4.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:36ef2fee50eee669587d742fb456e349634b4fcf8926208766078b089054b24b", size = 2366958 },
79
+ { url = "https://files.pythonhosted.org/packages/3e/3c/a5a17c0d413aa9d6c17bc35c2b472e9e79cda8068ba8e93433b5f43028e9/pymongo-4.16.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55f8d5a6fe2fa0b823674db2293f92d74cd5f970bc0360f409a1fc21003862d3", size = 2346081 },
80
+ { url = "https://files.pythonhosted.org/packages/65/19/f815533d1a88fb8a3b6c6e895bb085ffdae68ccb1e6ed7102202a307f8e2/pymongo-4.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9caacac0dd105e2555521002e2d17afc08665187017b466b5753e84c016628e6", size = 2246053 },
81
+ { url = "https://files.pythonhosted.org/packages/c6/88/4be3ec78828dc64b212c123114bd6ae8db5b7676085a7b43cc75d0131bd2/pymongo-4.16.0-cp314-cp314-win32.whl", hash = "sha256:c789236366525c3ee3cd6e4e450a9ff629a7d1f4d88b8e18a0aea0615fd7ecf8", size = 989461 },
82
+ { url = "https://files.pythonhosted.org/packages/af/5a/ab8d5af76421b34db483c9c8ebc3a2199fb80ae63dc7e18f4cf1df46306a/pymongo-4.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:2b0714d7764efb29bf9d3c51c964aed7c4c7237b341f9346f15ceaf8321fdb35", size = 1017803 },
83
+ { url = "https://files.pythonhosted.org/packages/f6/f4/98d68020728ac6423cf02d17cfd8226bf6cce5690b163d30d3f705e8297e/pymongo-4.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:12762e7cc0f8374a8cae3b9f9ed8dabb5d438c7b33329232dd9b7de783454033", size = 997184 },
84
+ { url = "https://files.pythonhosted.org/packages/50/00/dc3a271daf06401825b9c1f4f76f018182c7738281ea54b9762aea0560c1/pymongo-4.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1c01e8a7cd0ea66baf64a118005535ab5bf9f9eb63a1b50ac3935dccf9a54abe", size = 1083303 },
85
+ { url = "https://files.pythonhosted.org/packages/b8/4b/b5375ee21d12eababe46215011ebc63801c0d2c5ffdf203849d0d79f9852/pymongo-4.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4c4872299ebe315a79f7f922051061634a64fda95b6b17677ba57ef00b2ba2a4", size = 1083233 },
86
+ { url = "https://files.pythonhosted.org/packages/ee/e3/52efa3ca900622c7dcb56c5e70f15c906816d98905c22d2ee1f84d9a7b60/pymongo-4.16.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:78037d02389745e247fe5ab0bcad5d1ab30726eaac3ad79219c7d6bbb07eec53", size = 2527438 },
87
+ { url = "https://files.pythonhosted.org/packages/cb/96/43b1be151c734e7766c725444bcbfa1de6b60cc66bfb406203746839dd25/pymongo-4.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c126fb72be2518395cc0465d4bae03125119136462e1945aea19840e45d89cfc", size = 2600399 },
88
+ { url = "https://files.pythonhosted.org/packages/e7/62/fa64a5045dfe3a1cd9217232c848256e7bc0136cffb7da4735c5e0d30e40/pymongo-4.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f3867dc225d9423c245a51eaac2cfcd53dde8e0a8d8090bb6aed6e31bd6c2d4f", size = 2720960 },
89
+ { url = "https://files.pythonhosted.org/packages/54/7b/01577eb97e605502821273a5bc16ce0fb0be5c978fe03acdbff471471202/pymongo-4.16.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f25001a955073b80510c0c3db0e043dbbc36904fd69e511c74e3d8640b8a5111", size = 2699344 },
90
+ { url = "https://files.pythonhosted.org/packages/55/68/6ef6372d516f703479c3b6cbbc45a5afd307173b1cbaccd724e23919bb1a/pymongo-4.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d9885aad05f82fd7ea0c9ca505d60939746b39263fa273d0125170da8f59098", size = 2577133 },
91
+ { url = "https://files.pythonhosted.org/packages/15/c7/b5337093bb01da852f945802328665f85f8109dbe91d81ea2afe5ff059b9/pymongo-4.16.0-cp314-cp314t-win32.whl", hash = "sha256:948152b30eddeae8355495f9943a3bf66b708295c0b9b6f467de1c620f215487", size = 1040560 },
92
+ { url = "https://files.pythonhosted.org/packages/96/8c/5b448cd1b103f3889d5713dda37304c81020ff88e38a826e8a75ddff4610/pymongo-4.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f6e42c1bc985d9beee884780ae6048790eb4cd565c46251932906bdb1630034a", size = 1075081 },
93
+ { url = "https://files.pythonhosted.org/packages/32/cd/ddc794cdc8500f6f28c119c624252fb6dfb19481c6d7ed150f13cf468a6d/pymongo-4.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6b2a20edb5452ac8daa395890eeb076c570790dfce6b7a44d788af74c2f8cf96", size = 1047725 },
94
+ ]