Formatting.

This commit is contained in:
retoor 2025-01-24 23:35:44 +01:00
parent 18b76ebd5e
commit 9b93403a93
28 changed files with 342 additions and 246 deletions

View File

@ -1,9 +1,10 @@
import pathlib
from types import SimpleNamespace
from aiohttp import web
from app.app import Application as BaseApplication
from snek.docs.app import Application as DocsApplication
from app.cache import time_cache_async
from snek.mapper import get_mappers
from snek.service import get_services
from snek.system import http
@ -17,7 +18,6 @@ from snek.view.login_form import LoginFormView
from snek.view.register import RegisterView
from snek.view.register_form import RegisterFormView
from snek.view.web import WebView
from types import SimpleNamespace
class Application(BaseApplication):
@ -60,7 +60,10 @@ class Application(BaseApplication):
self.router.add_get("/http-get", self.handle_http_get)
self.router.add_get("/http-photo", self.handle_http_photo)
self.add_subapp("/docs", DocsApplication(path=pathlib.Path(__file__).parent.joinpath("docs")))
self.add_subapp(
"/docs",
DocsApplication(path=pathlib.Path(__file__).parent.joinpath("docs")),
)
async def handle_test(self, request):
@ -80,7 +83,6 @@ class Application(BaseApplication):
body=path.read_bytes(), headers={"Content-Type": "image/png"}
)
# @time_cache_async(60)
async def render_template(self, template, request, context=None):
return await super().render_template(template, request, context)

View File

@ -1,4 +1,5 @@
from snek.system.form import Form, HTMLElement,FormInputElement,FormButtonElement
from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement
class LoginForm(Form):
@ -11,14 +12,16 @@ class LoginForm(Form):
max_length=20,
regex=r"^[a-zA-Z0-9_]+$",
place_holder="Username",
type="text"
type="text",
)
password = FormInputElement(
name="password",
required=True,
regex=r"^[a-zA-Z0-9_.+-]{6,}",
type="password",
place_holder="Password",
)
password = FormInputElement(name="password",required=True,regex=r"^[a-zA-Z0-9_.+-]{6,}",type="password",place_holder="Password")
action = FormButtonElement(
name="action",
value="submit",
text="Login",
type="button"
name="action", value="submit", text="Login", type="button"
)

View File

@ -1,4 +1,5 @@
from snek.system.form import Form, HTMLElement,FormInputElement,FormButtonElement
from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement
class UsernameField(FormInputElement):
@ -9,6 +10,7 @@ class UsernameField(FormInputElement):
result.append("Username is not available.")
return result
class RegisterForm(Form):
title = HTMLElement(tag="h1", text="Register")
@ -20,21 +22,23 @@ class RegisterForm(Form):
max_length=20,
regex=r"^[a-zA-Z0-9_]+$",
place_holder="Username",
type="text"
type="text",
)
email = FormInputElement(
name="email",
required=False,
regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$",
place_holder="Email address",
type="email"
type="email",
)
password = FormInputElement(
name="password",
required=True,
regex=r"^[a-zA-Z0-9_.+-]{6,}",
type="password",
place_holder="Password",
)
password = FormInputElement(name="password",required=True,regex=r"^[a-zA-Z0-9_.+-]{6,}",type="password",place_holder="Password")
action = FormButtonElement(
name="action",
value="submit",
text="Register",
type="button"
name="action", value="submit", text="Register", type="button"
)

View File

@ -1,12 +1,12 @@
import functools
from snek.mapper.user import UserMapper
@functools.cache
def get_mappers(app=None):
return dict(
user=UserMapper(app=app)
return {"user": UserMapper(app=app)}
)
def get_mapper(name, app=None):
return get_mappers(app=app)[name]

View File

@ -1,5 +1,6 @@
from snek.system.mapper import BaseMapper
from snek.model.user import UserModel
from snek.system.mapper import BaseMapper
class UserMapper(BaseMapper):
table_name = "user"

View File

@ -1,12 +1,12 @@
from snek.model.user import UserModel
import functools
from snek.model.user import UserModel
@functools.cache
def get_models():
return dict(
user=UserModel
return {"user": UserModel}
)
def get_model(name):
return get_models()[name]

