pyweber 1.3.0.dev20260609__tar.gz → 1.5.0.dev0__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 (107) hide show
  1. {pyweber-1.3.0.dev20260609/pyweber.egg-info → pyweber-1.5.0.dev0}/PKG-INFO +3 -2
  2. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyproject.toml +4 -3
  3. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/__init__.py +49 -3
  4. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/cli/commands.py +1 -1
  5. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/components/form.py +23 -9
  6. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/components/general.py +2 -2
  7. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/components/input.py +13 -13
  8. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/connection/http.py +61 -9
  9. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/connection/websocket.py +37 -12
  10. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/core/__init__.py +9 -1
  11. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/core/element.py +18 -14
  12. pyweber-1.5.0.dev0/pyweber/core/html_parser.py +213 -0
  13. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/models/__init__.py +27 -1
  14. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/models/create_app.py +43 -8
  15. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/models/element.py +34 -16
  16. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/models/field_storage.py +13 -11
  17. pyweber-1.5.0.dev0/pyweber/models/file.py +35 -0
  18. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/models/middleware.py +104 -27
  19. pyweber-1.5.0.dev0/pyweber/models/openapi.py +1004 -0
  20. pyweber-1.5.0.dev0/pyweber/models/rate_limit.py +84 -0
  21. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/models/request.py +1 -0
  22. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/models/response.py +36 -6
  23. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/models/routes.py +189 -44
  24. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/models/run.py +2 -2
  25. pyweber-1.5.0.dev0/pyweber/models/security.py +394 -0
  26. pyweber-1.5.0.dev0/pyweber/models/stream_stats.py +5 -0
  27. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/pyweber/pyweber.py +479 -140
  28. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/static/config.toml +9 -0
  29. pyweber-1.5.0.dev0/pyweber/static/docs.html +19 -0
  30. pyweber-1.5.0.dev0/pyweber/testing/__init__.py +3 -0
  31. pyweber-1.5.0.dev0/pyweber/testing/client.py +126 -0
  32. pyweber-1.5.0.dev0/pyweber/utils/async_utils.py +38 -0
  33. pyweber-1.5.0.dev0/pyweber/utils/deprecation.py +87 -0
  34. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/utils/exceptions.py +22 -3
  35. pyweber-1.3.0.dev20260609/pyweber/utils/types.py → pyweber-1.5.0.dev0/pyweber/utils/icons.py +2 -705
  36. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/utils/loads.py +20 -2
  37. pyweber-1.5.0.dev0/pyweber/utils/mime.py +69 -0
  38. pyweber-1.5.0.dev0/pyweber/utils/security.py +197 -0
  39. pyweber-1.5.0.dev0/pyweber/utils/types.py +725 -0
  40. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0/pyweber.egg-info}/PKG-INFO +3 -2
  41. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber.egg-info/SOURCES.txt +11 -0
  42. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber.egg-info/requires.txt +3 -1
  43. pyweber-1.3.0.dev20260609/pyweber/models/file.py +0 -15
  44. pyweber-1.3.0.dev20260609/pyweber/models/openapi.py +0 -381
  45. pyweber-1.3.0.dev20260609/pyweber/static/docs.html +0 -28
  46. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/LICENSE +0 -0
  47. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/MANIFEST.in +0 -0
  48. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/NOTICE +0 -0
  49. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/README.md +0 -0
  50. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/admin/index.html +0 -0
  51. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/admin/src/script.js +0 -0
  52. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/admin/src/style.css +0 -0
  53. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/cli/__init__.py +0 -0
  54. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/components/__init__.py +0 -0
  55. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/config/__init__.py +0 -0
  56. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/config/config.py +0 -0
  57. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/connection/__init__.py +0 -0
  58. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/connection/reload.py +0 -0
  59. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/connection/selector.py +0 -0
  60. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/connection/session.py +0 -0
  61. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/core/events.py +0 -0
  62. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/core/template.py +0 -0
  63. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/core/window.py +0 -0
  64. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/models/context.py +0 -0
  65. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/models/cookies.py +0 -0
  66. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/models/error_pages.py +0 -0
  67. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/models/field.py +0 -0
  68. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/models/file_stream.py +0 -0
  69. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/models/handoff.py +0 -0
  70. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/models/headers.py +0 -0
  71. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/models/strem_stats.py +0 -0
  72. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/models/task_manager.py +0 -0
  73. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/models/template_diff.py +0 -0
  74. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/models/ws_message.py +0 -0
  75. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/pyweber/__init__.py +0 -0
  76. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/static/.gitignore +0 -0
  77. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/static/css.css +0 -0
  78. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/static/favicon/android-chrome-192x192.png +0 -0
  79. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/static/favicon/android-chrome-512x512.png +0 -0
  80. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/static/favicon/apple-touch-icon.png +0 -0
  81. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/static/favicon/favicon-16x16.png +0 -0
  82. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/static/favicon/favicon-32x32.png +0 -0
  83. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/static/favicon/favicon.ico +0 -0
  84. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/static/favicon/pyweber.png +0 -0
  85. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/static/favicon/site.webmanifest +0 -0
  86. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/static/handlers.js +0 -0
  87. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/static/html.html +0 -0
  88. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/static/html401.html +0 -0
  89. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/static/html404.html +0 -0
  90. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/static/html500.html +0 -0
  91. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/static/js.js +0 -0
  92. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/static/loading.html +0 -0
  93. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/static/main.py +0 -0
  94. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/static/pyweber.css +0 -0
  95. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/static/update.py +0 -0
  96. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/utils/__init__.py +0 -0
  97. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber/utils/utils.py +0 -0
  98. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber.egg-info/dependency_links.txt +0 -0
  99. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber.egg-info/entry_points.txt +0 -0
  100. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/pyweber.egg-info/top_level.txt +0 -0
  101. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/setup.cfg +0 -0
  102. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/tests/test_config.py +0 -0
  103. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/tests/test_context.py +0 -0
  104. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/tests/test_cookies.py +0 -0
  105. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/tests/test_public_api.py +0 -0
  106. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/tests/test_response.py +0 -0
  107. {pyweber-1.3.0.dev20260609 → pyweber-1.5.0.dev0}/tests/test_template_diff.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyweber
