cherrypy-foundation 1.0.0a1__py3-none-any.whl → 1.0.0a3__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.
Files changed (33) hide show
  1. cherrypy_foundation/components/Field.jinja +14 -6
  2. cherrypy_foundation/components/Typeahead.css +6 -1
  3. cherrypy_foundation/components/Typeahead.jinja +2 -2
  4. cherrypy_foundation/components/__init__.py +1 -1
  5. cherrypy_foundation/error_page.py +18 -15
  6. cherrypy_foundation/plugins/db.py +34 -8
  7. cherrypy_foundation/plugins/ldap.py +28 -22
  8. cherrypy_foundation/plugins/scheduler.py +197 -84
  9. cherrypy_foundation/plugins/smtp.py +71 -45
  10. cherrypy_foundation/plugins/tests/test_db.py +2 -2
  11. cherrypy_foundation/plugins/tests/test_scheduler.py +58 -50
  12. cherrypy_foundation/plugins/tests/test_smtp.py +9 -6
  13. cherrypy_foundation/tests/templates/test_form.html +10 -0
  14. cherrypy_foundation/tests/test_form.py +119 -0
  15. cherrypy_foundation/tools/auth.py +28 -11
  16. cherrypy_foundation/tools/auth_mfa.py +10 -13
  17. cherrypy_foundation/tools/errors.py +1 -1
  18. cherrypy_foundation/tools/i18n.py +15 -6
  19. cherrypy_foundation/tools/jinja2.py +15 -12
  20. cherrypy_foundation/tools/ratelimit.py +5 -9
  21. cherrypy_foundation/tools/secure_headers.py +0 -4
  22. cherrypy_foundation/tools/tests/components/Button.jinja +2 -0
  23. cherrypy_foundation/tools/tests/templates/test_jinja2.html +10 -0
  24. cherrypy_foundation/tools/tests/templates/test_jinja2_i18n.html +11 -0
  25. cherrypy_foundation/tools/tests/templates/test_jinjax.html +8 -0
  26. cherrypy_foundation/tools/tests/test_auth.py +1 -1
  27. cherrypy_foundation/tools/tests/test_auth_mfa.py +367 -0
  28. cherrypy_foundation/tools/tests/test_jinja2.py +123 -0
  29. {cherrypy_foundation-1.0.0a1.dist-info → cherrypy_foundation-1.0.0a3.dist-info}/METADATA +1 -1
  30. {cherrypy_foundation-1.0.0a1.dist-info → cherrypy_foundation-1.0.0a3.dist-info}/RECORD +33 -25
  31. {cherrypy_foundation-1.0.0a1.dist-info → cherrypy_foundation-1.0.0a3.dist-info}/WHEEL +0 -0
  32. {cherrypy_foundation-1.0.0a1.dist-info → cherrypy_foundation-1.0.0a3.dist-info}/licenses/LICENSE.md +0 -0
  33. {cherrypy_foundation-1.0.0a1.dist-info → cherrypy_foundation-1.0.0a3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,367 @@
1
+ # CherryPy
2
+ # Copyright (C) 2025 IKUS Software
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+
18
+ import datetime
19
+ from collections import namedtuple
20
+ from urllib.parse import urlencode
21
+
22
+ import cherrypy
23
+ from cherrypy.lib.sessions import RamSession
24
+ from cherrypy.test import helper
25
+
26
+ from ..auth import AUTH_LAST_PASSWORD_AT
27
+ from ..auth_mfa import (
28
+ MFA_CODE_TIME,
29
+ MFA_DEFAULT_CODE_TIMEOUT,
30
+ MFA_DEFAULT_TRUST_DURATION,
31
+ MFA_TRUSTED_IP_LIST,
32
+ MFA_USER_KEY,
33
+ MFA_VERIFICATION_TIME,
34
+ )
35
+ from ..sessions_timeout import SESSION_PERSISTENT
36
+
37
+ User = namedtuple('User', 'id,username,password,email,mfa', defaults=[False])
38
+
39
+ users = {
40
+ User(2, 'myuser', 'changeme', 'myuser@example.com', False),
41
+ User(3, 'mfauser', 'changeme', 'mfauser@example.com', True),
42
+ User(4, 'noemail', 'changeme', '', True),
43
+ }
44
+
45
+
46
+ def checkpassword(username, password):
47
+ for u in users:
48
+ if u.username == username and u.password == password:
49
+ return True
50
+ return False
51
+
52
+
53
+ def user_lookup_func(login, user_info):
54
+ for u in users:
55
+ if u.username == login:
56
+ return u.id, u
57
+ return None
58
+
59
+
60
+ def user_from_key_func(userkey):
61
+ for u in users:
62
+ if u.id == userkey:
63
+ return u
64
+ return None
65
+
66
+
67
+ @cherrypy.tools.sessions()
68
+ @cherrypy.tools.auth(
69
+ user_lookup_func=user_lookup_func,
70
+ user_from_key_func=user_from_key_func,
71
+ checkpassword=checkpassword,
72
+ )
73
+ @cherrypy.tools.auth_mfa(
74
+ mfa_enabled=lambda: hasattr(cherrypy.serving.request, 'currentuser') and cherrypy.request.currentuser.mfa
75
+ )
76
+ class Root:
77
+
78
+ @cherrypy.expose
79
+ def index(self):
80
+ return "OK"
81
+
82
+ @cherrypy.expose()
83
+ def login(self, username=None, password=None):
84
+ if cherrypy.serving.request.method == 'POST' and username and password:
85
+ userobj = cherrypy.tools.auth.login_with_credentials(username, password)
86
+ if userobj:
87
+ raise cherrypy.tools.auth.redirect_to_original_url()
88
+ else:
89
+ return "invalid credentials"
90
+ return "login"
91
+
92
+ @cherrypy.expose()
93
+ def mfa(self, code=None, resend_code=None, persistent=False):
94
+ html = "mfa\n"
95
+ if cherrypy.serving.request.method == 'POST' and code:
96
+ if cherrypy.tools.auth_mfa.verify_code(code=code, persistent=persistent):
97
+ raise cherrypy.tools.auth.redirect_to_original_url()
98
+ else:
99
+ html += "<p>invalid verification code</p>\n"
100
+ # Send verification code if previous code expired.
101
+ # Or when requested by user.
102
+ if (resend_code and cherrypy.serving.request.method == 'POST') or cherrypy.tools.auth_mfa.is_code_expired():
103
+ code = cherrypy.tools.auth_mfa.generate_code()
104
+ # Here the code should be send by email, SMS or any other means.
105
+ # For our test we store it in the session.
106
+ cherrypy.serving.session['code'] = code
107
+ html += "<p>new verification code sent to your email</p>\n"
108
+ return html
109
+
110
+
111
+ class AuthManagerMfaTest(helper.CPWebCase):
112
+ interactive = False
113
+ # Authenticated by default.
114
+ login = True
115
+
116
+ @classmethod
117
+ def setup_server(cls):
118
+ cherrypy.tree.mount(Root(), '/')
119
+
120
+ def getPage(self, *args, **kwargs):
121
+ """
122
+ This implementation keep track of session cookies.
123
+ """
124
+ headers = kwargs.pop('headers', [])
125
+ if hasattr(self, 'cookies') and self.cookies:
126
+ headers.extend(self.cookies)
127
+ return helper.CPWebCase.getPage(self, *args, headers=headers, **kwargs)
128
+
129
+ def _login(self, username, password):
130
+ self.getPage('/login/', method='POST', body=urlencode({'username': username, 'password': password}))
131
+ self.assertStatus(303)
132
+ self.assertHeaderItemValue('Location', f'http://{self.HOST}:{self.PORT}/')
133
+
134
+ @property
135
+ def _session_id(self):
136
+ if hasattr(self, 'cookies') and self.cookies:
137
+ for unused, value in self.cookies:
138
+ for part in value.split(';'):
139
+ key, unused, value = part.partition('=')
140
+ if key == 'session_id':
141
+ return value
142
+
143
+ def _get_code(self):
144
+ # Query MFA page to generate a code
145
+ self.getPage("/mfa/")
146
+ self.assertStatus(200)
147
+ self.assertInBody("new verification code sent to your email")
148
+ # Extract code from user session for testing only.
149
+ session = RamSession.cache[self._session_id][0]
150
+ return session['code']
151
+
152
+ def test_get_without_login(self):
153
+ # Given the user is not authenticated.
154
+ # When requesting /mfa/
155
+ self.getPage("/mfa/")
156
+ # Then user is redirected to /login/
157
+ self.assertStatus(303)
158
+ self.assertHeaderItemValue('Location', f'http://{self.HOST}:{self.PORT}/login/')
159
+
160
+ def test_get_with_mfa_disabled(self):
161
+ # Given an authenticated user with MFA Disable
162
+ self._login('myuser', 'changeme')
163
+ # When requesting /mfa/ page
164
+ self.getPage("/mfa/")
165
+ # Then user is redirected to root page
166
+ self.assertStatus(303)
167
+ self.assertHeaderItemValue('Location', f'http://{self.HOST}:{self.PORT}/')
168
+ # Then index is enabled.
169
+ self.getPage("/")
170
+ self.assertStatus(200)
171
+ self.assertInBody('OK')
172
+
173
+ def test_get_with_trusted(self):
174
+ # Given an authenticated user with MFA Disable
175
+ self._login('mfauser', 'changeme')
176
+ # Given an authenticated user with MFA enabled and already verified
177
+ session = RamSession.cache[self._session_id][0]
178
+ session[MFA_USER_KEY] = 3
179
+ session[MFA_VERIFICATION_TIME] = datetime.datetime.now()
180
+ session[MFA_TRUSTED_IP_LIST] = ['127.0.0.1']
181
+
182
+ # When requesting /mfa/ page when we are already trusted
183
+ self.getPage("/mfa/")
184
+ # Then user is redirected to root page
185
+ self.assertStatus(303)
186
+ self.assertHeaderItemValue('Location', f'http://{self.HOST}:{self.PORT}/')
187
+
188
+ def test_get_with_trusted_expired(self):
189
+ # Given an authenticated user with MFA enabled and already verified
190
+ self._login('mfauser', 'changeme')
191
+ session = RamSession.cache[self._session_id][0]
192
+ session[MFA_USER_KEY] = 3
193
+ session[MFA_VERIFICATION_TIME] = datetime.datetime.now() - datetime.timedelta(minutes=60)
194
+
195
+ # When requesting /mfa/ page
196
+ self.getPage("/mfa/")
197
+ self.assertStatus(200)
198
+ # Then a verification code is send to the user
199
+ self.assertInBody("new verification code sent to your email")
200
+
201
+ def test_get_with_trusted_different_ip(self):
202
+ # Given an authenticated user with MFA enabled and already verified
203
+ self._login('mfauser', 'changeme')
204
+ session = RamSession.cache[self._session_id][0]
205
+ session[MFA_USER_KEY] = 3
206
+ session[MFA_VERIFICATION_TIME] = datetime.datetime.now()
207
+
208
+ # When requesting /mfa/ page from a different ip
209
+ self.getPage("/mfa/", headers=[('X-Forwarded-For', '10.255.14.23')])
210
+ self.assertStatus(200)
211
+ # Then a verification code is send to the user
212
+ self.assertInBody("new verification code sent to your email")
213
+
214
+ def test_get_without_verified(self):
215
+ # Given an authenticated user With MFA enabled
216
+ self._login('mfauser', 'changeme')
217
+ # When requesting /mfa/ page
218
+ self.getPage("/mfa/")
219
+ self.assertStatus(200)
220
+ # Then a verification code is send to the user
221
+ self.assertInBody("new verification code sent to your email")
222
+
223
+ def test_verify_code_valid(self):
224
+ prev_session_id = self._session_id
225
+ # Given an authenticated user With MFA enabled
226
+ self._login('mfauser', 'changeme')
227
+ code = self._get_code()
228
+ # When sending a valid verification code
229
+ self.getPage("/mfa/", method='POST', body=urlencode({'code': code}))
230
+ # Then a new session_id is generated
231
+ self.assertNotEqual(prev_session_id, self._session_id)
232
+ # Then user is redirected to root page
233
+ self.assertStatus(303)
234
+ self.assertHeaderItemValue('Location', f'http://{self.HOST}:{self.PORT}/')
235
+ # Then user has access
236
+ self.getPage("/")
237
+ self.assertStatus(200)
238
+
239
+ def test_verify_code_invalid(self):
240
+ # Given an authenticated user With MFA enabled
241
+ # When sending an invalid verification code
242
+ self.getPage("/mfa/", method='POST', body=urlencode({'code': '1234567'}))
243
+ # Then user is redirected to login page
244
+ self.assertStatus(303)
245
+ self.assertHeaderItemValue('Location', f'http://{self.HOST}:{self.PORT}/login/')
246
+
247
+ def test_verify_code_expired(self):
248
+ # Given an authenticated user With MFA enabled
249
+ self._login('mfauser', 'changeme')
250
+ code = self._get_code()
251
+ # When sending a valid verification code that expired
252
+ session = RamSession.cache[self._session_id][0]
253
+
254
+ session[MFA_CODE_TIME] = datetime.datetime.now() - datetime.timedelta(minutes=MFA_DEFAULT_CODE_TIMEOUT + 1)
255
+
256
+ self.getPage("/mfa/", method='POST', body=urlencode({'code': code}))
257
+ # Then a new code get generated.
258
+ self.assertStatus(200)
259
+ self.assertInBody("invalid verification code")
260
+
261
+ def test_verify_code_invalid_after_3_tentative(self):
262
+ # Given an authenticated user With MFA
263
+ self._login('mfauser', 'changeme')
264
+ code = self._get_code()
265
+ # When user enter an invalid verification code 3 times
266
+ self.getPage("/mfa/", method='POST', body=urlencode({'code': '1234567'}))
267
+ self.assertStatus(200)
268
+ self.getPage("/mfa/", method='POST', body=urlencode({'code': '1234567'}))
269
+ self.assertStatus(200)
270
+ self.getPage("/mfa/", method='POST', body=urlencode({'code': '1234567'}))
271
+ # Then an error get displayed to the user
272
+ self.assertStatus(200)
273
+ self.assertInBody("invalid verification code")
274
+ # Then a new code get send to the user.
275
+ self.assertInBody("new verification code sent to your email")
276
+ session = RamSession.cache[self._session_id][0]
277
+ new_code = session['code']
278
+ self.assertNotEqual(code, new_code)
279
+
280
+ def test_resend_code(self):
281
+ # Given an authenticated user With MFA enabled with an existing code
282
+ self._login('mfauser', 'changeme')
283
+ code = self._get_code()
284
+ # When user request a new code
285
+ self.getPage("/mfa/", method='POST', body=urlencode({'resend_code': '1'}))
286
+ # Then A success message is displayedto the user.
287
+ self.assertInBody("new verification code sent to your email")
288
+ session = RamSession.cache[self._session_id][0]
289
+ new_code = session['code']
290
+ self.assertNotEqual(code, new_code)
291
+
292
+ def test_redirect_to_original_url(self):
293
+ # Given an authenticated user
294
+ self._login('mfauser', 'changeme')
295
+ # When querying a page that required mfa
296
+ self.getPage('/prefs/general')
297
+ # Then user is redirected to mfa page
298
+ self.assertStatus(303)
299
+ self.assertHeaderItemValue('Location', f'http://{self.HOST}:{self.PORT}/mfa/')
300
+ # When providing verification code
301
+ code = self._get_code()
302
+ self.getPage("/mfa/", method='POST', body=urlencode({'code': code}))
303
+ # Then user is redirected to original url
304
+ self.assertStatus(303)
305
+ self.assertHeaderItemValue('Location', f'http://{self.HOST}:{self.PORT}/prefs/general')
306
+
307
+ def test_login_persistent_when_login_timeout(self):
308
+ prev_session_id = self._session_id
309
+ # Given a user authenticated with MFA with "persistent"
310
+ self._login('mfauser', 'changeme')
311
+ code = self._get_code()
312
+ self.getPage("/mfa/", method='POST', body=urlencode({'code': code, 'persistent': '1'}))
313
+ self.assertStatus(303)
314
+ self.getPage("/")
315
+ self.assertStatus(200)
316
+ self.assertNotEqual(prev_session_id, self._session_id)
317
+ session = RamSession.cache[self._session_id][0]
318
+ self.assertTrue(session[SESSION_PERSISTENT])
319
+ # When the re-auth time expired (after 15 min)
320
+ session[AUTH_LAST_PASSWORD_AT] = datetime.datetime.now() - datetime.timedelta(minutes=60, seconds=1)
321
+
322
+ # Then next query redirect user to /login/ page (by mfa)
323
+ self.getPage("/")
324
+ self.assertStatus(303)
325
+ self.assertHeaderItemValue('Location', f'http://{self.HOST}:{self.PORT}/login/')
326
+ prev_session_id = self._session_id
327
+ # When user enter valid username password
328
+ self.getPage("/login/", method='POST', body=urlencode({'username': 'mfauser', 'password': 'changeme'}))
329
+ # Then user is redirected to original url without need to pass MFA again.
330
+ self.assertStatus(303)
331
+ self.assertHeaderItemValue('Location', f'http://{self.HOST}:{self.PORT}/')
332
+ self.assertNotEqual(prev_session_id, self._session_id)
333
+ self.getPage("/")
334
+ self.assertStatus(200)
335
+ self.assertInBody('OK')
336
+
337
+ def test_login_persistent_when_mfa_timeout(self):
338
+ prev_session_id = self._session_id
339
+ # Given a user authenticated with MFA with "persistent"
340
+ self._login('mfauser', 'changeme')
341
+ code = self._get_code()
342
+ self.getPage("/mfa/", method='POST', body=urlencode({'code': code, 'persistent': '1'}))
343
+ self.assertStatus(303)
344
+ self.getPage("/")
345
+ self.assertStatus(200)
346
+ self.assertNotEqual(prev_session_id, self._session_id)
347
+ session = RamSession.cache[self._session_id][0]
348
+
349
+ self.assertTrue(session[SESSION_PERSISTENT])
350
+ # When the mfa verification timeout (after 15 min)
351
+ session[MFA_VERIFICATION_TIME] = datetime.datetime.now() - datetime.timedelta(
352
+ minutes=MFA_DEFAULT_TRUST_DURATION, seconds=1
353
+ )
354
+
355
+ # Then next query redirect user to mfa page
356
+ self.getPage("/prefs/general")
357
+ self.assertStatus(303)
358
+ self.assertHeaderItemValue('Location', f'http://{self.HOST}:{self.PORT}/mfa/')
359
+ # When user enter valid code
360
+ code = self._get_code()
361
+ self.getPage("/mfa/", method='POST', body=urlencode({'code': code, 'persistent': '1'}))
362
+ # Then user is redirected to original page.
363
+ self.assertStatus(303)
364
+ self.assertHeaderItemValue('Location', f'http://{self.HOST}:{self.PORT}/prefs/general')
365
+ self.getPage("/")
366
+ self.assertStatus(200)
367
+ self.assertInBody('OK')
@@ -0,0 +1,123 @@
1
+ # CherryPy Foundation
2
+ # Copyright (C) 2025 IKUS Software
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ import importlib
17
+ from datetime import datetime, timezone
18
+ from unittest import skipUnless
19
+
20
+ import cherrypy
21
+ from cherrypy.test import helper
22
+ from parameterized import parameterized
23
+
24
+ from .. import i18n # noqa
25
+ from .. import jinja2 # noqa
26
+
27
+ HAS_JINJAX = importlib.util.find_spec("jinjax") is not None
28
+
29
+ env = cherrypy.tools.jinja2.create_env(
30
+ package_name=__package__,
31
+ globals={
32
+ 'const1': 'STATIC VALUE',
33
+ },
34
+ )
35
+
36
+
37
+ def extra_processor():
38
+ return {'var2': 'bar'}
39
+
40
+
41
+ @cherrypy.tools.jinja2(env=env, extra_processor=extra_processor)
42
+ class Root:
43
+
44
+ @cherrypy.expose
45
+ @cherrypy.tools.jinja2(template='test_jinja2.html')
46
+ def index(self):
47
+ return {'var1': 'test-jinja2'}
48
+
49
+ @cherrypy.expose
50
+ @cherrypy.tools.jinja2(template='test_jinjax.html')
51
+ def jinjax(self):
52
+ return {'var1': 'test-jinjax'}
53
+
54
+ @cherrypy.expose
55
+ @cherrypy.tools.jinja2(template='test_jinja2_i18n.html')
56
+ @cherrypy.tools.i18n(
57
+ default='fr',
58
+ default_timezone='America/Toronto',
59
+ mo_dir=importlib.resources.files(__package__) / 'locales',
60
+ domain='messages',
61
+ )
62
+ def localized(self):
63
+ return {
64
+ 'my_datetime': datetime(year=2025, month=11, day=26, hour=11, minute=16, tzinfo=timezone.utc),
65
+ 'my_date': datetime(year=2025, month=12, day=22, hour=14, minute=8, tzinfo=timezone.utc),
66
+ }
67
+
68
+
69
+ class Jinja2Test(helper.CPWebCase):
70
+ default_lang = None
71
+ interactive = False
72
+
73
+ @classmethod
74
+ def setup_server(cls):
75
+ cherrypy.tree.mount(Root(), '/')
76
+
77
+ def test_get_page(self):
78
+ # Given a page render using jinja2
79
+ # When querying the page
80
+ self.getPage("/")
81
+ # Then the page return without error
82
+ self.assertStatus(200)
83
+ # Then the page is render dynamically using page context
84
+ self.assertInBody('test-jinja2')
85
+ self.assertInBody('bar')
86
+ self.assertInBody('STATIC VALUE')
87
+
88
+ @skipUnless(HAS_JINJAX, reason='Required jinjax')
89
+ def test_get_page_jinjax(self):
90
+ # Given a page render using jinjax
91
+ # When querying the page
92
+ self.getPage("/jinjax")
93
+ # Then the page return without error
94
+ self.assertStatus(200)
95
+ # Then the page is render dynamically using page context
96
+ self.assertInBody('<a class="btn btn-primary" href="http://example.com">foo</a>')
97
+
98
+ @parameterized.expand(
99
+ [
100
+ ('server_default', {}, 'fr'),
101
+ ('accept_lang_fr', {'Accept-Language': 'fr'}, 'fr'),
102
+ ('accept_lang_en', {'Accept-Language': 'en'}, 'en'),
103
+ ]
104
+ )
105
+ def test_get_page_i18n(self, _name, headers, expected_lang):
106
+ # Given a localized page render with jinja2
107
+ # When querying the page
108
+ self.getPage("/localized", headers=list(headers.items()))
109
+ # Then the page return without error
110
+ self.assertStatus(200)
111
+ # Then the page is render dynamically using page context
112
+ if expected_lang == 'fr':
113
+ self.assertInBody('lang="fr"')
114
+ self.assertInBody('français')
115
+ self.assertInBody('Du texte à traduire')
116
+ self.assertInBody('mercredi 26 novembre 2025, 06:16:00 heure normale de l’Est nord-américain')
117
+ self.assertInBody('lundi, décembre 22, 2025')
118
+ else:
119
+ self.assertInBody('lang="en"')
120
+ self.assertInBody('English')
121
+ self.assertInBody('Some text to translate')
122
+ self.assertInBody('Wednesday, November 26, 2025, 6:16:00\u202fAM Eastern Standard Time')
123
+ self.assertInBody('Monday, December 22, 2025')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cherrypy-foundation
3
- Version: 1.0.0a1
3
+ Version: 1.0.0a3
4
4
  Summary: CherryPy Foundation
5
5
  Author-email: Patrik Dufresne <patrik@ikus-soft.com>
6
6
  License: GPLv3
@@ -1,5 +1,5 @@
1
1
  cherrypy_foundation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- cherrypy_foundation/error_page.py,sha256=dhZBdVztS5tHr7jFsKUYWTvIUiZAfQ4PvXm5bkuKiTQ,3052
2
+ cherrypy_foundation/error_page.py,sha256=Mz47u6n4eqQ85LKT9lWglqPPtmizikY9xBsrhGl7_nI,3198
3
3
  cherrypy_foundation/flash.py,sha256=fFRbutUX6c1lVHqjehmO9y98dJgmfNCjhd76t2mth2s,1542
4
4
  cherrypy_foundation/form.py,sha256=8c9dO0o47sK3CBosTkGXoVRtzNQwY0aw0vNZfTqmhvo,3994
5
5
  cherrypy_foundation/logging.py,sha256=YIOK5ZAZLCv52YDdP66yBYpEX1C336JnI3wnrTKl1Lw,3468
@@ -11,17 +11,17 @@ cherrypy_foundation/components/Datatable.css,sha256=7wSwgdA61vYCdEuQ0bp2o0oSvu5m
11
11
  cherrypy_foundation/components/Datatable.jinja,sha256=K-24vpN5TO8Jk2COvuC5uBaCj3PFLd6ubr28JHFq3KM,3375
12
12
  cherrypy_foundation/components/Datatable.js,sha256=jiV78bJPNMTLcjrX03vaYcsEdRbI6ctzSvCo880H8Hw,12072
13
13
  cherrypy_foundation/components/Field.css,sha256=CtOkvIbix7ykrOKLJxQJLJsWfEwFqfducJ1BH2vlMvA,244
14
- cherrypy_foundation/components/Field.jinja,sha256=QYJdB9xRysQrdC_oDlu9V8cpZyFJxQOlnZOi_wt-iP8,2762
14
+ cherrypy_foundation/components/Field.jinja,sha256=R-ZUm88XXOb3zi_hysTuwFUCGGR67A59P4TT5d1Qo_o,3152
15
15
  cherrypy_foundation/components/Field.js,sha256=SFixZ62WlLq7SSCEazMAGhSnc9EnQ1wg6PZX4ayO6ZE,2047
16
16
  cherrypy_foundation/components/Fields.jinja,sha256=UDu1txwMguvr7dyga9PXepEhFvz9C6ZvchBnhQCWgHI,318
17
17
  cherrypy_foundation/components/Icon.jinja,sha256=Z1RGYBg5xlDEoUy3glqb_k_LEjkJHeCxQXqDEvWzEF4,135
18
18
  cherrypy_foundation/components/SideBySideMultiSelect.css,sha256=_poMY9O8rvDsOh01pQLf9qtg1Gm4eCM2HsM_ekC5zkk,503
19
19
  cherrypy_foundation/components/SideBySideMultiSelect.jinja,sha256=sud1WP-6JzuP7ZLRr-JQqvgMRWZRlXvxUJfguFr_klk,478
20
20
  cherrypy_foundation/components/SideBySideMultiSelect.js,sha256=5YMz1pgkXeWC_SRRfDbQI3X-c4PuxiTIpbWJt9sY7Rc,197
21
- cherrypy_foundation/components/Typeahead.css,sha256=8VyPN6FP_l0_mRyES2tjrwr9rEkKT4y0obeBI6nfGLk,1645
22
- cherrypy_foundation/components/Typeahead.jinja,sha256=g7oiiROxIjwkgwiZtxxFlSIIaWUr2ml2eS0RWm7yUvU,3215
21
+ cherrypy_foundation/components/Typeahead.css,sha256=iaLKi4lx01MQqSSRrdDUIDCCD6Q_0INCsMRZ3nqdkuw,1718
22
+ cherrypy_foundation/components/Typeahead.jinja,sha256=TjCotrTIzSx7TujxmgNxLIgxvtequCARLCjqeDzlZXU,3218
23
23
  cherrypy_foundation/components/Typeahead.js,sha256=mnsPRwM_1xtI7ImGn_pu46QdFw_LK_PDHNCRKp_CVxc,236
24
- cherrypy_foundation/components/__init__.py,sha256=15gQpb1eSzMmPPS5Gna8E25DGN0Midrxdns7veFKOyU,2034
24
+ cherrypy_foundation/components/__init__.py,sha256=Ck_AY_eTkq4Pyf-jjyOxwfpoKXgtCx90hJpRHPRiA_Q,2012
25
25
  cherrypy_foundation/components/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
26
  cherrypy_foundation/components/tests/test_static.py,sha256=bDtS0CFM5LZxwxbXbUyLkXv7sM5KM-eQtHOWywZu_nc,3814
27
27
  cherrypy_foundation/components/vendor/bootstrap-icons/bootstrap-icons.css,sha256=AEMichyFVzMXWbxt2qy7aJsPBxXWiK7IK9BW0tW1zDs,99556
@@ -79,39 +79,47 @@ cherrypy_foundation/components/vendor/popper/popper.min.js,sha256=whL0tQWoY1Ku1i
79
79
  cherrypy_foundation/components/vendor/typeahead/jquery.typeahead.min.css,sha256=gBsvk2nlQnvCXdG7JITz1SrnqWQrO6EsUo_r06-KRtU,12464
80
80
  cherrypy_foundation/components/vendor/typeahead/jquery.typeahead.min.js,sha256=eFBtgouYdW587kYINWsjkN-UR8GVWAG_fQe1fpJfjOw,47828
81
81
  cherrypy_foundation/plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
82
- cherrypy_foundation/plugins/db.py,sha256=V-gk6Zg-DRNA6snfHLte3ye6hzowvuE-8lYKCTKs7Vg,9118
83
- cherrypy_foundation/plugins/ldap.py,sha256=MTg-TqdyziuqFYr-wXecHvu0zK7YCzr-k5E8jq3dF-k,9519
82
+ cherrypy_foundation/plugins/db.py,sha256=46S-2kR9BHVYRN9cxWwYfzY2WkpEVBCpx1ubsJolYfA,10108
83
+ cherrypy_foundation/plugins/ldap.py,sha256=N-bJT7daJcLybEigA3P_5iuDr4PFwmLt8YzcMrWafAY,9870
84
84
  cherrypy_foundation/plugins/restapi.py,sha256=S5GIxHL0M3Wcs33fyVOb0RfX1FXbp2fqUxJ_ufqBrD4,2843
85
- cherrypy_foundation/plugins/scheduler.py,sha256=aUk_X-z1zLD3oqfuMdm6AEV5j6Iyy422y7sMn3SrYVg,5467
86
- cherrypy_foundation/plugins/smtp.py,sha256=IimRsULIEEpwI8OcYAxNifwkjdQNBNX2YtrIu1635fQ,6789
85
+ cherrypy_foundation/plugins/scheduler.py,sha256=H6ExfmAlTPt2ypK5lBMynNfcUzcWK_n1EP2_K5x2bx8,10400
86
+ cherrypy_foundation/plugins/smtp.py,sha256=yTCUj2XzrJEJ84CIbYcrCUAP6Mg59aZfu6OZDGQyJbQ,7479
87
87
  cherrypy_foundation/plugins/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
88
- cherrypy_foundation/plugins/tests/test_db.py,sha256=TfI4JIWBZzzXE1Y_HceoQ4c4OY_Ff-CgW--Z0_oxRO8,3925
88
+ cherrypy_foundation/plugins/tests/test_db.py,sha256=PELi5ojj8VqcXyPbpaig2Ac4ZqlR_gEPGUYrWJPANWg,3921
89
89
  cherrypy_foundation/plugins/tests/test_ldap.py,sha256=7EhFvhxwDCCBoNlAD5XpZxLtKvkrjpQxrtCHMT9wkQ4,14393
90
- cherrypy_foundation/plugins/tests/test_scheduler.py,sha256=NCk0rqggOxj9RdKtVxU6EosGk_lOEiCqLXW9or6hovM,3054
91
- cherrypy_foundation/plugins/tests/test_smtp.py,sha256=J06pZTS2SPXpOvEDand7_xfkadO7vXSpAACyPre1rZA,3918
90
+ cherrypy_foundation/plugins/tests/test_scheduler.py,sha256=I-ZuQhMvCCvqFDwukwsyz_UkdJJ8JSLTkAanUo24GCE,3564
91
+ cherrypy_foundation/plugins/tests/test_smtp.py,sha256=qs5yezIpSXkBmLmFlqckfPW7NmntHZxQjDSkdQG_dNE,4183
92
92
  cherrypy_foundation/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
93
93
  cherrypy_foundation/tests/test_error_page.py,sha256=8u5p6lv_I4XvfGipjJXNFwW7G1N8AK8IOnc_Th-Ebto,2181
94
+ cherrypy_foundation/tests/test_form.py,sha256=urYi0qC8JqelopdeLBHvtndEv0QJ0hwlv3JkpAvdCYQ,4324
94
95
  cherrypy_foundation/tests/test_passwd.py,sha256=gC5O4yhHyU1YRYuDc0pG0T_5zvrG2qrr6P822iyK3Rg,1956
96
+ cherrypy_foundation/tests/templates/test_form.html,sha256=sm-n2cYvih2vbDE4Y8kkERSoulnKAbwoefbzBggMMnA,189
95
97
  cherrypy_foundation/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
96
- cherrypy_foundation/tools/auth.py,sha256=F9SMQMTOqHbtDw13VdlGf9gYJtUhXrr9p-RYPkcEySs,9090
97
- cherrypy_foundation/tools/auth_mfa.py,sha256=2TU_EnykYRmj1pluGHFUFGzrE0jkyekamECf9TUd8HQ,9250
98
- cherrypy_foundation/tools/errors.py,sha256=DhWl03tthV9tlN_3qdJzgWg6LQS7ANP5gCl6R_VgW4E,1027
99
- cherrypy_foundation/tools/i18n.py,sha256=pZVawC8ahzvm1HmMe_6zZuYTjsWW7OsKdFjV0KtF6fk,13885
100
- cherrypy_foundation/tools/jinja2.py,sha256=lZ3aiYerop9WDecrsX9B9OY49NmH-eIJZiXVnVqDY2I,5290
101
- cherrypy_foundation/tools/ratelimit.py,sha256=k0MRdanioF8UNRHcFUWyT5aVvHtSZ1-BlVYtEtLysmI,8386
102
- cherrypy_foundation/tools/secure_headers.py,sha256=PMKwL622sbBtjRnq-weZH_OewuaG4cR2jqIr1DOUNXM,3994
98
+ cherrypy_foundation/tools/auth.py,sha256=lTSajxCiReMzm-Fl-xhTByi4yFnInEWOoNsmUMnHQhs,9761
99
+ cherrypy_foundation/tools/auth_mfa.py,sha256=VaLvBz9wo6jTx-2mCGqFXPxl-z14f8UMWvd6_xeXd40,9212
100
+ cherrypy_foundation/tools/errors.py,sha256=ELpAj0N9kIxC22QW5xDQJz60zMpCwgm-Twu2WpELM1A,1005
101
+ cherrypy_foundation/tools/i18n.py,sha256=5pcyr7AleaqSj9rCESthvMIbNIORp-8351uJgmi2xog,14172
102
+ cherrypy_foundation/tools/jinja2.py,sha256=nppYnk2ASDsyfNHF9m83W4foiN3MhcwDJvo5baEgnGU,5520
103
+ cherrypy_foundation/tools/ratelimit.py,sha256=pT7vZRmjltNeuiQpdyXOmnpG9BcXjLaj-AXJ0e2x_zw,8300
104
+ cherrypy_foundation/tools/secure_headers.py,sha256=Yh-iA_Js4MUsx5nq4ilbc-iWy90ZC0oMb3TJJD_UwYo,3921
103
105
  cherrypy_foundation/tools/sessions_timeout.py,sha256=6iBWJntPMk_Qt94fBSfBISf1IXInSh-1XrxLbKXFV-g,7408
104
106
  cherrypy_foundation/tools/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
105
- cherrypy_foundation/tools/tests/test_auth.py,sha256=YA8mh9w__8z65LMjiVASCPpcTdLUTanzAwxztIkoFI0,2851
107
+ cherrypy_foundation/tools/tests/test_auth.py,sha256=oeM5t38M8DUC9dYn59dcf00jdGY6Ry0jZWhQd_PYQUk,2847
108
+ cherrypy_foundation/tools/tests/test_auth_mfa.py,sha256=911hnBbdg5CKb613uIBrlggoTAyBU9SoL7Sxd-tIKS0,15008
106
109
  cherrypy_foundation/tools/tests/test_i18n.py,sha256=-bJuKn8PHHidBWhoFSVr4PdX96m3A50YY4i5osyU7DY,6251
110
+ cherrypy_foundation/tools/tests/test_jinja2.py,sha256=_dkRJpjB0ybDV6YO0uEFFO8LAcWgVu3VBB8_vWthQ48,4296
107
111
  cherrypy_foundation/tools/tests/test_ratelimit.py,sha256=rrqybwMbh1GFlF2-Ut57zHPAc1uqX88aqea6VS_6p5E,3449
112
+ cherrypy_foundation/tools/tests/components/Button.jinja,sha256=uSLp1GpEIgZNXK_GWglu0E_a1c3jHpDLI66MRfMqGhE,95
108
113
  cherrypy_foundation/tools/tests/locales/messages.pot,sha256=5K9piTRL7H5MxDXFIWJsCccSJRA0HwfCQQU8b8VYo30,40
109
114
  cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.mo,sha256=cdyG2Js1TIU6eenDX1ICH8uP45yvl0OLN0-SMUXTBa4,259
110
115
  cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.po,sha256=JCpiRLLHUSYQhzta8ZYjfB50NmpwPGNCTNwo2Glww14,322
111
116
  cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.mo,sha256=u3_kl_nqZ3FNaSyKVQKmu4KJzN3xOxxJNVmcdhw37jA,327
112
117
  cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.po,sha256=6_Sk9Igqm7dtpyyS701p5qc4DvOJE7TRT0ajRZctFAQ,342
113
- cherrypy_foundation-1.0.0a1.dist-info/licenses/LICENSE.md,sha256=trSLYs5qlaow_bBwsLTRKpmTXsXzFksM_YUCMqrgAJQ,35149
114
- cherrypy_foundation-1.0.0a1.dist-info/METADATA,sha256=bQ3It-tIMNYHnif8OInatJvVDOyHONe-6uXE9cFVevI,2022
115
- cherrypy_foundation-1.0.0a1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
116
- cherrypy_foundation-1.0.0a1.dist-info/top_level.txt,sha256=B1vQPTLYhpKJ6W0JkRCWyAf8RPcnwJWdYxixv75-4ew,20
117
- cherrypy_foundation-1.0.0a1.dist-info/RECORD,,
118
+ cherrypy_foundation/tools/tests/templates/test_jinja2.html,sha256=v9AHxksbBvzE7sesPqE61HMhsvU4juXt3E0ZQo-zXVQ,190
119
+ cherrypy_foundation/tools/tests/templates/test_jinja2_i18n.html,sha256=98S51dgG7Vb4rvMZNZvomw1D9pBiM4g6pdlxAgvrxXA,373
120
+ cherrypy_foundation/tools/tests/templates/test_jinjax.html,sha256=NT19UaUzm8FRKOIc6H6HNGPDJU6KATnakd8zf3BCeAs,153
121
+ cherrypy_foundation-1.0.0a3.dist-info/licenses/LICENSE.md,sha256=trSLYs5qlaow_bBwsLTRKpmTXsXzFksM_YUCMqrgAJQ,35149
122
+ cherrypy_foundation-1.0.0a3.dist-info/METADATA,sha256=rnu-JnpVHWQcZ5ykeEYGGYYQB1eOeg5Uv9YfMou5yvA,2022
123
+ cherrypy_foundation-1.0.0a3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
124
+ cherrypy_foundation-1.0.0a3.dist-info/top_level.txt,sha256=B1vQPTLYhpKJ6W0JkRCWyAf8RPcnwJWdYxixv75-4ew,20
125
+ cherrypy_foundation-1.0.0a3.dist-info/RECORD,,