pyview-web 0.0.6a0__py3-none-any.whl → 0.0.8a0__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.

@@ -0,0 +1,2 @@
1
+ from .required import requires
2
+ from .provider import AuthProvider, AllowAllAuthProvider, AuthProviderFactory
@@ -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
@@ -0,0 +1,33 @@
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
+
8
+ _CallableType = typing.TypeVar("_CallableType", bound=typing.Callable)
9
+
10
+
11
+ @dataclass
12
+ class RequiredScopeAuthProvider(AuthProvider):
13
+ scopes: typing.Sequence[str]
14
+ status_code: int = 403
15
+ redirect: typing.Optional[str] = None
16
+
17
+ def wrap(self, func: _CallableType) -> _CallableType:
18
+ return starlette_requires(self.scopes, self.status_code, self.redirect)(func)
19
+
20
+ def has_required_auth(self, websocket: WebSocket) -> bool:
21
+ return has_required_scope(websocket, self.scopes)
22
+
23
+
24
+ def requires(
25
+ scopes: typing.Union[str, typing.Sequence[str]],
26
+ status_code: int = 403,
27
+ redirect: typing.Optional[str] = None,
28
+ ) -> typing.Callable[[type[LiveView]], type[LiveView]]:
29
+ def decorator(cls: type[LiveView]) -> type[LiveView]:
30
+ scopes_list = [scopes] if isinstance(scopes, str) else list(scopes)
31
+ return AuthProviderFactory.set(cls, RequiredScopeAuthProvider(scopes_list, status_code, redirect))
32
+
33
+ return decorator
pyview/csrf.py CHANGED
@@ -1,15 +1,14 @@
1
1
  from itsdangerous import BadData, SignatureExpired, URLSafeTimedSerializer
2
- import secrets
3
2
  from typing import Optional
4
3
  import hmac
5
- import os
4
+ from pyview.secret import get_secret
6
5
 
7
6
 
8
7
  def generate_csrf_token(value: str, salt: Optional[str] = None) -> str:
9
8
  """
10
9
  Generate a CSRF token.
11
10
  """
12
- s = URLSafeTimedSerializer(_get_secret(), salt=salt or "pyview-csrf-token")
11
+ s = URLSafeTimedSerializer(get_secret(), salt=salt or "pyview-csrf-token")
13
12
  return s.dumps(value) # type: ignore
14
13
 
15
14
 
@@ -17,27 +16,10 @@ def validate_csrf_token(data: str, expected: str, salt: Optional[str] = None) ->
17
16
  """
18
17
  Validate a CSRF token.
19
18
  """
20
- s = URLSafeTimedSerializer(_get_secret(), salt=salt or "pyview-csrf-token")
19
+ s = URLSafeTimedSerializer(get_secret(), salt=salt or "pyview-csrf-token")
21
20
  try:
22
21
  token = s.loads(data, max_age=3600)
23
22
  return hmac.compare_digest(token, expected)
24
23
  except (BadData, SignatureExpired) as e:
25
24
  print(e)
26
25
  return False
27
-
28
-
29
- _SECRET = None
30
-
31
-
32
- def _get_secret() -> str:
33
- """
34
- Get the secret key from the environment, or generate a new one.
35
- """
36
- global _SECRET
37
- if _SECRET is None:
38
- secret = os.environ.get("PYVIEW_SECRET")
39
- if secret is None:
40
- secret = secrets.token_urlsafe(16)
41
- _SECRET = secret
42
-
43
- return _SECRET
pyview/live_view.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import TypeVar, Generic, Optional, Union
1
+ from typing import TypeVar, Generic, Optional, Union, Any
2
2
  from .live_socket import LiveViewSocket, UnconnectedSocket
3
3
  from pyview.template import LiveTemplate, template_file, RenderedContent, LiveRender
4
4
  import inspect
@@ -7,13 +7,14 @@ from pyview.events import InfoEvent
7
7
  T = TypeVar("T")
8
8
 
9
9
  AnySocket = Union[LiveViewSocket[T], UnconnectedSocket[T]]
10
+ Session = dict[str, Any]
10
11
 
11
12
 
12
13
  class LiveView(Generic[T]):
13
14
  def __init__(self):
14
15
  pass
15
16
 