3
- Version: 1.3.0.dev20260609
3
+ Version: 1.5.0.dev0
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
@@ -15,7 +15,6 @@ Description-Content-Type: text/markdown
15
15
  License-File: LICENSE
16
16
  License-File: NOTICE
17
17
  Requires-Dist: watchdog
18
- Requires-Dist: lxml
19
18
  Requires-Dist: toml
20
19
  Requires-Dist: questionary
21
20
  Requires-Dist: qrcode[pil]
@@ -23,6 +22,8 @@ Provides-Extra: dev
23
22
  Requires-Dist: pytest; extra == "dev"
24
23
  Requires-Dist: pytest-cov; extra == "dev"
25
24
  Requires-Dist: pytest-asyncio; extra == "dev"
25
+ Provides-Extra: fast-html
26
+ Requires-Dist: lxml; extra == "fast-html"
26
27
  Dynamic: license-file
27
28
 
28
29
  # Pyweber Framework
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyweber"
7
- version = "1.3.0.dev20260609"
7
+ version = "1.5.0.dev0"
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" }]
11
11
  license = "Apache-2.0"
12
12
  requires-python = ">=3.10"
13
- dependencies = ["watchdog", "lxml", "toml", "questionary", "qrcode[pil]"]
13
+ dependencies = ["watchdog", "toml", "questionary", "qrcode[pil]"]
14
14
  [project.urls]
15
15
  Documentation = "https://pyweber.dev/"
16
16
  Source = "https://github.com/pyweber/pyweber"
@@ -28,6 +28,7 @@ include = ["pyweber*"]
28
28
 
29
29
  [project.optional-dependencies]
30
30
  dev = ["pytest", "pytest-cov", "pytest-asyncio"]
31
+ fast-html = ["lxml"]
31
32
 
32
33
  [tool.setuptools.package-data]
33
34
  pyweber = ["static/**/*", "admin/**/*"]
@@ -36,5 +37,5 @@ pyweber = ["static/**/*", "admin/**/*"]
36
37
  omit = ["pyweber/cli/commands.py"]
37
38
 
38
39
  [tool.coverage.report]
39
- fail_under = 84
40
+ fail_under = 87.5
40
41
  show_missing = true
