pyweber 1.5.0.dev3__tar.gz → 1.6.0__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 (127) hide show
  1. {pyweber-1.5.0.dev3/pyweber.egg-info → pyweber-1.6.0}/PKG-INFO +26 -4
  2. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/README.md +11 -3
  3. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyproject.toml +9 -1
  4. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/__init__.py +54 -3
  5. pyweber-1.6.0/pyweber/auth/__init__.py +44 -0
  6. pyweber-1.6.0/pyweber/auth/decorators.py +201 -0
  7. pyweber-1.6.0/pyweber/auth/passwords.py +52 -0
  8. pyweber-1.6.0/pyweber/auth/rbac.py +184 -0
  9. pyweber-1.6.0/pyweber/auth/session.py +146 -0
  10. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/cli/commands.py +9 -0
  11. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/components/__init__.py +0 -2
  12. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/components/form.py +4 -2
  13. pyweber-1.6.0/pyweber/components/input.py +663 -0
  14. pyweber-1.6.0/pyweber/connection/session.py +134 -0
  15. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/connection/websocket.py +283 -123
  16. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/core/element.py +66 -8
  17. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/core/events.py +70 -7
  18. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/core/template.py +77 -9
  19. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/core/window.py +60 -27
  20. pyweber-1.6.0/pyweber/db/__init__.py +133 -0
  21. pyweber-1.6.0/pyweber/db/cli.py +254 -0
  22. pyweber-1.6.0/pyweber/db/config.py +125 -0
  23. pyweber-1.6.0/pyweber/db/engine.py +64 -0
  24. pyweber-1.6.0/pyweber/db/model.py +44 -0
  25. pyweber-1.6.0/pyweber/db/query.py +26 -0
  26. pyweber-1.6.0/pyweber/db/session.py +49 -0
  27. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/models/context.py +15 -1
  28. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/models/cookies.py +18 -0
  29. pyweber-1.6.0/pyweber/models/dom_merge.py +92 -0
  30. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/models/element.py +19 -12
  31. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/models/field_storage.py +13 -4
  32. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/models/handoff.py +9 -2
  33. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/models/openapi.py +1 -0
  34. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/models/request.py +38 -4
  35. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/models/routes.py +62 -4
  36. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/models/ws_message.py +28 -4
  37. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/pyweber/pyweber.py +80 -371
  38. pyweber-1.6.0/pyweber/services/__init__.py +13 -0
  39. pyweber-1.6.0/pyweber/services/openapi_setup.py +73 -0
  40. pyweber-1.6.0/pyweber/services/response_pipeline.py +239 -0
  41. pyweber-1.6.0/pyweber/services/static_files.py +68 -0
  42. pyweber-1.6.0/pyweber/services/template_service.py +158 -0
  43. pyweber-1.6.0/pyweber/session_store/__init__.py +244 -0
  44. pyweber-1.6.0/pyweber/static/config.toml +54 -0
  45. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/static/js.js +78 -4
  46. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/utils/security.py +121 -0
  47. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/utils/types.py +8 -0
  48. {pyweber-1.5.0.dev3 → pyweber-1.6.0/pyweber.egg-info}/PKG-INFO +26 -4
  49. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber.egg-info/SOURCES.txt +19 -0
  50. pyweber-1.6.0/pyweber.egg-info/requires.txt +31 -0
  51. pyweber-1.5.0.dev3/pyweber/components/input.py +0 -814
  52. pyweber-1.5.0.dev3/pyweber/connection/session.py +0 -50
  53. pyweber-1.5.0.dev3/pyweber/static/config.toml +0 -45
  54. pyweber-1.5.0.dev3/pyweber.egg-info/requires.txt +0 -12
  55. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/LICENSE +0 -0
  56. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/MANIFEST.in +0 -0
  57. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/NOTICE +0 -0
  58. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/admin/index.html +0 -0
  59. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/admin/src/script.js +0 -0
  60. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/admin/src/style.css +0 -0
  61. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/cli/__init__.py +0 -0
  62. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/components/general.py +0 -0
  63. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/config/__init__.py +0 -0
  64. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/config/config.py +0 -0
  65. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/connection/__init__.py +0 -0
  66. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/connection/http.py +0 -0
  67. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/connection/reload.py +0 -0
  68. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/connection/selector.py +0 -0
  69. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/core/__init__.py +0 -0
  70. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/core/html_parser.py +0 -0
  71. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/models/__init__.py +0 -0
  72. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/models/create_app.py +0 -0
  73. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/models/error_pages.py +0 -0
  74. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/models/field.py +0 -0
  75. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/models/file.py +0 -0
  76. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/models/file_stream.py +0 -0
  77. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/models/headers.py +0 -0
  78. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/models/middleware.py +0 -0
  79. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/models/rate_limit.py +0 -0
  80. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/models/response.py +0 -0
  81. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/models/run.py +0 -0
  82. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/models/security.py +0 -0
  83. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/models/stream_stats.py +0 -0
  84. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/models/strem_stats.py +0 -0
  85. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/models/task_manager.py +0 -0
  86. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/models/template_diff.py +0 -0
  87. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/pyweber/__init__.py +0 -0
  88. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/static/.gitignore +0 -0
  89. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/static/css.css +0 -0
  90. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/static/docs.html +0 -0
  91. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/static/favicon/android-chrome-192x192.png +0 -0
  92. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/static/favicon/android-chrome-512x512.png +0 -0
  93. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/static/favicon/apple-touch-icon.png +0 -0
  94. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/static/favicon/favicon-16x16.png +0 -0
  95. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/static/favicon/favicon-32x32.png +0 -0
  96. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/static/favicon/favicon.ico +0 -0
  97. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/static/favicon/pyweber.png +0 -0
  98. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/static/favicon/site.webmanifest +0 -0
  99. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/static/handlers.js +0 -0
  100. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/static/html.html +0 -0
  101. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/static/html401.html +0 -0
  102. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/static/html404.html +0 -0
  103. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/static/html500.html +0 -0
  104. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/static/loading.html +0 -0
  105. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/static/main.py +0 -0
  106. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/static/pyweber.css +0 -0
  107. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/static/update.py +0 -0
  108. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/testing/__init__.py +0 -0
  109. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/testing/client.py +0 -0
  110. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/utils/__init__.py +0 -0
  111. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/utils/async_utils.py +0 -0
  112. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/utils/deprecation.py +0 -0
  113. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/utils/exceptions.py +0 -0
  114. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/utils/icons.py +0 -0
  115. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/utils/loads.py +0 -0
  116. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/utils/mime.py +0 -0
  117. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber/utils/utils.py +0 -0
  118. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber.egg-info/dependency_links.txt +0 -0
  119. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber.egg-info/entry_points.txt +0 -0
  120. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/pyweber.egg-info/top_level.txt +0 -0
  121. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/setup.cfg +0 -0
  122. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/tests/test_config.py +0 -0
  123. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/tests/test_context.py +0 -0
  124. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/tests/test_cookies.py +0 -0
  125. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/tests/test_public_api.py +0 -0
  126. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/tests/test_response.py +0 -0
  127. {pyweber-1.5.0.dev3 → pyweber-1.6.0}/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.dev3