16
- async def mount(self, socket: AnySocket):
17
+ async def mount(self, socket: AnySocket, session: Session):
17
18
  pass
18
19
 
19
20
  async def handle_event(self, event, payload, socket: LiveViewSocket[T]):
pyview/pyview.py CHANGED
@@ -3,26 +3,18 @@ from fastapi import WebSocket
3
3
  from fastapi.responses import HTMLResponse
4
4
  from starlette.middleware.gzip import GZipMiddleware
5
5
  from starlette.routing import Route
6
+ from starlette.requests import Request
6
7
  import uuid
7
8
  from urllib.parse import parse_qs
8
9
 
9
10
  from pyview.live_socket import UnconnectedSocket
10
11
  from pyview.csrf import generate_csrf_token
12
+ from pyview.session import serialize_session
13
+ from pyview.auth import AuthProviderFactory
11
14
  from .ws_handler import LiveSocketHandler
12
15
  from .live_view import LiveView
13
16
  from .live_routes import LiveViewLookup
14
- from typing import Callable, Optional, TypedDict
15
-
16
-
17
- class RootTemplateContext(TypedDict):
18
- id: str
19
- content: str
20
- title: Optional[str]
21
- css: Optional[str]
22
- csrf_token: str
23
-
24
-
25
- RootTemplate = Callable[[RootTemplateContext], str]
17
+ from .template import RootTemplate, RootTemplateContext, defaultRootTemplate
26
18
 
27
19
 
28
20
  class PyView(Starlette):
@@ -30,7 +22,7 @@ class PyView(Starlette):
30
22
 
31
23
  def __init__(self, *args, **kwargs):
32
24
  super().__init__(*args, **kwargs)
33
- self.rootTemplate = defaultRootTemplate("")
25
+ self.rootTemplate = defaultRootTemplate()
34
26
  self.view_lookup = LiveViewLookup()
35
27
  self.live_handler = LiveSocketHandler(self.view_lookup)
36
28
 
@@ -40,24 +32,28 @@ class PyView(Starlette):
40
32
  self.add_websocket_route("/live/websocket", live_websocket_endpoint)
41
33
  self.add_middleware(GZipMiddleware)
42
34
 
43
- def add_live_view(self, path: str, view: Callable[[], LiveView]):
44
- async def lv(request):
35
+ def add_live_view(self, path: str, view: type[LiveView]):
36
+ async def lv(request: Request):
45
37
  return await liveview_container(
46
38
  self.rootTemplate, self.view_lookup, request
47
39
  )
48
40
 
49
41
  self.view_lookup.add(path, view)
50
- self.routes.append(Route(path, lv, methods=["GET"]))
42
+ auth = AuthProviderFactory.get(view)
43
+ self.routes.append(Route(path, auth.wrap(lv), methods=["GET"]))
51
44
 
52
45
 
53
46
  async def liveview_container(
54
- template: RootTemplate, view_lookup: LiveViewLookup, request
47
+ template: RootTemplate, view_lookup: LiveViewLookup, request: Request
55
48
  ):
56
49
  url = request.url
57
50
  path = url.path
58
51
  lv: LiveView = view_lookup.get(path)
59
52
  s = UnconnectedSocket()
60
- await lv.mount(s)
53
+
54
+ session = request.session if "session" in request.scope else {}
55
+
56
+ await lv.mount(s, session)
61
57
  await lv.handle_params(url, parse_qs(url.query), s)
62
58
  r = await lv.render(s.context)
63
59
 
@@ -68,52 +64,7 @@ async def liveview_container(
68
64
  "content": r.text(),
69
65
  "title": s.live_title,
70
66
  "csrf_token": generate_csrf_token("lv:phx-" + id),
71
- "css": None,
67
+ "session": serialize_session(session),
72
68
  }
73
69
 
74
70
  return HTMLResponse(template(context))