@@ -1,4 +1,4 @@
1
- # pyweber run
1
+ # pyweber run
2
2
  from .models.run import (
3
3
  run,
4
4
  run_as_asgi
@@ -39,6 +39,22 @@ from .models.strem_stats import (
39
39
  AdaptiveController,
40
40
  StreamStats
41
41
  )
42
+ from .models.openapi import (
43
+ OpenApiProcessor,
44
+ OpenAPIConfig,
45
+ OpenAPIBuilder,
46
+ HTTPBearer,
47
+ HTTPBasic,
48
+ APIKeyHeader,
49
+ APIKeyQuery,
50
+ APIKeyCookie,
51
+ )
52
+ from .models.security import (
53
+ AuthContext,
54
+ SecurityEnforcer,
55
+ SecurityError,
56
+ ForbiddenError,
57
+ )
42
58
 
43
59
  # pyweber events
44
60
  from .core.events import (
@@ -54,6 +70,16 @@ from .utils.utils import (
54
70
  WriteLine,
55
71
  Colors
56
72
  )
73
+ from .utils.security import (
74
+ secure_filename,
75
+ safe_join,
76
+ sign_value,
77
+ unsign_value,
78
+ generate_csrf_token,
79
+ )
80
+ from .utils.icons import Icons
81
+ from .utils.deprecation import warn_deprecated
82
+ from .testing import TestClient
57
83
 
58
84
  from .utils.types import (
59
85
  ContentTypes,
@@ -64,7 +90,6 @@ from .utils.types import (
64
90
  NonSelfClosingHTMLTags,
65
91
  WebSocketStatusCode,
66
92
  WindowEventType,
67
- Icons,
68
93
  GetBy,
69
94
  DateFormat,
70
95
  DateTimeFormat,
@@ -79,7 +104,8 @@ from .utils.types import (
79
104
  Int32Format,
80
105
  Int64Format,
81
106
  FloatFormat,
82
- DoubleFormnat
107
+ DoubleFormat,
108
+ DoubleFormnat,
83
109
  )
84
110
  from .utils.exceptions import (
85
111
  InvalidRouteFormatError,
@@ -169,6 +195,11 @@ __all__ = [
169
195
  'PrintLine',
170
196
  'WriteLine',
171
197
  'LoadStaticFiles',
198
+ 'secure_filename',
199
+ 'safe_join',
200
+ 'sign_value',
201
+ 'unsign_value',
202
+ 'generate_csrf_token',
172
203
  'ContentTypes',
173
204
  'EventType',
174
205
  'HTMLTag',
@@ -187,6 +218,7 @@ __all__ = [
187
218
  'Int32Format',
188
219
  'Int64Format',
189
220
  'FloatFormat',
221
+ 'DoubleFormat',
190
222
  'DoubleFormnat',
191
223
  'HTTPStatusCode',
192
224
  'JWTAlgorithms',
@@ -198,6 +230,20 @@ __all__ = [
198
230
  'InvalidTemplateError',
199
231
  'RouteNotFoundError',
200
232
  'RouterError',
233
+ 'TestClient',
234
+ 'warn_deprecated',
235
+ 'OpenApiProcessor',
236
+ 'OpenAPIConfig',
237
+ 'OpenAPIBuilder',
238
+ 'HTTPBearer',
239
+ 'HTTPBasic',
240
+ 'APIKeyHeader',
241
+ 'APIKeyQuery',
242
+ 'APIKeyCookie',
243
+ 'AuthContext',
244
+ 'SecurityEnforcer',
245
+ 'SecurityError',
246
+ 'ForbiddenError',
201
247
  'Icon',
202
248
  'Style',
203
249
  'Script',
@@ -531,7 +531,7 @@ class CommandFunctions:
531
531
  config.save()
532
532
 
533
533
  def install_requirements(self, path: str):
534
- if not Path.exists(path):
534
+ if not Path(path).exists():
535
535
  raise FileNotFoundError('This project does not have a config file')
536
536
 
537
537
  requires: list[str] = config.get('requirements', 'packages')
@@ -1,7 +1,14 @@
1
1
  from pyweber.core.element import Element
2
- from pyweber.components.input import Input
2
+ from pyweber.components.input import Input, InputHidden
3
3
  from typing import Callable, Literal
4
4
 
5
+ from pyweber.utils.security import (
6
+ CSRF_FORM_FIELD,
7
+ csrf_enabled,
8
+ generate_csrf_token,
9
+ )
10
+
11
+
5
12
  class Form(Element):
6
13
  def __init__(
7
14
  self,
@@ -24,6 +31,7 @@ class Form(Element):
24
31
  novalidate: bool = True,
25
32
  autocapitalize: bool = False,
26
33
  spellcheck: bool = False,
34
+ csrf_token: str = None,
27
35
  **kwargs
28
36
  ):
29
37
  super().__init__(tag='form', classes=classes, style=style, id=id, childs=childs, **kwargs)
@@ -43,16 +51,22 @@ class Form(Element):
43
51
  self.onsubmit = onsubmit
44
52
  self.onreset = onreset
45
53
  self.attrs = {}
46
-
54
+
55
+ if csrf_enabled() and method and str(method).upper() == 'POST':
56
+ token = csrf_token or generate_csrf_token()
57
+ self.childs.append(
58
+ InputHidden(name=CSRF_FORM_FIELD, value=token, id=f'{id or "form"}_csrf')
59
+ )
60
+
47
61
  @property
48
62
  def onclick(self):
49
63
  return self.events.__dict__.get('onclick', None)
50
-
64
+
51
65
  @onclick.setter
52
66
  def onclick(self, value: Callable):
53
67
  if value and callable(value):
54
68
  setattr(self.events, 'onclick', value)
55
-
69
+
56
70
  @property
57
71
  def onsubmit(self):
58
72
  return self.events.__dict__.get('onsubmit', None)
@@ -61,7 +75,7 @@ class Form(Element):
61
75
  def onsubmit(self, value: Callable):
62
76
  if value and callable(value):
63
77
  setattr(self.events, 'onsubmit', value)
64
-
78
+
65
79
  @property
66
80
  def onreset(self):
67
81
  return self.events.__dict__.get('onreset', None)
@@ -70,16 +84,16 @@ class Form(Element):
70
84
  def onreset(self, value):
71
85
  if value and callable(value):
72
86
  setattr(self.events, 'onreset', value)
73
-
87
+
74
88
  @property
75
89
  def attrs(self):
76
90
  return self.__attrs
77
-
91
+
78
92
  @attrs.setter
79
93
  def attrs(self, value: dict[str, str]):
80
94
  if value:
81
95
  raise AttributeError('Subscript not allowed to this attribute')
82
-
96
+
83
97
  self.__attrs = {}
84
98
  for key, value in self.__dict__.items():
85
99
  if key in ['method', 'name', 'action', 'autocomplete', 'spellcheck', 'autocapitalize', 'novalidate', 'target', 'enctype', 'accept_charset', 'rel', 'tabindex'] and value:
@@ -89,4 +103,4 @@ class Form(Element):
89
103
  if key == 'accept_charset':
90
104
  self.__attrs['accept-charset'] = value
91
105
  else:
92
- self.__attrs[key] = value
106
+ self.__attrs[key] = value
@@ -95,7 +95,7 @@ class TextArea(Element):
95
95
  oninput: Callable = None,
96
96
  onclick: Callable = None,
97
97
  onselect: Callable = None,
98
- sanitize: bool = False
98
+ sanitize: bool = True
99
99
  ):
100
100
  super().__init__(tag='textarea')
101
101
  self.sanitize = sanitize
@@ -200,4 +200,4 @@ class TextArea(Element):
200
200
  elif key == 'spellcheck':
201
201
  self.__attrs[key] = 'true' if value else 'false'
202
202
  else:
203
- self.__attrs[key] = str(value)
203
+ self.__attrs[key] = str(value)
@@ -120,7 +120,7 @@ class InputColor(Input):
120
120
  onchange = None,
121
121
  oninput = None,
122
122
  onclick = None,
123
- sanitize: bool = False
123
+ sanitize: bool = True
124
124
  ):
125
125
  super().__init__('color', name, id, form, tabindex, classes, style, disabled, autofocus, required, onfocus, onblur, onchange, oninput, onclick,)
126
126
  self.sanitize = sanitize
@@ -151,7 +151,7 @@ class InputText(Input):
151
151
  onchange = None,
152
152
  oninput = None,
153
153
  onclick = None,
154
- sanitize: bool = False
154
+ sanitize: bool = True
155
155
  ):
156
156
  super().__init__('text', name, id, form, tabindex, classes, style, disabled, autofocus, required, onfocus, onblur, onchange, oninput, onclick,)
157
157
  self.sanitize = sanitize
@@ -200,7 +200,7 @@ class InputPassword(Input):
200
200
  onchange: Callable = None,
201
201
  oninput: Callable = None,
202
202
  onclick: Callable = None,
203
- sanitize: bool = False
203
+ sanitize: bool = True
204
204
  ):
205
205
  super().__init__('password', name, id, form, tabindex, classes, style, disabled, autofocus, required, onfocus, onblur, onchange, oninput, onclick,)
206
206
  self.sanitize = sanitize
@@ -259,7 +259,7 @@ class InputEmail(Input):
259
259
  onchange: Callable = None,
260
260
  oninput: Callable = None,
261
261
  onclick: Callable = None,
262
- sanitize: bool = False
262
+ sanitize: bool = True
263
263
  ):
264
264
  super().__init__('email', name, id, form, tabindex, classes, style, disabled, autofocus, required, onfocus, onblur, onchange, oninput, onclick,)
265
265
  self.sanitize = sanitize
@@ -307,7 +307,7 @@ class InputNumber(Input):
307
307
  onchange: Callable = None,
308
308
  oninput: Callable = None,
309
309
  onclick: Callable = None,
310
- sanitize: bool = False
310
+ sanitize: bool = True
311
311
  ):
312
312
  super().__init__('number', name, id, form, tabindex, classes, style, disabled, autofocus, required, onfocus, onblur, onchange, oninput, onclick,)
313
313
  self.sanitize = sanitize
@@ -581,7 +581,7 @@ class InputSearch(Input):
581
581
  onchange: Callable = None,
582
582
  oninput: Callable = None,
583
583
  onclick: Callable = None,
584
- sanitize: bool = False
584
+ sanitize: bool = True
585
585
  ):
586
586
  super().__init__('search', name, id, form, tabindex, classes, style, disabled, autofocus, required, onfocus, onblur, onchange, oninput, onclick,)
587
587
  self.sanitize = sanitize
@@ -630,7 +630,7 @@ class InputTel(Input):
630
630
  onchange: Callable = None,
631
631
  oninput: Callable = None,
632
632
  onclick: Callable = None,
633
- sanitize: bool = False
633
+ sanitize: bool = True
634
634
  ):
