pyweber 1.5.0.dev2__tar.gz → 1.5.0.dev3__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.
Files changed (104) hide show
  1. {pyweber-1.5.0.dev2/pyweber.egg-info → pyweber-1.5.0.dev3}/PKG-INFO +1 -1
  2. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyproject.toml +1 -1
  3. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/core/template.py +12 -1
  4. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/response.py +16 -13
  5. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/ws_message.py +15 -3
  6. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/pyweber/pyweber.py +8 -1
  7. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/config.toml +1 -0
  8. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/js.js +26 -5
  9. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/utils/security.py +34 -0
  10. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3/pyweber.egg-info}/PKG-INFO +1 -1
  11. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/LICENSE +0 -0
  12. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/MANIFEST.in +0 -0
  13. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/NOTICE +0 -0
  14. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/README.md +0 -0
  15. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/__init__.py +0 -0
  16. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/admin/index.html +0 -0
  17. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/admin/src/script.js +0 -0
  18. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/admin/src/style.css +0 -0
  19. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/cli/__init__.py +0 -0
  20. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/cli/commands.py +0 -0
  21. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/components/__init__.py +0 -0
  22. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/components/form.py +0 -0
  23. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/components/general.py +0 -0
  24. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/components/input.py +0 -0
  25. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/config/__init__.py +0 -0
  26. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/config/config.py +0 -0
  27. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/connection/__init__.py +0 -0
  28. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/connection/http.py +0 -0
  29. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/connection/reload.py +0 -0
  30. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/connection/selector.py +0 -0
  31. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/connection/session.py +0 -0
  32. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/connection/websocket.py +0 -0
  33. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/core/__init__.py +0 -0
  34. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/core/element.py +0 -0
  35. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/core/events.py +0 -0
  36. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/core/html_parser.py +0 -0
  37. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/core/window.py +0 -0
  38. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/__init__.py +0 -0
  39. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/context.py +0 -0
  40. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/cookies.py +0 -0
  41. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/create_app.py +0 -0
  42. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/element.py +0 -0
  43. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/error_pages.py +0 -0
  44. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/field.py +0 -0
  45. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/field_storage.py +0 -0
  46. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/file.py +0 -0
  47. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/file_stream.py +0 -0
  48. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/handoff.py +0 -0
  49. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/headers.py +0 -0
  50. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/middleware.py +0 -0
  51. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/openapi.py +0 -0
  52. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/rate_limit.py +0 -0
  53. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/request.py +0 -0
  54. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/routes.py +0 -0
  55. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/run.py +0 -0
  56. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/security.py +0 -0
  57. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/stream_stats.py +0 -0
  58. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/strem_stats.py +0 -0
  59. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/task_manager.py +0 -0
  60. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/template_diff.py +0 -0
  61. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/pyweber/__init__.py +0 -0
  62. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/.gitignore +0 -0
  63. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/css.css +0 -0
  64. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/docs.html +0 -0
  65. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/favicon/android-chrome-192x192.png +0 -0
  66. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/favicon/android-chrome-512x512.png +0 -0
  67. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/favicon/apple-touch-icon.png +0 -0
  68. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/favicon/favicon-16x16.png +0 -0
  69. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/favicon/favicon-32x32.png +0 -0
  70. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/favicon/favicon.ico +0 -0
  71. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/favicon/pyweber.png +0 -0
  72. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/favicon/site.webmanifest +0 -0
  73. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/handlers.js +0 -0
  74. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/html.html +0 -0
  75. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/html401.html +0 -0
  76. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/html404.html +0 -0
  77. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/html500.html +0 -0
  78. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/loading.html +0 -0
  79. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/main.py +0 -0
  80. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/pyweber.css +0 -0
  81. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/update.py +0 -0
  82. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/testing/__init__.py +0 -0
  83. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/testing/client.py +0 -0
  84. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/utils/__init__.py +0 -0
  85. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/utils/async_utils.py +0 -0
  86. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/utils/deprecation.py +0 -0
  87. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/utils/exceptions.py +0 -0
  88. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/utils/icons.py +0 -0
  89. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/utils/loads.py +0 -0
  90. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/utils/mime.py +0 -0
  91. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/utils/types.py +0 -0
  92. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/utils/utils.py +0 -0
  93. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber.egg-info/SOURCES.txt +0 -0
  94. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber.egg-info/dependency_links.txt +0 -0
  95. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber.egg-info/entry_points.txt +0 -0
  96. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber.egg-info/requires.txt +0 -0
  97. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber.egg-info/top_level.txt +0 -0
  98. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/setup.cfg +0 -0
  99. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/tests/test_config.py +0 -0
  100. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/tests/test_context.py +0 -0
  101. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/tests/test_cookies.py +0 -0
  102. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/tests/test_public_api.py +0 -0
  103. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/tests/test_response.py +0 -0
  104. {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/tests/test_template_diff.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyweber
3
- Version: 1.5.0.dev2
3
+ Version: 1.5.0.dev3
4
4
  Summary: A lightweight Python framework for building and managing web applications.
5
5
  Author-email: DevPythonMZ <pypi.dev@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyweber"
7
- version = "1.5.0.dev2"
7
+ version = "1.5.0.dev3"
8
8
  description = "A lightweight Python framework for building and managing web applications."
9
9
  readme = "README.md"
10
10
  authors = [{ name = "DevPythonMZ", email = "pypi.dev@gmail.com" }]
@@ -15,6 +15,11 @@ class Template:
15
15
  self.title = title
16
16
  self.__root = self.parse_html()
17
17
 
18
+ @property
19
+ def include_uuid(self) -> bool:
20
+ """When False, HTML is static (no uuid attrs) and the reactive WS script is not injected."""
21
+ return self.__include_uuid
22
+
18
23
  @property
19
24
  def template(self):
20
25
  return self.__template
@@ -135,6 +140,7 @@ class Template:
135
140
  return element
136
141
 
137
142
  def __create_default_element(self, *args, **kwargs):
143
+ kwargs.setdefault('include_uuid', self.__include_uuid)
138
144
  return Element(*args, **kwargs)
139
145
 
140
146
  @staticmethod
@@ -204,9 +210,13 @@ class Template:
204
210
  break
205
211
 
206
212
  if not has_websocket_script:
213
+ # Reactivity requires stable uuid attrs in HTML. Static pages
214
+ # (include_uuid=False) must not load the WS client — it stamps
215
+ # random uuids and can rewrite documentElement (breaking CSS).
207
216
  disable_ws = os.environ.get('PYWEBER_DISABLE_WS', False)
217
+ ws_disabled = disable_ws in [True, 'True', 'true', '1', 1]
208
218
 
209
- if disable_ws not in [True, 'True', 'true', '1', 1]:
219
+ if self.__include_uuid and not ws_disabled:
210
220
  head.childs.extend(
211
221
  [
212
222
  self.__create_default_element(
@@ -269,6 +279,7 @@ class Template:
269
279
  template=self.template,
270
280
  status_code=self.status_code,
271
281
  title=self.title,
282
+ include_uuid=self.__include_uuid,
272
283
  **self.kwargs
273
284
  )
274
285
  tpl.data = self.data
@@ -7,19 +7,16 @@ from typing import Any
7
7
  from pyweber.utils.types import ContentTypes, HTTPStatusCode
8
8
  from pyweber.models.request import Request
9
9
  from pyweber.utils.utils import PrintLine, Colors
10
- from pyweber.utils.security import get_allowed_origins, https_enabled
11
-
12
- DEFAULT_CSP = (
13
- "default-src 'self'; "
14
- "script-src 'self' 'unsafe-inline'; "
15
- "style-src 'self' 'unsafe-inline'; "
16
- "img-src 'self' data:; "
17
- "connect-src 'self' ws: wss:; "
18
- "frame-ancestors 'none'; "
19
- "base-uri 'self'; "
20
- "form-action 'self'"
10
+ from pyweber.utils.security import (
11
+ DEFAULT_CSP,
12
+ get_allowed_origins,
13
+ https_enabled,
14
+ resolve_csp,
21
15
  )
22
16
 
17
+ # Re-export for callers that imported DEFAULT_CSP from response
18
+ __all_csp__ = (DEFAULT_CSP, resolve_csp)
19
+
23
20
  # Internal bookkeeping headers — stripped on ASGI wire format
24
21
  INTERNAL_RESPONSE_HEADERS = frozenset({
25
22
  'method',
@@ -121,11 +118,14 @@ class Response:
121
118
  "X-Content-Type-Options": "nosniff",
122
119
  "X-Frame-Options": "DENY",
123
120
  "Referrer-Policy": "strict-origin-when-cross-origin",
124
- "Content-Security-Policy": DEFAULT_CSP,
125
121
  "X-Forwarded-Proto": "https" if https_enabled() else "http",
126
122
  "X-Forwarded-Host": getattr(request, 'host', None) or 'localhost',
127
123
  }
128
124
 
125
+ csp = resolve_csp()
126
+ if csp:
127
+ self.__headers["Content-Security-Policy"] = csp
128
+
129
129
  if https_enabled():
130
130
  self.__headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
131
131
 
@@ -133,7 +133,10 @@ class Response:
133
133
 
134
134
  if headers:
135
135
  for key, value in headers.items():
136
- self.__headers[key] = value
136
+ if value is None:
137
+ self.__headers.pop(key, None)
138
+ else:
139
+ self.__headers[key] = value
137
140
 
138
141
  self.http_status_code = HTTPStatusCode.search_by_code(status)
139
142
  self.__check_status_code()
@@ -41,6 +41,7 @@ class wsMessage:
41
41
 
42
42
  async def get_template(self):
43
43
  self.__app: 'Pyweber' = self.__app
44
+ used_handoff = False
44
45
  if self.session_id in sessions.all_sessions:
45
46
  session_template = sessions.get_session(session_id=self.session_id).template
46
47
  else:
@@ -48,12 +49,23 @@ class wsMessage:
48
49
  token=self.get_value(key='handoffToken'),
49
50
  route=self.route or '',
50
51
  )
52
+ if session_template is not None:
53
+ used_handoff = True
51
54
  if session_template is None:
52
55
  session_template = await self.__app.clone_template(route=self.route)
53
56
 
54
- if self.get_value(key='template'):
55
- session_template.root = session_template.parse_html(html=self.get_value('template'))
56
- self.insert_values(element=session_template.root)
57
+ client_html = self.get_value(key='template')
58
+ if client_html:
59
+ # With include_uuid=False (or handoff without uuid attrs in HTML),
60
+ # never parse client outerHTML over the server template — client
61
+ # may have stamped random uuids that poison the diff contract.
62
+ keep_server = (
63
+ not getattr(session_template, 'include_uuid', True)
64
+ or (used_handoff and 'uuid=' not in client_html.lower())
65
+ )
66
+ if not keep_server:
67
+ session_template.root = session_template.parse_html(html=client_html)
68
+ self.insert_values(element=session_template.root)
57
69
 
58
70
  return session_template
59
71
 
@@ -591,7 +591,14 @@ class Pyweber(
591
591
  return False
592
592
 
593
593
  template = template_result.template
594
- return not isinstance(template, (dict, list, set))
594
+ if isinstance(template, (dict, list, set)):
595
+ return False
596
+ # No stable uuid contract → no reactive session handoff
597
+ if isinstance(template, Template) and not template.include_uuid:
598
+ return False
599
+ if isinstance(template, Element) and not getattr(template, 'include_uuid', True):
600
+ return False
601
+ return True
595
602
 
596
603
  def _ensure_template_object(
597
604
  self,
@@ -40,5 +40,6 @@ rate_limit_enabled = false
40
40
  rate_limit_rpm = 120
41
41
  gzip_enabled = true
42
42
  gzip_min_bytes = 500
43
+ # csp = 'off' # or a full Content-Security-Policy string; env PYWEBER_CSP wins
43
44
 
44
45
  [api_keys]
@@ -41,7 +41,9 @@ function connectWebSocket() {
41
41
  socket.onopen = async function () {
42
42
  socketReady = true;
43
43
  reconnectAttempts = 0;
44
- sendToServer(await getEventData({ includeTemplate: true }));
44
+ // Só envia outerHTML no handshake se existir contrato uuid estável
45
+ const includeTemplate = pageHasStableUuids();
46
+ sendToServer(await getEventData({ includeTemplate }));
45
47
  };
46
48
 
47
49
  socket.onerror = function (error) {
@@ -250,20 +252,34 @@ async function send_file_chunk(file_id, status, data) {
250
252
  }
251
253
 
252
254
  // ─── DOM ──────────────────────────────────────────────────────────────────────
255
+ function pageHasStableUuids() {
256
+ return !!document.documentElement.querySelector('[uuid]');
257
+ }
258
+
253
259
  function applyDifferences(differences) {
260
+ if (!differences || typeof differences !== 'object') return;
261
+
254
262
  Object.keys(differences).forEach(key => {
255
263
  const diff = differences[key];
264
+ if (!diff) return;
256
265
 
266
+ // Root-level replace destroys <link rel="stylesheet"> / Bootstrap / scripts.
267
+ // Never rewrite documentElement.innerHTML — ignore and keep the live DOM.
257
268
  if (!diff.parent) {
258
269
  if (diff.status === 'Changed') {
259
- const doc = new DOMParser().parseFromString(diff.element, 'text/html');
260
- document.documentElement.innerHTML = doc.documentElement.innerHTML;
270
+ console.warn(
271
+ 'PyWeber: ignored root document replace (would break CSS/scripts). ' +
272
+ 'Use include_uuid=True for reactive updates.'
273
+ );
261
274
  }
262
275
  return;
263
276
  }
264
277
 
265
278
  const parentElement = document.querySelector(`[uuid="${diff.parent}"]`);
266
- if (!parentElement) return;
279
+ if (!parentElement) {
280
+ // UUID contract broken (e.g. stamped client uuids vs server) — skip, do not invent a full replace
281
+ return;
282
+ }
267
283
 
268
284
  switch (diff.status) {
269
285
  case 'Added':
@@ -286,6 +302,7 @@ function applyDifferences(differences) {
286
302
  if (toChange) {
287
303
  toChange.parentNode.replaceChild(createElementFromHTML(diff.element), toChange);
288
304
  }
305
+ // missing uuid target → ignore (do not fall back to full-document rewrite)
289
306
  }
290
307
  break;
291
308
  }
@@ -377,9 +394,12 @@ function getHandoffToken() {
377
394
  return document.querySelector('meta[name="pyweber-handoff"]')?.content || null;
378
395
  }
379
396
 
380
- /** Atribui uuid a nós injectados por JS (ou libs externas) antes do sync com o servidor. */
397
+ /** Atribui uuid a nós injectados por JS (ou libs externas) antes do sync com o servidor.
398
+ * Só corre se a página já tiver o contrato uuid (include_uuid=True). Nunca inventa
399
+ * um universo de UUIDs em páginas estáticas — isso parte o handoff/diff. */
381
400
  function stampMissingUuids(root = document.documentElement) {
382
401
  if (!(root instanceof Element)) return;
402
+ if (!pageHasStableUuids()) return;
383
403
 
384
404
  const nodes = root === document.documentElement
385
405
  ? root.querySelectorAll('*')
@@ -395,6 +415,7 @@ function stampMissingUuids(root = document.documentElement) {
395
415
  /** Re-sincroniza DOM → servidor depois de JS injectar HTML (sem uuid) após o WS abrir. */
396
416
  async function resyncDomWithServer() {
397
417
  if (!socketReady || socket.readyState !== WebSocket.OPEN) return;
418
+ if (!pageHasStableUuids()) return;
398
419
  stampMissingUuids();
399
420
  await sendToServer(await getEventData({ includeTemplate: true }));
400
421
  }
@@ -110,6 +110,40 @@ def csrf_enabled() -> bool:
110
110
  return bool(value)
111
111
 
112
112
 
113
+ # Allows HTTPS CDNs (Bootstrap, Google Fonts, jsDelivr icons) while still
114
+ # blocking exotic schemes and clickjacking. Override with PYWEBER_CSP /
115
+ # [security].csp; set to "off" to omit the header.
116
+ DEFAULT_CSP = (
117
+ "default-src 'self'; "
118
+ "script-src 'self' 'unsafe-inline' https:; "
119
+ "style-src 'self' 'unsafe-inline' https:; "
120
+ "font-src 'self' data: https:; "
121
+ "img-src 'self' data: https:; "
122
+ "connect-src 'self' ws: wss: https:; "
123
+ "frame-ancestors 'none'; "
124
+ "base-uri 'self'; "
125
+ "form-action 'self'"
126
+ )
127
+
128
+ _CSP_OFF = frozenset({'', '0', 'false', 'off', 'none', 'disable', 'disabled'})
129
+
130
+
131
+ def resolve_csp() -> str | None:
132
+ """Return CSP header value, or None to omit the header.
133
+
134
+ Precedence: ``PYWEBER_CSP`` env → ``[security].csp`` config → ``DEFAULT_CSP``.
135
+ """
136
+ raw = os.environ.get('PYWEBER_CSP')
137
+ if raw is None:
138
+ raw = _config().get('security', 'csp', default=None)
139
+ if raw is None:
140
+ return DEFAULT_CSP
141
+ value = str(raw).strip()
142
+ if value.lower() in _CSP_OFF:
143
+ return None
144
+ return value
145
+
146
+
113
147
  def get_allowed_origins() -> set[str]:
114
148
  origins = _config().get('security', 'allowed_origins', default=None)
115
149
  if origins is None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyweber
3
- Version: 1.5.0.dev2
3
+ Version: 1.5.0.dev3
4
4
  Summary: A lightweight Python framework for building and managing web applications.
5
5
  Author-email: DevPythonMZ <pypi.dev@gmail.com>
6
6
  License-Expression: Apache-2.0
File without changes
File without changes
File without changes
File without changes
File without changes