3
+ Version: 1.6.0
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
@@ -10,6 +10,7 @@ Project-URL: Tracker, https://github.com/pyweber/pyweber/issues
10
10
  Project-URL: PyPI, https://pypi.org/project/pyweber/
11
11
  Project-URL: YouTube, https://youtube.com/@devpythonMZ
12
12
  Project-URL: Examples, https://github.com/pyweber-examples
13
+ Project-URL: Security, https://pyweber.dev/guides/supported-versions/
13
14
  Requires-Python: >=3.10
14
15
  Description-Content-Type: text/markdown
15
16
  License-File: LICENSE
@@ -24,6 +25,19 @@ Requires-Dist: pytest-cov; extra == "dev"
24
25
  Requires-Dist: pytest-asyncio; extra == "dev"
25
26
  Provides-Extra: fast-html
26
27
  Requires-Dist: lxml; extra == "fast-html"
28
+ Provides-Extra: db
29
+ Requires-Dist: sqlalchemy[asyncio]>=2.0; extra == "db"
30
+ Requires-Dist: alembic>=1.13; extra == "db"
31
+ Provides-Extra: db-sqlite
32
+ Requires-Dist: aiosqlite>=0.20; extra == "db-sqlite"
33
+ Provides-Extra: db-pg
34
+ Requires-Dist: asyncpg>=0.29; extra == "db-pg"
35
+ Provides-Extra: db-mysql
36
+ Requires-Dist: aiomysql>=0.2; extra == "db-mysql"
37
+ Provides-Extra: db-mssql
38
+ Requires-Dist: aioodbc>=0.5; extra == "db-mssql"
39
+ Provides-Extra: redis
40
+ Requires-Dist: redis[hiredis]>=8.1.0; extra == "redis"
27
41
  Dynamic: license-file