635
635
  super().__init__('tel', name, id, form, tabindex, classes, style, disabled, autofocus, required, onfocus, onblur, onchange, oninput, onclick,)
636
636
  self.sanitize = sanitize
@@ -679,7 +679,7 @@ class InputUrl(Input):
679
679
  onchange: Callable = None,
680
680
  oninput: Callable = None,
681
681
  onclick: Callable = None,
682
- sanitize: bool = False
682
+ sanitize: bool = True
683
683
  ):
684
684
  super().__init__('url', name, id, form, tabindex, classes, style, disabled, autofocus, required, onfocus, onblur, onchange, oninput, onclick,)
685
685
  self.sanitize = sanitize
@@ -717,7 +717,7 @@ class InputHidden(Input):
717
717
  onchange: Callable = None,
718
718
  oninput: Callable = None,
719
719
  onclick: Callable = None,
720
- sanitize: bool = False
720
+ sanitize: bool = True
721
721
  ):
722
722
  super().__init__('hidden', name, id, form, None, classes, style, None, None, None, onfocus, onblur, onchange, oninput, onclick,)
723
723
  self.sanitize = sanitize
@@ -745,7 +745,7 @@ class InputSubmit(Input):
745
745
  onchange: Callable = None,
746
746
  oninput: Callable = None,