View File

@ -1,5 +1,6 @@
from snek.system.model import BaseModel, ModelField
class UserModel(BaseModel):
username = ModelField(
@ -12,8 +13,6 @@ class UserModel(BaseModel):
email = ModelField(
name="email",
required=False,
regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$",
)
password = ModelField(name="password", required=True, regex=r"^[a-zA-Z0-9_.+-]{6,}")

View File

@ -1,12 +1,13 @@
from snek.service.user import UserService
import functools
from snek.service.user import UserService
@functools.cache
def get_services(app):
return dict(
user = UserService(app=app)
return {"user": UserService(app=app)}
)
def get_service(name, app=None):
return get_services(app=app)[name]

View File

@ -1,5 +1,6 @@
from snek.system.service import BaseService
from snek.system import security
from snek.system.service import BaseService
class UserService(BaseService):
mapper_name = "user"
@ -14,4 +15,3 @@ class UserService(BaseService):
if await self.save(model):
return model
raise Exception(f"Failed to create user: {model.errors}.")

View File

@ -1,8 +1,8 @@
import functools
cache = functools.cache
def async_cache(func):
cache = {}

View File

@ -26,8 +26,19 @@
from snek.system import model
class HTMLElement(model.ModelField):
def __init__(self, id=None, tag="div", name=None, html=None, class_name=None, text=None, *args, **kwargs):
def __init__(
self,
id=None,
tag="div",
name=None,
html=None,
class_name=None,
text=None,
*args,
**kwargs,
):
self.tag = tag
self.text = text
self.id = id
@ -37,16 +48,18 @@ class HTMLElement(model.ModelField):
async def to_json(self):
result = await super().to_json()
result['text'] = self.text
result['id'] = self.id
result['html'] = self.html
result['class_name'] = self.class_name
result['tag'] = self.tag
result["text"] = self.text
result["id"] = self.id
result["html"] = self.html
result["class_name"] = self.class_name
result["tag"] = self.tag
return result
class FormElement(HTMLElement):
pass
class FormInputElement(FormElement):
def __init__(self, type="text", place_holder=None, *args, **kwargs):
super().__init__(tag="input", *args, **kwargs)
@ -59,23 +72,25 @@ class FormInputElement(FormElement):
data["type"] = self.type
return data
class FormButtonElement(FormElement):
def __init__(self, tag="button", *args, **kwargs):
super().__init__(tag=tag, *args, **kwargs)
class Form(model.BaseModel):
@property
def html_elements(self):
return [element for element in self.fields if isinstance(element, HTMLElement)]
def set_user_data(self, data):
return super().set_user_data(data.get('fields'))
return super().set_user_data(data.get("fields"))
async def to_json(self, encode=False):
elements = await super().to_json()
html_elements = {}
for element in elements.keys():
if element == 'is_valid':
if element == "is_valid":
# is_valid is async get property so we can't do getattr on it
continue
field = getattr(self, element)
@ -85,8 +100,12 @@ class Form(model.BaseModel):
except KeyError:
pass
is_valid = all(field['is_valid'] for field in html_elements.values())
return dict(fields=html_elements, is_valid=is_valid, errors=await self.errors)
is_valid = all(field["is_valid"] for field in html_elements.values())
return {
"fields": html_elements,
"is_valid": is_valid,
"errors": await self.errors,
}
@property
async def errors(self):

View File

@ -24,17 +24,17 @@
# SOFTWARE.
from aiohttp import web
import aiohttp
from app.cache import time_cache_async
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import asyncio
import pathlib
import uuid
import imgkit
import asyncio
import zlib
import io
from urllib.parse import urljoin
import aiohttp
import imgkit
from app.cache import time_cache_async
from bs4 import BeautifulSoup
async def crc32(data):
try:
@ -43,6 +43,7 @@ async def crc32(data):
pass
return "crc32" + str(zlib.crc32(data))
async def get_file(name, suffix=".cache"):
name = await crc32(name)
path = pathlib.Path(".").joinpath("cache")
@ -50,11 +51,13 @@ async def get_file(name, suffix=".cache"):
path.mkdir(parents=True, exist_ok=True)
return path.joinpath(name + suffix)
async def public_touch(name=None):
path = pathlib.Path(".").joinpath(str(uuid.uuid4()) + name)
path.open("wb").close()
return path
async def create_site_photo(url):
loop = asyncio.get_event_loop()
if not url.startswith("https"):
@ -71,21 +74,23 @@ async def create_site_photo(url):
return await loop.run_in_executor(None, make_photo)
async def repair_links(base_url, html_content):
soup = BeautifulSoup(html_content, "html.parser")
for tag in soup.find_all(['a', 'img', 'link']):
if tag.has_attr('href') and not tag['href'].startswith("http"):
tag['href'] = urljoin(base_url, tag['href'])
if tag.has_attr('src') and not tag['src'].startswith("http"):
tag['src'] = urljoin(base_url, tag['src'])
for tag in soup.find_all(["a", "img", "link"]):
if tag.has_attr("href") and not tag["href"].startswith("http"):
tag["href"] = urljoin(base_url, tag["href"])
if tag.has_attr("src") and not tag["src"].startswith("http"):
tag["src"] = urljoin(base_url, tag["src"])
return soup.prettify()
async def is_html_content(content: bytes):
try:
content = content.decode(errors='ignore')
content = content.decode(errors="ignore")
except:
pass
marks = ['<html', '<img', '<p', '<span', '<div']
marks = ["<html", "<img", "<p", "<span", "<div"]
try:
content = content.lower()
for mark in marks:
@ -95,6 +100,7 @@ async def is_html_content(content: bytes):
print(ex)
return False
@time_cache_async(120)
async def get(url):
async with aiohttp.ClientSession() as session:

View File

@ -1,9 +1,8 @@
DEFAULT_LIMIT = 30
import typing
from snek.system.model import BaseModel
import types
class BaseMapper:
@ -29,8 +28,8 @@ class BaseMapper:
async def get(self, uid: str = None, **kwargs) -> BaseModel:
if uid:
kwargs['uid'] = uid
model = self.new()
kwargs["uid"] = uid
self.new()
record = self.table.find_one(**kwargs)
return await self.model_class.from_record(mapper=self, record=record)
@ -42,9 +41,9 @@ class BaseMapper:
async def save(self, model: BaseModel) -> bool:
record = await model.record
if not record.get('uid'):
if not record.get("uid"):
raise Exception(f"Attempt to save without uid: {record}.")
return self.table.upsert(record,['uid'])
return self.table.upsert(record, ["uid"])
async def find(self, **kwargs) -> typing.AsyncGenerator:
if not kwargs.get("_limit"):

View File

@ -1,29 +1,29 @@
# Original source: https://brandonjay.dev/posts/2021/render-markdown-html-in-python-with-jinja2
from types import SimpleNamespace
from mistune import escape
from mistune import Markdown
from mistune import HTMLRenderer
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import html
from pygments.styles import get_style_by_name
import functools
from app.cache import time_cache_async
from mistune import HTMLRenderer, Markdown
from pygments import highlight
from pygments.formatters import html
from pygments.lexers import get_lexer_by_name
class MarkdownRenderer(HTMLRenderer):
_allow_harmful_protocols = True
def __init__(self, app, template):
self.template = template
self.app = app
self.env = self.app.jinja2_env
formatter = html.HtmlFormatter()
self.env.globals['highlight_styles'] = formatter.get_style_defs()
self.env.globals["highlight_styles"] = formatter.get_style_defs()
def _escape(self, str):
return str ##escape(str)
def block_code(self, code, lang=None, info=None):
if not lang:
lang = info
@ -33,6 +33,7 @@ class MarkdownRenderer(HTMLRenderer):
lexer = get_lexer_by_name(lang, stripall=True)
formatter = html.HtmlFormatter(lineseparator="<br>")
return highlight(code, lexer, formatter)
def render(self):
markdown_string = self.app.template_path.joinpath(self.template).read_text()
renderer = MarkdownRenderer(self.app, self.template)
@ -40,23 +41,25 @@ class MarkdownRenderer(HTMLRenderer):
return markdown(markdown_string)
def render_markdown_sync(app, markdown_string):
renderer = MarkdownRenderer(app, None)
markdown = Markdown(renderer=renderer)
return markdown(markdown_string)
@time_cache_async(120)
async def render_markdown(app, markdown_string):
return render_markdown_sync(app, markdown_string)
from jinja2 import nodes, TemplateSyntaxError
from jinja2 import TemplateSyntaxError, nodes
from jinja2.ext import Extension
from jinja2.nodes import Const
# Source: https://ron.sh/how-to-write-a-jinja2-extension/
class MarkdownExtension(Extension):
tags = {'markdown'}
tags = {"markdown"}
def __init__(self, environment):
self.app = SimpleNamespace(jinja2_env=environment)
@ -64,13 +67,15 @@ class MarkdownExtension(Extension):
def parse(self, parser):
line_number = next(parser.stream).lineno
md_file = [Const('')]
body = ''
md_file = [Const("")]
body = ""
try:
md_file = [parser.parse_expression()]
except TemplateSyntaxError:
body = parser.parse_statements(['name:endmarkdown'], drop_needle=True)
return nodes.CallBlock(self.call_method('_to_html', md_file), [], [], body).set_lineno(line_number)
body = parser.parse_statements(["name:endmarkdown"], drop_needle=True)
return nodes.CallBlock(
self.call_method("_to_html", md_file), [], [], body
).set_lineno(line_number)
def _to_html(self, md_file, caller):
return render_markdown_sync(self.app, caller())

View File

@ -8,12 +8,14 @@
from aiohttp import web
@web.middleware
async def no_cors_middleware(request, handler):
response = await handler(request)
response.headers.pop("Access-Control-Allow-Origin", None)
return response
@web.middleware
async def cors_allow_middleware(request, handler):
response = await handler(request)
@ -22,12 +24,15 @@ async def cors_allow_middleware(request, handler):
response.headers["Access-Control-Allow-Headers"] = "*"
return response
@web.middleware
async def cors_middleware(request, handler):
if request.method == "OPTIONS":
response = web.Response()
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"
response.headers["Access-Control-Allow-Methods"] = (
"GET, POST, PUT, DELETE, OPTIONS"
)
response.headers["Access-Control-Allow-Headers"] = "*"
return response

View File

@ -25,12 +25,12 @@
# SOFTWARE.
import copy
import json
import re
import uuid
import json
from datetime import datetime, timezone
from collections import OrderedDict
import copy
from datetime import datetime, timezone
TIMESTAMP_REGEX = r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}\+\d{2}:\d{2}$"
@ -44,12 +44,21 @@ def add_attrs(**kwargs):
for key, value in kwargs.items():
setattr(func, key, value)
return func
return decorator
def validate_attrs(required=False, min_length=None, max_length=None, regex=None, **kwargs):
def validate_attrs(
required=False, min_length=None, max_length=None, regex=None, **kwargs
):
def decorator(func):
return add_attrs(required=required, min_length=min_length, max_length=max_length, regex=regex, **kwargs)(func)
return add_attrs(
required=required,
min_length=min_length,
max_length=max_length,
regex=regex,
**kwargs,
)(func)
class Validator:
@ -70,7 +79,21 @@ class Validator:
def custom_validation(self):
return True
def __init__(self, required=False, min_num=None, max_num=None, min_length=None, max_length=None, regex=None, value=None, kind=None, help_text=None, app=None, model=None, **kwargs):
def __init__(
self,
required=False,
min_num=None,
max_num=None,
min_length=None,
max_length=None,
regex=None,
value=None,
kind=None,
help_text=None,
app=None,
model=None,
**kwargs,
):
self.index = Validator._index
Validator._index += 1
self.app = app
@ -103,9 +126,13 @@ class Validator:
if self.max_num is not None and self.value > self.max_num:
error_list.append(f"Field should be maximal {self.max_num}.")
if self.min_length is not None and len(self.value) < self.min_length:
error_list.append(f"Field should be minimal {self.min_length} characters long.")
error_list.append(
f"Field should be minimal {self.min_length} characters long."
)
if self.max_length is not None and len(self.value) > self.max_length:
error_list.append(f"Field should be maximal {self.max_length} characters long.")
error_list.append(
f"Field should be maximal {self.max_length} characters long."
)
if self.regex and self.value and not re.match(self.regex, self.value):
error_list.append("Invalid value.")
if self.kind and not isinstance(self.value, self.kind):
@ -141,7 +168,7 @@ class Validator:
"help_text": self.help_text,
"errors": errors,
"is_valid": is_valid,
"index": self.index
"index": self.index,
}
@ -156,7 +183,7 @@ class ModelField(Validator):
async def to_json(self):
result = await super().to_json()
result['name'] = self.name
result["name"] = self.name
return result
@ -193,9 +220,18 @@ class UUIDField(ModelField):
class BaseModel:
uid = UUIDField(name="uid", required=True)
created_at = CreatedField(name="created_at", required=True, regex=TIMESTAMP_REGEX, place_holder="Created at")
updated_at = UpdatedField(name="updated_at", regex=TIMESTAMP_REGEX, place_holder="Updated at")
deleted_at = DeletedField(name="deleted_at", regex=TIMESTAMP_REGEX, place_holder="Deleted at")
created_at = CreatedField(
name="created_at",
required=True,
regex=TIMESTAMP_REGEX,
place_holder="Created at",
)
updated_at = UpdatedField(
name="updated_at", regex=TIMESTAMP_REGEX, place_holder="Updated at"
)
deleted_at = DeletedField(
name="deleted_at", regex=TIMESTAMP_REGEX, place_holder="Deleted at"
)
@classmethod
async def from_record(cls, record, mapper):
@ -233,10 +269,12 @@ class BaseModel:
if isinstance(obj, Validator):
self.__dict__[key] = copy.deepcopy(obj)
self.__dict__[key].value = kwargs.pop(key, self.__dict__[key].initial_value)
self.__dict__[key].value = kwargs.pop(
key, self.__dict__[key].initial_value
)
self.fields[key] = self.__dict__[key]
self.fields[key].model = self
self.fields[key].app = kwargs.get('app')
self.fields[key].app = kwargs.get("app")
def __setitem__(self, key, value):
obj = self.__dict__.get(key)
@ -254,17 +292,14 @@ class BaseModel:
field = self.fields.get(key)
if not field:
continue
if value.get('name'):
value = value.get('value')
if value.get("name"):
value = value.get("value")
field.value = value
@property
async def is_valid(self):
return all([await field.is_valid for field in self.fields.values()])
def __getitem__(self, key):
obj = self.__dict__.get(key)
if isinstance(obj, Validator):
@ -282,20 +317,22 @@ class BaseModel:
obj = await self.to_json()
record = {}
for key, value in obj.items():
if not isinstance(value, dict) or not 'value' in value:
if not isinstance(value, dict) or "value" not in value:
continue
if getattr(self, key).save:
record[key] = value.get('value')
record[key] = value.get("value")
return record
async def to_json(self, encode=False):
model_data = OrderedDict({
model_data = OrderedDict(
{
"uid": self.uid.value,
"created_at": self.created_at.value,
"updated_at": self.updated_at.value,
"deleted_at": self.deleted_at.value,
"is_valid": await self.is_valid
})
"is_valid": await self.is_valid,
}
)
for key, value in self.fields.items():
if key == "record":