28
42
 
29
43
  # Pyweber Framework
@@ -42,17 +56,25 @@ Pyweber is a lightweight Python web framework designed to create dynamic, reacti
42
56
  - **Intuitive DOM Manipulation**: Query and modify elements with familiar selectors
43
57
  - **Event-Driven Programming**: Handle user interactions with Python event handlers
44
58
  - **WebSocket Integration**: Real-time communication between client and server
45
- - **Template Handoff (1.3.0)**: WebSocket sessions reuse the HTTP-rendered template — handlers are not executed twice on connect
59
+ - **Template Handoff (1.3.0+)**: WebSocket sessions reuse the HTTP-rendered template — handlers are not executed twice on connect
60
+ - **Identity-preserving sync (1.6.0+)**: `self` / subclass element refs stay valid after WS connect
61
+ - **Optional ORM & Redis sessions (1.6.0+)**: `pyweber[db]`, `pyweber[redis]`
62
+ - **Auth & RBAC (1.5+)**: signed login cookie, roles/permissions
46
63
  - **Minimal Configuration**: Get started quickly with sensible defaults
47
64
 
48
65
  ## Quick Start
49
66
 
50
67
  ### Installation
51
68
 
69
+ !!! danger "Avoid ≤ 1.3.1"
70
+ Older lines are **not recommended** for production. Use a current release:
71
+
52
72
  ```bash
53
- pip install pyweber
73
+ pip install -U 'pyweber>=1.6'
54
74
  ```
55
75
 
