pyturnstile 0.3.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dong-Chen-1031
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,172 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyturnstile
3
+ Version: 0.3.0
4
+ Summary: A Python library for validating Cloudflare Turnstile tokens with async and sync support
5
+ Author-email: Dong-Chen-1031 <dcdcdc1031@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Dong-Chen-1031/pyturnstile
8
+ Project-URL: Repository, https://github.com/Dong-Chen-1031/pyturnstile
9
+ Project-URL: Issues, https://github.com/Dong-Chen-1031/pyturnstile/issues
10
+ Keywords: Cloudflare,turnstile,captcha,validation
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.8
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: httpx>=0.23.0
27
+ Requires-Dist: tenacity>=9.0.0
28
+ Dynamic: license-file
29
+
30
+ <div align="center">
31
+ <h1>PyTurnstile</h1>
32
+ <a href="https://pypi.org/project/pyturnstile" target="_blank">
33
+ <img src="https://github.com/Dong-Chen-1031/pyturnstile/blob/main/img/logo.png?raw=true" width="300" alt="Cloudflare Turnstile widget" />
34
+ </a>
35
+ <p>A Python library for validating <a href="https://developers.cloudflare.com/turnstile/">Cloudflare Turnstile</a> tokens with both async and sync support.</p>
36
+
37
+ <a href="https://github.com/dong-chen-1031/pyturnstile/actions?query=workflow%3ATest+event%3Apush+branch%3Amain" target="_blank">
38
+ <img src="https://github.com/dong-chen-1031/pyturnstile/actions/workflows/test.yml/badge.svg?event=push&branch=main" alt="Test">
39
+ </a>
40
+ <a href="https://pypi.org/project/pyturnstile" target="_blank">
41
+ <img src="https://img.shields.io/pypi/v/pyturnstile?color=%2334D058&label=pypi%20package" alt="Package version">
42
+ </a>
43
+ <a href="https://pypi.org/project/pyturnstile" target="_blank">
44
+ <img src="https://img.shields.io/badge/Python-3.8%2B?color=%2334D058&logo=Python&logoColor=rgb(255%2C%20255%2C%20255)" alt="Supported Python versions">
45
+ </a>
46
+ <a href="https://docs.astral.sh/ruff/" target="_blank">
47
+ <img src="https://camo.githubusercontent.com/d6c7524504b7d886a9d34c11f44b9d31b2de1a579325b42e932744c4575a063b/68747470733a2f2f696d672e736869656c64732e696f2f656e64706f696e743f75726c3d68747470733a2f2f7261772e67697468756275736572636f6e74656e742e636f6d2f61737472616c2d73682f727566662f6d61696e2f6173736574732f62616467652f76322e6a736f6e" alt="Ruff" />
48
+ </a>
49
+ <img src="https://img.shields.io/badge/License-MIT-%2334D058.svg" alt="License: MIT" />
50
+ <a href="https://github.com/dong-chen-1031/pyturnstile/pulls" target="_blank">
51
+ <img src="https://img.shields.io/badge/PRs-welcome-%2334D058.svg" alt="PRs are welcome" />
52
+ </a>
53
+ </div>
54
+
55
+ ## Features
56
+
57
+ - 🔄 Async & Sync Support
58
+ - 🚀 Simple API
59
+ - đŸ“Ļ Lightweight - Only requires `httpx`
60
+
61
+ ## What is PyTurnstile?
62
+
63
+ PyTurnstile simplifies Cloudflare Turnstile token validation. It handles all communication with Cloudflare's API.
64
+
65
+ <img src="https://github.com/Dong-Chen-1031/pyturnstile/blob/main/img/turnstile_verification.svg?raw=true" alt="Sequence diagram showing how PyTurnstile works" />
66
+
67
+ > Learn more at: https://developers.cloudflare.com/turnstile/
68
+
69
+ ## Installation
70
+
71
+ Install the package using your preferred dependency manager.
72
+
73
+ ### uv
74
+
75
+ ```bash
76
+ uv add pyturnstile
77
+ ```
78
+
79
+ ### pip
80
+
81
+ ```bash
82
+ pip install pyturnstile
83
+ ```
84
+
85
+ ## Usage
86
+
87
+ > ### 💡 TIP
88
+ >
89
+ > You can follow [this documentation](https://developers.cloudflare.com/turnstile/get-started/) and create your own Turnstile secret key at the [Cloudflare Turnstile dashboard](https://dash.cloudflare.com/?to=/:account/turnstile).
90
+
91
+ ### Quick Start
92
+
93
+ PyTurnstile provides two ways to validate tokens:
94
+
95
+ #### 1. Using the `Turnstile` class (Recommended)
96
+
97
+ ```python
98
+ from pyturnstile import Turnstile
99
+
100
+ turnstile = Turnstile(secret="your-secret-key")
101
+
102
+ response = await turnstile.async_validate(token="user-token-from-frontend")
103
+
104
+ # or validate synchronously
105
+ # response = turnstile.validate(token="user-token-from-frontend")
106
+
107
+ if response.success:
108
+ print("✅ Token is valid!")
109
+ ```
110
+
111
+ #### 2. Using functions directly
112
+
113
+ ```python
114
+ from pyturnstile import validate, async_validate
115
+
116
+ response = await async_validate(
117
+ token="user-token-from-frontend",
118
+ secret="your-secret-key"
119
+ )
120
+
121
+ # or validate synchronously
122
+ # response = validate(
123
+ # token="user-token-from-frontend",
124
+ # secret="your-secret-key"
125
+ # )
126
+
127
+ if response.success:
128
+ print("✅ Token is valid!")
129
+ ```
130
+
131
+ ### Optional Parameters
132
+
133
+ > ### â„šī¸ NOTE
134
+ >
135
+ > For more details on all available parameters, see the [Cloudflare documentation](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#required-parameters)
136
+
137
+ ```python
138
+ response = turnstile.validate(
139
+ token="user-token", # The token from the client-side widget
140
+ idempotency_key="unique-uuid", # Optional: UUID for retry protection
141
+ expected_remoteip="203.0.113.1", # Optional: The visitor's IP address that the challenge response must match
142
+ expected_hostname="example.com", # Optional: The hostname that the challenge response must match
143
+ expected_action="submit_form", # Optional: The action identifier that the challenge must match
144
+ timeout=10 # Optional: request timeout in seconds
145
+ )
146
+ ```
147
+
148
+ ### Response Object
149
+
150
+ > ### â„šī¸ NOTE
151
+ >
152
+ > For more details on all response fields, see the [Cloudflare documentation](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#response-fields)
153
+
154
+ The `TurnstileResponse` object contains:
155
+
156
+ ```python
157
+ response.success # bool: Whether validation succeeded
158
+ response.error_codes # list[TurnstileErrorCodes]: Error codes (if any)
159
+ response.challenge_ts # str: ISO timestamp of challenge completion
160
+ response.hostname # str: Hostname where challenge was served
161
+ response.action # str: Custom action identifier
162
+ response.cdata # str: Custom data payload from client-side
163
+ response.metadata["ephemeral_id"] # Device fingerprint ID (Enterprise only)
164
+ ```
165
+
166
+ ## Contributing
167
+
168
+ Any contributions are greatly appreciated. If you have a suggestion that would make this project better, please fork the repo and create a Pull Request. You can also [open an issue](https://github.com/Dong-Chen-1031/pyturnstile/issues).
169
+
170
+ ## License
171
+
172
+ Published under the [MIT License](LICENSE).
@@ -0,0 +1,158 @@
1
+ <div align="center">
2
+ <h1>PyTurnstile</h1>
3
+ <a href="https://pypi.org/project/pyturnstile" target="_blank">
4
+ <img src="https://github.com/Dong-Chen-1031/pyturnstile/blob/main/img/logo.png?raw=true" width="300" alt="Cloudflare Turnstile widget" />
5
+ </a>
6
+ <p>A Python library for validating <a href="https://developers.cloudflare.com/turnstile/">Cloudflare Turnstile</a> tokens with both async and sync support.</p>
7
+
8
+ <a href="https://github.com/dong-chen-1031/pyturnstile/actions?query=workflow%3ATest+event%3Apush+branch%3Amain" target="_blank">
9
+ <img src="https://github.com/dong-chen-1031/pyturnstile/actions/workflows/test.yml/badge.svg?event=push&branch=main" alt="Test">
10
+ </a>
11
+ <a href="https://pypi.org/project/pyturnstile" target="_blank">
12
+ <img src="https://img.shields.io/pypi/v/pyturnstile?color=%2334D058&label=pypi%20package" alt="Package version">
13
+ </a>
14
+ <a href="https://pypi.org/project/pyturnstile" target="_blank">
15
+ <img src="https://img.shields.io/badge/Python-3.8%2B?color=%2334D058&logo=Python&logoColor=rgb(255%2C%20255%2C%20255)" alt="Supported Python versions">
16
+ </a>
17
+ <a href="https://docs.astral.sh/ruff/" target="_blank">
18
+ <img src="https://camo.githubusercontent.com/d6c7524504b7d886a9d34c11f44b9d31b2de1a579325b42e932744c4575a063b/68747470733a2f2f696d672e736869656c64732e696f2f656e64706f696e743f75726c3d68747470733a2f2f7261772e67697468756275736572636f6e74656e742e636f6d2f61737472616c2d73682f727566662f6d61696e2f6173736574732f62616467652f76322e6a736f6e" alt="Ruff" />
19
+ </a>
20
+ <img src="https://img.shields.io/badge/License-MIT-%2334D058.svg" alt="License: MIT" />
21
+ <a href="https://github.com/dong-chen-1031/pyturnstile/pulls" target="_blank">
22
+ <img src="https://img.shields.io/badge/PRs-welcome-%2334D058.svg" alt="PRs are welcome" />
23
+ </a>
24
+ </div>
25
+
26
+ ## Features
27
+
28
+ - 🔄 Async & Sync Support
29
+ - 🚀 Simple & Intuitive API
30
+ - ✅ Type-safe response handling
31
+ - đŸ›Ąī¸ Enhanced security validation
32
+
33
+ ## What is PyTurnstile?
34
+
35
+ PyTurnstile simplifies Cloudflare Turnstile token validation. It handles all communication with Cloudflare's API.
36
+
37
+ ```mermaid
38
+ sequenceDiagram
39
+ participant Frontend as đŸ–Ĩī¸ Frontend
40
+ participant Backend as 🐍 Your Backend
41
+ participant Cloudflare as â˜ī¸ Cloudflare
42
+
43
+ Frontend->>Cloudflare: 1. Complete challenge
44
+ Cloudflare-->>Frontend: 2. Return token
45
+ Frontend->>Backend: 3. Submit form + token
46
+
47
+ rect rgb(50, 179, 238)
48
+ Note over Backend,Cloudflare: 🔍 PyTurnstile handles this
49
+ Backend->>Cloudflare: 4. Verify token
50
+ Cloudflare-->>Backend: 5. Valid ✅ / Invalid ❌
51
+ end
52
+
53
+ Backend->>Frontend: 6. Allow / Deny request
54
+ ```
55
+
56
+ > Learn more at: https://developers.cloudflare.com/turnstile/
57
+
58
+ ## Installation
59
+
60
+ Install the package using your preferred dependency manager.
61
+
62
+ ### uv
63
+
64
+ ```bash
65
+ uv add pyturnstile
66
+ ```
67
+
68
+ ### pip
69
+
70
+ ```bash
71
+ pip install pyturnstile
72
+ ```
73
+
74
+ ## Usage
75
+
76
+ > [!TIP]
77
+ > You can follow [this documentation](https://developers.cloudflare.com/turnstile/get-started/) and create your own Turnstile secret key at the [Cloudflare Turnstile dashboard](https://dash.cloudflare.com/?to=/:account/turnstile).
78
+
79
+ ### Quick Start
80
+
81
+ PyTurnstile provides two ways to validate tokens:
82
+
83
+ #### 1. Using the `Turnstile` class (Recommended)
84
+
85
+ ```python
86
+ from pyturnstile import Turnstile
87
+
88
+ turnstile = Turnstile(secret="your-secret-key")
89
+
90
+ response = await turnstile.async_validate(token="user-token-from-frontend")
91
+
92
+ # or validate synchronously
93
+ # response = turnstile.validate(token="user-token-from-frontend")
94
+
95
+ if response.success:
96
+ print("✅ Token is valid!")
97
+ ```
98
+
99
+ #### 2. Using functions directly
100
+
101
+ ```python
102
+ from pyturnstile import validate, async_validate
103
+
104
+ response = await async_validate(
105
+ token="user-token-from-frontend",
106
+ secret="your-secret-key"
107
+ )
108
+
109
+ # or validate synchronously
110
+ # response = validate(
111
+ # token="user-token-from-frontend",
112
+ # secret="your-secret-key"
113
+ # )
114
+
115
+ if response.success:
116
+ print("✅ Token is valid!")
117
+ ```
118
+
119
+ ### Optional Parameters
120
+
121
+ > [!NOTE]
122
+ > For more details on all available parameters, see the [Cloudflare documentation](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#required-parameters)
123
+
124
+ ```python
125
+ response = turnstile.validate(
126
+ token="user-token", # The token from the client-side widget
127
+ idempotency_key="unique-uuid", # Optional: UUID for retry protection
128
+ expected_remoteip="203.0.113.1", # Optional: The visitor's IP address that the challenge response must match
129
+ expected_hostname="example.com", # Optional: The hostname that the challenge response must match
130
+ expected_action="submit_form", # Optional: The action identifier that the challenge must match
131
+ timeout=10 # Optional: request timeout in seconds
132
+ )
133
+ ```
134
+
135
+ ### Response Object
136
+
137
+ > [!NOTE]
138
+ > For more details on all response fields, see the [Cloudflare documentation](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#response-fields)
139
+
140
+ The `TurnstileResponse` object contains:
141
+
142
+ ```python
143
+ response.success # bool: Whether validation succeeded
144
+ response.error_codes # list[TurnstileErrorCodes]: Error codes (if any)
145
+ response.challenge_ts # str: ISO timestamp of challenge completion
146
+ response.hostname # str: Hostname where challenge was served
147
+ response.action # str: Custom action identifier
148
+ response.cdata # str: Custom data payload from client-side
149
+ response.metadata["ephemeral_id"] # Device fingerprint ID (Enterprise only)
150
+ ```
151
+
152
+ ## Contributing
153
+
154
+ Any contributions are greatly appreciated. If you have a suggestion that would make this project better, please fork the repo and create a Pull Request. You can also [open an issue](https://github.com/Dong-Chen-1031/pyturnstile/issues).
155
+
156
+ ## License
157
+
158
+ Published under the [MIT License](LICENSE).
@@ -0,0 +1,143 @@
1
+ <div align="center">
2
+ <h1>PyTurnstile</h1>
3
+ <a href="https://pypi.org/project/pyturnstile" target="_blank">
4
+ <img src="https://github.com/Dong-Chen-1031/pyturnstile/blob/main/img/logo.png?raw=true" width="300" alt="Cloudflare Turnstile widget" />
5
+ </a>
6
+ <p>A Python library for validating <a href="https://developers.cloudflare.com/turnstile/">Cloudflare Turnstile</a> tokens with both async and sync support.</p>
7
+
8
+ <a href="https://github.com/dong-chen-1031/pyturnstile/actions?query=workflow%3ATest+event%3Apush+branch%3Amain" target="_blank">
9
+ <img src="https://github.com/dong-chen-1031/pyturnstile/actions/workflows/test.yml/badge.svg?event=push&branch=main" alt="Test">
10
+ </a>
11
+ <a href="https://pypi.org/project/pyturnstile" target="_blank">
12
+ <img src="https://img.shields.io/pypi/v/pyturnstile?color=%2334D058&label=pypi%20package" alt="Package version">
13
+ </a>
14
+ <a href="https://pypi.org/project/pyturnstile" target="_blank">
15
+ <img src="https://img.shields.io/badge/Python-3.8%2B?color=%2334D058&logo=Python&logoColor=rgb(255%2C%20255%2C%20255)" alt="Supported Python versions">
16
+ </a>
17
+ <a href="https://docs.astral.sh/ruff/" target="_blank">
18
+ <img src="https://camo.githubusercontent.com/d6c7524504b7d886a9d34c11f44b9d31b2de1a579325b42e932744c4575a063b/68747470733a2f2f696d672e736869656c64732e696f2f656e64706f696e743f75726c3d68747470733a2f2f7261772e67697468756275736572636f6e74656e742e636f6d2f61737472616c2d73682f727566662f6d61696e2f6173736574732f62616467652f76322e6a736f6e" alt="Ruff" />
19
+ </a>
20
+ <img src="https://img.shields.io/badge/License-MIT-%2334D058.svg" alt="License: MIT" />
21
+ <a href="https://github.com/dong-chen-1031/pyturnstile/pulls" target="_blank">
22
+ <img src="https://img.shields.io/badge/PRs-welcome-%2334D058.svg" alt="PRs are welcome" />
23
+ </a>
24
+ </div>
25
+
26
+ ## Features
27
+
28
+ - 🔄 Async & Sync Support
29
+ - 🚀 Simple API
30
+ - đŸ“Ļ Lightweight - Only requires `httpx`
31
+
32
+ ## What is PyTurnstile?
33
+
34
+ PyTurnstile simplifies Cloudflare Turnstile token validation. It handles all communication with Cloudflare's API.
35
+
36
+ <img src="https://github.com/Dong-Chen-1031/pyturnstile/blob/main/img/turnstile_verification.svg?raw=true" alt="Sequence diagram showing how PyTurnstile works" />
37
+
38
+ > Learn more at: https://developers.cloudflare.com/turnstile/
39
+
40
+ ## Installation
41
+
42
+ Install the package using your preferred dependency manager.
43
+
44
+ ### uv
45
+
46
+ ```bash
47
+ uv add pyturnstile
48
+ ```
49
+
50
+ ### pip
51
+
52
+ ```bash
53
+ pip install pyturnstile
54
+ ```
55
+
56
+ ## Usage
57
+
58
+ > ### 💡 TIP
59
+ >
60
+ > You can follow [this documentation](https://developers.cloudflare.com/turnstile/get-started/) and create your own Turnstile secret key at the [Cloudflare Turnstile dashboard](https://dash.cloudflare.com/?to=/:account/turnstile).
61
+
62
+ ### Quick Start
63
+
64
+ PyTurnstile provides two ways to validate tokens:
65
+
66
+ #### 1. Using the `Turnstile` class (Recommended)
67
+
68
+ ```python
69
+ from pyturnstile import Turnstile
70
+
71
+ turnstile = Turnstile(secret="your-secret-key")
72
+
73
+ response = await turnstile.async_validate(token="user-token-from-frontend")
74
+
75
+ # or validate synchronously
76
+ # response = turnstile.validate(token="user-token-from-frontend")
77
+
78
+ if response.success:
79
+ print("✅ Token is valid!")
80
+ ```
81
+
82
+ #### 2. Using functions directly
83
+
84
+ ```python
85
+ from pyturnstile import validate, async_validate
86
+
87
+ response = await async_validate(
88
+ token="user-token-from-frontend",
89
+ secret="your-secret-key"
90
+ )
91
+
92
+ # or validate synchronously
93
+ # response = validate(
94
+ # token="user-token-from-frontend",
95
+ # secret="your-secret-key"
96
+ # )
97
+
98
+ if response.success:
99
+ print("✅ Token is valid!")
100
+ ```
101
+
102
+ ### Optional Parameters
103
+
104
+ > ### â„šī¸ NOTE
105
+ >
106
+ > For more details on all available parameters, see the [Cloudflare documentation](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#required-parameters)
107
+
108
+ ```python
109
+ response = turnstile.validate(
110
+ token="user-token", # The token from the client-side widget
111
+ idempotency_key="unique-uuid", # Optional: UUID for retry protection
112
+ expected_remoteip="203.0.113.1", # Optional: The visitor's IP address that the challenge response must match
113
+ expected_hostname="example.com", # Optional: The hostname that the challenge response must match
114
+ expected_action="submit_form", # Optional: The action identifier that the challenge must match
115
+ timeout=10 # Optional: request timeout in seconds
116
+ )
117
+ ```
118
+
119
+ ### Response Object
120
+
121
+ > ### â„šī¸ NOTE
122
+ >
123
+ > For more details on all response fields, see the [Cloudflare documentation](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#response-fields)
124
+
125
+ The `TurnstileResponse` object contains:
126
+
127
+ ```python
128
+ response.success # bool: Whether validation succeeded
129
+ response.error_codes # list[TurnstileErrorCodes]: Error codes (if any)
130
+ response.challenge_ts # str: ISO timestamp of challenge completion
131
+ response.hostname # str: Hostname where challenge was served
132
+ response.action # str: Custom action identifier
133
+ response.cdata # str: Custom data payload from client-side
134
+ response.metadata["ephemeral_id"] # Device fingerprint ID (Enterprise only)
135
+ ```
136
+
137
+ ## Contributing
138
+
139
+ Any contributions are greatly appreciated. If you have a suggestion that would make this project better, please fork the repo and create a Pull Request. You can also [open an issue](https://github.com/Dong-Chen-1031/pyturnstile/issues).
140
+
141
+ ## License
142
+
143
+ Published under the [MIT License](LICENSE).
@@ -0,0 +1,43 @@
1
+ [project]
2
+ name = "pyturnstile"
3
+ version = "0.3.0"
4
+ description = "A Python library for validating Cloudflare Turnstile tokens with async and sync support"
5
+ readme = "README_PYPI.md"
6
+ requires-python = ">=3.8"
7
+ license = { text = "MIT" }
8
+ keywords = ["Cloudflare", "turnstile", "captcha", "validation"]
9
+ authors = [
10
+ { name = "Dong-Chen-1031", email = "dcdcdc1031@gmail.com" }
11
+ ]
12
+
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.8",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
+ "Typing :: Typed",
26
+ ]
27
+
28
+ dependencies = [
29
+ "httpx>=0.23.0",
30
+ "tenacity>=9.0.0",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/Dong-Chen-1031/pyturnstile"
35
+ Repository = "https://github.com/Dong-Chen-1031/pyturnstile"
36
+ Issues = "https://github.com/Dong-Chen-1031/pyturnstile/issues"
37
+
38
+ [dependency-groups]
39
+ dev = [
40
+ "pytest>=8.3.5",
41
+ "pytest-asyncio>=0.24.0",
42
+ "ruff>=0.15.1",
43
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,12 @@
1
+ """PyTurnstile: A Python library for validating Cloudflare Turnstile tokens."""
2
+
3
+ from ._core import TurnstileResponse, TurnstileValidationError, async_validate, validate
4
+ from ._turnstile import Turnstile
5
+
6
+ __all__ = [
7
+ "Turnstile",
8
+ "TurnstileResponse",
9
+ "TurnstileValidationError",
10
+ "validate",
11
+ "async_validate",
12
+ ]
@@ -0,0 +1,139 @@
1
+ """Async and sync functions to validate Turnstile tokens with Cloudflare's API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ import httpx
8
+
9
+ from ._types import TurnstileResponse, TurnstileResponseDict, TurnstileValidationError
10
+
11
+
12
+ def _additional_validation(
13
+ response: dict,
14
+ expected_hostname: Optional[str],
15
+ expected_action: Optional[str],
16
+ ) -> TurnstileResponse:
17
+ """
18
+ Perform additional validation checks on the TurnstileResponse.
19
+
20
+ Args:
21
+ response: The TurnstileResponse object to validate.
22
+ expected_hostname: The expected hostname to match against the response.
23
+ expected_action: The expected action identifier to match against the response.
24
+ """
25
+ response_dict = TurnstileResponseDict(**response)
26
+
27
+ if not response_dict["success"]:
28
+ return TurnstileResponse(response_dict)
29
+
30
+ if expected_hostname and response_dict["hostname"] != expected_hostname:
31
+ response_dict["error_codes"] = ["hostname-mismatch"]
32
+ response_dict["success"] = False
33
+ return TurnstileResponse(response_dict)
34
+
35
+ if expected_action and response_dict["action"] != expected_action:
36
+ response_dict["error_codes"] = ["action-mismatch"]
37
+ response_dict["success"] = False
38
+ return TurnstileResponse(response_dict)
39
+
40
+ return TurnstileResponse(response_dict)
41
+
42
+
43
+ async def async_validate(
44
+ token: str,
45
+ secret: str,
46
+ *,
47
+ idempotency_key: Optional[str] = None,
48
+ expected_remoteip: Optional[str] = None,
49
+ expected_hostname: Optional[str] = None,
50
+ expected_action: Optional[str] = None,
51
+ timeout: int = 10,
52
+ ) -> TurnstileResponse:
53
+ """
54
+ Asynchronously validate a Turnstile token with Cloudflare's API.
55
+ Args:
56
+ secret: Your widget's secret key from the Cloudflare dashboard.
57
+ token: The token from the client-side widget
58
+ idempotency_key: (Optional) UUID for retry protection
59
+ expected_remoteip: (Optional) The visitor's IP address that the challenge response must match
60
+ expected_hostname: (Optional) The hostname that the challenge response must match.
61
+ expected_action: (Optional) The action identifier that the challenge must match.
62
+ timeout: (Optional) Timeout for the API request in seconds
63
+ Returns:
64
+ TurnstileResponse: The response from the Turnstile API
65
+
66
+ For more details on all available parameters, see the [Cloudflare documentation](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#required-parameters)
67
+ """
68
+ url = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
69
+
70
+ data = {"secret": secret, "response": token}
71
+
72
+ if expected_remoteip:
73
+ data["remoteip"] = expected_remoteip
74
+
75
+ if idempotency_key:
76
+ data["idempotency_key"] = idempotency_key
77
+
78
+ try:
79
+ async with httpx.AsyncClient(timeout=timeout) as client:
80
+ response = await client.post(url, data=data)
81
+ response.raise_for_status()
82
+ return _additional_validation(
83
+ response.json(), expected_hostname, expected_action
84
+ )
85
+ except Exception as e:
86
+ raise TurnstileValidationError(f"Turnstile validation failed: {e}") from e
87
+
88
+
89
+ def validate(
90
+ token: str,
91
+ secret: str,
92
+ *,
93
+ idempotency_key: Optional[str] = None,
94
+ expected_remoteip: Optional[str] = None,
95
+ expected_hostname: Optional[str] = None,
96
+ expected_action: Optional[str] = None,
97
+ timeout: int = 10,
98
+ ) -> TurnstileResponse:
99
+ """
100
+ Validate a Turnstile token with Cloudflare's API.
101
+
102
+ Args:
103
+ secret: Your widget's secret key from the Cloudflare dashboard.
104
+ token: The token from the client-side widget
105
+ idempotency_key: (Optional) UUID for retry protection
106
+ expected_remoteip: (Optional) The visitor's IP address that the challenge response must match
107
+ expected_hostname: (Optional) The hostname that the challenge response must match.
108
+ expected_action: (Optional) The action identifier that the challenge must match.
109
+ timeout: (Optional) Timeout for the API request in seconds
110
+ Returns:
111
+ TurnstileResponse: The response from the Turnstile API
112
+
113
+ For more details on all available parameters, see the [Cloudflare documentation](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#required-parameters)
114
+ """
115
+ url = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
116
+
117
+ data = {"secret": secret, "response": token}
118
+
119
+ if expected_remoteip:
120
+ data["remoteip"] = expected_remoteip
121
+
122
+ if idempotency_key:
123
+ data["idempotency_key"] = idempotency_key
124
+
125
+ try:
126
+ with httpx.Client(timeout=timeout) as client:
127
+ response = client.post(url, data=data)
128
+ response.raise_for_status()
129
+ return _additional_validation(
130
+ response.json(), expected_hostname, expected_action
131
+ )
132
+ except Exception as e:
133
+ raise TurnstileValidationError(f"Turnstile validation failed: {e}") from e
134
+
135
+
136
+ __all__ = [
137
+ "validate",
138
+ "async_validate",
139
+ ]
@@ -0,0 +1,117 @@
1
+ """Client class for Cloudflare Turnstile token validation, providing both sync and async methods."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ from . import _core
8
+
9
+
10
+ class Turnstile:
11
+ """
12
+ A client for validating Cloudflare Turnstile tokens.
13
+
14
+ This class provides both synchronous and asynchronous methods to validate
15
+ Turnstile tokens with Cloudflare's verification API.
16
+
17
+ Methods:
18
+ validate: Synchronously validate a Turnstile token.
19
+ async_validate: Asynchronously validate a Turnstile token.
20
+
21
+ Example:
22
+ Asynchronous usage:
23
+ >>> turnstile = Turnstile(secret="your-secret-key")
24
+ >>> response = await turnstile.async_validate(token="user-token")
25
+ >>> if response.success:
26
+ ... print("Valid token")
27
+
28
+ Synchronous usage:
29
+ >>> turnstile = Turnstile(secret="your-secret-key")
30
+ >>> response = turnstile.validate(token="user-token")
31
+ >>> if response.success:
32
+ ... print("Valid token")
33
+
34
+ """
35
+
36
+ def __init__(self, secret: str):
37
+ """
38
+ Initialize the Turnstile client with your secret key.
39
+ Args:
40
+ secret: Your widget's secret key from the Cloudflare dashboard.
41
+ """
42
+ self.secret = secret
43
+
44
+ def validate(
45
+ self,
46
+ token: str,
47
+ *,
48
+ idempotency_key: Optional[str] = None,
49
+ expected_remoteip: Optional[str] = None,
50
+ expected_hostname: Optional[str] = None,
51
+ expected_action: Optional[str] = None,
52
+ timeout: int = 10,
53
+ ) -> _core.TurnstileResponse:
54
+ """
55
+ Validate a Turnstile token with Cloudflare's API.
56
+ Args:
57
+ token: The token from the client-side widget
58
+ idempotency_key: (Optional) UUID for retry protection
59
+ expected_remoteip: (Optional) The visitor's IP address that the challenge response must match
60
+ expected_hostname: (Optional) The hostname that the challenge response must match.
61
+ expected_action: (Optional) The action identifier that the challenge must match.
62
+ timeout: (Optional) Timeout for the API request in seconds
63
+ Returns:
64
+ TurnstileResponse: The response from the Turnstile API
65
+ Raises:
66
+ TurnstileValidationError: If the validation fails due to an API error or network issue
67
+
68
+ For more details on all available parameters, see the [Cloudflare documentation](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#required-parameters)
69
+ """
70
+ return _core.validate(
71
+ token=token,
72
+ secret=self.secret,
73
+ expected_remoteip=expected_remoteip,
74
+ expected_hostname=expected_hostname,
75
+ expected_action=expected_action,
76
+ idempotency_key=idempotency_key,
77
+ timeout=timeout,
78
+ )
79
+
80
+ async def async_validate(
81
+ self,
82
+ token: str,
83
+ *,
84
+ idempotency_key: Optional[str] = None,
85
+ expected_remoteip: Optional[str] = None,
86
+ expected_hostname: Optional[str] = None,
87
+ expected_action: Optional[str] = None,
88
+ timeout: int = 10,
89
+ ) -> _core.TurnstileResponse:
90
+ """
91
+ Asynchronously validate a Turnstile token with Cloudflare's API.
92
+ Args:
93
+ token: The token from the client-side widget
94
+ idempotency_key: (Optional) UUID for retry protection
95
+ expected_remoteip: (Optional) The visitor's IP address that the challenge response must match
96
+ expected_hostname: (Optional) The hostname that the challenge response must match.
97
+ expected_action: (Optional) The action identifier that the challenge must match.
98
+ timeout: (Optional) Timeout for the API request in seconds
99
+ Returns:
100
+ TurnstileResponse: The response from the Turnstile API
101
+ Raises:
102
+ TurnstileValidationError: If the validation fails due to an API error or network issue
103
+
104
+ For more details on all available parameters, see the [Cloudflare documentation](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#required-parameters)
105
+ """
106
+ return await _core.async_validate(
107
+ token=token,
108
+ secret=self.secret,
109
+ expected_remoteip=expected_remoteip,
110
+ expected_hostname=expected_hostname,
111
+ expected_action=expected_action,
112
+ idempotency_key=idempotency_key,
113
+ timeout=timeout,
114
+ )
115
+
116
+
117
+ __all__ = ["Turnstile"]
@@ -0,0 +1,117 @@
1
+ """Type definitions for PyTurnstile."""
2
+
3
+ from typing import Any, Literal, TypedDict
4
+
5
+
6
+ class TurnstileValidationError(Exception):
7
+ """Custom exception for Turnstile validation errors."""
8
+
9
+
10
+ TurnstileErrorCodes = Literal[
11
+ "missing-input-secret",
12
+ "invalid-input-secret",
13
+ "missing-input-response",
14
+ "invalid-input-response",
15
+ "bad-request",
16
+ "timeout-or-duplicate",
17
+ "internal-error",
18
+ "hostname-mismatch",
19
+ "action-mismatch",
20
+ ]
21
+ """
22
+ Literal type for Turnstile error codes returned by the API.
23
+
24
+ For more details on all Turnstile error codes, see the [Cloudflare documentation](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#error-codes-reference)
25
+ """
26
+
27
+
28
+ class TurnstileResponseDict(TypedDict):
29
+ """Type definition for the TurnstileResponse dictionary representation."""
30
+
31
+ success: bool
32
+ action: str
33
+ cdata: str
34
+ challenge_ts: str
35
+ error_codes: list[TurnstileErrorCodes] | list[str]
36
+ hostname: str
37
+ metadata: dict[str, Any]
38
+
39
+
40
+ class TurnstileResponse:
41
+ """
42
+ Represents the response from Cloudflare's Turnstile validation API.
43
+
44
+ For more details on all response fields, see the [Cloudflare documentation](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#response-fields)
45
+ """
46
+
47
+ action: str
48
+ """Custom action identifier from client-side"""
49
+ cdata: str
50
+ """Custom data payload from client-side"""
51
+ challenge_ts: str
52
+ """ISO timestamp when the challenge was solved"""
53
+ error_codes: list[TurnstileErrorCodes] | list[str]
54
+ """Array of error codes (if validation failed)"""
55
+ hostname: str
56
+ """Hostname where the challenge was served"""
57
+ metadata: dict[str, Any]
58
+ """
59
+ Additional metadata returned by the API.
60
+
61
+ Including "ephemeral_id" for device fingerprinting (Enterprise only)
62
+ """
63
+ success: bool
64
+ """Boolean indicating if validation was successful"""
65
+
66
+ def __init__(self, data: dict | TurnstileResponseDict) -> None:
67
+ """
68
+ Initialize the TurnstileResponse from the API response data.
69
+ Args:
70
+ data: The JSON response from the Turnstile API as a dictionary.
71
+ """
72
+ self.action = data.get("action", "")
73
+ self.cdata = data.get("cdata", "")
74
+ self.challenge_ts = data.get("challenge_ts", "")
75
+ self.error_codes = data.get("error-codes", [])
76
+ self.hostname = data.get("hostname", "")
77
+ self.metadata = data.get("metadata", {})
78
+ self.success = data.get("success", False)
79
+
80
+ def to_dict(self) -> TurnstileResponseDict:
81
+ """Convert the TurnstileResponse to a dictionary."""
82
+ return {
83
+ "success": self.success,
84
+ "action": self.action,
85
+ "cdata": self.cdata,
86
+ "challenge_ts": self.challenge_ts,
87
+ "error_codes": self.error_codes,
88
+ "hostname": self.hostname,
89
+ "metadata": self.metadata,
90
+ }
91
+
92
+ def model_dump(self) -> TurnstileResponseDict:
93
+ """Alias for to_dict() to match common naming conventions."""
94
+ return self.to_dict()
95
+
96
+ def __str__(self) -> str:
97
+ return (
98
+ "TurnstileResponse("
99
+ f"success={self.success}, "
100
+ f"action={self.action}, "
101
+ f"hostname={self.hostname}, "
102
+ f"error_codes={self.error_codes}"
103
+ ")"
104
+ )
105
+
106
+ def __repr__(self) -> str:
107
+ return self.__str__()
108
+
109
+ def __bool__(self) -> bool:
110
+ return self.success
111
+
112
+
113
+ __all__ = [
114
+ "TurnstileResponse",
115
+ "TurnstileValidationError",
116
+ "TurnstileErrorCodes",
117
+ ]
File without changes
@@ -0,0 +1,172 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyturnstile
3
+ Version: 0.3.0
4
+ Summary: A Python library for validating Cloudflare Turnstile tokens with async and sync support
5
+ Author-email: Dong-Chen-1031 <dcdcdc1031@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Dong-Chen-1031/pyturnstile
8
+ Project-URL: Repository, https://github.com/Dong-Chen-1031/pyturnstile
9
+ Project-URL: Issues, https://github.com/Dong-Chen-1031/pyturnstile/issues
10
+ Keywords: Cloudflare,turnstile,captcha,validation
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.8
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: httpx>=0.23.0
27
+ Requires-Dist: tenacity>=9.0.0
28
+ Dynamic: license-file
29
+
30
+ <div align="center">
31
+ <h1>PyTurnstile</h1>
32
+ <a href="https://pypi.org/project/pyturnstile" target="_blank">
33
+ <img src="https://github.com/Dong-Chen-1031/pyturnstile/blob/main/img/logo.png?raw=true" width="300" alt="Cloudflare Turnstile widget" />
34
+ </a>
35
+ <p>A Python library for validating <a href="https://developers.cloudflare.com/turnstile/">Cloudflare Turnstile</a> tokens with both async and sync support.</p>
36
+
37
+ <a href="https://github.com/dong-chen-1031/pyturnstile/actions?query=workflow%3ATest+event%3Apush+branch%3Amain" target="_blank">
38
+ <img src="https://github.com/dong-chen-1031/pyturnstile/actions/workflows/test.yml/badge.svg?event=push&branch=main" alt="Test">
39
+ </a>
40
+ <a href="https://pypi.org/project/pyturnstile" target="_blank">
41
+ <img src="https://img.shields.io/pypi/v/pyturnstile?color=%2334D058&label=pypi%20package" alt="Package version">
42
+ </a>
43
+ <a href="https://pypi.org/project/pyturnstile" target="_blank">
44
+ <img src="https://img.shields.io/badge/Python-3.8%2B?color=%2334D058&logo=Python&logoColor=rgb(255%2C%20255%2C%20255)" alt="Supported Python versions">
45
+ </a>
46
+ <a href="https://docs.astral.sh/ruff/" target="_blank">
47
+ <img src="https://camo.githubusercontent.com/d6c7524504b7d886a9d34c11f44b9d31b2de1a579325b42e932744c4575a063b/68747470733a2f2f696d672e736869656c64732e696f2f656e64706f696e743f75726c3d68747470733a2f2f7261772e67697468756275736572636f6e74656e742e636f6d2f61737472616c2d73682f727566662f6d61696e2f6173736574732f62616467652f76322e6a736f6e" alt="Ruff" />
48
+ </a>
49
+ <img src="https://img.shields.io/badge/License-MIT-%2334D058.svg" alt="License: MIT" />
50
+ <a href="https://github.com/dong-chen-1031/pyturnstile/pulls" target="_blank">
51
+ <img src="https://img.shields.io/badge/PRs-welcome-%2334D058.svg" alt="PRs are welcome" />
52
+ </a>
53
+ </div>
54
+
55
+ ## Features
56
+
57
+ - 🔄 Async & Sync Support
58
+ - 🚀 Simple API
59
+ - đŸ“Ļ Lightweight - Only requires `httpx`
60
+
61
+ ## What is PyTurnstile?
62
+
63
+ PyTurnstile simplifies Cloudflare Turnstile token validation. It handles all communication with Cloudflare's API.
64
+
65
+ <img src="https://github.com/Dong-Chen-1031/pyturnstile/blob/main/img/turnstile_verification.svg?raw=true" alt="Sequence diagram showing how PyTurnstile works" />
66
+
67
+ > Learn more at: https://developers.cloudflare.com/turnstile/
68
+
69
+ ## Installation
70
+
71
+ Install the package using your preferred dependency manager.
72
+
73
+ ### uv
74
+
75
+ ```bash
76
+ uv add pyturnstile
77
+ ```
78
+
79
+ ### pip
80
+
81
+ ```bash
82
+ pip install pyturnstile
83
+ ```
84
+
85
+ ## Usage
86
+
87
+ > ### 💡 TIP
88
+ >
89
+ > You can follow [this documentation](https://developers.cloudflare.com/turnstile/get-started/) and create your own Turnstile secret key at the [Cloudflare Turnstile dashboard](https://dash.cloudflare.com/?to=/:account/turnstile).
90
+
91
+ ### Quick Start
92
+
93
+ PyTurnstile provides two ways to validate tokens:
94
+
95
+ #### 1. Using the `Turnstile` class (Recommended)
96
+
97
+ ```python
98
+ from pyturnstile import Turnstile
99
+
100
+ turnstile = Turnstile(secret="your-secret-key")
101
+
102
+ response = await turnstile.async_validate(token="user-token-from-frontend")
103
+
104
+ # or validate synchronously
105
+ # response = turnstile.validate(token="user-token-from-frontend")
106
+
107
+ if response.success:
108
+ print("✅ Token is valid!")
109
+ ```
110
+
111
+ #### 2. Using functions directly
112
+
113
+ ```python
114
+ from pyturnstile import validate, async_validate
115
+
116
+ response = await async_validate(
117
+ token="user-token-from-frontend",
118
+ secret="your-secret-key"
119
+ )
120
+
121
+ # or validate synchronously
122
+ # response = validate(
123
+ # token="user-token-from-frontend",
124
+ # secret="your-secret-key"
125
+ # )
126
+
127
+ if response.success:
128
+ print("✅ Token is valid!")
129
+ ```
130
+
131
+ ### Optional Parameters
132
+
133
+ > ### â„šī¸ NOTE
134
+ >
135
+ > For more details on all available parameters, see the [Cloudflare documentation](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#required-parameters)
136
+
137
+ ```python
138
+ response = turnstile.validate(
139
+ token="user-token", # The token from the client-side widget
140
+ idempotency_key="unique-uuid", # Optional: UUID for retry protection
141
+ expected_remoteip="203.0.113.1", # Optional: The visitor's IP address that the challenge response must match
142
+ expected_hostname="example.com", # Optional: The hostname that the challenge response must match
143
+ expected_action="submit_form", # Optional: The action identifier that the challenge must match
144
+ timeout=10 # Optional: request timeout in seconds
145
+ )
146
+ ```
147
+
148
+ ### Response Object
149
+
150
+ > ### â„šī¸ NOTE
151
+ >
152
+ > For more details on all response fields, see the [Cloudflare documentation](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#response-fields)
153
+
154
+ The `TurnstileResponse` object contains:
155
+
156
+ ```python
157
+ response.success # bool: Whether validation succeeded
158
+ response.error_codes # list[TurnstileErrorCodes]: Error codes (if any)
159
+ response.challenge_ts # str: ISO timestamp of challenge completion
160
+ response.hostname # str: Hostname where challenge was served
161
+ response.action # str: Custom action identifier
162
+ response.cdata # str: Custom data payload from client-side
163
+ response.metadata["ephemeral_id"] # Device fingerprint ID (Enterprise only)
164
+ ```
165
+
166
+ ## Contributing
167
+
168
+ Any contributions are greatly appreciated. If you have a suggestion that would make this project better, please fork the repo and create a Pull Request. You can also [open an issue](https://github.com/Dong-Chen-1031/pyturnstile/issues).
169
+
170
+ ## License
171
+
172
+ Published under the [MIT License](LICENSE).
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ README_PYPI.md
4
+ pyproject.toml
5
+ src/pyturnstile/__init__.py
6
+ src/pyturnstile/_core.py
7
+ src/pyturnstile/_turnstile.py
8
+ src/pyturnstile/_types.py
9
+ src/pyturnstile/py.typed
10
+ src/pyturnstile.egg-info/PKG-INFO
11
+ src/pyturnstile.egg-info/SOURCES.txt
12
+ src/pyturnstile.egg-info/dependency_links.txt
13
+ src/pyturnstile.egg-info/requires.txt
14
+ src/pyturnstile.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ httpx>=0.23.0
2
+ tenacity>=9.0.0
@@ -0,0 +1 @@
1
+ pyturnstile