747
747
  onclick: Callable = None,
748
- sanitize: bool = False
748
+ sanitize: bool = True
749
749
  ):
750
750
  super().__init__('submit', name, id, form, tabindex, classes, style, disabled, autofocus, None, onfocus, onblur, onchange, oninput, onclick,)
751
751
  self.sanitize = sanitize
@@ -783,7 +783,7 @@ class InputButton(Input):
783
783
  onchange: Callable = None,
784
784
  oninput: Callable = None,
785
785
  onclick: Callable = None,
786
- sanitize: bool = False
786
+ sanitize: bool = True
787
787
  ):
788
788
  super().__init__('button', name, id, form, tabindex, classes, style, disabled, autofocus, None, onfocus, onblur, onchange, oninput, onclick,)
789
789
  self.sanitize = sanitize
@@ -807,8 +807,8 @@ class InputReset(Input):
807
807
  onchange: Callable = None,
808
808
  oninput: Callable = None,
809
809
  onclick: Callable = None,
810
- sanitize: bool = False
810
+ sanitize: bool = True
811
811
  ):
812
812
  super().__init__('reset', name, id, form, tabindex, classes, style, disabled, autofocus, None, onfocus, onblur, onchange, oninput, onclick,)
813
813
  self.sanitize = sanitize
814
- self.value = value
814
+ self.value = value
@@ -11,10 +11,13 @@ from typing import Union, Any
11
11
  from concurrent.futures import ThreadPoolExecutor