76
+ See [Supported versions](https://pyweber.dev/guides/supported-versions/) (docs) for yank / security notes. `pyproject.toml` cannot warn when someone pins an old wheel — that requires [PyPI yanking](https://docs.pypi.org/project-management/yanking/).
77
+
56
78
  ### Create a Project
57
79
 
58
80
  ```bash
@@ -249,7 +271,7 @@ pyweber add-section --section-name database
249
271
  pyweber --update
250
272
  ```
251
273
 
252
- Visit [Pyweber Docs](https://pyweber.dev/) for complete documentation. See [CHANGELOG.md](CHANGELOG.md) for release notes (latest: **1.3.0** — Template Handoff, 405 fixes, recursion tracking).
274
+ Visit [Pyweber Docs](https://pyweber.dev/) for complete documentation. See [CHANGELOG.md](CHANGELOG.md) for release notes (latest: **1.6.0** — optional ORM/Redis, identity-preserving WS sync, reactive event fixes).
253
275
 
254
276
  ## License
255
277
 
@@ -14,17 +14,25 @@ Pyweber is a lightweight Python web framework designed to create dynamic, reacti
14
14
  - **Intuitive DOM Manipulation**: Query and modify elements with familiar selectors
15
15
  - **Event-Driven Programming**: Handle user interactions with Python event handlers
16
16
  - **WebSocket Integration**: Real-time communication between client and server
17
- - **Template Handoff (1.3.0)**: WebSocket sessions reuse the HTTP-rendered template — handlers are not executed twice on connect
17
+ - **Template Handoff (1.3.0+)**: WebSocket sessions reuse the HTTP-rendered template — handlers are not executed twice on connect
18
+ - **Identity-preserving sync (1.6.0+)**: `self` / subclass element refs stay valid after WS connect
19
+ - **Optional ORM & Redis sessions (1.6.0+)**: `pyweber[db]`, `pyweber[redis]`
20
+ - **Auth & RBAC (1.5+)**: signed login cookie, roles/permissions
18
21
  - **Minimal Configuration**: Get started quickly with sensible defaults
19
22
 
20
23
  ## Quick Start
21
24
 
22
25
  ### Installation
23
26
 
27
+ !!! danger "Avoid ≤ 1.3.1"
28
+ Older lines are **not recommended** for production. Use a current release:
29
+
24
30
  ```bash
25
- pip install pyweber
31
+ pip install -U 'pyweber>=1.6'
26
32
  ```
27
33
 
34
+ See [Supported versions](https://pyweber.dev/guides/supported-versions/) (docs) for yank / security notes. `pyproject.toml` cannot warn when someone pins an old wheel — that requires [PyPI yanking](https://docs.pypi.org/project-management/yanking/).
35
+
28
36
  ### Create a Project
29
37
 
30
38
  ```bash
@@ -221,7 +229,7 @@ pyweber add-section --section-name database
221
229
  pyweber --update
222
230
  ```
223
231
 
224
- Visit [Pyweber Docs](https://pyweber.dev/) for complete documentation. See [CHANGELOG.md](CHANGELOG.md) for release notes (latest: **1.3.0** — Template Handoff, 405 fixes, recursion tracking).
232
+ Visit [Pyweber Docs](https://pyweber.dev/) for complete documentation. See [CHANGELOG.md](CHANGELOG.md) for release notes (latest: **1.6.0** — optional ORM/Redis, identity-preserving WS sync, reactive event fixes).
225
233
 
226
234
  ## License
227
235
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyweber"
7
- version = "1.5.0.dev3"
7
+ version = "1.6.0"
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" }]
@@ -18,6 +18,8 @@ Tracker = "https://github.com/pyweber/pyweber/issues"
18
18
  PyPI = "https://pypi.org/project/pyweber/"
19
19
  YouTube = "https://youtube.com/@devpythonMZ"
20
20
  Examples = "https://github.com/pyweber-examples"
21
+ # Supported versions / security policy (≤1.3.1 not recommended; yank on PyPI — not via this file)
22
+ Security = "https://pyweber.dev/guides/supported-versions/"
21
23
 
22
24
  [project.scripts]
23
25
  pyweber = "pyweber.cli.commands:app"
@@ -29,6 +31,12 @@ include = ["pyweber*"]
29
31
  [project.optional-dependencies]
30
32
  dev = ["pytest", "pytest-cov", "pytest-asyncio"]
31
33
  fast-html = ["lxml"]
34
+ db = ["sqlalchemy[asyncio]>=2.0", "alembic>=1.13"]
35
+ db-sqlite = ["aiosqlite>=0.20"]
36
+ db-pg = ["asyncpg>=0.29"]
37
+ db-mysql = ["aiomysql>=0.2"]
38
+ db-mssql = ["aioodbc>=0.5"]
39
+ redis = ["redis[hiredis]>=8.1.0"]
32
40
 
33
41
  [tool.setuptools.package-data]
34
42
  pyweber = ["static/**/*", "admin/**/*"]
@@ -76,17 +76,35 @@ from .utils.security import (
76
76
  sign_value,
77
77
  unsign_value,
78
78
  generate_csrf_token,
79
+ get_csrf_token,
79
80
  )
80
81
  from .utils.icons import Icons
81
82
  from .utils.deprecation import warn_deprecated
82
83
  from .testing import TestClient
84
+ from .auth import (
85
+ login_required,
86
+ login_user,
87
+ logout_user,
88
+ current_user,
89
+ get_user_id,
90
+ hash_password,
91
+ check_password,
92
+ permission_required,
93
+ role_required,
94
+ register_roles,
95
+ clear_roles,
96
+ define_role,
97
+ has_role,
98
+ has_all_roles,
99
+ has_permission,
100
+ user_permissions,
101
+ )
83
102
 
84
103
  from .utils.types import (
85
104
  ContentTypes,
86
105
  EventType,
87
106
  HTMLTag,
88
107
  HTTPStatusCode,
89
- JWTAlgorithms,
90
108
  NonSelfClosingHTMLTags,
91
109
  WebSocketStatusCode,
92
110
  WindowEventType,
@@ -129,7 +147,6 @@ from .components.input import (
129
147
  Input,
130
148
  InputButton,
131
149
  InputCheckbox,
132
- InputCheckbox,
133
150
  InputColor,
134
151
  InputDate,
135
152
  InputEmail,
@@ -200,6 +217,7 @@ __all__ = [
200
217
  'sign_value',
201
218
  'unsign_value',
202
219
  'generate_csrf_token',
220
+ 'get_csrf_token',
203
221
  'ContentTypes',
204
222
  'EventType',
205
223
  'HTMLTag',
@@ -244,6 +262,22 @@ __all__ = [
244
262
  'SecurityEnforcer',
245
263
  'SecurityError',
246
264
  'ForbiddenError',
265
+ 'login_required',
266
+ 'login_user',
267
+ 'logout_user',
268
+ 'current_user',
269
+ 'get_user_id',
270
+ 'hash_password',
271
+ 'check_password',
272
+ 'permission_required',
273
+ 'role_required',
274
+ 'register_roles',
275
+ 'clear_roles',
276
+ 'define_role',
277
+ 'has_role',
278
+ 'has_all_roles',
279
+ 'has_permission',
280
+ 'user_permissions',
247
281
  'Icon',
248
282
  'Style',
249
283
  'Script',
@@ -251,7 +285,6 @@ __all__ = [
251
285
  'Input',
252
286
  'InputButton',
253
287
  'InputCheckbox',
254
- 'InputCheckbox',
255
288
  'InputColor',
256
289
  'InputDate',
257
290
  'InputEmail',
@@ -271,3 +304,21 @@ __all__ = [
271
304
  'Label',
272
305
  'TextArea',
273
306
  ]
307
+
308
+
309
+ def __getattr__(name: str):
310
+ if name == 'JWTAlgorithms':
311
+ from pyweber.utils.deprecation import warn_deprecated
312
+ from pyweber.utils.types import JWTAlgorithms as _JWTAlgorithms
313
+
314
+ warn_deprecated(
315
+ 'pyweber.JWTAlgorithms',
316
+ alternative=(
317
+ 'pyjwt (or similar) for encode/decode/verify; '
318
+ 'pyweber.auth for session login. '
319
+ 'JWTAlgorithms remains an algorithm-name enum only until 2.0'
320
+ ),
321
+ removal='2.0',
322
+ )
323
+ return _JWTAlgorithms
324
+ raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
@@ -0,0 +1,44 @@
1
+ """Lightweight session login helpers (Flask-Login style, no ORM) + RBAC."""
2
+
3
+ from pyweber.auth.decorators import login_required, permission_required, role_required
4
+ from pyweber.auth.passwords import check_password, hash_password
5
+ from pyweber.auth.rbac import (
6
+ RoleRegistry,
7
+ clear_roles,
8
+ define_role,
9
+ get_role_registry,
10
+ has_all_roles,
11
+ has_permission,
12
+ has_role,
13
+ register_roles,
14
+ user_permissions,
15
+ )
16
+ from pyweber.auth.session import (
17
+ USER_COOKIE_NAME,
18
+ current_user,
19
+ get_user_id,
20
+ login_user,
21
+ logout_user,
22
+ )
23
+
24
+ __all__ = [
25
+ 'USER_COOKIE_NAME',
26
+ 'RoleRegistry',
27
+ 'check_password',
28
+ 'clear_roles',
29
+ 'current_user',
30
+ 'define_role',
31
+ 'get_role_registry',
32
+ 'get_user_id',
33
+ 'has_all_roles',
34
+ 'has_permission',
35
+ 'has_role',
36
+ 'hash_password',
37
+ 'login_required',
38
+ 'login_user',
39
+ 'logout_user',
40
+ 'permission_required',
41
+ 'register_roles',
42
+ 'role_required',
43
+ 'user_permissions',
44
+ ]
@@ -0,0 +1,201 @@
1
+ """``@login_required`` / ``@permission_required`` / ``@role_required``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from functools import wraps
7
+ from typing import Any, Callable
8
+
9
+ from pyweber.auth.rbac import user_has_permissions, user_has_roles
10
+ from pyweber.auth.session import current_user
11
+ from pyweber.models.context import get_current_request
12
+ from pyweber.models.response import Response
13
+ from pyweber.utils.types import ContentTypes
14
+
15
+
16
+ def _wants_json(request) -> bool:
17
+ if request is None:
18
+ return False
19
+ accept = (request.headers.get('accept') or '').lower()
20
+ if 'application/json' in accept:
21
+ if 'text/html' not in accept:
22
+ return True
23
+ return accept.find('application/json') <= accept.find('text/html')
24
+ return False
25
+
26
+
27
+ def _unauthorized(*, redirect: str | None, request) -> Response:
28
+ if redirect and not _wants_json(request):
29
+ return Response(
30
+ content=b'',
31
+ status=302,
32
+ request=request,
33
+ route=redirect,
34
+ headers={'Location': redirect},
35
+ content_type=ContentTypes.txt,
36
+ )
37
+ return Response.json({'detail': 'Unauthorized'}, status=401, request=request)
38
+
39
+
40
+ def _forbidden(*, request) -> Response:
41
+ return Response.json({'detail': 'Forbidden'}, status=403, request=request)
42
+
43
+
44
+ def _authorize(
45
+ user: dict[str, Any] | None,
46
+ *,
47
+ roles: list[str] | None,
48
+ roles_all: list[str] | None,
49
+ permissions: list[str] | None,
50
+ permissions_all: list[str] | None,
51
+ ) -> str | None:
52
+ """Return ``'unauthorized'``, ``'forbidden'``, or ``None`` if allowed."""
53
+ if user is None:
54
+ return 'unauthorized'
55
+ if not user_has_roles(user, roles, roles_all=roles_all):
56
+ return 'forbidden'
57
+ if not user_has_permissions(user, permissions, permissions_all=permissions_all):
58
+ return 'forbidden'
59
+ return None
60
+
61
+
62
+ def _wrap_handler(
63
+ func: Callable,
64
+ *,
65
+ redirect: str | None,
66
+ roles: list[str] | None,
67
+ roles_all: list[str] | None,
68
+ permissions: list[str] | None,
69
+ permissions_all: list[str] | None,
70
+ ):
71
+ if inspect.iscoroutinefunction(func):
72
+ @wraps(func)
73
+ async def async_wrapper(*args, **kwargs):
74
+ request = get_current_request()
75
+ decision = _authorize(
76
+ current_user(),
77
+ roles=roles,
78
+ roles_all=roles_all,
79
+ permissions=permissions,
80
+ permissions_all=permissions_all,
81
+ )
82
+ if decision == 'unauthorized':
83
+ return _unauthorized(redirect=redirect, request=request)
84
+ if decision == 'forbidden':
85
+ return _forbidden(request=request)
86
+ return await func(*args, **kwargs)
87
+
88
+ return async_wrapper
89
+
90
+ @wraps(func)
91
+ def sync_wrapper(*args, **kwargs):
92
+ request = get_current_request()
93
+ decision = _authorize(
94
+ current_user(),
95
+ roles=roles,
96
+ roles_all=roles_all,
97
+ permissions=permissions,
98
+ permissions_all=permissions_all,
99
+ )
100
+ if decision == 'unauthorized':
101
+ return _unauthorized(redirect=redirect, request=request)
102
+ if decision == 'forbidden':
103
+ return _forbidden(request=request)
104
+ return func(*args, **kwargs)
105
+
106
+ return sync_wrapper
107
+
108
+
109
+ def login_required(
110
+ fn: Callable | None = None,
111
+ *,
112
+ redirect: str | None = None,
113
+ roles: list[str] | None = None,
114
+ roles_all: list[str] | None = None,
115
+ permissions: list[str] | None = None,
116
+ permissions_all: list[str] | None = None,
117
+ ):
118
+ """Require a logged-in user (cookie) or ``request.auth`` from OpenAPI schemes.
119
+
120
+ RBAC (optional)::
121
+
122
+ @login_required(roles=['admin', 'moderator']) # any role
123
+ @login_required(roles_all=['staff', 'verified']) # all roles
124
+ @login_required(permissions=['posts:write']) # via register_roles()
125
+ @login_required(permissions_all=['posts:read', 'posts:write'])
126
+
127
+ Usage::
128
+
129
+ @app.route('/dash')
130
+ @login_required(redirect='/login')
131
+ def dash():
132
+ ...
133
+
134
+ @login_required # bare form
135
+ def api_me():
136
+ ...
137
+ """
138
+
139
+ def decorator(func: Callable):
140
+ return _wrap_handler(
141
+ func,
142
+ redirect=redirect,
143
+ roles=roles,
144
+ roles_all=roles_all,
145
+ permissions=permissions,
146
+ permissions_all=permissions_all,
147
+ )
148
+
149
+ if fn is not None and callable(fn):
150
+ return decorator(fn)
151
+ return decorator
152
+
153
+
154
+ def permission_required(
155
+ *permissions: str,
156
+ redirect: str | None = None,
157
+ require_all: bool = False,
158
+ ):
159
+ """Shorthand for ``@login_required(permissions=…)`` / ``permissions_all=``.
160
+
161
+ ::
162
+
163
+ @permission_required('posts:write')
164
+ def create(): ...
165
+
166
+ @permission_required('a', 'b', require_all=True)
167
+ def both(): ...
168
+ """
169
+ if not permissions:
170
+ raise TypeError('permission_required() needs at least one permission')
171
+ perms = list(permissions)
172
+ return login_required(
173
+ redirect=redirect,
174
+ permissions=None if require_all else perms,
175
+ permissions_all=perms if require_all else None,
176
+ )
177
+
178
+
179
+ def role_required(
180
+ *roles: str,
181
+ redirect: str | None = None,
182
+ require_all: bool = False,
183
+ ):
184
+ """Shorthand for ``@login_required(roles=…)`` / ``roles_all=``.
185
+
186
+ ::
187
+
188
+ @role_required('admin')
189
+ def admin_panel(): ...
190
+
191
+ @role_required('staff', 'verified', require_all=True)
192
+ def staff_only(): ...
193
+ """
194
+ if not roles:
195
+ raise TypeError('role_required() needs at least one role')
196
+ rs = list(roles)
197
+ return login_required(
198
+ redirect=redirect,
199
+ roles=None if require_all else rs,
200
+ roles_all=rs if require_all else None,
201
+ )
@@ -0,0 +1,52 @@
1
+ """Password hashing helpers (stdlib PBKDF2-HMAC-SHA256)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import hmac
7
+ import secrets
8
+
9
+ DEFAULT_ITERATIONS = 260_000
10
+ _SCHEME = 'pbkdf2_sha256'
11
+
12
+
13
+ def hash_password(password: str, *, iterations: int = DEFAULT_ITERATIONS) -> str:
14
+ """Hash a password for storage.
15
+
16
+ Format: ``pbkdf2_sha256$<iterations>$<salt_hex>$<digest_hex>``
17
+ """
18
+ if not isinstance(password, str) or password == '':
19
+ raise ValueError('password must be a non-empty string')
20
+ salt = secrets.token_hex(16)
21
+ digest = hashlib.pbkdf2_hmac(
22
+ 'sha256',
23
+ password.encode('utf-8'),
24
+ bytes.fromhex(salt),
25
+ iterations,
26
+ ).hex()
27
+ return f'{_SCHEME}${iterations}${salt}${digest}'
28
+
29
+
30
+ def check_password(password: str, encoded: str) -> bool:
31
+ """Constant-time verify of ``password`` against ``hash_password`` output."""
32
+ if not password or not encoded or not isinstance(encoded, str):
33
+ return False
34
+ try:
35
+ scheme, iter_s, salt, digest = encoded.split('$', 3)
36
+ except ValueError:
37
+ return False
38
+ if scheme != _SCHEME:
39
+ return False
40
+ try:
41
+ iterations = int(iter_s)
42
+ salt_b = bytes.fromhex(salt)
43
+ expected = bytes.fromhex(digest)
44
+ except (TypeError, ValueError):
45
+ return False
46
+ actual = hashlib.pbkdf2_hmac(
47
+ 'sha256',
48
+ password.encode('utf-8'),
49
+ salt_b,
50
+ iterations,
51
+ )
52
+ return hmac.compare_digest(actual, expected)