pyview-web 0.0.7a0__py3-none-any.whl → 0.0.9a0__py3-none-any.whl
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.
Potentially problematic release.
This version of pyview-web might be problematic. Click here for more details.
- pyview/auth/__init__.py +2 -0
- pyview/auth/provider.py +32 -0
- pyview/auth/required.py +43 -0
- pyview/pyview.py +12 -63
- pyview/template/__init__.py +1 -0
- pyview/template/root_template.py +71 -0
- pyview/ws_handler.py +17 -13
- {pyview_web-0.0.7a0.dist-info → pyview_web-0.0.9a0.dist-info}/METADATA +15 -5
- {pyview_web-0.0.7a0.dist-info → pyview_web-0.0.9a0.dist-info}/RECORD +11 -7
- {pyview_web-0.0.7a0.dist-info → pyview_web-0.0.9a0.dist-info}/WHEEL +1 -1
- {pyview_web-0.0.7a0.dist-info → pyview_web-0.0.9a0.dist-info}/LICENSE +0 -0
pyview/auth/__init__.py
ADDED
pyview/auth/provider.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from typing import Protocol, TypeVar, Callable
|
|
2
|
+
from starlette.websockets import WebSocket
|
|
3
|
+
from pyview import LiveView
|
|
4
|
+
|
|
5
|
+
_CallableType = TypeVar("_CallableType", bound=Callable)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AuthProvider(Protocol):
|
|
9
|
+
async def has_required_auth(self, websocket: WebSocket) -> bool:
|
|
10
|
+
...
|
|
11
|
+
|
|
12
|
+
def wrap(self, func: _CallableType) -> _CallableType:
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AllowAllAuthProvider(AuthProvider):
|
|
17
|
+
async def has_required_auth(self, websocket: WebSocket) -> bool:
|
|
18
|
+
return True
|
|
19
|
+
|
|
20
|
+
def wrap(self, func: _CallableType) -> _CallableType:
|
|
21
|
+
return func
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AuthProviderFactory:
|
|
25
|
+
@classmethod
|
|
26
|
+
def get(cls, lv: type[LiveView]) -> AuthProvider:
|
|
27
|
+
return getattr(lv, "__pyview_auth_provider__", AllowAllAuthProvider())
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def set(cls, lv: type[LiveView], auth_provider: AuthProvider) -> type[LiveView]:
|
|
31
|
+
setattr(lv, "__pyview_auth_provider__", auth_provider)
|
|
32
|
+
return lv
|
pyview/auth/required.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from starlette.websockets import WebSocket
|
|
4
|
+
from starlette.authentication import requires as starlette_requires, has_required_scope
|
|
5
|
+
from .provider import AuthProvider, AuthProviderFactory
|
|
6
|
+
from pyview import LiveView
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
if sys.version_info >= (3, 10): # pragma: no cover
|
|
10
|
+
from typing import ParamSpec
|
|
11
|
+
else: # pragma: no cover
|
|
12
|
+
from typing_extensions import ParamSpec
|
|
13
|
+
|
|
14
|
+
_P = ParamSpec("_P")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class RequiredScopeAuthProvider(AuthProvider):
|
|
19
|
+
scopes: typing.Sequence[str]
|
|
20
|
+
status_code: int = 403
|
|
21
|
+
redirect: typing.Optional[str] = None
|
|
22
|
+
|
|
23
|
+
def wrap(
|
|
24
|
+
self, func: typing.Callable[_P, typing.Any]
|
|
25
|
+
) -> typing.Callable[_P, typing.Any]:
|
|
26
|
+
return starlette_requires(self.scopes, self.status_code, self.redirect)(func)
|
|
27
|
+
|
|
28
|
+
async def has_required_auth(self, websocket: WebSocket) -> bool:
|
|
29
|
+
return has_required_scope(websocket, self.scopes)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def requires(
|
|
33
|
+
scopes: typing.Union[str, typing.Sequence[str]],
|
|
34
|
+
status_code: int = 403,
|
|
35
|
+
redirect: typing.Optional[str] = None,
|
|
36
|
+
) -> typing.Callable[[type[LiveView]], type[LiveView]]:
|
|
37
|
+
def decorator(cls: type[LiveView]) -> type[LiveView]:
|
|
38
|
+
scopes_list = [scopes] if isinstance(scopes, str) else list(scopes)
|
|
39
|
+
return AuthProviderFactory.set(
|
|
40
|
+
cls, RequiredScopeAuthProvider(scopes_list, status_code, redirect)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return decorator
|
pyview/pyview.py
CHANGED
|
@@ -2,7 +2,6 @@ from starlette.applications import Starlette
|
|
|
2
2
|
from fastapi import WebSocket
|
|
3
3
|
from fastapi.responses import HTMLResponse
|
|
4
4
|
from starlette.middleware.gzip import GZipMiddleware
|
|
5
|
-
from starlette.middleware.sessions import SessionMiddleware
|
|
6
5
|
from starlette.routing import Route
|
|
7
6
|
from starlette.requests import Request
|
|
8
7
|
import uuid
|
|
@@ -11,23 +10,11 @@ from urllib.parse import parse_qs
|
|
|
11
10
|
from pyview.live_socket import UnconnectedSocket
|
|
12
11
|
from pyview.csrf import generate_csrf_token
|
|
13
12
|
from pyview.session import serialize_session
|
|
14
|
-
from pyview.
|
|
13
|
+
from pyview.auth import AuthProviderFactory
|
|
15
14
|
from .ws_handler import LiveSocketHandler
|
|
16
15
|
from .live_view import LiveView
|
|
17
16
|
from .live_routes import LiveViewLookup
|
|
18
|
-
from
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class RootTemplateContext(TypedDict):
|
|
22
|
-
id: str
|
|
23
|
-
content: str
|
|
24
|
-
title: Optional[str]
|
|
25
|
-
css: Optional[str]
|
|
26
|
-
csrf_token: str
|
|
27
|
-
session: Optional[str]
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
RootTemplate = Callable[[RootTemplateContext], str]
|
|
17
|
+
from .template import RootTemplate, RootTemplateContext, defaultRootTemplate
|
|
31
18
|
|
|
32
19
|
|
|
33
20
|
class PyView(Starlette):
|
|
@@ -35,7 +22,7 @@ class PyView(Starlette):
|
|
|
35
22
|
|
|
36
23
|
def __init__(self, *args, **kwargs):
|
|
37
24
|
super().__init__(*args, **kwargs)
|
|
38
|
-
self.rootTemplate = defaultRootTemplate(
|
|
25
|
+
self.rootTemplate = defaultRootTemplate()
|
|
39
26
|
self.view_lookup = LiveViewLookup()
|
|
40
27
|
self.live_handler = LiveSocketHandler(self.view_lookup)
|
|
41
28
|
|
|
@@ -44,17 +31,21 @@ class PyView(Starlette):
|
|
|
44
31
|
|
|
45
32
|
self.add_websocket_route("/live/websocket", live_websocket_endpoint)
|
|
46
33
|
self.add_middleware(GZipMiddleware)
|
|
47
|
-
# self.add_middleware(SessionMiddleware, secret_key=get_secret())
|
|
48
34
|
|
|
49
|
-
def add_live_view(self, path: str, view:
|
|
35
|
+
def add_live_view(self, path: str, view: type[LiveView]):
|
|
50
36
|
async def lv(request: Request):
|
|
51
|
-
return await liveview_container(
|
|
37
|
+
return await liveview_container(
|
|
38
|
+
self.rootTemplate, self.view_lookup, request
|
|
39
|
+
)
|
|
52
40
|
|
|
53
41
|
self.view_lookup.add(path, view)
|
|
54
|
-
|
|
42
|
+
auth = AuthProviderFactory.get(view)
|
|
43
|
+
self.routes.append(Route(path, auth.wrap(lv), methods=["GET"]))
|
|
55
44
|
|
|
56
45
|
|
|
57
|
-
async def liveview_container(
|
|
46
|
+
async def liveview_container(
|
|
47
|
+
template: RootTemplate, view_lookup: LiveViewLookup, request: Request
|
|
48
|
+
):
|
|
58
49
|
url = request.url
|
|
59
50
|
path = url.path
|
|
60
51
|
lv: LiveView = view_lookup.get(path)
|
|
@@ -73,49 +64,7 @@ async def liveview_container(template: RootTemplate, view_lookup: LiveViewLookup
|
|
|
73
64
|
"content": r.text(),
|
|
74
65
|
"title": s.live_title,
|
|
75
66
|
"csrf_token": generate_csrf_token("lv:phx-" + id),
|
|
76
|
-
"css": None,
|
|
77
67
|
"session": serialize_session(session),
|
|
78
68
|
}
|
|
79
69
|
|
|
80
70
|
return HTMLResponse(template(context))
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def defaultRootTemplate(css: str) -> RootTemplate:
|
|
84
|
-
def template(context: RootTemplateContext) -> str:
|
|
85
|
-
context["css"] = css
|
|
86
|
-
return _defaultRootTemplate(context)
|
|
87
|
-
|
|
88
|
-
return template
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _defaultRootTemplate(context: RootTemplateContext) -> str:
|
|
92
|
-
suffix = " | LiveView"
|
|
93
|
-
render_title = (context["title"] + suffix) if context.get("title", None) is not None else "LiveView" # type: ignore
|
|
94
|
-
css = context["css"] if context.get("css", None) is not None else ""
|
|
95
|
-
return f"""
|
|
96
|
-
<!DOCTYPE html>
|
|
97
|
-
<html lang="en">
|
|
98
|
-
<head>
|
|
99
|
-
<title data-suffix="{suffix}">{render_title}</title>
|
|
100
|
-
<meta name="csrf-token" content="{context['csrf_token']}" />
|
|
101
|
-
<meta charset="utf-8">
|
|
102
|
-
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
103
|
-
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
104
|
-
<script defer type="text/javascript" src="/static/assets/app.js"></script>
|
|
105
|
-
{css}
|
|
106
|
-
</head>
|
|
107
|
-
<body>
|
|
108
|
-
<div>
|
|
109
|
-
<a href="/">Home</a>
|
|
110
|
-
<div
|
|
111
|
-
data-phx-main="true"
|
|
112
|
-
data-phx-session="{context['session']}"
|
|
113
|
-
data-phx-static=""
|
|
114
|
-
id="phx-{context['id']}"
|
|
115
|
-
>
|
|
116
|
-
{context['content']}
|
|
117
|
-
</div>
|
|
118
|
-
</div>
|
|
119
|
-
</body>
|
|
120
|
-
</html>
|
|
121
|
-
"""
|
pyview/template/__init__.py
CHANGED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from typing import Callable, Optional, TypedDict
|
|
2
|
+
from markupsafe import Markup
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class RootTemplateContext(TypedDict):
|
|
6
|
+
id: str
|
|
7
|
+
content: str
|
|
8
|
+
title: Optional[str]
|
|
9
|
+
csrf_token: str
|
|
10
|
+
session: Optional[str]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
RootTemplate = Callable[[RootTemplateContext], str]
|
|
14
|
+
ContentWrapper = Callable[[RootTemplateContext, Markup], Markup]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def defaultRootTemplate(
|
|
18
|
+
css: Optional[Markup] = None, content_wrapper: Optional[ContentWrapper] = None
|
|
19
|
+
) -> RootTemplate:
|
|
20
|
+
content_wrapper = content_wrapper or (lambda c, m: m)
|
|
21
|
+
|
|
22
|
+
def template(context: RootTemplateContext) -> str:
|
|
23
|
+
return _defaultRootTemplate(context, css or Markup(""), content_wrapper)
|
|
24
|
+
|
|
25
|
+
return template
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _defaultRootTemplate(
|
|
29
|
+
context: RootTemplateContext, css: Markup, contentWrapper: ContentWrapper
|
|
30
|
+
) -> str:
|
|
31
|
+
suffix = " | LiveView"
|
|
32
|
+
render_title = (context["title"] + suffix) if context.get("title", None) is not None else "LiveView" # type: ignore
|
|
33
|
+
main_content = contentWrapper(
|
|
34
|
+
context,
|
|
35
|
+
Markup(
|
|
36
|
+
f"""
|
|
37
|
+
<div
|
|
38
|
+
data-phx-main="true"
|
|
39
|
+
data-phx-session="{context['session']}"
|
|
40
|
+
data-phx-static=""
|
|
41
|
+
id="phx-{context['id']}"
|
|
42
|
+
>
|
|
43
|
+
{context['content']}
|
|
44
|
+
</div>"""
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
Markup(
|
|
50
|
+
f"""
|
|
51
|
+
<!DOCTYPE html>
|
|
52
|
+
<html lang="en">
|
|
53
|
+
<head>
|
|
54
|
+
<title data-suffix="{suffix}">{render_title}</title>
|
|
55
|
+
<meta name="csrf-token" content="{context['csrf_token']}" />
|
|
56
|
+
<meta charset="utf-8">
|
|
57
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
58
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
59
|
+
<script defer type="text/javascript" src="/static/assets/app.js"></script>
|
|
60
|
+
{css}
|
|
61
|
+
</head>
|
|
62
|
+
<body>"""
|
|
63
|
+
)
|
|
64
|
+
+ main_content
|
|
65
|
+
+ Markup(
|
|
66
|
+
"""
|
|
67
|
+
</body>
|
|
68
|
+
</html>
|
|
69
|
+
"""
|
|
70
|
+
)
|
|
71
|
+
)
|
pyview/ws_handler.py
CHANGED
|
@@ -6,6 +6,11 @@ from pyview.live_socket import LiveViewSocket
|
|
|
6
6
|
from pyview.live_routes import LiveViewLookup
|
|
7
7
|
from pyview.csrf import validate_csrf_token
|
|
8
8
|
from pyview.session import deserialize_session
|
|
9
|
+
from pyview.auth import AuthProviderFactory
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AuthException(Exception):
|
|
13
|
+
pass
|
|
9
14
|
|
|
10
15
|
|
|
11
16
|
class LiveSocketHandler:
|
|
@@ -14,6 +19,10 @@ class LiveSocketHandler:
|
|
|
14
19
|
self.manager = ConnectionManager()
|
|
15
20
|
self.sessions = 0
|
|
16
21
|
|
|
22
|
+
async def check_auth(self, websocket: WebSocket, lv):
|
|
23
|
+
if not await AuthProviderFactory.get(lv).has_required_auth(websocket):
|
|
24
|
+
raise AuthException()
|
|
25
|
+
|
|
17
26
|
async def handle(self, websocket: WebSocket):
|
|
18
27
|
await self.manager.connect(websocket)
|
|
19
28
|
|
|
@@ -25,13 +34,14 @@ class LiveSocketHandler:
|
|
|
25
34
|
data = await websocket.receive_text()
|
|
26
35
|
[joinRef, mesageRef, topic, event, payload] = json.loads(data)
|
|
27
36
|
if event == "phx_join":
|
|
37
|
+
if not validate_csrf_token(payload["params"]["_csrf_token"], topic):
|
|
38
|
+
raise AuthException("Invalid CSRF token")
|
|
39
|
+
|
|
28
40
|
url = urlparse(payload["url"])
|
|
29
41
|
lv = self.routes.get(url.path)
|
|
42
|
+
await self.check_auth(websocket, lv)
|
|
30
43
|
socket = LiveViewSocket(websocket, topic, lv)
|
|
31
44
|
|
|
32
|
-
if not validate_csrf_token(payload["params"]["_csrf_token"], topic):
|
|
33
|
-
raise Exception("Invalid CSRF token")
|
|
34
|
-
|
|
35
45
|
session = {}
|
|
36
46
|
if "session" in payload:
|
|
37
47
|
session = deserialize_session(payload["session"])
|
|
@@ -53,10 +63,12 @@ class LiveSocketHandler:
|
|
|
53
63
|
await self.handle_connected(socket)
|
|
54
64
|
|
|
55
65
|
except WebSocketDisconnect:
|
|
56
|
-
self.manager.disconnect(websocket)
|
|
57
66
|
if socket:
|
|
58
67
|
await socket.close()
|
|
59
68
|
self.sessions -= 1
|
|
69
|
+
except AuthException:
|
|
70
|
+
await websocket.close()
|
|
71
|
+
self.sessions -= 1
|
|
60
72
|
|
|
61
73
|
async def handle_connected(self, socket: LiveViewSocket):
|
|
62
74
|
while True:
|
|
@@ -122,18 +134,10 @@ async def _render(socket: LiveViewSocket):
|
|
|
122
134
|
|
|
123
135
|
class ConnectionManager:
|
|
124
136
|
def __init__(self):
|
|
125
|
-
|
|
137
|
+
pass
|
|
126
138
|
|
|
127
139
|
async def connect(self, websocket: WebSocket):
|
|
128
140
|
await websocket.accept()
|
|
129
|
-
self.active_connections.append(websocket)
|
|
130
|
-
|
|
131
|
-
def disconnect(self, websocket: WebSocket):
|
|
132
|
-
self.active_connections.remove(websocket)
|
|
133
141
|
|
|
134
142
|
async def send_personal_message(self, message: str, websocket: WebSocket):
|
|
135
143
|
await websocket.send_text(message)
|
|
136
|
-
|
|
137
|
-
async def broadcast(self, message: str):
|
|
138
|
-
for connection in self.active_connections:
|
|
139
|
-
await connection.send_text(message)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pyview-web
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.9a0
|
|
4
4
|
Summary: LiveView in Python
|
|
5
5
|
Home-page: https://pyview.rocks
|
|
6
6
|
License: MIT
|
|
@@ -22,10 +22,8 @@ Classifier: Programming Language :: Python
|
|
|
22
22
|
Classifier: Programming Language :: Python :: 3
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.10
|
|
24
24
|
Classifier: Programming Language :: Python :: 3.11
|
|
25
|
-
Classifier: Programming Language :: Python :: 3
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
26
26
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
27
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
28
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
29
27
|
Classifier: Programming Language :: Python :: 3.9
|
|
30
28
|
Classifier: Topic :: Internet
|
|
31
29
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
@@ -36,7 +34,7 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
|
36
34
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
37
35
|
Classifier: Typing :: Typed
|
|
38
36
|
Requires-Dist: APScheduler (==3.9.1.post1)
|
|
39
|
-
Requires-Dist: fastapi (==0.
|
|
37
|
+
Requires-Dist: fastapi (==0.111.0)
|
|
40
38
|
Requires-Dist: itsdangerous (>=2.1.2,<3.0.0)
|
|
41
39
|
Requires-Dist: markupsafe (>=2.1.2,<3.0.0)
|
|
42
40
|
Requires-Dist: psutil (>=5.9.4,<6.0.0)
|
|
@@ -59,10 +57,22 @@ PyView enables dynamic, real-time web apps, using server-rendered HTML.
|
|
|
59
57
|
|
|
60
58
|
`pip install pyview-web`
|
|
61
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
|
+
|
|
62
68
|
# Live Examples
|
|
63
69
|
|
|
64
70
|
[https://examples.pyview.rocks/](https://examples.pyview.rocks/)
|
|
65
71
|
|
|
72
|
+
## Other Examples
|
|
73
|
+
|
|
74
|
+
- [pyview AI chat](https://github.com/pyview/pyview-example-ai-chat)
|
|
75
|
+
|
|
66
76
|
## Simple Counter
|
|
67
77
|
|
|
68
78
|
[See it live!](https://examples.pyview.rocks/count)
|
|
@@ -2,6 +2,9 @@ pyview/__init__.py,sha256=MN3RrkiFdM6bOZV6qJgvwifXV81Qx9k_PN2p8zJvq2I,223
|
|
|
2
2
|
pyview/assets/js/app.js,sha256=n7FiM_VoxZ3ZUkLjtyxbOnSZU88zZkN1A5b8jjhiFoo,2508
|
|
3
3
|
pyview/assets/package-lock.json,sha256=kFCrEUJc3G7VD7EsBQf6__EKQhaKAok-I5rrwiAoX0w,2425
|
|
4
4
|
pyview/assets/package.json,sha256=E6xaX8KMUAektIIedLmI55jGnmlNMSeD2tgKYXWk1vg,151
|
|
5
|
+
pyview/auth/__init__.py,sha256=vMlirETRhD4va61NOzwg8VY8ep9wVOF96GznJGBmzD0,109
|
|
6
|
+
pyview/auth/provider.py,sha256=fwriy2JZcOStutVXD-8VlMPAFXjILCM0l08lhTgmuyE,935
|
|
7
|
+
pyview/auth/required.py,sha256=ZtNmLFth9nK39RxDiJkSzArXwS5Cvr55MUAzfJ1F2e0,1418
|
|
5
8
|
pyview/changesets/__init__.py,sha256=55CLari2JHZtwy4hapHe7CqUyKjcP4dkM_t5d3CY2gU,46
|
|
6
9
|
pyview/changesets/changesets.py,sha256=B1q1nXwI2iuZZQpE3P2T0PpwI21PHjqcsuIQmkKPCvI,1747
|
|
7
10
|
pyview/csrf.py,sha256=VIURva9EJqXXYGC7engweh3SwDQCnHlhV2zWdcdnFqc,789
|
|
@@ -10,12 +13,13 @@ pyview/js.py,sha256=4OnPEfBfuvmekeQlm9444As4PLR22zLMIyyzQIIkmls,751
|
|
|
10
13
|
pyview/live_routes.py,sha256=tsKFh2gmH2BWsjsZQZErzRp_-KiAZcn4lFKNLRIN5Nc,498
|
|
11
14
|
pyview/live_socket.py,sha256=p3eTynzGtvLiooOZyTemmXTrusAJKaoueuZ6Q5gfeYk,3314
|
|
12
15
|
pyview/live_view.py,sha256=RRhj89NMianv6dkYd9onOQEJgpRF-pqUb7nmbQr6R6E,1255
|
|
13
|
-
pyview/pyview.py,sha256=
|
|
16
|
+
pyview/pyview.py,sha256=LdW2irgsd4KRls5TLU22DZJIy5Pmo_2L-ajOwXi9cEs,2302
|
|
14
17
|
pyview/secret.py,sha256=HbaNpGAkFs4uxMVAmk9HwE3FIehg7dmwEOlED7C9moM,363
|
|
15
18
|
pyview/session.py,sha256=nC8ExyVwfCgQfx9T-aJGyFhr2C7jsrEY_QFkaXtP28U,432
|
|
16
19
|
pyview/static/assets/app.js,sha256=vyD-RACuZxOBWGy7VD-BY5-qkgbFyJM43-M-WGgaAHo,199984
|
|
17
|
-
pyview/template/__init__.py,sha256=
|
|
20
|
+
pyview/template/__init__.py,sha256=bDaxDV7QhUwBXfy672Ft0NBDNvhT4kEKDV5VUWhADe8,206
|
|
18
21
|
pyview/template/live_template.py,sha256=wSKyBw7ejpUY5qXUZdE36Jeeix8Of0CUq8eZdQwxXyg,1864
|
|
22
|
+
pyview/template/root_template.py,sha256=0U50QIBhLVshLEog0SCbTDWgRjMpkfNDVcqowKFmGSQ,1876
|
|
19
23
|
pyview/template/serializer.py,sha256=WDZfqJr2LMlf36fUW2CmWc2aREc63553_y_GRP2-qYc,826
|
|
20
24
|
pyview/test_csrf.py,sha256=QWTOtfagDMkoYDK_ehYxua34F7-ltPsSeTwQGEOlqHU,684
|
|
21
25
|
pyview/vendor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -31,8 +35,8 @@ pyview/vendor/ibis/nodes.py,sha256=jNRmlTCHXC4xCF8nfDRlLWINlaYiFa8NGrzebJFJgEM,2
|
|
|
31
35
|
pyview/vendor/ibis/template.py,sha256=IX9z-Ig13yJyRnMqtB52eiRLe002qdIxnfa7fYEXLqM,2314
|
|
32
36
|
pyview/vendor/ibis/tree.py,sha256=5LAjl3q9iPMZBb6QbKurWj9-QGKLVf11K2_bQotWlUc,2293
|
|
33
37
|
pyview/vendor/ibis/utils.py,sha256=nLSaxPR9vMphzV9qinlz_Iurv9c49Ps6Knv8vyNlewU,2768
|
|
34
|
-
pyview/ws_handler.py,sha256=
|
|
35
|
-
pyview_web-0.0.
|
|
36
|
-
pyview_web-0.0.
|
|
37
|
-
pyview_web-0.0.
|
|
38
|
-
pyview_web-0.0.
|
|
38
|
+
pyview/ws_handler.py,sha256=tokN9gtC_Gn1tlPTxfhYGSFfquiSUvD3sHigzjh1gYM,4738
|
|
39
|
+
pyview_web-0.0.9a0.dist-info/LICENSE,sha256=M_bADaBm9_MV9llX3lCicksLhwk3eZUjA2srE0uUWr0,1071
|
|
40
|
+
pyview_web-0.0.9a0.dist-info/METADATA,sha256=A5xUGWuuj2y8FvzoqsSynuwkD3osTc2ZO1HWzYzUm2U,5220
|
|
41
|
+
pyview_web-0.0.9a0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
42
|
+
pyview_web-0.0.9a0.dist-info/RECORD,,
|
|
File without changes
|