cherrypy-foundation 1.0.0__py3-none-any.whl → 1.0.0a2__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 (68) hide show
  1. cherrypy_foundation/components/ColorModes.jinja +4 -5
  2. cherrypy_foundation/components/Datatable.jinja +2 -2
  3. cherrypy_foundation/components/Datatable.js +2 -2
  4. cherrypy_foundation/components/Field.jinja +2 -4
  5. cherrypy_foundation/components/Fields.jinja +2 -0
  6. cherrypy_foundation/components/Typeahead.css +1 -6
  7. cherrypy_foundation/components/Typeahead.jinja +2 -2
  8. cherrypy_foundation/components/__init__.py +2 -2
  9. cherrypy_foundation/components/tests/test_static.py +1 -1
  10. cherrypy_foundation/error_page.py +3 -3
  11. cherrypy_foundation/flash.py +15 -17
  12. cherrypy_foundation/form.py +2 -2
  13. cherrypy_foundation/logging.py +2 -2
  14. cherrypy_foundation/passwd.py +2 -2
  15. cherrypy_foundation/plugins/db.py +1 -1
  16. cherrypy_foundation/plugins/ldap.py +25 -27
  17. cherrypy_foundation/plugins/restapi.py +1 -1
  18. cherrypy_foundation/plugins/scheduler.py +3 -14
  19. cherrypy_foundation/plugins/smtp.py +2 -8
  20. cherrypy_foundation/plugins/tests/test_db.py +2 -2
  21. cherrypy_foundation/plugins/tests/test_ldap.py +3 -76
  22. cherrypy_foundation/plugins/tests/test_scheduler.py +1 -1
  23. cherrypy_foundation/plugins/tests/test_smtp.py +1 -31
  24. cherrypy_foundation/tests/__init__.py +0 -72
  25. cherrypy_foundation/tests/templates/test_form.html +1 -7
  26. cherrypy_foundation/tests/test_error_page.py +1 -7
  27. cherrypy_foundation/tests/test_form.py +11 -40
  28. cherrypy_foundation/tests/test_passwd.py +2 -2
  29. cherrypy_foundation/tools/auth.py +27 -31
  30. cherrypy_foundation/tools/auth_mfa.py +83 -87
  31. cherrypy_foundation/tools/errors.py +27 -0
  32. cherrypy_foundation/tools/i18n.py +151 -235
  33. cherrypy_foundation/tools/jinja2.py +2 -15
  34. cherrypy_foundation/tools/ratelimit.py +18 -32
  35. cherrypy_foundation/tools/secure_headers.py +1 -1
  36. cherrypy_foundation/tools/sessions_timeout.py +21 -23
  37. cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.mo +0 -0
  38. cherrypy_foundation/tools/tests/locales/{de → en}/LC_MESSAGES/messages.po +2 -2
  39. cherrypy_foundation/tools/tests/templates/test_jinja2.html +1 -2
  40. cherrypy_foundation/tools/tests/templates/test_jinja2_i18n.html +11 -0
  41. cherrypy_foundation/tools/tests/templates/test_jinjax.html +2 -3
  42. cherrypy_foundation/tools/tests/test_auth.py +3 -20
  43. cherrypy_foundation/tools/tests/test_auth_mfa.py +4 -6
  44. cherrypy_foundation/tools/tests/test_i18n.py +6 -81
  45. cherrypy_foundation/tools/tests/test_jinja2.py +5 -35
  46. cherrypy_foundation/tools/tests/test_ratelimit.py +2 -2
  47. cherrypy_foundation/url.py +25 -25
  48. cherrypy_foundation/widgets.py +2 -2
  49. cherrypy_foundation-1.0.0a2.dist-info/METADATA +42 -0
  50. {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a2.dist-info}/RECORD +53 -64
  51. {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a2.dist-info}/WHEEL +1 -1
  52. cherrypy_foundation/components/Flash.jinja +0 -13
  53. cherrypy_foundation/components/LocaleSelection.jinja +0 -13
  54. cherrypy_foundation/components/LocaleSelection.js +0 -26
  55. cherrypy_foundation/plugins/tests/test_scheduler_db.py +0 -107
  56. cherrypy_foundation/sessions.py +0 -93
  57. cherrypy_foundation/tests/templates/test_flash.html +0 -9
  58. cherrypy_foundation/tests/templates/test_url.html +0 -15
  59. cherrypy_foundation/tests/test_flash.py +0 -61
  60. cherrypy_foundation/tests/test_logging.py +0 -78
  61. cherrypy_foundation/tests/test_sessions.py +0 -89
  62. cherrypy_foundation/tests/test_url.py +0 -161
  63. cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.mo +0 -0
  64. cherrypy_foundation/tools/tests/templates/test_jinjax_i18n.html +0 -22
  65. cherrypy_foundation/tools/tests/test_secure_headers.py +0 -200
  66. cherrypy_foundation-1.0.0.dist-info/METADATA +0 -71
  67. {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a2.dist-info}/licenses/LICENSE.md +0 -0
  68. {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a2.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,3 @@
1
- {# def header="Toggle theme", light_label="Light", dark_label="Dark", auto_label="Auto" #}
2
1
  {#css vendor/bootstrap5/css/bootstrap.min.css #}
3
2
  {#js vendor/popper/popper.min.js, vendor/bootstrap5/js/bootstrap.min.js, vendor/bootstrap5/js/color-modes.js #}
4
3
  <svg class="d-none" xmlns="http://www.w3.org/2000/svg">
@@ -22,7 +21,7 @@
22
21
  </svg>
23
22
  <li>
24
23
  <span id="bd-theme" class="dropdown-item disabled">
25
- <span id="bd-theme-text">{{ header }}</span>
24
+ <span id="bd-theme-text">{% trans %}Toggle theme{% endtrans %}</span>
26
25
  <svg aria-hidden="true"
27
26
  class="theme-icon-active visually-hidden"
28
27
  width="16"
@@ -41,7 +40,7 @@
41
40
  <use href="#sun-fill">
42
41
  </use>
43
42
  </svg>
44
- {{ light_label }}
43
+ {% trans %}Light{% endtrans %}
45
44
  </button>
46
45
  </li>
47
46
  <li>
@@ -53,7 +52,7 @@
53
52
  <use href="#moon-stars-fill">
54
53
  </use>
55
54
  </svg>
56
- {{ dark_label }}
55
+ {% trans %}Dark{% endtrans %}
57
56
  </button>
58
57
  </li>
59
58
  <li>
@@ -65,6 +64,6 @@
65
64
  <use href="#circle-half">
66
65
  </use>
67
66
  </svg>
68
- {{ auto_label }}
67
+ {% trans %}Auto{% endtrans %}
69
68
  </button>
70
69
  </li>
@@ -15,14 +15,14 @@
15
15
  {% set buttons_cfg_default = {
16
16
  "dom": {
17
17
  "button": {
18
- "className": "btn btn-sm ms-1 mb-2 mb-sm-0",
18
+ "className": "btn btn-sm ms-1 mb-1",
19
19
  "active": "active"
20
20
  },
21
21
  "collection": {
22
22
  "tag": "div",
23
23
  "button": {
24
24
  "tag": "a",
25
- "className": "btn btn-sm btn-link mb-2 mb-sm-0 w-100 text-start",
25
+ "className": "btn btn-sm btn-link mb-1 w-100 text-start",
26
26
  "active": "active",
27
27
  "disabled": "disabled"
28
28
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Cherrypy-foundation
3
- * Copyright (C) 2026 IKUS Software
2
+ * CherryPy Foundation
3
+ * Copyright (C) 2025 IKUS Software
4
4
  *
5
5
  * This program is free software: you can redistribute it and/or modify
6
6
  * it under the terms of the GNU General Public License as published by
@@ -1,6 +1,4 @@
1
1
  {# def field, floating=False #}
2
- {#css vendor/bootstrap5/css/bootstrap.min.css #}
3
- {#js vendor/jquery/jquery.min.js, vendor/popper/popper.min.js, vendor/bootstrap5/js/bootstrap.min.js #}
4
2
  {# djlint:off #}
5
3
  {% set bootstrap_class_table = {
6
4
  "CheckboxInput": ("form-check-input", "form-check-label", False, True),
@@ -40,10 +38,10 @@
40
38
  {% set label_attrs = attrs.__class__({'class': label_class}) %}
41
39
  {% for key, value in attrs.as_dict.items() %}
42
40
  {% if key.startswith('container-') or key.startswith('container_') %}
43
- {% do container_attrs.set(**{key[10:]: value|string}) %}
41
+ {% do container_attrs.set(**{key[10:]: value}) %}
44
42
  {% endif %}
45
43
  {% if key.startswith('label-') or key.startswith('label_') %}
46
- {% do label_attrs.set(**{key[6:]: value|string}) %}
44
+ {% do label_attrs.set(**{key[6:]: value}) %}
47
45
  {% endif %}
48
46
  {% endfor %}
49
47
  <div {{ container_attrs.as_dict | xmlattr }}>
@@ -1,4 +1,6 @@
1
1
  {# def form, floating=False #}
2
+ {#css vendor/bootstrap5/css/bootstrap.min.css #}
3
+ {#js vendor/jquery/jquery.min.js, vendor/popper/popper.min.js, vendor/bootstrap5/js/bootstrap.min.js #}
2
4
  <div class="row">
3
5
  {% for id, field in form._fields.items() %}<Field field={{ field }} floating={{ floating }} />{% endfor %}
4
6
  </div>
@@ -10,11 +10,6 @@
10
10
  top: 0.5rem;
11
11
  }
12
12
 
13
- /* Fix lense position */
14
- .typeahead__button button {
15
- height: 100%;
16
- }
17
-
18
13
  /* Fix color for DarkMode */
19
14
  .typeahead__container button,
20
15
  .typeahead__container button.disabled,
@@ -52,4 +47,4 @@
52
47
  .typeahead__list .typeahead__item:not([disabled])>a:hover {
53
48
  background-color: var(--bs-tertiary-bg);
54
49
  color: var(--bs-body-color);
55
- }
50
+ }
@@ -1,5 +1,5 @@
1
1
  {#def
2
- accent=False,
2
+ accent=True,
3
3
  async_result=False,
4
4
  autofocus=False,
5
5
  backdrop=False,
@@ -27,7 +27,7 @@ href=None,
27
27
  loading_animation=True,
28
28
  matcher=None,
29
29
  max_item=8,
30
- max_item_per_group=None,
30
+ max_item_per_group=10,
31
31
  max_length=None,
32
32
  min_length=2,
33
33
  multiselect=None,
@@ -1,5 +1,5 @@
1
- # Cherrypy-foundation
2
- # Copyright (C) 2026 IKUS Software
1
+ # Cherrypy Foundation
2
+ # Copyright (C) 2025 IKUS Software inc.
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
1
1
  # CherryPy
2
- # Copyright (C) 2026 IKUS Software
2
+ # Copyright (C) 2025 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
1
- # Cherrypy-foundation
2
- # Copyright (C) 2020-2026 IKUS Software
1
+ # CherryPy Foundation
2
+ # Copyright (C) 2020-2025 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -63,7 +63,7 @@ def error_page(status='', message='', traceback='', version=''):
63
63
  )
64
64
 
65
65
  # Replace message by generic one for 404. Default implementation leak path info.
66
- if status == '404 Not Found' and cherrypy.serving.request.path_info in message:
66
+ if status == '404 Not Found':
67
67
  message = 'Nothing matches the given URI'
68
68
 
69
69
  # Check expected response type.
@@ -1,5 +1,5 @@
1
- # Cherrypy-foundation
2
- # Copyright (C) 2020-2026 IKUS Software
1
+ # CherryPy Foundation
2
+ # Copyright (C) 2020-2025 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -16,7 +16,7 @@
16
16
 
17
17
  from collections import namedtuple
18
18
 
19
- from cherrypy_foundation.sessions import session_lock
19
+ import cherrypy
20
20
 
21
21
  FlashMessage = namedtuple('FlashMessage', ['message', 'level'])
22
22
 
@@ -27,24 +27,22 @@ def flash(message, level='info'):
27
27
  """
28
28
  assert message
29
29
  assert level in ['info', 'error', 'warning', 'success']
30
- with session_lock() as session:
31
- if 'flash' not in session:
32
- session['flash'] = []
33
- # Support Markup and string
34
- if hasattr(message, '__html__'):
35
- flash_message = FlashMessage(message, level)
36
- else:
37
- flash_message = FlashMessage(str(message), level)
38
- session['flash'].append(flash_message)
30
+ if 'flash' not in cherrypy.session:
31
+ cherrypy.session['flash'] = []
32
+ # Support Markup and string
33
+ if hasattr(message, '__html__'):
34
+ flash_message = FlashMessage(message, level)
35
+ else:
36
+ flash_message = FlashMessage(str(message), level)
37
+ cherrypy.session['flash'].append(flash_message)
39
38
 
40
39
 
41
40
  def get_flashed_messages():
42
41
  """
43
42
  Return all flash message.
44
43
  """
45
- with session_lock() as session:
46
- if 'flash' in session:
47
- messages = session['flash']
48
- del session['flash']
49
- return messages
44
+ if 'flash' in cherrypy.session:
45
+ messages = cherrypy.session['flash']
46
+ del cherrypy.session['flash']
47
+ return messages
50
48
  return []
@@ -1,5 +1,5 @@
1
- # Cherrypy-foundation
2
- # Copyright (C) 2020-2026 IKUS Software
1
+ # CherryPy Foundation
2
+ # Copyright (C) 2020-2025 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
1
- # Cherrypy-foundation
2
- # Copyright (C) 2026 IKUS Software
1
+ # CherryPy Foundation
2
+ # Copyright (C) 2025 IKUS Software inc.
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
1
- # Cherrypy-foundation
2
- # Copyright (C) 2026 IKUS Software
1
+ # CherryPy Foundation
2
+ # Copyright (C) 2025 IKUS Software inc.
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
1
1
  # SQLAlchemy plugins for cherrypy
2
- # Copyright (C) 2022-2026 IKUS Software
2
+ # Copyright (C) 2022-2025 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
1
1
  # LDAP Plugins for cherrypy
2
- # Copyright (C) 2026 IKUS Software
2
+ # Copyright (C) 2025 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -26,7 +26,7 @@ _safe = ldap3.utils.conv.escape_filter_chars
26
26
 
27
27
  def all_attribute(attributes, keys, default=None):
28
28
  """
29
- Extract all values from LDAP attributes.
29
+ Extract the all value from LDAP attributes.
30
30
  """
31
31
  # Skip loopkup if key is not defined.
32
32
  if not keys:
@@ -61,9 +61,7 @@ def first_attribute(attributes, keys, default=None):
61
61
  for attr in keys:
62
62
  try:
63
63
  value = attributes[attr]
64
- if isinstance(value, list):
65
- if len(value) == 0:
66
- continue
64
+ if isinstance(value, list) and len(value) > 0:
67
65
  return value[0]
68
66
  else:
69
67
  return value
@@ -77,8 +75,8 @@ class LdapPlugin(SimplePlugin):
77
75
  """
78
76
  Used this plugin to authenticate user against an LDAP server.
79
77
 
80
- `authenticate(login, password)` return None if the credentials are not
81
- valid. Otherwise it return a tuple or login and extra attributes.
78
+ `authenticate(username, password)` return None if the credentials are not
79
+ valid. Otherwise it return a tuple or username and extra attributes.
82
80
  The extra attribute may contains `_fullname` and `_email`.
83
81
  """
84
82
 
@@ -89,7 +87,7 @@ class LdapPlugin(SimplePlugin):
89
87
  scope = 'subtree'
90
88
  tls = False
91
89
  user_filter = '(objectClass=*)'
92
- login_attribute = ['uid']
90
+ username_attribute = ['uid']
93
91
  required_group = None
94
92
  group_attribute = 'member'
95
93
  group_attribute_is_dn = False
@@ -101,7 +99,6 @@ class LdapPlugin(SimplePlugin):
101
99
  firstname_attribute = None
102
100
  lastname_attribute = None
103
101
  email_attribute = None
104
- pool_size = 10
105
102
 
106
103
  def start(self):
107
104
  # Don't configure this plugin if the ldap URI is not provided.
@@ -119,7 +116,6 @@ class LdapPlugin(SimplePlugin):
119
116
  version=self.version,
120
117
  raise_exceptions=True,
121
118
  client_strategy=ldap3.REUSABLE,
122
- pool_size=self.pool_size,
123
119
  )
124
120
 
125
121
  def stop(self):
@@ -133,14 +129,14 @@ class LdapPlugin(SimplePlugin):
133
129
  self.stop()
134
130
  self.start()
135
131
 
136
- def authenticate(self, login, password):
132
+ def authenticate(self, username, password):
137
133
  """
138
134
  Check if the given credential as valid according to LDAP.
139
135
  Return False if invalid.
140
136
  Return None if the plugin is unavailable to validate credentials or if the plugin is disabled.
141
- Return tuple (<login>, <attributes>) if the credentials are valid.
137
+ Return tuple (<username>, <attributes>) if the credentials are valid.
142
138
  """
143
- assert isinstance(login, str)
139
+ assert isinstance(username, str)
144
140
  assert isinstance(password, str)
145
141
 
146
142
  if not hasattr(self, '_pool'):
@@ -150,14 +146,14 @@ class LdapPlugin(SimplePlugin):
150
146
  with self._pool as conn:
151
147
  try:
152
148
  # Search the LDAP server for user's DN.
153
- safe_login = _safe(login)
154
- attr_filter = ''.join([f'({_safe(attr)}={safe_login})' for attr in self.login_attribute])
149
+ safe_username = _safe(username)
150
+ attr_filter = ''.join([f'({_safe(attr)}={safe_username})' for attr in self.username_attribute])
155
151
  search_filter = f"(&{self.user_filter}(|{attr_filter}))"
156
152
  response = self._search(conn, search_filter)
157
153
  if not response:
158
- cherrypy.log(f"lookup failed login={login} reason=not_found", context='LDAP')
154
+ cherrypy.log(f"lookup failed username={username} reason=not_found", context='LDAP')
159
155
  return False
160
- cherrypy.log(f"lookup successful login={login}", context='LDAP')
156
+ cherrypy.log(f"lookup successful username={username}", context='LDAP')
161
157
  user_dn = response[0]['dn']
162
158
 
163
159
  # Use a separate connection to validate credentials
@@ -171,18 +167,18 @@ class LdapPlugin(SimplePlugin):
171
167
  )
172
168
  if not login_conn.bind():
173
169
  cherrypy.log(
174
- f'ldap authentication failed login={login} reason=wrong_password',
170
+ f'ldap authentication failed username={username} reason=wrong_password',
175
171
  context='LDAP',
176
172
  severity=logging.WARNING,
177
173
  )
178
174
  return False
179
175
 
180
- # Get user's login
176
+ # Get username
181
177
  attrs = response[0]['attributes']
182
- new_login = first_attribute(attrs, self.login_attribute[0])
183
- if not new_login:
178
+ new_username = first_attribute(attrs, self.username_attribute)
179
+ if not new_username:
184
180
  cherrypy.log(
185
- f"object missing login attribute user_dn={user_dn} attribute={self.login_attribute[0]}",
181
+ f"object missing username attribute user_dn={user_dn} attribute={self.username_attribute}",
186
182
  context='LDAP',
187
183
  severity=logging.WARNING,
188
184
  )
@@ -192,7 +188,7 @@ class LdapPlugin(SimplePlugin):
192
188
  if self.required_group:
193
189
  if not isinstance(self.required_group, list):
194
190
  self.required_group = [self.required_group]
195
- user_value = user_dn if self.group_attribute_is_dn else new_login
191
+ user_value = user_dn if self.group_attribute_is_dn else new_username
196
192
  group_filter = '(&(%s=%s)(|%s)%s)' % (
197
193
  _safe(self.group_attribute),
198
194
  _safe(user_value),
@@ -201,13 +197,13 @@ class LdapPlugin(SimplePlugin):
201
197
  )
202
198
  # Search LDAP Server for matching groups.
203
199
  cherrypy.log(
204
- f"group check start login={user_value} required_groups={' '.join(self.required_group)}",
200
+ f"group check start username={user_value} required_groups={' '.join(self.required_group)}",
205
201
  context='LDAP',
206
202
  )
207
203
  response = self._search(conn, group_filter, attributes=['cn'])
208
204
  if not response:
209
205
  cherrypy.log(
210
- f"group check failed login={user_value} required_groups={' '.join(self.required_group)}",
206
+ f"group check failed username={user_value} required_groups={' '.join(self.required_group)}",
211
207
  context='LDAP',
212
208
  )
213
209
  return False
@@ -220,11 +216,13 @@ class LdapPlugin(SimplePlugin):
220
216
  firstname = first_attribute(attrs, self.firstname_attribute, '')
221
217
  lastname = first_attribute(attrs, self.lastname_attribute, '')
222
218
  attrs['fullname'] = ' '.join([name for name in [firstname, lastname] if name])
223
- return (new_login, attrs)
219
+ return (new_username, attrs)
224
220
  except LDAPInvalidCredentialsResult:
225
221
  return False
226
222
  except LDAPException:
227
- cherrypy.log(f"unexpected error login={login}", context='LDAP', severity=logging.ERROR, traceback=True)
223
+ cherrypy.log(
224
+ f"unexpected error username={username}", context='LDAP', severity=logging.ERROR, traceback=True
225
+ )
228
226
  return None
229
227
 
230
228
  def search(self, filter, attributes=ldap3.ALL_ATTRIBUTES, search_base=None, paged_size=None):
@@ -1,5 +1,5 @@
1
1
  # RestAPI plugin for cherrypy
2
- # Copyright (C) 2024-2026 IKUS Software
2
+ # Copyright (C) 2024-2025 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
1
1
  # Scheduler plugins for Cherrypy
2
- # Copyright (C) 2026 IKUS Software
2
+ # Copyright (C) 2025 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -35,6 +35,7 @@ def clear_db_sessions(func):
35
35
 
36
36
  @wraps(func)
37
37
  def func_wrapper(*args, **kwargs):
38
+ cherrypy.db.clear_sessions()
38
39
  try:
39
40
  result = func(*args, **kwargs)
40
41
  finally:
@@ -205,7 +206,6 @@ class Scheduler(SimplePlugin):
205
206
  """
206
207
  hour, minute = execution_time.split(':', 2)
207
208
  return self.add_job(
208
- id=getattr(func, '__name__', str(func)),
209
209
  func=func,
210
210
  name=getattr(func, '__name__', str(func)),
211
211
  args=args,
@@ -214,8 +214,6 @@ class Scheduler(SimplePlugin):
214
214
  hour=hour,
215
215
  minute=minute,
216
216
  misfire_grace_time=None,
217
- coalesce=True,
218
- replace_existing=True,
219
217
  )
220
218
 
221
219
  def add_job_now(self, func, *args, **kwargs):
@@ -245,29 +243,20 @@ class Scheduler(SimplePlugin):
245
243
  Remove the given job from scheduler.
246
244
  """
247
245
  # Search for a matching job
248
- return_value = False
249
- if self._scheduler is None:
250
- return return_value
251
246
  for j in self._scheduler.get_jobs(jobstore=jobstore):
252
- if j.func == job or j.name == job:
247
+ if j.func == job:
253
248
  self._scheduler.remove_job(job_id=j.id, jobstore=jobstore)
254
- return_value = True
255
- return return_value
256
249
 
257
250
  def remove_all_jobs(self, jobstore=None):
258
251
  """
259
252
  Remove all jobs from scheduler.
260
253
  """
261
- if self._scheduler is None:
262
- return
263
254
  self._scheduler.remove_all_jobs(jobstore=jobstore)
264
255
 
265
256
  def wait_for_jobs(self, jobstore=None):
266
257
  """
267
258
  Used to wait for all running jobs to complete.
268
259
  """
269
- if self._scheduler is None:
270
- return
271
260
  # Wait until the queue is empty.
272
261
  while any(
273
262
  job for job in self._scheduler.get_jobs(jobstore=jobstore) if job.next_run_time < datetime.now(timezone.utc)
@@ -1,5 +1,5 @@
1
1
  # SMTP Plugins for cherrypy
2
- # Copyright (C) 2026 IKUS Software
2
+ # Copyright (C) 2025 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -127,7 +127,6 @@ class SmtpPlugin(SimplePlugin):
127
127
  password = None
128
128
  encryption = None
129
129
  email_from = None
130
- bcc = None
131
130
 
132
131
  def _create_msg(self, subject: str, message: str, to=None, cc=None, bcc=None, reply_to=None, headers={}):
133
132
  assert subject
@@ -142,13 +141,8 @@ class SmtpPlugin(SimplePlugin):
142
141
  msg['To'] = _formataddr(to)
143
142
  if cc:
144
143
  msg['Cc'] = _formataddr(cc)
145
- bcc_list = []
146
- if self.bcc:
147
- bcc_list.append(_formataddr(self.bcc))
148
144
  if bcc:
149
- bcc_list.append(_formataddr(bcc))
150
- if bcc_list:
151
- msg['Bcc'] = ', '.join(bcc_list)
145
+ msg['Bcc'] = _formataddr(bcc)
152
146
  if reply_to:
153
147
  msg['Reply-To'] = _formataddr(reply_to)
154
148
  msg['Message-ID'] = email.utils.make_msgid()
@@ -1,5 +1,5 @@
1
- # Cherrypy-foundation
2
- # Copyright (C) 2022-2026 IKUS Software
1
+ # Cherrypy Foundation
2
+ # Copyright (C) 2022-2025 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
1
1
  # LDAP Plugins for cherrypy
2
- # Copyright (C) 2026 IKUS Software
2
+ # Copyright (C) 2025 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -19,13 +19,13 @@ Created on Oct 17, 2015
19
19
  @author: Patrik Dufresne <patrik@ikus-soft.com>
20
20
  """
21
21
  import os
22
- from unittest import TestCase, mock, skipUnless
22
+ from unittest import mock, skipUnless
23
23
 
24
24
  import cherrypy
25
25
  import ldap3
26
26
  from cherrypy.test import helper
27
27
 
28
- from ..ldap import all_attribute, first_attribute # noqa
28
+ from .. import ldap # noqa
29
29
 
30
30
  original_connection = ldap3.Connection
31
31
 
@@ -35,79 +35,6 @@ def mock_ldap_connection(*args, **kwargs):
35
35
  return original_connection(*args, client_strategy=ldap3.MOCK_ASYNC, **kwargs)
36
36
 
37
37
 
38
- class LdapFirstAttributeTest(TestCase):
39
-
40
- def test_no_keys_returns_default(self):
41
- attributes = {"cn": ["John Doe"]}
42
- self.assertIsNone(first_attribute(attributes, None))
43
- self.assertEqual(first_attribute(attributes, [], default="fallback"), "fallback")
44
-
45
- def test_single_key_with_scalar_value(self):
46
- attributes = {"uid": "jdoe"}
47
- self.assertEqual(first_attribute(attributes, "uid"), "jdoe")
48
-
49
- def test_single_key_with_list_value(self):
50
- attributes = {"mail": ["john@example.com", "alt@example.com"]}
51
- self.assertEqual(first_attribute(attributes, "mail"), "john@example.com")
52
-
53
- def test_empty_list_value_is_skipped(self):
54
- attributes = {
55
- "mail": [],
56
- "uid": ["jdoe"],
57
- }
58
- self.assertEqual(first_attribute(attributes, ["mail", "uid"]), "jdoe")
59
-
60
- def test_missing_first_key_uses_next_key(self):
61
- attributes = {"cn": ["John Doe"]}
62
- self.assertEqual(first_attribute(attributes, ["sn", "cn"]), "John Doe")
63
-
64
- def test_all_keys_missing_returns_default(self):
65
- attributes = {"cn": ["John Doe"]}
66
- self.assertEqual(first_attribute(attributes, ["sn", "uid"], default="unknown"), "unknown")
67
-
68
- def test_key_not_found_without_default_returns_none(self):
69
- attributes = {"cn": ["John Doe"]}
70
- self.assertIsNone(first_attribute(attributes, "uid"))
71
-
72
-
73
- class LdapAllAttributeTest(TestCase):
74
-
75
- def test_no_keys_returns_default(self):
76
- attributes = {"cn": ["John Doe"]}
77
- self.assertIsNone(all_attribute(attributes, None))
78
- self.assertEqual(all_attribute(attributes, [], default="fallback"), "fallback")
79
-
80
- def test_single_key_with_scalar_value(self):
81
- attributes = {"uid": "jdoe"}
82
- self.assertEqual(all_attribute(attributes, "uid"), ["jdoe"])
83
-
84
- def test_single_key_with_list_value(self):
85
- attributes = {"mail": ["john@example.com", "alt@example.com"]}
86
- self.assertEqual(all_attribute(attributes, "mail"), ["john@example.com"])
87
-
88
- def test_multiple_keys_collect_values(self):
89
- attributes = {
90
- "uid": "jdoe",
91
- "cn": ["John Doe"],
92
- }
93
- self.assertEqual(all_attribute(attributes, ["uid", "cn"]), ["jdoe", "John Doe"])
94
-
95
- def test_missing_keys_are_skipped(self):
96
- attributes = {"cn": ["John Doe"]}
97
- self.assertEqual(all_attribute(attributes, ["sn", "cn", "uid"]), ["John Doe"])
98
-
99
- def test_all_keys_missing_returns_default(self):
100
- attributes = {"cn": ["John Doe"]}
101
- self.assertEqual(all_attribute(attributes, ["sn", "uid"], default=[]), [])
102
-
103
- def test_empty_list_value_is_appended_as_empty_list(self):
104
- attributes = {
105
- "mail": [],
106
- "uid": "jdoe",
107
- }
108
- self.assertEqual(all_attribute(attributes, ["mail", "uid"]), [[], "jdoe"])
109
-
110
-
111
38
  class LdapPluginTest(helper.CPWebCase):
112
39
 
113
40
  @classmethod
@@ -1,5 +1,5 @@
1
1
  # Scheduler plugins for Cherrypy
2
- # Copyright (C) 2026 IKUS Software
2
+ # Copyright (C) 2025 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by