View File

@ -2,6 +2,7 @@ import hashlib
DEFAULT_SALT = b"snekker-de-snek-"
async def hash(data, salt=DEFAULT_SALT):
try:
data = data.encode(errors="ignore")
@ -16,5 +17,6 @@ async def hash(data,salt=DEFAULT_SALT):
obj = hashlib.sha256(salted)
return obj.hexdigest()
async def verify(string: str, hashed: str):
return await hash(string) == hashed

View File

@ -1,9 +1,7 @@
from snek.mapper import get_mapper
from snek.system.mapper import BaseMapper
from snek.model.user import UserModel
from snek.system.mapper import BaseMapper
class BaseService:
@ -32,7 +30,6 @@ class BaseService:
# if model.is_valid: You Know why not
return await self.mapper.save(model) and True
async def find(self, **kwargs):
return await self.mapper.find(**kwargs)

View File

@ -2,6 +2,7 @@ from aiohttp import web
from snek.system.markdown import render_markdown
class BaseView(web.View):
@property
@ -17,10 +18,15 @@ class BaseView(web.View):
async def render_template(self, template_name, context=None):
if template_name.endswith(".md"):
response = await self.request.app.render_template(template_name,self.request,context)
response = await self.request.app.render_template(
template_name, self.request, context
)
body = await render_markdown(self.app, response.body.decode())
return web.Response(body=body, content_type="text/html")
return await self.request.app.render_template(template_name, self.request,context)
return await self.request.app.render_template(
template_name, self.request, context
)
class BaseFormView(BaseView):
@ -34,12 +40,12 @@ class BaseFormView(BaseView):
async def post(self):
form = self.form(app=self.app)
post = await self.request.json()
form.set_user_data(post['form'])
form.set_user_data(post["form"])
result = await form.to_json()
if post.get('action') == 'validate':
if post.get("action") == "validate":
# Pass
pass
if post.get('action') == 'submit' and result['is_valid']:
if post.get("action") == "submit" and result["is_valid"]:
await self.submit(form)
return await self.json_response(result)

