pyview-web 0.7.2__tar.gz → 0.8.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 (84) hide show
  1. {pyview_web-0.7.2 → pyview_web-0.8.0}/PKG-INFO +38 -34
  2. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyproject.toml +56 -32
  3. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/__init__.py +8 -0
  4. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/assets/package-lock.json +16 -14
  5. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/assets/package.json +2 -2
  6. pyview_web-0.8.0/pyview/binding/__init__.py +47 -0
  7. pyview_web-0.8.0/pyview/binding/binder.py +134 -0
  8. pyview_web-0.8.0/pyview/binding/context.py +33 -0
  9. pyview_web-0.8.0/pyview/binding/converters.py +191 -0
  10. pyview_web-0.8.0/pyview/binding/helpers.py +78 -0
  11. pyview_web-0.8.0/pyview/binding/injectables.py +119 -0
  12. pyview_web-0.8.0/pyview/binding/params.py +105 -0
  13. pyview_web-0.8.0/pyview/binding/result.py +32 -0
  14. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/changesets/changesets.py +5 -1
  15. pyview_web-0.8.0/pyview/components/__init__.py +72 -0
  16. pyview_web-0.8.0/pyview/components/base.py +212 -0
  17. pyview_web-0.8.0/pyview/components/lifecycle.py +85 -0
  18. pyview_web-0.8.0/pyview/components/manager.py +366 -0
  19. pyview_web-0.8.0/pyview/components/renderer.py +14 -0
  20. pyview_web-0.8.0/pyview/components/slots.py +73 -0
  21. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/events/BaseEventHandler.py +27 -4
  22. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/live_socket.py +70 -4
  23. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/live_view.py +14 -2
  24. pyview_web-0.8.0/pyview/meta.py +18 -0
  25. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/phx_message.py +2 -2
  26. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/pyview.py +10 -3
  27. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/static/assets/app.js +2117 -817
  28. pyview_web-0.8.0/pyview/stream.py +308 -0
  29. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/template/live_template.py +4 -2
  30. pyview_web-0.8.0/pyview/template/live_view_template.py +338 -0
  31. pyview_web-0.8.0/pyview/template/render_diff.py +65 -0
  32. pyview_web-0.8.0/pyview/template/template_view.py +170 -0
  33. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/vendor/ibis/nodes.py +13 -1
  34. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/vendor/ibis/tree.py +62 -3
  35. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/ws_handler.py +50 -19
  36. {pyview_web-0.7.2 → pyview_web-0.8.0}/readme.md +13 -7
  37. pyview_web-0.7.2/LICENSE +0 -21
  38. pyview_web-0.7.2/pyview/meta.py +0 -6
  39. pyview_web-0.7.2/pyview/template/live_view_template.py +0 -158
  40. pyview_web-0.7.2/pyview/template/render_diff.py +0 -35
  41. pyview_web-0.7.2/pyview/template/template_view.py +0 -120
  42. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/assets/js/app.js +0 -0
  43. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/assets/js/uploaders.js +0 -0
  44. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/async_stream_runner.py +0 -0
  45. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/auth/__init__.py +0 -0
  46. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/auth/provider.py +0 -0
  47. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/auth/required.py +0 -0
  48. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/changesets/__init__.py +0 -0
  49. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/cli/__init__.py +0 -0
  50. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/cli/commands/__init__.py +0 -0
  51. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/cli/commands/create_view.py +0 -0
  52. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/cli/main.py +0 -0
  53. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/csrf.py +0 -0
  54. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/events/AutoEventDispatch.py +0 -0
  55. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/events/__init__.py +0 -0
  56. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/events/info_event.py +0 -0
  57. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/instrumentation/__init__.py +0 -0
  58. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/instrumentation/interfaces.py +0 -0
  59. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/instrumentation/noop.py +0 -0
  60. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/js.py +0 -0
  61. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/live_routes.py +0 -0
  62. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/playground/__init__.py +0 -0
  63. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/playground/builder.py +0 -0
  64. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/playground/favicon.py +0 -0
  65. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/secret.py +0 -0
  66. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/session.py +0 -0
  67. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/static/assets/uploaders.js +0 -0
  68. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/template/__init__.py +0 -0
  69. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/template/context_processor.py +0 -0
  70. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/template/root_template.py +0 -0
  71. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/template/serializer.py +0 -0
  72. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/template/utils.py +0 -0
  73. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/uploads.py +0 -0
  74. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/vendor/__init__.py +0 -0
  75. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/vendor/flet/pubsub/__init__.py +0 -0
  76. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/vendor/flet/pubsub/pub_sub.py +0 -0
  77. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/vendor/ibis/__init__.py +0 -0
  78. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/vendor/ibis/compiler.py +0 -0
  79. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/vendor/ibis/context.py +0 -0
  80. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/vendor/ibis/errors.py +0 -0
  81. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/vendor/ibis/filters.py +0 -0
  82. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/vendor/ibis/loaders.py +0 -0
  83. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/vendor/ibis/template.py +0 -0
  84. {pyview_web-0.7.2 → pyview_web-0.8.0}/pyview/vendor/ibis/utils.py +0 -0