75
-
76
-
77
- def defaultRootTemplate(css: str) -> RootTemplate:
78
- def template(context: RootTemplateContext) -> str:
79
- context["css"] = css
80
- return _defaultRootTemplate(context)
81
-
82
- return template
83
-
84
-
85
- def _defaultRootTemplate(context: RootTemplateContext) -> str:
86
- suffix = " | LiveView"
87
- render_title = (
88
- (context["title"] + suffix) # type: ignore
89
- if context.get("title", None) is not None
90
- else "LiveView"
91
- )
92
- css = context["css"] if context.get("css", None) is not None else ""
93
- return f"""
94
- <!DOCTYPE html>
95
- <html lang="en">
96
- <head>
97
- <title data-suffix="{suffix}">{render_title}</title>
98
- <meta name="csrf-token" content="{context['csrf_token']}" />
99
- <meta charset="utf-8">
100
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
101
- <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
102
- <script defer type="text/javascript" src="/static/assets/app.js"></script>
103
- {css}
104
- </head>
105
- <body>
106
- <div>
107
- <a href="/">Home</a>
108
- <div
109
- data-phx-main="true"
110
- data-phx-session=""
111
- data-phx-static=""
112
- id="phx-{context['id']}"
113
- >
114
- {context['content']}
115
- </div>
116
- </div>
117
- </body>
118
- </html>
119
- """
pyview/secret.py ADDED
@@ -0,0 +1,18 @@
1
+ import os
2
+ import secrets
3
+
4
+ _SECRET = None
5
+
6
+
7
+ def get_secret() -> str:
8
+ """
9
+ Get the secret key from the environment, or generate a new one.
10
+ """
11
+ global _SECRET
12
+ if _SECRET is None:
13
+ secret = os.environ.get("PYVIEW_SECRET")
14
+ if secret is None:
15
+ secret = secrets.token_urlsafe(16)
16
+ _SECRET = secret
17
+
18
+ return _SECRET
pyview/session.py ADDED
@@ -0,0 +1,13 @@
1
+ from typing import Any, cast
2
+ from itsdangerous import URLSafeSerializer
3
+ from pyview.secret import get_secret
4
+
5
+
6
+ def serialize_session(session: dict[str, Any]) -> str:
7
+ s = URLSafeSerializer(get_secret(), salt="pyview-session")
8
+ return cast(str, s.dumps(session))
9
+
10
+
11
+ def deserialize_session(ser: str) -> dict[str, Any]:
12
+ s = URLSafeSerializer(get_secret(), salt="pyview-session")
13
+ return cast(dict[str, Any], s.loads(ser))
@@ -1,2 +1,3 @@
1
1
  from pyview.vendor.ibis import Template
2
2
  from .live_template import LiveTemplate, template_file, RenderedContent, LiveRender
3
+ from .root_template import RootTemplate, RootTemplateContext, defaultRootTemplate
@@ -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
@@ -5,6 +5,12 @@ from urllib.parse import urlparse, parse_qs
5
5
  from pyview.live_socket import LiveViewSocket
6
6
  from pyview.live_routes import LiveViewLookup
7
7
  from pyview.csrf import validate_csrf_token
8
+ from pyview.session import deserialize_session
9
+ from pyview.auth import AuthProviderFactory
10
+
11
+
12
+ class AuthException(Exception):
13
+ pass
8
14
 
9
15
 
10
16
  class LiveSocketHandler:
@@ -13,6 +19,10 @@ class LiveSocketHandler:
13
19
  self.manager = ConnectionManager()
14
20
  self.sessions = 0
15
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
+
16
26
  async def handle(self, websocket: WebSocket):
17
27
  await self.manager.connect(websocket)
18
28
 
@@ -24,14 +34,19 @@ class LiveSocketHandler:
24
34
  data = await websocket.receive_text()
25
35
  [joinRef, mesageRef, topic, event, payload] = json.loads(data)
26
36
  if event == "phx_join":
37
+ if not validate_csrf_token(payload["params"]["_csrf_token"], topic):
38
+ raise AuthException("Invalid CSRF token")
39
+
27
40
  url = urlparse(payload["url"])
28
41
  lv = self.routes.get(url.path)
42
+ await self.check_auth(websocket, lv)
29
43
  socket = LiveViewSocket(websocket, topic, lv)
30
44
 
31
- if not validate_csrf_token(payload["params"]["_csrf_token"], topic):
32
- raise Exception("Invalid CSRF token")
45
+ session = {}
46
+ if "session" in payload:
47
+ session = deserialize_session(payload["session"])
33
48
 