View File

@ -9,8 +9,7 @@ Currently only some details about the internal API are available.
# of the snek.system.security module.
new_user_object = await app.service.user.register(
username="retoor",
password="retoorded"
username="retoor", password="retoorded"
)
```
@ -23,13 +22,14 @@ var1 = security.encrypt("data")
var2 = security.encrypt(b"data")
# Is correct:
assert(var1 == var2)
assert var1 == var2
```
## How to create a basic HTML / Markdown view
```python
from snek.system.view import BaseView
class IndexView(BaseView):
async def get(self):
@ -40,8 +40,9 @@ class IndexView(BaseView):
```
## How to create a FormView
```python
from snek.system.view import BaseFormView
from snek.form.register import RegisterForm
from snek.system.view import BaseFormView
class RegisterFormView(BaseFormView):

View File

@ -1,5 +1,3 @@
from snek.system.view import BaseView
@ -8,6 +6,7 @@ class AboutHTMLView(BaseView):
async def get(self):
return await self.render_template("about.html")
class AboutMDView(BaseView):
async def get(self):

View File

@ -1,6 +1,3 @@
from snek.system.view import BaseView
@ -9,6 +6,7 @@ class DocsHTMLView(BaseView):
async def get(self):
return await self.render_template("docs.html")
class DocsMDView(BaseView):
async def get(self):

View File

@ -1,5 +1,6 @@
from snek.system.view import BaseView
class IndexView(BaseView):
async def get(self):

View File

@ -1,13 +1,18 @@
from snek.form.register import RegisterForm
from snek.system.view import BaseView
class LoginView(BaseView):
async def get(self):
return await self.render_template("login.html") #web.json_response({"form": RegisterForm().to_json()})
return await self.render_template(
"login.html"
) # web.json_response({"form": RegisterForm().to_json()})
async def post(self):
form = RegisterForm()
form.set_user_data(await self.request.post())
print(form.is_valid())
return await self.render_template("login.html", self.request) #web.json_response({"form": RegisterForm().to_json()})
return await self.render_template(
"login.html", self.request
) # web.json_response({"form": RegisterForm().to_json()})

View File

@ -1,5 +1,6 @@
from snek.system.view import BaseFormView
from snek.form.login import LoginForm
from snek.system.view import BaseFormView
class LoginFormView(BaseFormView):
form = LoginForm

View File

@ -1,5 +1,6 @@
from snek.system.view import BaseView
class RegisterView(BaseView):
async def get(self):

View File

@ -1,9 +1,12 @@
from snek.form.register import RegisterForm
from snek.system.view import BaseFormView
class RegisterFormView(BaseFormView):
form = RegisterForm
async def submit(self, form):
result = await self.app.services.user.register(form.email.value,form.username.value,form.password.value)
result = await self.app.services.user.register(
form.email.value, form.username.value, form.password.value
)
print("SUBMITTED:", result)

View File

@ -1,5 +1,6 @@
from snek.system.view import BaseView
class WebView(BaseView):
async def get(self):