@@ -1,44 +1,43 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.3
2
2
  Name: pyview-web
3
- Version: 0.7.2
3
+ Version: 0.8.0
4
4
  Summary: LiveView in Python
5
- License: MIT
6
- License-File: LICENSE
7
5
  Keywords: web,api,LiveView
8
6
  Author: Larry Ogrodnek
9
- Author-email: ogrodnek@gmail.com
10
- Requires-Python: >=3.11,<3.15
7
+ Author-email: Larry Ogrodnek <ogrodnek@gmail.com>
8
+ License: MIT
9
+ Classifier: Intended Audience :: Information Technology
10
+ Classifier: Intended Audience :: System Administrators
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Topic :: Internet
15
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Classifier: Topic :: Software Development
19
+ Classifier: Typing :: Typed
11
20
  Classifier: Development Status :: 4 - Beta
12
21
  Classifier: Environment :: Web Environment
13
22
  Classifier: Framework :: AsyncIO
14
23
  Classifier: Framework :: Pydantic
15
24
  Classifier: Intended Audience :: Developers
16
- Classifier: Intended Audience :: Information Technology
17
- Classifier: Intended Audience :: System Administrators
18
25
  Classifier: License :: OSI Approved :: MIT License
19
- Classifier: Operating System :: OS Independent
20
- Classifier: Programming Language :: Python
21
- Classifier: Programming Language :: Python :: 3
26
+ Classifier: Programming Language :: Python :: 3 :: Only
22
27
  Classifier: Programming Language :: Python :: 3.11
23
28
  Classifier: Programming Language :: Python :: 3.12
24
29
  Classifier: Programming Language :: Python :: 3.13
25
30
  Classifier: Programming Language :: Python :: 3.14
26
- Classifier: Programming Language :: Python :: 3 :: Only
27
- Classifier: Topic :: Internet
28
- Classifier: Topic :: Internet :: WWW/HTTP
29
31
  Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
30
- Classifier: Topic :: Software Development
31
- Classifier: Topic :: Software Development :: Libraries
32
- Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
33
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
34
- Classifier: Typing :: Typed
35
- Requires-Dist: APScheduler (>=3.11.0,<4.0.0)
36
- Requires-Dist: click (>=8.1.7,<9.0.0)
37
- Requires-Dist: itsdangerous (>=2.2.0,<3.0.0)
38
- Requires-Dist: markupsafe (>=3.0.2,<4.0.0)
39
- Requires-Dist: pydantic (>=2.11,<3.0)
40
- Requires-Dist: starlette (>=0.50.0,<0.51.0)
41
- Requires-Dist: wsproto (>=1.3.0,<2.0.0)
32
+ Classifier: Topic :: Internet :: WWW/HTTP
33
+ Requires-Dist: starlette>=0.50.0,<0.51
34
+ Requires-Dist: wsproto>=1.3.0,<2
35
+ Requires-Dist: apscheduler>=3.11.0,<4
36
+ Requires-Dist: markupsafe>=3.0.2,<4
37
+ Requires-Dist: itsdangerous>=2.2.0,<3
38
+ Requires-Dist: pydantic>=2.11,<3
39
+ Requires-Dist: click>=8.1.7,<9
40
+ Requires-Python: >=3.11, <3.15
42
41
  Project-URL: Homepage, https://pyview.rocks
43
42
  Project-URL: Repository, https://github.com/ogrodnek/pyview