34
- await lv.mount(socket)
49
+ await lv.mount(socket, session)
35
50
  await lv.handle_params(url, parse_qs(url.query), socket)
36
51
 
37
52
  rendered = await _render(socket)
@@ -48,10 +63,12 @@ class LiveSocketHandler:
48
63
  await self.handle_connected(socket)
49
64
 
50
65
  except WebSocketDisconnect:
51
- self.manager.disconnect(websocket)
52
66
  if socket:
53
67
  await socket.close()
54
68
  self.sessions -= 1
69
+ except AuthException:
70
+ await websocket.close()
71
+ self.sessions -= 1
55
72
 
56
73
  async def handle_connected(self, socket: LiveViewSocket):
57
74
  while True:
@@ -66,9 +83,7 @@ class LiveSocketHandler:
66
83
  "phx_reply",
67
84
  {"response": {}, "status": "ok"},
68
85
  ]
69
- await self.manager.send_personal_message(
70
- json.dumps(resp), socket.websocket
71
- )
86
+ await self.manager.send_personal_message(json.dumps(resp), socket.websocket)
72
87
  continue
73
88
 
74
89
  if event == "event":
@@ -86,9 +101,7 @@ class LiveSocketHandler:
86
101
  "phx_reply",
87
102
  {"response": {"diff": rendered}, "status": "ok"},
88
103
  ]
89
- await self.manager.send_personal_message(
90
- json.dumps(resp), socket.websocket
91
- )
104
+ await self.manager.send_personal_message(json.dumps(resp), socket.websocket)
92
105
  continue
93
106
 
94
107
  if event == "live_patch":
@@ -105,9 +118,7 @@ class LiveSocketHandler:
105
118
  "phx_reply",
106
119
  {"response": {"diff": rendered}, "status": "ok"},
107
120
  ]
108
- await self.manager.send_personal_message(
109
- json.dumps(resp), socket.websocket
110
- )
121
+ await self.manager.send_personal_message(json.dumps(resp), socket.websocket)
111
122
  continue
112
123
 
113
124
 
@@ -123,18 +134,10 @@ async def _render(socket: LiveViewSocket):
123
134
 
124
135
  class ConnectionManager:
125
136
  def __init__(self):
126
- self.active_connections: list[WebSocket] = []
137
+ pass
127
138
 
128
139
  async def connect(self, websocket: WebSocket):
129
140
  await websocket.accept()
130
- self.active_connections.append(websocket)
131
-
132
- def disconnect(self, websocket: WebSocket):
133
- self.active_connections.remove(websocket)
134
141
 
135
142
  async def send_personal_message(self, message: str, websocket: WebSocket):
136
143
  await websocket.send_text(message)
137
-
138
- async def broadcast(self, message: str):
139
- for connection in self.active_connections:
140
- await connection.send_text(message)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyview-web
3
- Version: 0.0.6a0
3
+ Version: 0.0.8a0
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
@@ -79,7 +77,7 @@ class CountContext(TypedDict):
79
77
 
80
78
 
81
79
  class CountLiveView(LiveView[CountContext]):
82
- async def mount(self, socket: LiveViewSocket[CountContext]):
80
+ async def mount(self, socket: LiveViewSocket[CountContext], _session):
83
81
  socket.context = {"count": 0}
84
82
 
85
83
  async def handle_event(self, event, payload, socket: LiveViewSocket[CountContext]):
@@ -2,18 +2,24 @@ 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=4Xat6-LePwm82NhBABnp4KFznxj9rRYU1yevW9u7DRs,1223
5
8
  pyview/changesets/__init__.py,sha256=55CLari2JHZtwy4hapHe7CqUyKjcP4dkM_t5d3CY2gU,46
6
9
  pyview/changesets/changesets.py,sha256=B1q1nXwI2iuZZQpE3P2T0PpwI21PHjqcsuIQmkKPCvI,1747
7
- pyview/csrf.py,sha256=2TIN1fI3IlSbwp1OCfK3WQosIwX6ad35I5Qv4SLxsv0,1119
10
+ pyview/csrf.py,sha256=VIURva9EJqXXYGC7engweh3SwDQCnHlhV2zWdcdnFqc,789
8
11
  pyview/events.py,sha256=Zv8G2F1XeXUk1wrnfomeFfxB0OPYmHdjSvxRjQew3No,125