12
12
 
13
13
  from pyweber.pyweber.pyweber import Pyweber
14
+ from pyweber.utils.async_utils import async_timeout
14
15
  from pyweber.utils.utils import Colors, PrintLine
16
+ from pyweber.utils.security import get_max_body_size
15
17
  from pyweber.models.request import Request, ClientInfo
16
18
  from pyweber.connection.websocket import WebsocketUpgrade, WebsocketServer
17
19
  from pyweber.connection.selector import IOSelector
20
+ from pyweber.utils.types import ContentTypes, HTTPStatusCode
18
21
 
19
22
  class HttpServer:
20
23
  def __init__(self, *args, **kwargs):
@@ -47,10 +50,32 @@ class HttpServer:
47
50
  loop = asyncio.get_event_loop()
48
51
  return await loop.run_in_executor(None, client.recv, length)
49
52
 
53
+ def _parse_cookies(self, header_text: str) -> dict[str, str]:
54
+ cookies = {}
55
+ match = re.search(r'(?im)^Cookie:\s*(.+)$', header_text)
56
+ if not match:
57
+ return cookies
58
+ for part in match.group(1).split(';'):
59
+ if '=' not in part:
60
+ continue
61
+ name, value = part.split('=', 1)
62
+ cookies[name.strip()] = value.strip()
63
+ return cookies
64
+
65
+ async def _send_simple_error(self, client, code: int, body: bytes, content_type: str = 'text/plain'):
66
+ reason = HTTPStatusCode.search_by_code(code) if hasattr(HTTPStatusCode, 'search_by_code') else f'{code}'
67
+ response = (
68
+ f'HTTP/1.1 {reason}\r\n'
69
+ f'Content-Type: {content_type}; charset=UTF-8\r\n'
70
+ f'Content-Length: {len(body)}\r\n'
71
+ 'Connection: close\r\n\r\n'
72
+ ).encode() + body
73
+ await self.send_data(client, response)
74
+
50
75
  async def process_request(self, client: Union[socket.socket, ssl.SSLSocket]):
51
76
  try:
52
77
  request_data = bytearray()
53
- async with asyncio.timeout(self.timeout):
78
+ async with async_timeout(self.timeout):
54
79
  while b'\r\n\r\n' not in request_data:
55
80
  chunk = await self.read_data(client, 4096)
56
81
  if not chunk: break
@@ -64,14 +89,30 @@ class HttpServer:
64
89
  if content_match:
65
90
  content_length = int(content_match.group(1))
66
91
 
92
+ max_body = get_max_body_size()
93
+ if content_length > max_body:
94
+ await self._send_simple_error(
95
+ client,
96
+ 413,
97
+ b'Payload Too Large',
98
+ )
99
+ return None, None
100
+
67
101
  body = bytearray(body_start)
68
- async with asyncio.timeout(self.timeout):
102
+ if len(body) > max_body:
103
+ await self._send_simple_error(client, 413, b'Payload Too Large')
104
+ return None, None
105
+
106
+ async with async_timeout(self.timeout):
69
107
  while len(body) < content_length:
70
- remaining = content_length - len(body)
108
+ remaining = min(content_length - len(body), max_body - len(body) + 1)
71
109
  chunk = await self.read_data(client, remaining)
72
110
  if not chunk:
73
111
  break
74
112
  body.extend(chunk)
113
+ if len(body) > max_body:
114
+ await self._send_simple_error(client, 413, b'Payload Too Large')
115
+ return None, None
75
116
 
76
117
  return header_bytes, bytes(body)
77
118
 
@@ -84,7 +125,7 @@ class HttpServer:
84
125
  try:
85
126
  headers, body = await self.process_request(client)
86
127
 