44
43
  Description-Content-Type: text/markdown
@@ -51,6 +50,8 @@ Description-Content-Type: text/markdown
51
50
 
52
51
  PyView enables dynamic, real-time web apps, using server-rendered HTML.
53
52
 
53
+ **Documentation**: <a href="https://pyview.rocks" target="_blank">https://pyview.rocks</a>
54
+
54
55
  **Source Code**: <a href="https://github.com/ogrodnek/pyview" target="_blank">https://github.com/ogrodnek/pyview</a>
55
56
 
56
57
  # Installation
@@ -136,28 +137,31 @@ PyView is in the very early stages of active development. Please check it out an
136
137
  ## Setup
137
138
 
138
139
  ```
139
- poetry install
140
+ uv sync
140
141
  ```
141
142
 
142
143
  ## Running
143
144
 
144
145
  ```
145
- poetry run uvicorn examples.app:app --reload
146
+ uv run uvicorn examples.app:app --reload
146
147
  ```
147
148
 
148
149
  Then go to http://localhost:8000/
149
150
 
150
- ### Poetry Install
151
+ ### uv Install
152
+
153
+ ```
154
+ brew install uv
155
+ ```
156
+
157
+ or
151
158
 
152
159
  ```
153
- brew install pipx
154
- pipx install poetry
155
- pipx ensurepath
160
+ curl -LsSf https://astral.sh/uv/install.sh | sh
156
161
  ```
157
162
 