9
12
  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
- pyview/live_view.py,sha256=asoZ-klppjOQFT9vU5UHOoINE61UxiJFZWhbij9uwio,1207
13
- pyview/pyview.py,sha256=oB5XKmuRyy0fLrhxxox5xNaK2pshTiiRcvks9YWYdx8,3423
15
+ pyview/live_view.py,sha256=RRhj89NMianv6dkYd9onOQEJgpRF-pqUb7nmbQr6R6E,1255
16
+ pyview/pyview.py,sha256=LdW2irgsd4KRls5TLU22DZJIy5Pmo_2L-ajOwXi9cEs,2302
17
+ pyview/secret.py,sha256=HbaNpGAkFs4uxMVAmk9HwE3FIehg7dmwEOlED7C9moM,363
18
+ pyview/session.py,sha256=nC8ExyVwfCgQfx9T-aJGyFhr2C7jsrEY_QFkaXtP28U,432
14
19
  pyview/static/assets/app.js,sha256=vyD-RACuZxOBWGy7VD-BY5-qkgbFyJM43-M-WGgaAHo,199984
15
- pyview/template/__init__.py,sha256=qQDtCWlOZ_trRUj2LdmV5mE1b5ThGPlovrrX5jkNgjE,124
20
+ pyview/template/__init__.py,sha256=bDaxDV7QhUwBXfy672Ft0NBDNvhT4kEKDV5VUWhADe8,206
16
21
  pyview/template/live_template.py,sha256=wSKyBw7ejpUY5qXUZdE36Jeeix8Of0CUq8eZdQwxXyg,1864
22
+ pyview/template/root_template.py,sha256=0U50QIBhLVshLEog0SCbTDWgRjMpkfNDVcqowKFmGSQ,1876
17
23
  pyview/template/serializer.py,sha256=WDZfqJr2LMlf36fUW2CmWc2aREc63553_y_GRP2-qYc,826
18
24
  pyview/test_csrf.py,sha256=QWTOtfagDMkoYDK_ehYxua34F7-ltPsSeTwQGEOlqHU,684
19
25
  pyview/vendor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -29,8 +35,8 @@ pyview/vendor/ibis/nodes.py,sha256=jNRmlTCHXC4xCF8nfDRlLWINlaYiFa8NGrzebJFJgEM,2
29
35
  pyview/vendor/ibis/template.py,sha256=IX9z-Ig13yJyRnMqtB52eiRLe002qdIxnfa7fYEXLqM,2314
30
36
  pyview/vendor/ibis/tree.py,sha256=5LAjl3q9iPMZBb6QbKurWj9-QGKLVf11K2_bQotWlUc,2293
31
37
  pyview/vendor/ibis/utils.py,sha256=nLSaxPR9vMphzV9qinlz_Iurv9c49Ps6Knv8vyNlewU,2768
32
- pyview/ws_handler.py,sha256=ow-kFreFHC05FGGE3kdg8VjtSvQ8W02eUJXKaUZv5dc,4624
33
- pyview_web-0.0.6a0.dist-info/LICENSE,sha256=M_bADaBm9_MV9llX3lCicksLhwk3eZUjA2srE0uUWr0,1071
34
- pyview_web-0.0.6a0.dist-info/METADATA,sha256=LhV-OjTqX-HCnWFZ83vqZua3sbdFLuLaLxKgtHDN9nk,5059
35
- pyview_web-0.0.6a0.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
36
- pyview_web-0.0.6a0.dist-info/RECORD,,
38
+ pyview/ws_handler.py,sha256=tokN9gtC_Gn1tlPTxfhYGSFfquiSUvD3sHigzjh1gYM,4738
39
+ pyview_web-0.0.8a0.dist-info/LICENSE,sha256=M_bADaBm9_MV9llX3lCicksLhwk3eZUjA2srE0uUWr0,1071
40
+ pyview_web-0.0.8a0.dist-info/METADATA,sha256=xURzdj61BS-VXiF-eDfi1wO330RonBEhPOb9rThSbR4,4970
41
+ pyview_web-0.0.8a0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
42
+ pyview_web-0.0.8a0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.5.2
2
+ Generator: poetry-core 1.9.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any