87
- if not headers:
128
+ if headers is None or not headers:
88
129
  return
89
130
 
90
131
  client_details = client.getpeername()
@@ -114,7 +155,7 @@ class HttpServer:
114
155
  try:
115
156
  headers, body = await self.process_request(client)
116
157
 
117
- if not headers:
158
+ if headers is None or not headers:
118
159
  return
119
160
 
120
161
  request = Request(
@@ -126,7 +167,7 @@ class HttpServer:
126
167
  upgrade = WebsocketUpgrade(headers=headers)
127
168
  await self.send_data(client, upgrade.upgrade_response.encode('utf-8'))
128
169
 
129
- ws_connection = WebsocketServer(client)
170
+ ws_connection = WebsocketServer(client, cookies=dict(request.cookies))
130
171
  await self.app.ws_server.connect_wsgi(ws_connection=ws_connection)
131
172
 
132
173
  except TypeError:
@@ -185,15 +226,24 @@ class HttpServer:
185
226
  if match:
186
227
  content_length = int(match.group(1))
187
228
 
229
+ max_body = get_max_body_size()
230
+ if content_length > max_body:
231
+ await self._send_simple_error(client, 413, b'Payload Too Large')
232
+ return
233
+
188
234
  body = bytearray(body_start)
189
- async with asyncio.timeout(self.timeout):
235
+ async with async_timeout(self.timeout):
190
236
  while len(body) < content_length:
237
+ remaining = min(content_length - len(body), max_body - len(body) + 1)
191
238
  chunk = await asyncio.get_event_loop().run_in_executor(
192
- None, client.recv, content_length - len(body)
239
+ None, client.recv, remaining
193
240
  )
194
241
  if not chunk:
195
242
  break
196
243
  body.extend(chunk)
244
+ if len(body) > max_body:
245
+ await self._send_simple_error(client, 413, b'Payload Too Large')
246
+ return
197
247
 
198
248
  client_details = client.getpeername()
199
249
  client_info = ClientInfo(
@@ -221,11 +271,13 @@ class HttpServer:
221
271
  """WebSocket com headers já lidos."""
222
272
  try:
223
273
  header_bytes = raw.partition(b'\r\n\r\n')[0]
274
+ header_text = header_bytes.decode('iso-8859-1')
275
+ cookies = self._parse_cookies(header_text)
224
276
 
225
277
  upgrade = WebsocketUpgrade(headers=header_bytes)
226
278
  client.sendall(upgrade.upgrade_response.encode('utf-8'))
227
279
 
228
- ws_connection = WebsocketServer(client)
280
+ ws_connection = WebsocketServer(client, cookies=cookies)
229
281
  await self.app.ws_server.connect_wsgi(ws_connection=ws_connection)
230
282
 
231
283
  except TypeError:
@@ -12,6 +12,11 @@ from time import time
12
12
 
13
13
  from pyweber.models.ws_message import wsMessage
14
14
  from pyweber.utils.utils import PrintLine
15
+ from pyweber.utils.security import (
16
+ SESSION_COOKIE_NAME,
17
+ generate_session_id,
18
+ unsign_value,
19
+ )
15
20
  from pyweber.connection.session import sessions, Session
16
21
  from pyweber.models.template_diff import TemplateDiff
17
22
  from pyweber.models.task_manager import TaskManager
@@ -85,9 +90,10 @@ class WebsocketUpgrade:
85
90
 
86
91
  class WebsocketServer:
87
92
 
88
- def __init__(self, client: Union[socket.socket, ssl.SSLSocket]):
93
+ def __init__(self, client: Union[socket.socket, ssl.SSLSocket], cookies: dict[str, str] | None = None):
89
94
  self.id = None
90
95
  self.client = client
96
+ self.cookies = cookies or {}
91
97
  self.__all_message: bytes = b''
92
98
  self.__messages: list[bytes] = []
93
99
 
@@ -491,8 +497,15 @@ class WebsocketManager(BaseWebsockets):
491
497
  group, route = self.app.get_group_and_route(route=route)
492
498
  self.app.update_route(route=route, group=group, template=new_template)
493
499
 
494
- def get_session_id(self, session_id: str = None):
495
- return session_id or str(uuid4())
500
+ def get_session_id(self, session_id: str = None, cookies: dict[str, str] | None = None):
501
+ """Resolve session id from signed HttpOnly cookie; never trust client id alone."""
502
+ cookie_jar = cookies or {}
503
+ signed = cookie_jar.get(SESSION_COOKIE_NAME)
504
+ cookie_sid = unsign_value(signed) if signed else None
505
+ if cookie_sid:
506
+ return cookie_sid
507
+ # Ignore client-supplied session_id when cookie is missing/invalid
508
+ return generate_session_id()
496
509
 
497
510
  async def get_sync_template(self, message: wsMessage):
498
511
  assert isinstance(message, wsMessage)
@@ -502,11 +515,6 @@ class WebsocketManager(BaseWebsockets):
502
515
  session.old_template = sync_template.clone()
503
516
  return sync_template
504
517
 
505
- def update_app_template(self, new_template: 'Template', route: str):
506
- if route in self.app.list_routes:
507
- group, route = self.app.get_group_and_route(route=route)
508
- self.app.update_route(route=route, group=group, template=new_template)
509
-
510
518
  def add_session(self, session_id: str, template: 'Template', window: 'Window', route: str):
511
519
  sessions.add_session(
512
520
  session_id=session_id,
@@ -593,7 +601,10 @@ class WebsocketManager(BaseWebsockets):
593
601
  )
594
602
 
595
603
  if is_new_session:
596
- ws_server.id = self.get_session_id(session_id=message.session_id)
604
+ ws_server.id = self.get_session_id(
605
+ session_id=message.session_id,
606
+ cookies=getattr(ws_server, 'cookies', {}) or {},
607
+ )
597
608
  self.ws_connections[ws_server.id] = ws_server
598
609
 
599
610
  self.add_session(
@@ -633,8 +644,9 @@ class WebsocketManager(BaseWebsockets):
633
644
  )
634
645
  await self.message_handler(message=message)
635
646
 
636
- async def ws_handler_asgi(self, receive: Callable, send: Callable):
647
+ async def ws_handler_asgi(self, receive: Callable, send: Callable, cookies: dict[str, str] | None = None):
637
648
  ws_connection: str = None
649
+ handshake_cookies = cookies or {}
638
650
 
639
651
  try:
640
652
  while True:
@@ -665,7 +677,10 @@ class WebsocketManager(BaseWebsockets):
665
677
  )
666
678
 
667
679
  if is_new_session:
668
- ws_connection = self.get_session_id(session_id=message.session_id)
680
+ ws_connection = self.get_session_id(
681
+ session_id=message.session_id,
682
+ cookies=handshake_cookies,
683
+ )
669
684
 
670
685
  self.add_session(
671
686
  session_id=ws_connection,
@@ -720,7 +735,17 @@ class WebsocketManager(BaseWebsockets):
720
735
 
721
736
  async def __call__(self, scope, receive, send):
722
737
  assert scope.get('type', None) == 'websocket'
723
- await self.ws_handler_asgi(receive=receive, send=send)
738
+ headers = {
739
+ (k.decode() if isinstance(k, bytes) else k).lower():
740
+ (v.decode() if isinstance(v, bytes) else v)
741
+ for k, v in scope.get('headers', [])
742
+ }
743
+ cookie_header = headers.get('cookie', '')
744
+ cookies = {
745
+ part.split('=', 1)[0].strip(): part.split('=', 1)[-1].strip()
746
+ for part in cookie_header.split(';') if part.strip() and '=' in part
747
+ }
748
+ await self.ws_handler_asgi(receive=receive, send=send, cookies=cookies)
724
749
 
725
750
  for id, handler in self.ws_connections.items():
726
751
  if handler == send:
@@ -6,6 +6,11 @@ from .events import (
6
6
  WindowEvents,
7
7
  TemplateEvents
8
8
  )
9
+ from .html_parser import (
10
+ get_html_parser_backend,
11
+ set_html_parser_backend,
12
+ reset_html_parser_backend,
13
+ )
9
14
 
10
15
  __all__ = [
11
16
  'Element',
@@ -13,5 +18,8 @@ __all__ = [
13
18
  'window',
14
19
  'EventHandler',
15
20
  'WindowEvents',
16
- 'TemplateEvents'
21
+ 'TemplateEvents',
22
+ 'get_html_parser_backend',
23
+ 'set_html_parser_backend',
24
+ 'reset_html_parser_backend',
17
25
  ]