158
- (see https://python-poetry.org/docs/#installation for more details)
163
+ (see https://docs.astral.sh/uv/getting-started/installation/ for more details)
159
164
 
160
165
  # License
161
166
 
162
167
  PyView is licensed under the [MIT License](LICENSE).
163
-
@@ -1,17 +1,11 @@
1
- [tool.poetry]
1
+ [project]
2
2
  name = "pyview-web"
3
-
4
- packages = [
5
- { include = "pyview" },
6
- ]
7
-
8
- version = "0.7.2"
3
+ version = "0.8.0"
9
4
  description = "LiveView in Python"
10
- authors = ["Larry Ogrodnek <ogrodnek@gmail.com>"]
11
- license = "MIT"
5
+ authors = [{ name = "Larry Ogrodnek", email = "ogrodnek@gmail.com" }]
6
+ license = { text = "MIT" }
12
7
  readme = "readme.md"
13
- homepage = "https://pyview.rocks"
14
- repository = "https://github.com/ogrodnek/pyview"
8
+ requires-python = ">=3.11,<3.15"
15
9
  keywords = ["web", "api", "LiveView"]
16
10
 
17
11
  classifiers = [
@@ -41,33 +35,49 @@ classifiers = [
41
35
  "Topic :: Internet :: WWW/HTTP",
42
36
  ]
43
37
 
44
- [tool.poetry.dependencies]
45
- python = ">=3.11,<3.15"
46
- starlette = "^0.50.0"
47
- wsproto = "^1.3.0"
48
- APScheduler = "^3.11.0"
49
- markupsafe = "^3.0.2"
50
- itsdangerous = "^2.2.0"
51
- pydantic = "^2.11"
52
- click = "^8.1.7"
38
+ dependencies = [
39
+ "starlette>=0.50.0,<0.51",
40
+ "wsproto>=1.3.0,<2",
41
+ "APScheduler>=3.11.0,<4",
42
+ "markupsafe>=3.0.2,<4",
43
+ "itsdangerous>=2.2.0,<3",
44
+ "pydantic>=2.11,<3",
45
+ "click>=8.1.7,<9",
46
+ ]
53
47
 
54
- [tool.poetry.scripts]
48
+ [project.scripts]
55
49
  pv = "pyview.cli.main:cli"
56
50
 
57
- [tool.poetry.group.dev.dependencies]
58
- pytest = "^8.2.0"
59
- ruff = "^0.14.0"
60
- pyright = "^1.1.403"
61
- pytest-cov = "^6.1.1"
62
- pytest-asyncio = "^1.3.0"
51
+ [project.urls]
52
+ Homepage = "https://pyview.rocks"
53
+ Repository = "https://github.com/ogrodnek/pyview"
54
+
55
+ [dependency-groups]
56
+ dev = [
57
+ "pytest>=8.2.0,<9",
58
+ "ruff>=0.14.0",
59
+ "pyright>=1.1.403,<2",
60
+ "pytest-cov>=6.1.1,<7",
61
+ "pytest-asyncio>=1.3.0,<2",
62
+ ]
63
+ profiling = [
64
+ "scalene>=1.5.51,<2; python_version != '3.11.0'",
65
+ ]
66
+ docs = [
67
+ "mkdocs>=1.6.1,<2",
68
+ "mkdocs-material>=9.6.14,<10",
69
+ ]
63
70
 
64
- [tool.poetry.group.profiling.dependencies]
65
- scalene = {version = "^1.5.51", python = "!=3.11.0,>=3.11"}
71
+ [build-system]
72
+ requires = ["uv_build>=0.7,<1"]
73
+ build-backend = "uv_build"
66
74
 
75
+ [tool.uv]
76
+ package = true
67
77
 
68
- [build-system]
69
- requires = ["poetry-core"]
70
- build-backend = "poetry.core.masonry.api"
78
+ [tool.uv.build-backend]
79
+ module-root = "."
80
+ module-name = "pyview"
71
81
 
72
82
  [tool.pyright]
73
83
  exclude = [
@@ -78,8 +88,13 @@ exclude = [
78
88
  "**/node_modules",
79
89
  "**/__pycache__",
80
90
  # T-string files require Python 3.14+
91
+ "pyview/components/renderer.py",
81
92
  "tests/test_live_view_template.py",
82
93
  "tests/test_template_view.py",
94
+ "tests/test_stream_tstring.py",
95
+ "tests/components/test_statics_sharing.py",
96
+ "tests/components/test_lifecycle.py",
97
+ "tests/components/test_slots.py",
83
98
  ]
84
99
 
85
100
  [tool.pytest.ini_options]
@@ -93,9 +108,18 @@ exclude = [
93
108
  "pyview/template/__init__.py",
94
109
  "pyview/template/template_view.py",
95
110
  "pyview/template/live_view_template.py",
111
+ "pyview/components/slots.py",
96
112
  "examples/views/count/count_tstring.py",
113
+ "examples/views/streams/streams_tstring.py",
114
+ "examples/views/components/slots_demo.py",
115
+ "examples/views/components/stateful_demo.py",
116
+ "examples/views/components/stateless_demo.py",
97
117
  "tests/test_live_view_template.py",
98
118
  "tests/test_template_view.py",
119
+ "tests/test_stream_tstring.py",
120
+ "tests/components/test_slots.py",
121
+ "tests/components/test_statics_sharing.py",
122
+ "tests/components/test_lifecycle.py",
99
123
  ]
100
124
 
101
125
  [tool.ruff.lint]
@@ -1,3 +1,4 @@
1
+ from pyview.components import ComponentMeta, ComponentsManager, ComponentSocket, LiveComponent
1
2
  from pyview.js import JsCommand
2
3
  from pyview.live_socket import (
3
4
  ConnectedLiveViewSocket,
@@ -8,6 +9,7 @@ from pyview.live_socket import (
8
9
  from pyview.live_view import LiveView
9
10
  from pyview.playground import playground
10
11
  from pyview.pyview import PyView, RootTemplate, RootTemplateContext, defaultRootTemplate
12
+ from pyview.stream import Stream
11
13
 
12
14
  __all__ = [
13
15
  "LiveView",
@@ -21,4 +23,10 @@ __all__ = [
21
23
  "ConnectedLiveViewSocket",
22
24
  "UnconnectedSocket",
23
25
  "playground",
26
+ "Stream",
27
+ # Components
28
+ "LiveComponent",
29
+ "ComponentMeta",
30
+ "ComponentSocket",
31
+ "ComponentsManager",
24
32
  ]
@@ -6,9 +6,9 @@
6
6
  "": {
7
7
  "dependencies": {
8
8
  "nprogress": "^0.2.0",
9
- "phoenix": "^1.7.0-rc.2",
9
+ "phoenix": "^1.7.0",
10
10
  "phoenix_html": "^3.2.0",
11
- "phoenix_live_view": "^0.18.11"
11
+ "phoenix_live_view": "^0.20.17"
12
12
  }
13
13
  },
14
14
  "node_modules/nprogress": {
@@ -17,9 +17,10 @@
17
17
  "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA=="
18
18
  },
19
19
  "node_modules/phoenix": {
20
- "version": "1.7.0-rc.2",
21
- "resolved": "https://registry.npmjs.org/phoenix/-/phoenix-1.7.0-rc.2.tgz",
22
- "integrity": "sha512-05DaSo/ws2VtieY3Z6CJVx36DyhTil6/KesK1a4JlQPxYgNHZn7swv7R/R7etoN8SGtEjMV/a9HBkPS5wF/Xdg=="
20
+ "version": "1.8.3",
21
+ "resolved": "https://registry.npmjs.org/phoenix/-/phoenix-1.8.3.tgz",
22
+ "integrity": "sha512-5bMYQI30wl3erxbHnXMdt1xuQeRTeEOpQrakf3yqj/1HRHl7Gj4Cdk2NKXkUcCD5WpbxrilvZEMexM1VhWbnDg==",
23
+ "license": "MIT"
23
24
  },
24
25
  "node_modules/phoenix_html": {
25
26
  "version": "3.2.0",
@@ -27,9 +28,10 @@
27
28
  "integrity": "sha512-zv7PIZk0MPkF0ax8n465Q6w86+sGAy5cTem6KcbkUbdgxGc0y3WZmzkM2bSlYdSGbLEZfjXxos1G72xXsha6xA=="
28
29
  },
29
30
  "node_modules/phoenix_live_view": {
30
- "version": "0.18.11",
31
- "resolved": "https://registry.npmjs.org/phoenix_live_view/-/phoenix_live_view-0.18.11.tgz",
32
- "integrity": "sha512-p/mBu/O3iVLvAreUoDeSZ4/myQJJeR8BH7Yu9LVCMI2xe2IZ2mffxtDGJb0mxnJrUQa7p03HHNlKGXj7LSJDdg=="
31
+ "version": "0.20.17",
32
+ "resolved": "https://registry.npmjs.org/phoenix_live_view/-/phoenix_live_view-0.20.17.tgz",
33
+ "integrity": "sha512-qGT3Jtj2wUawOaMrE8NKXmkexfaUn6bx5PuPMxWMzYuyp6Qv9i4xRZ2T3U6avC5Kf+oJEiBVIiWrODooC0vpQw==",
34
+ "license": "MIT"
33
35
  }
34
36
  },
35
37
  "dependencies": {
@@ -39,9 +41,9 @@
39
41
  "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA=="
40
42
  },
41
43
  "phoenix": {
42
- "version": "1.7.0-rc.2",
43
- "resolved": "https://registry.npmjs.org/phoenix/-/phoenix-1.7.0-rc.2.tgz",
44
- "integrity": "sha512-05DaSo/ws2VtieY3Z6CJVx36DyhTil6/KesK1a4JlQPxYgNHZn7swv7R/R7etoN8SGtEjMV/a9HBkPS5wF/Xdg=="
44
+ "version": "1.8.3",
45
+ "resolved": "https://registry.npmjs.org/phoenix/-/phoenix-1.8.3.tgz",
46
+ "integrity": "sha512-5bMYQI30wl3erxbHnXMdt1xuQeRTeEOpQrakf3yqj/1HRHl7Gj4Cdk2NKXkUcCD5WpbxrilvZEMexM1VhWbnDg=="
45
47
  },
46
48
  "phoenix_html": {
47
49
  "version": "3.2.0",
@@ -49,9 +51,9 @@
49
51
  "integrity": "sha512-zv7PIZk0MPkF0ax8n465Q6w86+sGAy5cTem6KcbkUbdgxGc0y3WZmzkM2bSlYdSGbLEZfjXxos1G72xXsha6xA=="
50
52
  },
51
53
  "phoenix_live_view": {
52
- "version": "0.18.11",
53
- "resolved": "https://registry.npmjs.org/phoenix_live_view/-/phoenix_live_view-0.18.11.tgz",
54
- "integrity": "sha512-p/mBu/O3iVLvAreUoDeSZ4/myQJJeR8BH7Yu9LVCMI2xe2IZ2mffxtDGJb0mxnJrUQa7p03HHNlKGXj7LSJDdg=="
54
+ "version": "0.20.17",
55
+ "resolved": "https://registry.npmjs.org/phoenix_live_view/-/phoenix_live_view-0.20.17.tgz",
56
+ "integrity": "sha512-qGT3Jtj2wUawOaMrE8NKXmkexfaUn6bx5PuPMxWMzYuyp6Qv9i4xRZ2T3U6avC5Kf+oJEiBVIiWrODooC0vpQw=="
55
57
  }
56
58
  }
57
59
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "dependencies": {
3
3
  "nprogress": "^0.2.0",
4
- "phoenix": "^1.7.0-rc.2",
4
+ "phoenix": "^1.7.0",
5
5
  "phoenix_html": "^3.2.0",
6
- "phoenix_live_view": "^0.18.11"
6
+ "phoenix_live_view": "^0.20.17"
7
7
  }
8
8
  }
@@ -0,0 +1,47 @@
1
+ """Parameter binding for pyview handlers.
2
+
3
+ This module provides signature-driven parameter binding, allowing handlers to
4
+ declare typed parameters that are automatically converted from request data.
5
+
6
+ Example:
7
+ # Old style (manual extraction):
8
+ async def handle_params(self, url, params, socket):
9
+ page = int(params["page"][0]) if "page" in params else 1
10
+
11
+ # New style (typed binding):
12
+ async def handle_params(self, socket: LiveViewSocket[MyContext], page: int = 1):
13
+ # page is already an int!
14
+ pass
15
+
16
+ Reserved parameter names (injected from context, not from URL params):
17
+ - socket: The LiveViewSocket instance
18
+ - url: The parsed URL
19
+ - event: The event name (for event handlers)
20
+ - payload: The event payload dict (for event handlers)
21
+
22
+ Type-based injection:
23
+ - params: Params -> injects Params container
24
+ - params: dict -> injects params as dict
25
+ - params: str -> treats "params" as a URL param name (not injected)
26
+ """
27
+
28
+ from .binder import Binder
29
+ from .context import BindContext
30
+ from .converters import ConversionError, ConverterRegistry
31
+ from .helpers import call_handle_event, call_handle_params
32
+ from .injectables import InjectableRegistry
33
+ from .params import Params
34
+ from .result import BindResult, ParamError
35
+
36
+ __all__ = [
37
+ "Params",
38
+ "BindContext",
39
+ "BindResult",
40
+ "ParamError",
41
+ "ConverterRegistry",
42
+ "ConversionError",
43
+ "InjectableRegistry",
44
+ "Binder",
45
+ "call_handle_event",
46
+ "call_handle_params",
47
+ ]
@@ -0,0 +1,134 @@
1
+ """Core binder for signature-driven parameter binding."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ import inspect
7
+ import logging
8
+ from typing import Any, Callable, Generic, TypeVar, get_type_hints
9
+
10
+ from .context import BindContext
11
+ from .converters import ConversionError, ConverterRegistry
12
+ from .injectables import _NOT_FOUND, InjectableRegistry
13
+ from .result import BindResult, ParamError
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ T = TypeVar("T")
18
+
19
+
20
+ class Binder(Generic[T]):
21
+ """Binds function parameters from BindContext based on type annotations.
22
+
23
+ The binder inspects a function's signature and type hints, then:
24
+ 1. Resolves injectable parameters (socket, event, payload, url, params)
25
+ 2. Pulls values from params or payload for other parameters
26
+ 3. Converts values to the expected types
27
+ 4. Returns a BindResult with bound_args and any errors
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ converter: ConverterRegistry | None = None,
33
+ injectables: InjectableRegistry[T] | None = None,
34
+ ):
35
+ self.converter = converter or ConverterRegistry()
36
+ self.injectables = injectables or InjectableRegistry()
37
+
38
+ def bind(self, func: Callable[..., Any], ctx: BindContext[T]) -> BindResult:
39
+ """Bind context values to function signature.
40
+
41
+ Args:
42
+ func: The function to bind parameters for
43
+ ctx: Binding context with params, payload, socket, etc.
44
+
45
+ Returns:
46
+ BindResult with bound_args dict and any errors
47
+ """
48
+ sig = inspect.signature(func)
49
+
50
+ # Get type hints, falling back to empty for missing annotations
51
+ # NameError: forward reference can't be resolved
52
+ # AttributeError: accessing annotations on some objects
53
+ # RecursionError: circular type references
54
+ try:
55
+ hints = get_type_hints(func)
56
+ except (NameError, AttributeError, RecursionError) as e:
57
+ logger.debug("Could not resolve type hints for %s: %s", func.__name__, e)
58
+ hints = {}
59
+
60
+ bound: dict[str, Any] = {}
61
+ errors: list[ParamError] = []
62
+
63
+ for name, param in sig.parameters.items():
64
+ # Skip 'self' for methods
65
+ if name == "self":
66
+ continue
67
+
68
+ # Skip *args and **kwargs (VAR_POSITIONAL and VAR_KEYWORD)
69
+ if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
70
+ continue
71
+
72
+ expected = hints.get(name, Any)
73
+
74
+ # 1) Try injectables first
75
+ injected = self.injectables.resolve(name, expected, ctx)
76
+ if injected is not _NOT_FOUND:
77
+ bound[name] = injected
78
+ continue
79
+
80
+ # 2) Check for dataclass parameter - gather fields from params
81
+ if dataclasses.is_dataclass(expected) and isinstance(expected, type):
82
+ raw = self._resolve_dataclass_fields(expected, ctx)
83
+ try:
84
+ bound[name] = self.converter.convert(raw, expected)
85
+ except ConversionError as e:
86
+ errors.append(ParamError(name, repr(expected), raw, str(e)))
87
+ continue
88
+
89
+ # 3) Pull raw value from params or payload
90
+ raw = self._resolve_raw(name, ctx)
91
+
92
+ # 4) Handle missing values
93
+ if raw is None:
94
+ if param.default is not inspect.Parameter.empty:
95
+ bound[name] = param.default
96
+ continue
97
+ if self.converter.is_optional(expected):
98
+ bound[name] = None
99
+ continue
100
+ errors.append(ParamError(name, repr(expected), None, "missing required parameter"))
101
+ continue
102
+
103
+ # 5) Convert to expected type
104
+ try:
105
+ bound[name] = self.converter.convert(raw, expected)
106
+ except ConversionError as e:
107
+ errors.append(ParamError(name, repr(expected), raw, str(e)))
108
+
109
+ return BindResult(bound, errors)
110
+
111
+ def _resolve_dataclass_fields(self, expected: type, ctx: BindContext[T]) -> dict[str, Any]:
112
+ """Gather dataclass fields from params."""
113
+ fields = dataclasses.fields(expected)
114
+ result: dict[str, Any] = {}
115
+
116
+ for field in fields:
117
+ if ctx.params.has(field.name):
118
+ result[field.name] = ctx.params.getlist(field.name)
119
+ elif ctx.payload and field.name in ctx.payload:
120
+ result[field.name] = ctx.payload[field.name]
121
+
122
+ return result
123
+
124
+ def _resolve_raw(self, name: str, ctx: BindContext[T]) -> Any | None:
125
+ """Resolve raw value from params or payload."""
126
+ # Check params first
127
+ if ctx.params.has(name):
128
+ return ctx.params.getlist(name)
129
+
130
+ # Check payload
131
+ if ctx.payload and name in ctx.payload:
132
+ return ctx.payload[name]
133
+
134
+ return None
@@ -0,0 +1,33 @@
1
+ """Binding context for parameter resolution."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import TYPE_CHECKING, Any, Generic, Optional, TypeVar
5
+ from urllib.parse import ParseResult
6
+
7
+ if TYPE_CHECKING:
8
+ from pyview.live_socket import LiveViewSocket
9
+
10
+ from .params import Params
11
+
12
+ T = TypeVar("T")
13
+
14
+
15
+ @dataclass
16
+ class BindContext(Generic[T]):
17
+ """Context provided to the binder for resolving parameter values.
18
+
19
+ Attributes:
20
+ params: Multi-value parameter container (query/path/form merged)
21
+ payload: Event payload dict (for handle_event)
22
+ url: Parsed URL (for handle_params)
23
+ socket: LiveView socket instance
24
+ event: Event name (for handle_event)
25
+ extra: Additional injectable values
26
+ """
27
+
28
+ params: "Params"
29
+ payload: Optional[dict[str, Any]]
30
+ url: Optional[ParseResult]
31
+ socket: Optional["LiveViewSocket[T]"]
32
+ event: Optional[str]
33
+ extra: dict[str, Any] = field(default_factory=dict)