pyview-web 0.7.2__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.
- pyview_web-0.7.2/LICENSE +21 -0
- pyview_web-0.7.2/PKG-INFO +163 -0
- pyview_web-0.7.2/pyproject.toml +136 -0
- pyview_web-0.7.2/pyview/__init__.py +24 -0
- pyview_web-0.7.2/pyview/assets/js/app.js +79 -0
- pyview_web-0.7.2/pyview/assets/js/uploaders.js +221 -0
- pyview_web-0.7.2/pyview/assets/package-lock.json +57 -0
- pyview_web-0.7.2/pyview/assets/package.json +8 -0
- pyview_web-0.7.2/pyview/async_stream_runner.py +66 -0
- pyview_web-0.7.2/pyview/auth/__init__.py +4 -0
- pyview_web-0.7.2/pyview/auth/provider.py +32 -0
- pyview_web-0.7.2/pyview/auth/required.py +40 -0
- pyview_web-0.7.2/pyview/changesets/__init__.py +3 -0
- pyview_web-0.7.2/pyview/changesets/changesets.py +63 -0
- pyview_web-0.7.2/pyview/cli/__init__.py +0 -0
- pyview_web-0.7.2/pyview/cli/commands/__init__.py +0 -0
- pyview_web-0.7.2/pyview/cli/commands/create_view.py +200 -0
- pyview_web-0.7.2/pyview/cli/main.py +17 -0
- pyview_web-0.7.2/pyview/csrf.py +30 -0
- pyview_web-0.7.2/pyview/events/AutoEventDispatch.py +98 -0
- pyview_web-0.7.2/pyview/events/BaseEventHandler.py +84 -0
- pyview_web-0.7.2/pyview/events/__init__.py +5 -0
- pyview_web-0.7.2/pyview/events/info_event.py +16 -0
- pyview_web-0.7.2/pyview/instrumentation/__init__.py +21 -0
- pyview_web-0.7.2/pyview/instrumentation/interfaces.py +206 -0
- pyview_web-0.7.2/pyview/instrumentation/noop.py +100 -0
- pyview_web-0.7.2/pyview/js.py +117 -0
- pyview_web-0.7.2/pyview/live_routes.py +49 -0
- pyview_web-0.7.2/pyview/live_socket.py +280 -0
- pyview_web-0.7.2/pyview/live_view.py +53 -0
- pyview_web-0.7.2/pyview/meta.py +6 -0
- pyview_web-0.7.2/pyview/phx_message.py +54 -0
- pyview_web-0.7.2/pyview/playground/__init__.py +10 -0
- pyview_web-0.7.2/pyview/playground/builder.py +118 -0
- pyview_web-0.7.2/pyview/playground/favicon.py +39 -0
- pyview_web-0.7.2/pyview/pyview.py +116 -0
- pyview_web-0.7.2/pyview/secret.py +18 -0
- pyview_web-0.7.2/pyview/session.py +15 -0
- pyview_web-0.7.2/pyview/static/assets/app.js +5484 -0
- pyview_web-0.7.2/pyview/static/assets/uploaders.js +221 -0
- pyview_web-0.7.2/pyview/template/__init__.py +29 -0
- pyview_web-0.7.2/pyview/template/context_processor.py +17 -0
- pyview_web-0.7.2/pyview/template/live_template.py +85 -0
- pyview_web-0.7.2/pyview/template/live_view_template.py +158 -0
- pyview_web-0.7.2/pyview/template/render_diff.py +35 -0
- pyview_web-0.7.2/pyview/template/root_template.py +87 -0
- pyview_web-0.7.2/pyview/template/serializer.py +28 -0
- pyview_web-0.7.2/pyview/template/template_view.py +120 -0
- pyview_web-0.7.2/pyview/template/utils.py +25 -0
- pyview_web-0.7.2/pyview/uploads.py +586 -0
- pyview_web-0.7.2/pyview/vendor/__init__.py +0 -0
- pyview_web-0.7.2/pyview/vendor/flet/pubsub/__init__.py +3 -0
- pyview_web-0.7.2/pyview/vendor/flet/pubsub/pub_sub.py +232 -0
- pyview_web-0.7.2/pyview/vendor/ibis/__init__.py +13 -0
- pyview_web-0.7.2/pyview/vendor/ibis/compiler.py +184 -0
- pyview_web-0.7.2/pyview/vendor/ibis/context.py +136 -0
- pyview_web-0.7.2/pyview/vendor/ibis/errors.py +46 -0
- pyview_web-0.7.2/pyview/vendor/ibis/filters.py +292 -0
- pyview_web-0.7.2/pyview/vendor/ibis/loaders.py +95 -0
- pyview_web-0.7.2/pyview/vendor/ibis/nodes.py +743 -0
- pyview_web-0.7.2/pyview/vendor/ibis/template.py +55 -0
- pyview_web-0.7.2/pyview/vendor/ibis/tree.py +92 -0
- pyview_web-0.7.2/pyview/vendor/ibis/utils.py +91 -0
- pyview_web-0.7.2/pyview/ws_handler.py +394 -0
- pyview_web-0.7.2/readme.md +117 -0
pyview_web-0.7.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Larry Ogrodnek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyview-web
|
|
3
|
+
Version: 0.7.2
|
|
4
|
+
Summary: LiveView in Python
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: web,api,LiveView
|
|
8
|
+
Author: Larry Ogrodnek
|
|
9
|
+
Author-email: ogrodnek@gmail.com
|
|
10
|
+
Requires-Python: >=3.11,<3.15
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Web Environment
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Framework :: Pydantic
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Information Technology
|
|
17
|
+
Classifier: Intended Audience :: System Administrators
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Programming Language :: Python
|
|
21
|
+
Classifier: Programming Language :: Python :: 3
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
26
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
27
|
+
Classifier: Topic :: Internet
|
|
28
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
29
|
+
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)
|
|
42
|
+
Project-URL: Homepage, https://pyview.rocks
|
|
43
|
+
Project-URL: Repository, https://github.com/ogrodnek/pyview
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
|
|
46
|
+
<img src="https://pyview.rocks/images/pyview_logo_512.png" width="128px" align="right" />
|
|
47
|
+
|
|
48
|
+
# PyView
|
|
49
|
+
|
|
50
|
+
> A Python implementation of Phoenix LiveView
|
|
51
|
+
|
|
52
|
+
PyView enables dynamic, real-time web apps, using server-rendered HTML.
|
|
53
|
+
|
|
54
|
+
**Source Code**: <a href="https://github.com/ogrodnek/pyview" target="_blank">https://github.com/ogrodnek/pyview</a>
|
|
55
|
+
|
|
56
|
+
# Installation
|
|
57
|
+
|
|
58
|
+
`pip install pyview-web`
|
|
59
|
+
|
|
60
|
+
## Quickstart
|
|
61
|
+
|
|
62
|
+
There's a [cookiecutter](https://github.com/cookiecutter/cookiecutter) template available
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
cookiecutter gh:ogrodnek/pyview-cookiecutter
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
# Live Examples
|
|
69
|
+
|
|
70
|
+
[https://examples.pyview.rocks/](https://examples.pyview.rocks/)
|
|
71
|
+
|
|
72
|
+
## Other Examples
|
|
73
|
+
|
|
74
|
+
- [pyview AI chat](https://github.com/pyview/pyview-example-ai-chat)
|
|
75
|
+
- [pyview auth example](https://github.com/pyview/pyview-example-auth) (using [authlib](https://docs.authlib.org/en/latest/))
|
|
76
|
+
|
|
77
|
+
## Simple Counter
|
|
78
|
+
|
|
79
|
+
[See it live!](https://examples.pyview.rocks/count)
|
|
80
|
+
|
|
81
|
+
count.py:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from pyview import LiveView, LiveViewSocket
|
|
85
|
+
from typing import TypedDict
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class CountContext(TypedDict):
|
|
89
|
+
count: int
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class CountLiveView(LiveView[CountContext]):
|
|
93
|
+
async def mount(self, socket: LiveViewSocket[CountContext], _session):
|
|
94
|
+
socket.context = {"count": 0}
|
|
95
|
+
|
|
96
|
+
async def handle_event(self, event, payload, socket: LiveViewSocket[CountContext]):
|
|
97
|
+
if event == "decrement":
|
|
98
|
+
socket.context["count"] -= 1
|
|
99
|
+
|
|
100
|
+
if event == "increment":
|
|
101
|
+
socket.context["count"] += 1
|
|
102
|
+
|
|
103
|
+
async def handle_params(self, url, params, socket: LiveViewSocket[CountContext]):
|
|
104
|
+
if "c" in params:
|
|
105
|
+
socket.context["count"] = int(params["c"][0])
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
count.html:
|
|
109
|
+
|
|
110
|
+
```html
|
|
111
|
+
<div>
|
|
112
|
+
<h1>Count is {{count}}</h1>
|
|
113
|
+
<button phx-click="decrement">-</button>
|
|
114
|
+
<button phx-click="increment">+</button>
|
|
115
|
+
</div>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
# Acknowledgements
|
|
119
|
+
|
|
120
|
+
- Obviously this project wouldn't exist without [Phoenix LiveView](https://github.com/phoenixframework/phoenix_live_view), which is a wonderful paradigm and implementation. Besides using their ideas, we also directly use the LiveView JavaScript code.
|
|
121
|
+
- Thanks to [Donnie Flood](https://github.com/floodfx) for the encouragement, inspiration, help, and even pull requests to get this project started! Check out [LiveViewJS](https://github.com/floodfx/liveviewjs) for a TypeScript implementation of LiveView (that's much more mature than this one!)
|
|
122
|
+
|
|
123
|
+
- Thanks to [Darren Mulholland](https://github.com/dmulholl) for both his [Let's Build a Template Language](https://www.dmulholl.com/lets-build/a-template-language.html) tutorial, as well as his [ibis template engine](https://github.com/dmulholl/ibis), which he very generously released into the public domain, and forms the basis of templating in PyView.
|
|
124
|
+
|
|
125
|
+
## Additional Thanks
|
|
126
|
+
|
|
127
|
+
- We're using the [pubsub implementation from flet](https://github.com/flet-dev/flet)
|
|
128
|
+
- PyView is built on top of [Starlette](https://www.starlette.io/).
|
|
129
|
+
|
|
130
|
+
# Status
|
|
131
|
+
|
|
132
|
+
PyView is in the very early stages of active development. Please check it out and give feedback! Note that the API is likely to change, and there are many features that are not yet implemented.
|
|
133
|
+
|
|
134
|
+
# Running the included Examples
|
|
135
|
+
|
|
136
|
+
## Setup
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
poetry install
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Running
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
poetry run uvicorn examples.app:app --reload
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Then go to http://localhost:8000/
|
|
149
|
+
|
|
150
|
+
### Poetry Install
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
brew install pipx
|
|
154
|
+
pipx install poetry
|
|
155
|
+
pipx ensurepath
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
(see https://python-poetry.org/docs/#installation for more details)
|
|
159
|
+
|
|
160
|
+
# License
|
|
161
|
+
|
|
162
|
+
PyView is licensed under the [MIT License](LICENSE).
|
|
163
|
+
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "pyview-web"
|
|
3
|
+
|
|
4
|
+
packages = [
|
|
5
|
+
{ include = "pyview" },
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
version = "0.7.2"
|
|
9
|
+
description = "LiveView in Python"
|
|
10
|
+
authors = ["Larry Ogrodnek <ogrodnek@gmail.com>"]
|
|
11
|
+
license = "MIT"
|
|
12
|
+
readme = "readme.md"
|
|
13
|
+
homepage = "https://pyview.rocks"
|
|
14
|
+
repository = "https://github.com/ogrodnek/pyview"
|
|
15
|
+
keywords = ["web", "api", "LiveView"]
|
|
16
|
+
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Intended Audience :: Information Technology",
|
|
19
|
+
"Intended Audience :: System Administrators",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python",
|
|
23
|
+
"Topic :: Internet",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
"Topic :: Software Development :: Libraries",
|
|
27
|
+
"Topic :: Software Development",
|
|
28
|
+
"Typing :: Typed",
|
|
29
|
+
"Development Status :: 4 - Beta",
|
|
30
|
+
"Environment :: Web Environment",
|
|
31
|
+
"Framework :: AsyncIO",
|
|
32
|
+
"Framework :: Pydantic",
|
|
33
|
+
"Intended Audience :: Developers",
|
|
34
|
+
"License :: OSI Approved :: MIT License",
|
|
35
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
36
|
+
"Programming Language :: Python :: 3.11",
|
|
37
|
+
"Programming Language :: Python :: 3.12",
|
|
38
|
+
"Programming Language :: Python :: 3.13",
|
|
39
|
+
"Programming Language :: Python :: 3.14",
|
|
40
|
+
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
|
41
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
42
|
+
]
|
|
43
|
+
|
|
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"
|
|
53
|
+
|
|
54
|
+
[tool.poetry.scripts]
|
|
55
|
+
pv = "pyview.cli.main:cli"
|
|
56
|
+
|
|
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"
|
|
63
|
+
|
|
64
|
+
[tool.poetry.group.profiling.dependencies]
|
|
65
|
+
scalene = {version = "^1.5.51", python = "!=3.11.0,>=3.11"}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
[build-system]
|
|
69
|
+
requires = ["poetry-core"]
|
|
70
|
+
build-backend = "poetry.core.masonry.api"
|
|
71
|
+
|
|
72
|
+
[tool.pyright]
|
|
73
|
+
exclude = [
|
|
74
|
+
".venv",
|
|
75
|
+
"examples",
|
|
76
|
+
"examples/.venv",
|
|
77
|
+
"**/vendor",
|
|
78
|
+
"**/node_modules",
|
|
79
|
+
"**/__pycache__",
|
|
80
|
+
# T-string files require Python 3.14+
|
|
81
|
+
"tests/test_live_view_template.py",
|
|
82
|
+
"tests/test_template_view.py",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
[tool.pytest.ini_options]
|
|
86
|
+
asyncio_mode = "auto"
|
|
87
|
+
|
|
88
|
+
[tool.ruff]
|
|
89
|
+
line-length = 100
|
|
90
|
+
target-version = "py311"
|
|
91
|
+
exclude = [
|
|
92
|
+
# T-string files require Python 3.14+ syntax
|
|
93
|
+
"pyview/template/__init__.py",
|
|
94
|
+
"pyview/template/template_view.py",
|
|
95
|
+
"pyview/template/live_view_template.py",
|
|
96
|
+
"examples/views/count/count_tstring.py",
|
|
97
|
+
"tests/test_live_view_template.py",
|
|
98
|
+
"tests/test_template_view.py",
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
[tool.ruff.lint]
|
|
102
|
+
select = [
|
|
103
|
+
"E", # pycodestyle errors
|
|
104
|
+
"W", # pycodestyle warnings
|
|
105
|
+
"F", # pyflakes
|
|
106
|
+
"I", # isort
|
|
107
|
+
"N", # pep8-naming
|
|
108
|
+
"UP", # pyupgrade
|
|
109
|
+
"B", # flake8-bugbear
|
|
110
|
+
"C4", # flake8-comprehensions
|
|
111
|
+
"SIM", # flake8-simplify
|
|
112
|
+
]
|
|
113
|
+
ignore = [
|
|
114
|
+
"E501", # line too long (handled by formatter)
|
|
115
|
+
"UP007", # use-pep604-annotation (Union[X, Y] -> X | Y)
|
|
116
|
+
"UP035", # deprecated-import (keep typing.Iterator instead of collections.abc.Iterator)
|
|
117
|
+
"UP045", # non-pep604-annotation (keep Optional[X] instead of X | None)
|
|
118
|
+
"N802", # mixed case function names
|
|
119
|
+
"N803", # mixed case variable
|
|
120
|
+
"N806", # mixed case variable in function
|
|
121
|
+
"N815", # mixed case attributes
|
|
122
|
+
"N818", # exception naming (keep AuthException, not AuthError)
|
|
123
|
+
"N999", # invalid module name (allow PascalCase module names)
|
|
124
|
+
"SIM108", # prefer if-else blocks over ternary operators for readability
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
[tool.ruff.lint.isort]
|
|
128
|
+
known-first-party = ["pyview"]
|
|
129
|
+
|
|
130
|
+
[tool.ruff.lint.per-file-ignores]
|
|
131
|
+
"pyview/vendor/**" = ["E722", "F811", "N818", "SIM102", "SIM103", "SIM118", "UP031"] # Ignore vendored code issues
|
|
132
|
+
|
|
133
|
+
[tool.ruff.format]
|
|
134
|
+
quote-style = "double"
|
|
135
|
+
indent-style = "space"
|
|
136
|
+
line-ending = "auto"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from pyview.js import JsCommand
|
|
2
|
+
from pyview.live_socket import (
|
|
3
|
+
ConnectedLiveViewSocket,
|
|
4
|
+
LiveViewSocket,
|
|
5
|
+
UnconnectedSocket,
|
|
6
|
+
is_connected,
|
|
7
|
+
)
|
|
8
|
+
from pyview.live_view import LiveView
|
|
9
|
+
from pyview.playground import playground
|
|
10
|
+
from pyview.pyview import PyView, RootTemplate, RootTemplateContext, defaultRootTemplate
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"LiveView",
|
|
14
|
+
"LiveViewSocket",
|
|
15
|
+
"PyView",
|
|
16
|
+
"defaultRootTemplate",
|
|
17
|
+
"JsCommand",
|
|
18
|
+
"RootTemplateContext",
|
|
19
|
+
"RootTemplate",
|
|
20
|
+
"is_connected",
|
|
21
|
+
"ConnectedLiveViewSocket",
|
|
22
|
+
"UnconnectedSocket",
|
|
23
|
+
"playground",
|
|
24
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// We import the CSS which is extracted to its own file by esbuild.
|
|
2
|
+
// Remove this line if you add a your own CSS build pipeline (e.g postcss).
|
|
3
|
+
//import "../css/app.css";
|
|
4
|
+
|
|
5
|
+
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
|
|
6
|
+
// to get started and then uncomment the line below.
|
|
7
|
+
// import "./user_socket.js"
|
|
8
|
+
|
|
9
|
+
// You can include dependencies in two ways.
|
|
10
|
+
//
|
|
11
|
+
// The simplest option is to put them in assets/vendor and
|
|
12
|
+
// import them using relative paths:
|
|
13
|
+
//
|
|
14
|
+
// import "./vendor/some-package.js"
|
|
15
|
+
//
|
|
16
|
+
// Alternatively, you can `npm install some-package` and import
|
|
17
|
+
// them using a path starting with the package name:
|
|
18
|
+
//
|
|
19
|
+
// import "some-package"
|
|
20
|
+
//
|
|
21
|
+
|
|
22
|
+
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
|
|
23
|
+
import "phoenix_html";
|
|
24
|
+
// Establish Phoenix Socket and LiveView configuration.
|
|
25
|
+
import { Socket } from "phoenix";
|
|
26
|
+
import { LiveSocket } from "phoenix_live_view";
|
|
27
|
+
import NProgress from "nprogress";
|
|
28
|
+
|
|
29
|
+
let Hooks = window.Hooks ?? {};
|
|
30
|
+
|
|
31
|
+
let scrollAt = () => {
|
|
32
|
+
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
|
|
33
|
+
let scrollHeight =
|
|
34
|
+
document.documentElement.scrollHeight || document.body.scrollHeight;
|
|
35
|
+
let clientHeight = document.documentElement.clientHeight;
|
|
36
|
+
|
|
37
|
+
return (scrollTop / (scrollHeight - clientHeight)) * 100;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
Hooks.InfiniteScroll = {
|
|
41
|
+
page() {
|
|
42
|
+
return this.el.dataset.page;
|
|
43
|
+
},
|
|
44
|
+
mounted() {
|
|
45
|
+
this.pending = this.page();
|
|
46
|
+
window.addEventListener("scroll", (e) => {
|
|
47
|
+
if (this.pending == this.page() && scrollAt() > 90) {
|
|
48
|
+
this.pending = this.page() + 1;
|
|
49
|
+
this.pushEvent("load-more", {});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
updated() {
|
|
54
|
+
this.pending = this.page();
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
let csrfToken = document
|
|
59
|
+
.querySelector("meta[name='csrf-token']")
|
|
60
|
+
.getAttribute("content");
|
|
61
|
+
let liveSocket = new LiveSocket("/live", Socket, {
|
|
62
|
+
hooks: Hooks,
|
|
63
|
+
params: { _csrf_token: csrfToken },
|
|
64
|
+
uploaders: window.Uploaders || {},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Show progress bar on live navigation and form submits
|
|
68
|
+
//topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" });
|
|
69
|
+
window.addEventListener("phx:page-loading-start", (info) => NProgress.start());
|
|
70
|
+
window.addEventListener("phx:page-loading-stop", (info) => NProgress.done());
|
|
71
|
+
|
|
72
|
+
// connect if there are any LiveViews on the page
|
|
73
|
+
liveSocket.connect();
|
|
74
|
+
|
|
75
|
+
// expose liveSocket on window for web console debug logs and latency simulation:
|
|
76
|
+
// >> liveSocket.enableDebug()
|
|
77
|
+
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
|
|
78
|
+
// >> liveSocket.disableLatencySim()
|
|
79
|
+
window.liveSocket = liveSocket;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PyView External S3 Uploaders
|
|
3
|
+
*
|
|
4
|
+
* Client-side uploaders for external S3 uploads.
|
|
5
|
+
*
|
|
6
|
+
* Available uploaders:
|
|
7
|
+
* - S3: Simple POST upload to S3 using presigned POST URLs
|
|
8
|
+
* - S3Multipart: Multipart upload for large files (>5GB)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
window.Uploaders = window.Uploaders || {};
|
|
12
|
+
|
|
13
|
+
// S3 Simple POST uploader
|
|
14
|
+
// Uses presigned POST URLs for direct upload to S3
|
|
15
|
+
// Works for files up to ~5GB
|
|
16
|
+
if (!window.Uploaders.S3) {
|
|
17
|
+
window.Uploaders.S3 = function (entries, onViewError) {
|
|
18
|
+
entries.forEach((entry) => {
|
|
19
|
+
let formData = new FormData();
|
|
20
|
+
let { url, fields } = entry.meta;
|
|
21
|
+
|
|
22
|
+
// Add all fields from presigned POST
|
|
23
|
+
Object.entries(fields).forEach(([key, val]) =>
|
|
24
|
+
formData.append(key, val)
|
|
25
|
+
);
|
|
26
|
+
formData.append("file", entry.file);
|
|
27
|
+
|
|
28
|
+
let xhr = new XMLHttpRequest();
|
|
29
|
+
onViewError(() => xhr.abort());
|
|
30
|
+
|
|
31
|
+
xhr.onload = () => {
|
|
32
|
+
if (xhr.status === 204 || xhr.status === 200) {
|
|
33
|
+
entry.progress(100);
|
|
34
|
+
} else {
|
|
35
|
+
entry.error(`S3 upload failed with status ${xhr.status}`);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
xhr.onerror = () => entry.error("Network error during upload");
|
|
39
|
+
|
|
40
|
+
xhr.upload.addEventListener("progress", (event) => {
|
|
41
|
+
if (event.lengthComputable) {
|
|
42
|
+
let percent = Math.round((event.loaded / event.total) * 100);
|
|
43
|
+
if (percent < 100) {
|
|
44
|
+
entry.progress(percent);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
xhr.open("POST", url, true);
|
|
50
|
+
xhr.send(formData);
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// S3 Multipart uploader for large files
|
|
56
|
+
// Uploads file in chunks with retry logic and concurrency control
|
|
57
|
+
//
|
|
58
|
+
// - Exponential backoff retry (max 3 attempts per part)
|
|
59
|
+
// - Concurrency limit (max 6 parallel uploads)
|
|
60
|
+
// - Automatic cleanup on fatal errors
|
|
61
|
+
//
|
|
62
|
+
// Based on AWS best practices:
|
|
63
|
+
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html
|
|
64
|
+
//
|
|
65
|
+
// Server must:
|
|
66
|
+
// 1. Return metadata with: uploader="S3Multipart", upload_id, part_urls, chunk_size
|
|
67
|
+
// 2. Provide entry_complete callback to finalize the upload
|
|
68
|
+
if (!window.Uploaders.S3Multipart) {
|
|
69
|
+
window.Uploaders.S3Multipart = function (entries, onViewError) {
|
|
70
|
+
entries.forEach((entry) => {
|
|
71
|
+
const { upload_id, part_urls, chunk_size, key } = entry.meta;
|
|
72
|
+
const file = entry.file;
|
|
73
|
+
const parts = []; // Store {PartNumber, ETag} for each uploaded part
|
|
74
|
+
|
|
75
|
+
const MAX_RETRIES = 3;
|
|
76
|
+
const MAX_CONCURRENT = 6;
|
|
77
|
+
let uploadedParts = 0;
|
|
78
|
+
let activeUploads = 0;
|
|
79
|
+
let partIndex = 0;
|
|
80
|
+
let hasError = false;
|
|
81
|
+
const totalParts = part_urls.length;
|
|
82
|
+
|
|
83
|
+
console.log(`[S3Multipart] Starting upload for ${entry.file.name}`);
|
|
84
|
+
console.log(`[S3Multipart] Total parts: ${totalParts}, chunk size: ${chunk_size}`);
|
|
85
|
+
console.log(`[S3Multipart] Max concurrent uploads: ${MAX_CONCURRENT}, max retries: ${MAX_RETRIES}`);
|
|
86
|
+
|
|
87
|
+
// Add a custom method to send completion data directly
|
|
88
|
+
// This bypasses entry.progress() which only handles numbers
|
|
89
|
+
entry.complete = function(completionData) {
|
|
90
|
+
console.log(`[S3Multipart] Calling entry.complete with:`, completionData);
|
|
91
|
+
// Call pushFileProgress directly with the completion data
|
|
92
|
+
entry.view.pushFileProgress(entry.fileEl, entry.ref, completionData);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Upload a single part with retry logic
|
|
96
|
+
const uploadPart = (index, retryCount = 0) => {
|
|
97
|
+
if (hasError) return; // Stop if we've hit a fatal error
|
|
98
|
+
|
|
99
|
+
const partNumber = index + 1;
|
|
100
|
+
const url = part_urls[index];
|
|
101
|
+
const start = index * chunk_size;
|
|
102
|
+
const end = Math.min(start + chunk_size, file.size);
|
|
103
|
+
const chunk = file.slice(start, end);
|
|
104
|
+
|
|
105
|
+
console.log(`[S3Multipart] Starting part ${partNumber}/${totalParts}, size: ${chunk.size} bytes, attempt ${retryCount + 1}`);
|
|
106
|
+
|
|
107
|
+
const xhr = new XMLHttpRequest();
|
|
108
|
+
onViewError(() => xhr.abort());
|
|
109
|
+
|
|
110
|
+
// Track upload progress within this chunk
|
|
111
|
+
xhr.upload.addEventListener("progress", (event) => {
|
|
112
|
+
if (event.lengthComputable) {
|
|
113
|
+
// Calculate overall progress: completed parts + current part's progress
|
|
114
|
+
const completedBytes = uploadedParts * chunk_size;
|
|
115
|
+
const currentPartBytes = event.loaded;
|
|
116
|
+
const totalBytes = file.size;
|
|
117
|
+
const overallPercent = Math.round(((completedBytes + currentPartBytes) / totalBytes) * 100);
|
|
118
|
+
|
|
119
|
+
// Don't report 100% until all parts complete and we send completion data
|
|
120
|
+
if (overallPercent < 100) {
|
|
121
|
+
entry.progress(overallPercent);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
xhr.onload = () => {
|
|
127
|
+
activeUploads--;
|
|
128
|
+
|
|
129
|
+
if (xhr.status === 200) {
|
|
130
|
+
const etag = xhr.getResponseHeader('ETag');
|
|
131
|
+
console.log(`[S3Multipart] Part ${partNumber} succeeded, ETag: ${etag}`);
|
|
132
|
+
|
|
133
|
+
if (!etag) {
|
|
134
|
+
console.error(`[S3Multipart] Part ${partNumber} missing ETag!`);
|
|
135
|
+
entry.error(`Part ${partNumber} upload succeeded but no ETag returned`);
|
|
136
|
+
hasError = true;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Store the part with its ETag
|
|
141
|
+
parts.push({
|
|
142
|
+
PartNumber: partNumber,
|
|
143
|
+
ETag: etag.replace(/"/g, '')
|
|
144
|
+
});
|
|
145
|
+
uploadedParts++;
|
|
146
|
+
|
|
147
|
+
// Update progress
|
|
148
|
+
const progressPercent = Math.round((uploadedParts / totalParts) * 100);
|
|
149
|
+
console.log(`[S3Multipart] Progress: ${uploadedParts}/${totalParts} parts (${progressPercent}%)`);
|
|
150
|
+
|
|
151
|
+
if (uploadedParts < totalParts) {
|
|
152
|
+
entry.progress(progressPercent < 100 ? progressPercent : 99);
|
|
153
|
+
uploadNextPart(); // Start next part
|
|
154
|
+
} else {
|
|
155
|
+
// All parts complete!
|
|
156
|
+
const completionData = {
|
|
157
|
+
complete: true,
|
|
158
|
+
upload_id: upload_id,
|
|
159
|
+
key: key,
|
|
160
|
+
parts: parts.sort((a, b) => a.PartNumber - b.PartNumber)
|
|
161
|
+
};
|
|
162
|
+
console.log(`[S3Multipart] All parts complete! Sending completion data`);
|
|
163
|
+
entry.complete(completionData);
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
// Upload failed - retry with exponential backoff
|
|
167
|
+
console.error(`[S3Multipart] Part ${partNumber} failed with status ${xhr.status}, attempt ${retryCount + 1}`);
|
|
168
|
+
|
|
169
|
+
if (retryCount < MAX_RETRIES) {
|
|
170
|
+
// Exponential backoff: 1s, 2s, 4s, max 10s
|
|
171
|
+
const delay = Math.min(1000 * (2 ** retryCount), 10000);
|
|
172
|
+
console.log(`[S3Multipart] Retrying part ${partNumber} in ${delay}ms...`);
|
|
173
|
+
|
|
174
|
+
setTimeout(() => {
|
|
175
|
+
uploadPart(index, retryCount + 1);
|
|
176
|
+
}, delay);
|
|
177
|
+
} else {
|
|
178
|
+
// Max retries exceeded - fatal error
|
|
179
|
+
console.error(`[S3Multipart] Part ${partNumber} failed after ${MAX_RETRIES} retries, aborting upload`);
|
|
180
|
+
entry.error(`Part ${partNumber} failed after ${MAX_RETRIES} attempts. Upload aborted.`);
|
|
181
|
+
hasError = true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
xhr.onerror = () => {
|
|
187
|
+
activeUploads--;
|
|
188
|
+
console.error(`[S3Multipart] Network error on part ${partNumber}, attempt ${retryCount + 1}`);
|
|
189
|
+
|
|
190
|
+
if (retryCount < MAX_RETRIES) {
|
|
191
|
+
const delay = Math.min(1000 * (2 ** retryCount), 10000);
|
|
192
|
+
console.log(`[S3Multipart] Retrying part ${partNumber} after network error in ${delay}ms...`);
|
|
193
|
+
|
|
194
|
+
setTimeout(() => {
|
|
195
|
+
uploadPart(index, retryCount + 1);
|
|
196
|
+
}, delay);
|
|
197
|
+
} else {
|
|
198
|
+
console.error(`[S3Multipart] Part ${partNumber} network error after ${MAX_RETRIES} retries, aborting upload`);
|
|
199
|
+
entry.error(`Part ${partNumber} network error after ${MAX_RETRIES} attempts. Upload aborted.`);
|
|
200
|
+
hasError = true;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
xhr.open('PUT', url, true);
|
|
205
|
+
xhr.send(chunk);
|
|
206
|
+
activeUploads++;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Upload next part if we haven't hit the concurrency limit
|
|
210
|
+
const uploadNextPart = () => {
|
|
211
|
+
while (partIndex < totalParts && activeUploads < MAX_CONCURRENT && !hasError) {
|
|
212
|
+
uploadPart(partIndex);
|
|
213
|
+
partIndex++;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Start initial batch of uploads
|
|
218
|
+
uploadNextPart();
|
|
219
|
+
});
|
|
220
|
+
};
|
|
221
|
+
}
|