flask-Humanify 0.1.0__py3-none-any.whl → 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.
- {flask_Humanify → flask_humanify}/__init__.py +1 -1
- flask_humanify/datasets/ipset.json +1 -0
- flask_humanify/features/rate_limiter.py +77 -0
- {flask_Humanify → flask_humanify}/ipset.py +28 -3
- flask_humanify/templates/access_denied.html +110 -0
- flask_humanify/templates/oneclick_captcha.html +192 -0
- flask_humanify/templates/rate_limited.html +110 -0
- {flask_humanify-0.1.0.dist-info → flask_humanify-0.1.2.dist-info}/METADATA +5 -3
- flask_humanify-0.1.2.dist-info/RECORD +14 -0
- flask_humanify-0.1.2.dist-info/top_level.txt +1 -0
- flask_humanify-0.1.0.dist-info/RECORD +0 -9
- flask_humanify-0.1.0.dist-info/top_level.txt +0 -1
- {flask_Humanify → flask_humanify}/humanify.py +0 -0
- {flask_Humanify → flask_humanify}/utils.py +0 -0
- {flask_humanify-0.1.0.dist-info → flask_humanify-0.1.2.dist-info}/WHEEL +0 -0
- {flask_humanify-0.1.0.dist-info → flask_humanify-0.1.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
import hashlib
|
2
|
+
import time
|
3
|
+
from collections import defaultdict, deque
|
4
|
+
|
5
|
+
from flask import request, redirect, url_for, render_template
|
6
|
+
from flask_humanify.utils import get_client_ip, get_return_url
|
7
|
+
|
8
|
+
|
9
|
+
class RateLimiter:
|
10
|
+
"""
|
11
|
+
Rate limiter.
|
12
|
+
"""
|
13
|
+
|
14
|
+
def __init__(self, app=None, max_requests: int = 2, time_window: int = 10):
|
15
|
+
"""
|
16
|
+
Initialize the rate limiter.
|
17
|
+
"""
|
18
|
+
self.app = app
|
19
|
+
if app is not None:
|
20
|
+
self.init_app(app)
|
21
|
+
self.max_requests = max_requests
|
22
|
+
self.time_window = time_window
|
23
|
+
self.ip_request_times = defaultdict(deque)
|
24
|
+
|
25
|
+
def init_app(self, app):
|
26
|
+
"""
|
27
|
+
Initialize the rate limiter.
|
28
|
+
"""
|
29
|
+
self.app = app
|
30
|
+
self.app.before_request(self.before_request)
|
31
|
+
|
32
|
+
@self.app.route(
|
33
|
+
"/humanify/rate_limited",
|
34
|
+
methods=["GET"],
|
35
|
+
endpoint="humanify.rate_limited",
|
36
|
+
)
|
37
|
+
def rate_limited():
|
38
|
+
"""
|
39
|
+
Rate limited route.
|
40
|
+
"""
|
41
|
+
return (
|
42
|
+
render_template("rate_limited.html").replace(
|
43
|
+
"RETURN_URL", get_return_url(request)
|
44
|
+
),
|
45
|
+
429,
|
46
|
+
{"Cache-Control": "public, max-age=15552000"},
|
47
|
+
)
|
48
|
+
|
49
|
+
def before_request(self):
|
50
|
+
"""
|
51
|
+
Before request hook.
|
52
|
+
"""
|
53
|
+
ip = get_client_ip(request)
|
54
|
+
if request.endpoint == "humanify.rate_limited":
|
55
|
+
return
|
56
|
+
if self.is_rate_limited(ip or "127.0.0.1"):
|
57
|
+
return redirect(
|
58
|
+
url_for("humanify.rate_limited", return_url=request.full_path)
|
59
|
+
)
|
60
|
+
|
61
|
+
def is_rate_limited(self, ip: str) -> bool:
|
62
|
+
"""
|
63
|
+
Check if the IP is rate limited.
|
64
|
+
"""
|
65
|
+
hashed_ip = hashlib.sha256(ip.encode()).hexdigest()
|
66
|
+
|
67
|
+
current_time = time.time()
|
68
|
+
request_times = self.ip_request_times[hashed_ip]
|
69
|
+
|
70
|
+
while request_times and request_times[0] <= current_time - self.time_window:
|
71
|
+
request_times.popleft()
|
72
|
+
|
73
|
+
if len(request_times) < self.max_requests:
|
74
|
+
request_times.append(current_time)
|
75
|
+
return False
|
76
|
+
|
77
|
+
return True
|
@@ -4,7 +4,10 @@ import socket
|
|
4
4
|
import time
|
5
5
|
import threading
|
6
6
|
import os
|
7
|
+
import importlib.metadata
|
8
|
+
import importlib.resources
|
7
9
|
import urllib.request
|
10
|
+
from pathlib import Path
|
8
11
|
from typing import Dict, List, Optional, Tuple
|
9
12
|
from datetime import datetime, timedelta
|
10
13
|
from netaddr import IPNetwork, IPAddress
|
@@ -13,20 +16,39 @@ from netaddr import IPNetwork, IPAddress
|
|
13
16
|
logger = logging.getLogger(__name__)
|
14
17
|
|
15
18
|
|
19
|
+
try:
|
20
|
+
importlib.metadata.distribution("flask-humanify")
|
21
|
+
BASE_DIR = importlib.resources.files("flask_humanify")
|
22
|
+
except importlib.metadata.PackageNotFoundError:
|
23
|
+
BASE_DIR = Path(__file__).parent
|
24
|
+
|
25
|
+
DATASET_DIR = BASE_DIR / "datasets"
|
26
|
+
if not DATASET_DIR.exists():
|
27
|
+
DATASET_DIR.mkdir(parents=True)
|
28
|
+
|
29
|
+
IPSET_DATA_PATH = str(DATASET_DIR / "ipset.json")
|
30
|
+
|
31
|
+
|
16
32
|
class IPSetMemoryServer:
|
17
33
|
"""A singleton memory server that manages IP sets and provides lookup functionality."""
|
18
34
|
|
19
35
|
_instance = None
|
20
36
|
_lock = threading.Lock()
|
21
37
|
|
22
|
-
def __new__(cls, port: int = 9876, data_path: str =
|
38
|
+
def __new__(cls, port: int = 9876, data_path: Optional[str] = None):
|
39
|
+
if data_path is None:
|
40
|
+
data_path = IPSET_DATA_PATH
|
41
|
+
|
23
42
|
with cls._lock:
|
24
43
|
if cls._instance is None:
|
25
44
|
cls._instance = super(IPSetMemoryServer, cls).__new__(cls)
|
26
45
|
cls._instance.initialized = False
|
27
46
|
return cls._instance
|
28
47
|
|
29
|
-
def __init__(self, port: int = 9876, data_path: str =
|
48
|
+
def __init__(self, port: int = 9876, data_path: Optional[str] = None):
|
49
|
+
if data_path is None:
|
50
|
+
data_path = IPSET_DATA_PATH
|
51
|
+
|
30
52
|
if getattr(self, "initialized", False):
|
31
53
|
return
|
32
54
|
|
@@ -281,8 +303,11 @@ class IPSetClient:
|
|
281
303
|
self.socket = None
|
282
304
|
|
283
305
|
|
284
|
-
def ensure_server_running(port: int = 9876, data_path: str =
|
306
|
+
def ensure_server_running(port: int = 9876, data_path: Optional[str] = None) -> None:
|
285
307
|
"""Ensure that the memory server is running."""
|
308
|
+
if data_path is None:
|
309
|
+
data_path = IPSET_DATA_PATH
|
310
|
+
|
286
311
|
server = IPSetMemoryServer(port=port, data_path=data_path)
|
287
312
|
server.start()
|
288
313
|
|
@@ -0,0 +1,110 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
+
<title>Access Denied</title>
|
7
|
+
<style>
|
8
|
+
body {
|
9
|
+
font-family: system-ui, sans-serif;
|
10
|
+
background: #f2f2f2;
|
11
|
+
color: #181818;
|
12
|
+
margin: 0;
|
13
|
+
line-height: 1.5;
|
14
|
+
text-align: center;
|
15
|
+
display: grid;
|
16
|
+
place-items: center;
|
17
|
+
height: 100vh;
|
18
|
+
padding: 0 20px;
|
19
|
+
}
|
20
|
+
|
21
|
+
@media (prefers-color-scheme: dark) {
|
22
|
+
body {
|
23
|
+
background: #121212;
|
24
|
+
color: #f2f2f2;
|
25
|
+
}
|
26
|
+
|
27
|
+
.btn {
|
28
|
+
background: #f2f2f2;
|
29
|
+
color: #121212;
|
30
|
+
}
|
31
|
+
|
32
|
+
.fill {
|
33
|
+
background: rgba(0,0,0,0.15);
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
.content {
|
38
|
+
max-width: 600px;
|
39
|
+
}
|
40
|
+
|
41
|
+
.emoji {
|
42
|
+
font-size: 48px;
|
43
|
+
margin-bottom: 10px;
|
44
|
+
}
|
45
|
+
|
46
|
+
h1 {
|
47
|
+
font-size: 22px;
|
48
|
+
margin: 15px 0;
|
49
|
+
}
|
50
|
+
|
51
|
+
p {
|
52
|
+
margin: 15px 0;
|
53
|
+
opacity: 0.8;
|
54
|
+
}
|
55
|
+
|
56
|
+
.btn {
|
57
|
+
display: inline-block;
|
58
|
+
padding: 12px 24px;
|
59
|
+
background: #181818;
|
60
|
+
color: #f2f2f2;
|
61
|
+
border-radius: 6px;
|
62
|
+
text-decoration: none;
|
63
|
+
margin-top: 20px;
|
64
|
+
position: relative;
|
65
|
+
overflow: hidden;
|
66
|
+
pointer-events: none;
|
67
|
+
}
|
68
|
+
|
69
|
+
.fill {
|
70
|
+
position: absolute;
|
71
|
+
left: 0;
|
72
|
+
top: 0;
|
73
|
+
height: 100%;
|
74
|
+
width: 0;
|
75
|
+
background: rgba(255,255,255,0.15);
|
76
|
+
animation: fillBtn 10s linear forwards;
|
77
|
+
}
|
78
|
+
|
79
|
+
@keyframes fillBtn {
|
80
|
+
to { width: 100%; }
|
81
|
+
}
|
82
|
+
</style>
|
83
|
+
</head>
|
84
|
+
<body>
|
85
|
+
<div class="content">
|
86
|
+
<div class="emoji">
|
87
|
+
🚨
|
88
|
+
</div>
|
89
|
+
<h1>Sorry, but you can't see the requested page.</h1>
|
90
|
+
<p>Automated scripts are likely attempting to request this page. If you use anonymizing tools like VPNs or proxies, consider disabling them temporarily.</p>
|
91
|
+
<a href="RETURN_URL" id="retry" class="btn">
|
92
|
+
<div class="fill"></div>
|
93
|
+
<span>Try again</span>
|
94
|
+
</a>
|
95
|
+
</div>
|
96
|
+
|
97
|
+
<noscript>
|
98
|
+
<style>
|
99
|
+
.btn { pointer-events: all !important; }
|
100
|
+
.fill { display: none; }
|
101
|
+
</style>
|
102
|
+
</noscript>
|
103
|
+
|
104
|
+
<script>
|
105
|
+
setTimeout(function() {
|
106
|
+
document.getElementById('retry').style.pointerEvents = 'all';
|
107
|
+
}, 10000);
|
108
|
+
</script>
|
109
|
+
</body>
|
110
|
+
</html>
|
@@ -0,0 +1,192 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
+
<title>Verify Human</title>
|
7
|
+
<style>
|
8
|
+
body {
|
9
|
+
font-family: system-ui, sans-serif;
|
10
|
+
background: #f2f2f2;
|
11
|
+
color: #181818;
|
12
|
+
margin: 0;
|
13
|
+
line-height: 1.5;
|
14
|
+
text-align: center;
|
15
|
+
display: grid;
|
16
|
+
place-items: center;
|
17
|
+
height: 100vh;
|
18
|
+
padding: 0 20px;
|
19
|
+
}
|
20
|
+
|
21
|
+
@media (prefers-color-scheme: dark) {
|
22
|
+
body {
|
23
|
+
background: #121212;
|
24
|
+
color: #f2f2f2;
|
25
|
+
}
|
26
|
+
|
27
|
+
.fill {
|
28
|
+
background: rgba(0,0,0,0.15);
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
.content {
|
33
|
+
max-width: 600px;
|
34
|
+
display: flex;
|
35
|
+
flex-direction: column;
|
36
|
+
align-items: center;
|
37
|
+
}
|
38
|
+
|
39
|
+
.emoji {
|
40
|
+
font-size: 48px;
|
41
|
+
margin-bottom: 10px;
|
42
|
+
}
|
43
|
+
|
44
|
+
h1 {
|
45
|
+
font-size: 22px;
|
46
|
+
margin: 15px 0;
|
47
|
+
}
|
48
|
+
|
49
|
+
p {
|
50
|
+
margin: 15px 0;
|
51
|
+
opacity: 0.8;
|
52
|
+
}
|
53
|
+
|
54
|
+
.preview-container {
|
55
|
+
width: 200px;
|
56
|
+
height: 200px;
|
57
|
+
margin-bottom: 20px;
|
58
|
+
}
|
59
|
+
|
60
|
+
.preview-container img {
|
61
|
+
width: 100%;
|
62
|
+
height: 100%;
|
63
|
+
object-fit: cover;
|
64
|
+
border-radius: 8px;
|
65
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
66
|
+
}
|
67
|
+
|
68
|
+
.text-container {
|
69
|
+
text-align: center;
|
70
|
+
margin-bottom: 25px;
|
71
|
+
}
|
72
|
+
|
73
|
+
.error {
|
74
|
+
color: #e53935;
|
75
|
+
margin: 10px 0;
|
76
|
+
font-size: 16px;
|
77
|
+
}
|
78
|
+
|
79
|
+
.images-row {
|
80
|
+
display: flex;
|
81
|
+
flex-direction: row;
|
82
|
+
justify-content: center;
|
83
|
+
flex-wrap: wrap;
|
84
|
+
gap: 15px;
|
85
|
+
margin-bottom: 25px;
|
86
|
+
width: 100%;
|
87
|
+
}
|
88
|
+
|
89
|
+
.image-button {
|
90
|
+
background: none;
|
91
|
+
border: none;
|
92
|
+
padding: 0;
|
93
|
+
cursor: pointer;
|
94
|
+
width: 100px;
|
95
|
+
height: 100px;
|
96
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
97
|
+
border-radius: 8px;
|
98
|
+
overflow: hidden;
|
99
|
+
position: relative;
|
100
|
+
}
|
101
|
+
|
102
|
+
.image-button:hover {
|
103
|
+
transform: translateY(-3px);
|
104
|
+
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
105
|
+
}
|
106
|
+
|
107
|
+
.image-button img {
|
108
|
+
width: 100%;
|
109
|
+
height: 100%;
|
110
|
+
object-fit: cover;
|
111
|
+
}
|
112
|
+
|
113
|
+
.audio-challenge-link {
|
114
|
+
display: flex;
|
115
|
+
align-items: center;
|
116
|
+
justify-content: center;
|
117
|
+
margin-top: 15px;
|
118
|
+
color: #4a6ed0;
|
119
|
+
text-decoration: none;
|
120
|
+
font-size: 14px;
|
121
|
+
transition: all 0.2s ease;
|
122
|
+
}
|
123
|
+
|
124
|
+
.audio-challenge-link:hover {
|
125
|
+
transform: translateY(-2px);
|
126
|
+
}
|
127
|
+
|
128
|
+
.audio-challenge-link svg {
|
129
|
+
margin-right: 6px;
|
130
|
+
width: 16px;
|
131
|
+
height: 16px;
|
132
|
+
fill: currentColor;
|
133
|
+
}
|
134
|
+
|
135
|
+
@media (max-width: 600px) {
|
136
|
+
.preview-container {
|
137
|
+
width: 180px;
|
138
|
+
height: 180px;
|
139
|
+
}
|
140
|
+
|
141
|
+
.images-row {
|
142
|
+
gap: 10px;
|
143
|
+
justify-content: center;
|
144
|
+
}
|
145
|
+
|
146
|
+
.image-button {
|
147
|
+
width: 80px;
|
148
|
+
height: 80px;
|
149
|
+
}
|
150
|
+
}
|
151
|
+
</style>
|
152
|
+
</head>
|
153
|
+
<body>
|
154
|
+
<div class="content">
|
155
|
+
<div class="preview-container">
|
156
|
+
<img src="{{ preview_image }}" alt="Reference image">
|
157
|
+
</div>
|
158
|
+
|
159
|
+
<div class="text-container">
|
160
|
+
{% if subject == "smiling dog" %}
|
161
|
+
<p>To verify you're not a bot, select the dog that smiles like shown above.</p>
|
162
|
+
{% else %}
|
163
|
+
<p>To verify you're not a bot, select the image that matches the motif shown above.</p>
|
164
|
+
{% endif %}
|
165
|
+
{% if error %}
|
166
|
+
<p class="error">{{ error }}</p>
|
167
|
+
{% endif %}
|
168
|
+
</div>
|
169
|
+
|
170
|
+
<div class="images-row">
|
171
|
+
{% for image in images %}
|
172
|
+
<form action="{{ url_for('captchaify.verify') }}" method="POST">
|
173
|
+
<input type="hidden" name="return_url" value="{{ return_url }}">
|
174
|
+
<input type="hidden" name="captcha_data" value="{{ captcha_data }}">
|
175
|
+
<button type="submit" class="image-button" name="{{ loop.index }}" value="1">
|
176
|
+
<img src="{{ image }}" alt="Selection image {{ loop.index }}">
|
177
|
+
</button>
|
178
|
+
</form>
|
179
|
+
{% endfor %}
|
180
|
+
</div>
|
181
|
+
|
182
|
+
{% if audio_challenge_available %}
|
183
|
+
<a class="audio-challenge-link" href="{{ url_for('captchaify.audio_challenge', return_url=return_url) }}">
|
184
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
185
|
+
<path d="M10 1a1 1 0 0 1 1 1v13.59l1.29-1.29a1 1 0 1 1 1.42 1.42l-3 3a1 1 0 0 1-1.42 0l-3-3a1 1 0 0 1 1.42-1.42L9 15.59V2a1 1 0 0 1 1-1zm2-1a1 1 0 1 1 0 2 5 5 0 0 0-5 5 1 1 0 1 1-2 0 7 7 0 0 1 7-7zm2 3a1 1 0 1 1 0 2 3 3 0 0 0-3 3 1 1 0 1 1-2 0 5 5 0 0 1 5-5z" />
|
186
|
+
</svg>
|
187
|
+
Audio challenge
|
188
|
+
</a>
|
189
|
+
{% endif %}
|
190
|
+
</div>
|
191
|
+
</body>
|
192
|
+
</html>
|
@@ -0,0 +1,110 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
+
<title>Rate Limited</title>
|
7
|
+
<style>
|
8
|
+
body {
|
9
|
+
font-family: system-ui, sans-serif;
|
10
|
+
background: #f2f2f2;
|
11
|
+
color: #181818;
|
12
|
+
margin: 0;
|
13
|
+
line-height: 1.5;
|
14
|
+
text-align: center;
|
15
|
+
display: grid;
|
16
|
+
place-items: center;
|
17
|
+
height: 100vh;
|
18
|
+
padding: 0 20px;
|
19
|
+
}
|
20
|
+
|
21
|
+
@media (prefers-color-scheme: dark) {
|
22
|
+
body {
|
23
|
+
background: #121212;
|
24
|
+
color: #f2f2f2;
|
25
|
+
}
|
26
|
+
|
27
|
+
.btn {
|
28
|
+
background: #f2f2f2;
|
29
|
+
color: #121212;
|
30
|
+
}
|
31
|
+
|
32
|
+
.fill {
|
33
|
+
background: rgba(0,0,0,0.15);
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
.content {
|
38
|
+
max-width: 600px;
|
39
|
+
}
|
40
|
+
|
41
|
+
.emoji {
|
42
|
+
font-size: 48px;
|
43
|
+
margin-bottom: 10px;
|
44
|
+
}
|
45
|
+
|
46
|
+
h1 {
|
47
|
+
font-size: 22px;
|
48
|
+
margin: 15px 0;
|
49
|
+
}
|
50
|
+
|
51
|
+
p {
|
52
|
+
margin: 15px 0;
|
53
|
+
opacity: 0.8;
|
54
|
+
}
|
55
|
+
|
56
|
+
.btn {
|
57
|
+
display: inline-block;
|
58
|
+
padding: 12px 24px;
|
59
|
+
background: #181818;
|
60
|
+
color: #f2f2f2;
|
61
|
+
border-radius: 6px;
|
62
|
+
text-decoration: none;
|
63
|
+
margin-top: 20px;
|
64
|
+
position: relative;
|
65
|
+
overflow: hidden;
|
66
|
+
pointer-events: none;
|
67
|
+
}
|
68
|
+
|
69
|
+
.fill {
|
70
|
+
position: absolute;
|
71
|
+
left: 0;
|
72
|
+
top: 0;
|
73
|
+
height: 100%;
|
74
|
+
width: 0;
|
75
|
+
background: rgba(255,255,255,0.15);
|
76
|
+
animation: fillBtn 10s linear forwards;
|
77
|
+
}
|
78
|
+
|
79
|
+
@keyframes fillBtn {
|
80
|
+
to { width: 100%; }
|
81
|
+
}
|
82
|
+
</style>
|
83
|
+
</head>
|
84
|
+
<body>
|
85
|
+
<div class="content">
|
86
|
+
<div class="emoji">
|
87
|
+
🫖
|
88
|
+
</div>
|
89
|
+
<h1>Have some Tea</h1>
|
90
|
+
<p>It appears that you are sending an excessive number of requests to this website. Please reduce the frequency of your requests.</p>
|
91
|
+
<a href="RETURN_URL" id="retry" class="btn">
|
92
|
+
<div class="fill"></div>
|
93
|
+
<span>Try again</span>
|
94
|
+
</a>
|
95
|
+
</div>
|
96
|
+
|
97
|
+
<noscript>
|
98
|
+
<style>
|
99
|
+
.btn { pointer-events: all !important; }
|
100
|
+
.fill { display: none; }
|
101
|
+
</style>
|
102
|
+
</noscript>
|
103
|
+
|
104
|
+
<script>
|
105
|
+
setTimeout(function() {
|
106
|
+
document.getElementById('retry').style.pointerEvents = 'all';
|
107
|
+
}, 10000);
|
108
|
+
</script>
|
109
|
+
</body>
|
110
|
+
</html>
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: flask-Humanify
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.2
|
4
4
|
Summary: Protect against bots and DDoS attacks
|
5
5
|
Author-email: TN3W <tn3w@protonmail.com>
|
6
6
|
License-Expression: Apache-2.0
|
@@ -55,15 +55,17 @@ if __name__ == "__main__":
|
|
55
55
|
app.run()
|
56
56
|
```
|
57
57
|
|
58
|
+
## Usage
|
59
|
+
|
58
60
|
### Installation
|
59
61
|
Install the package with pip:
|
60
62
|
```bash
|
61
|
-
pip install flask-
|
63
|
+
pip install flask-humanify --upgrade
|
62
64
|
```
|
63
65
|
|
64
66
|
Import the extension:
|
65
67
|
```python
|
66
|
-
from
|
68
|
+
from flask_humanify import Humanify
|
67
69
|
```
|
68
70
|
|
69
71
|
Add the extension to your Flask app:
|
@@ -0,0 +1,14 @@
|
|
1
|
+
flask_humanify/__init__.py,sha256=L67JR_FDgC9iNnBrnRchNPZe_z7tXbwV_xJ4NEQKGFw,269
|
2
|
+
flask_humanify/humanify.py,sha256=JJeTcJlMte6zj2QRlat2876iJzFg40YNoMEX5kWepiU,3772
|
3
|
+
flask_humanify/ipset.py,sha256=ZZZRtgtddkZr2q1zuI6803hJQ8OodVHNdyZvZGqpmMI,10866
|
4
|
+
flask_humanify/utils.py,sha256=CJ4FPhNJ75FuWcAn_ZuZkqRa9HR3jVvYOu9NZaN4L1o,2461
|
5
|
+
flask_humanify/datasets/ipset.json,sha256=YNPqwI109lYkfvZeOPsoDH_dKJxOCs0G2nvx_s2mvqU,30601191
|
6
|
+
flask_humanify/features/rate_limiter.py,sha256=kl3l1SHtDOPzOb1TLZizIRp-mJo5d30uW62ocmBm_fM,2175
|
7
|
+
flask_humanify/templates/access_denied.html,sha256=Y7EzM53LRBMJBniczTWYvinHuPPFQVKrqvCmKaqLHak,3165
|
8
|
+
flask_humanify/templates/oneclick_captcha.html,sha256=CnK4qTOdjIrlnFB8lytuK8pEcpj1dL5uYzj0ZNBkx-E,6241
|
9
|
+
flask_humanify/templates/rate_limited.html,sha256=LSS1-c9F3YLxXF-jiyyNN6vIMoIAdmd6ObO1PDcjIxI,3110
|
10
|
+
flask_humanify-0.1.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
11
|
+
flask_humanify-0.1.2.dist-info/METADATA,sha256=9oxqmwHRjGz9RVxSwxuZWdXcjYdPtaC2j17rOkOWVyE,2787
|
12
|
+
flask_humanify-0.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
13
|
+
flask_humanify-0.1.2.dist-info/top_level.txt,sha256=9-c6uhxwCpPE3BJYge1Y9Z_bYmWitI0fY5RgqMiFWr0,15
|
14
|
+
flask_humanify-0.1.2.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
flask_humanify
|
@@ -1,9 +0,0 @@
|
|
1
|
-
flask_Humanify/__init__.py,sha256=me3NQI32tVcx5iTq5hpCJ4JOG4wdlj9x8Lnx-Y2Rhvk,269
|
2
|
-
flask_Humanify/humanify.py,sha256=JJeTcJlMte6zj2QRlat2876iJzFg40YNoMEX5kWepiU,3772
|
3
|
-
flask_Humanify/ipset.py,sha256=LK2OrqwccSrikofGrdSEzgOx17i-HsPYsR9gHJyPDhQ,10219
|
4
|
-
flask_Humanify/utils.py,sha256=CJ4FPhNJ75FuWcAn_ZuZkqRa9HR3jVvYOu9NZaN4L1o,2461
|
5
|
-
flask_humanify-0.1.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
6
|
-
flask_humanify-0.1.0.dist-info/METADATA,sha256=oIDLoHKKyylisbMBTwAaPVnwBVffeWzEZjoxpbUxNYU,2767
|
7
|
-
flask_humanify-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
8
|
-
flask_humanify-0.1.0.dist-info/top_level.txt,sha256=XcJXzmBdGt7o2wY93Yhc0Epzeb2nORaZ8-fLsbwOYQ8,15
|
9
|
-
flask_humanify-0.1.0.dist-info/RECORD,,
|
@@ -1 +0,0 @@
|
|
1
|
-
flask_Humanify
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|