lema-basic-web-backend 0.1.0__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.
- basic_web_backend/__init__.py +86 -0
- basic_web_backend/adapters.py +39 -0
- basic_web_backend/application.py +217 -0
- basic_web_backend/config.py +214 -0
- basic_web_backend/exceptions.py +176 -0
- basic_web_backend/logging_config.py +61 -0
- basic_web_backend/multipart.py +109 -0
- basic_web_backend/request.py +180 -0
- basic_web_backend/response.py +197 -0
- basic_web_backend/routing.py +343 -0
- basic_web_backend/static.py +29 -0
- basic_web_backend/template/__init__.py +3 -0
- basic_web_backend/template/environment.py +110 -0
- basic_web_backend/template/evaluator.py +114 -0
- basic_web_backend/template/lexer.py +127 -0
- basic_web_backend/template/nodes.py +34 -0
- basic_web_backend/template/parser.py +221 -0
- lema_basic_web_backend-0.1.0.dist-info/METADATA +546 -0
- lema_basic_web_backend-0.1.0.dist-info/RECORD +22 -0
- lema_basic_web_backend-0.1.0.dist-info/WHEEL +5 -0
- lema_basic_web_backend-0.1.0.dist-info/licenses/LICENSE +21 -0
- lema_basic_web_backend-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from html import escape
|
|
3
|
+
|
|
4
|
+
from ..exceptions import TemplateRenderError, UndefinedVariableError
|
|
5
|
+
from .nodes import TextNode, VariableNode, IfNode, ForNode
|
|
6
|
+
|
|
7
|
+
_MISSING = object()
|
|
8
|
+
|
|
9
|
+
def render(nodes, context, autoescape=True, template_name=None):
|
|
10
|
+
if context is None:
|
|
11
|
+
context = {}
|
|
12
|
+
else:
|
|
13
|
+
context = dict(context)
|
|
14
|
+
|
|
15
|
+
return _render_nodes(nodes=nodes, context=context, autoescape=autoescape, template_name=template_name)
|
|
16
|
+
|
|
17
|
+
def _render_nodes(nodes, context, autoescape, template_name):
|
|
18
|
+
output_parts = []
|
|
19
|
+
|
|
20
|
+
for node in nodes:
|
|
21
|
+
if isinstance(node, TextNode):
|
|
22
|
+
output_parts.append(node.content)
|
|
23
|
+
continue
|
|
24
|
+
|
|
25
|
+
if isinstance(node, VariableNode):
|
|
26
|
+
value = _resolve_variable(path=node.path, context=context)
|
|
27
|
+
|
|
28
|
+
rendered_value = str(value)
|
|
29
|
+
if autoescape:
|
|
30
|
+
rendered_value = escape(rendered_value, quote=True)
|
|
31
|
+
|
|
32
|
+
output_parts.append(rendered_value)
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
if isinstance(node, IfNode):
|
|
36
|
+
condition_value = _resolve_variable(path=node.condition, context=context)
|
|
37
|
+
|
|
38
|
+
if condition_value:
|
|
39
|
+
selected_nodes = node.body
|
|
40
|
+
else:
|
|
41
|
+
selected_nodes = node.else_body
|
|
42
|
+
|
|
43
|
+
rendered_branch = _render_nodes(
|
|
44
|
+
nodes=selected_nodes,
|
|
45
|
+
context=context,
|
|
46
|
+
autoescape=autoescape,
|
|
47
|
+
template_name=template_name
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
output_parts.append(rendered_branch)
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
if isinstance(node, ForNode):
|
|
54
|
+
iterable_value = _resolve_variable(path=node.iterable, context=context)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
iterator = iter(iterable_value)
|
|
58
|
+
except TypeError as error:
|
|
59
|
+
iterable_name = '.'.join(node.iterable)
|
|
60
|
+
raise TemplateRenderError(
|
|
61
|
+
f"Template value is not iterable: {iterable_name}") from error
|
|
62
|
+
|
|
63
|
+
for item in iterator:
|
|
64
|
+
local_context = dict(context)
|
|
65
|
+
|
|
66
|
+
local_context[node.variable_name] = item
|
|
67
|
+
rendered_item = _render_nodes(
|
|
68
|
+
nodes=node.body,
|
|
69
|
+
context=local_context,
|
|
70
|
+
autoescape=autoescape,
|
|
71
|
+
template_name=template_name
|
|
72
|
+
)
|
|
73
|
+
output_parts.append(rendered_item)
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
raise TemplateRenderError(
|
|
77
|
+
f"Unsupported node type: {type(node).__name__}")
|
|
78
|
+
|
|
79
|
+
return ''.join(output_parts)
|
|
80
|
+
|
|
81
|
+
def _resolve_variable(path, context):
|
|
82
|
+
variable_name = '.'.join(path)
|
|
83
|
+
|
|
84
|
+
if not path:
|
|
85
|
+
raise UndefinedVariableError(variable_name="")
|
|
86
|
+
|
|
87
|
+
first_name = path[0]
|
|
88
|
+
|
|
89
|
+
if first_name not in context:
|
|
90
|
+
raise UndefinedVariableError(variable_name=variable_name)
|
|
91
|
+
|
|
92
|
+
value = context[first_name]
|
|
93
|
+
|
|
94
|
+
for name in path[1:]:
|
|
95
|
+
value = _resolve_part(
|
|
96
|
+
value=value,
|
|
97
|
+
name=name,
|
|
98
|
+
variable_name=variable_name
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return value
|
|
102
|
+
|
|
103
|
+
def _resolve_part(value, name, variable_name):
|
|
104
|
+
if isinstance(value, Mapping):
|
|
105
|
+
if name not in value:
|
|
106
|
+
raise UndefinedVariableError(variable_name=variable_name)
|
|
107
|
+
return value[name]
|
|
108
|
+
|
|
109
|
+
attribute = getattr(value, name, _MISSING)
|
|
110
|
+
|
|
111
|
+
if attribute is _MISSING:
|
|
112
|
+
raise UndefinedVariableError(variable_name=variable_name)
|
|
113
|
+
|
|
114
|
+
return attribute
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
from ..exceptions import TemplateSyntaxError, TemplateSyntaxError
|
|
5
|
+
|
|
6
|
+
class TokenType(Enum):
|
|
7
|
+
TEXT = "TEXT"
|
|
8
|
+
VARIABLE = "VARIABLE"
|
|
9
|
+
STATEMENT = "STATEMENT"
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Token:
|
|
13
|
+
type: TokenType
|
|
14
|
+
value: str
|
|
15
|
+
line: int
|
|
16
|
+
column: int
|
|
17
|
+
|
|
18
|
+
OPENING_DELIMITER = {
|
|
19
|
+
"{{": ("}}", TokenType.VARIABLE, "Unclosed variable expression."),
|
|
20
|
+
"{%": ("%}", TokenType.STATEMENT, "Unclosed statement block."),
|
|
21
|
+
"{#": ("#}", None, "Unclosed template comment.")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
def tokenize(source, template_name=None):
|
|
25
|
+
tokens = []
|
|
26
|
+
|
|
27
|
+
position = 0
|
|
28
|
+
line = 1
|
|
29
|
+
column = 1
|
|
30
|
+
|
|
31
|
+
while position < len(source):
|
|
32
|
+
opening_position, opening = (
|
|
33
|
+
_find_next_opening(source, position)
|
|
34
|
+
)
|
|
35
|
+
if opening is None:
|
|
36
|
+
remaining_text = source[position:]
|
|
37
|
+
|
|
38
|
+
if remaining_text:
|
|
39
|
+
tokens.append(
|
|
40
|
+
Token(
|
|
41
|
+
type=TokenType.TEXT,
|
|
42
|
+
value=remaining_text,
|
|
43
|
+
line=line,
|
|
44
|
+
column=column
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
break
|
|
48
|
+
|
|
49
|
+
if opening_position > position:
|
|
50
|
+
text = source[position:opening_position]
|
|
51
|
+
tokens.append(
|
|
52
|
+
Token(
|
|
53
|
+
type=TokenType.TEXT,
|
|
54
|
+
value=text,
|
|
55
|
+
line=line,
|
|
56
|
+
column=column
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
line, column = _advance_position(text, line, column)
|
|
60
|
+
|
|
61
|
+
position = opening_position
|
|
62
|
+
|
|
63
|
+
token_line = line
|
|
64
|
+
token_column = column
|
|
65
|
+
|
|
66
|
+
closing, token_type, error_message = OPENING_DELIMITER[opening]
|
|
67
|
+
closing_position = source.find(closing, position + len(opening))
|
|
68
|
+
|
|
69
|
+
if closing_position == -1:
|
|
70
|
+
raise TemplateSyntaxError(
|
|
71
|
+
message=error_message,
|
|
72
|
+
template_name=template_name,
|
|
73
|
+
line=token_line,
|
|
74
|
+
column=token_column
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
content_start = position + len(opening)
|
|
78
|
+
content = source[content_start:closing_position]
|
|
79
|
+
|
|
80
|
+
end_position = closing_position + len(closing)
|
|
81
|
+
|
|
82
|
+
complete_section = source[position:end_position]
|
|
83
|
+
|
|
84
|
+
if token_type is not None:
|
|
85
|
+
tokens.append(
|
|
86
|
+
Token(
|
|
87
|
+
type=token_type,
|
|
88
|
+
value=content.strip(),
|
|
89
|
+
line=token_line,
|
|
90
|
+
column=token_column
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
line, column = _advance_position(text=complete_section, line=line, column=column)
|
|
95
|
+
position = end_position
|
|
96
|
+
|
|
97
|
+
return tokens
|
|
98
|
+
|
|
99
|
+
def _find_next_opening(source, start):
|
|
100
|
+
next_position = None
|
|
101
|
+
next_opening = None
|
|
102
|
+
|
|
103
|
+
for opening in OPENING_DELIMITER:
|
|
104
|
+
position = source.find(opening, start)
|
|
105
|
+
|
|
106
|
+
if position == -1:
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
if next_position is None or position < next_position:
|
|
110
|
+
next_position = position
|
|
111
|
+
next_opening = opening
|
|
112
|
+
|
|
113
|
+
return next_position, next_opening
|
|
114
|
+
|
|
115
|
+
def _advance_position(text, line, column):
|
|
116
|
+
newline_count = text.count("\n")
|
|
117
|
+
|
|
118
|
+
if newline_count == 0:
|
|
119
|
+
return line, column + len(text)
|
|
120
|
+
|
|
121
|
+
line += newline_count
|
|
122
|
+
|
|
123
|
+
text_after_last_newline = text.rsplit("\n", maxsplit=1)[1]
|
|
124
|
+
column = len(text_after_last_newline) + 1
|
|
125
|
+
|
|
126
|
+
return line, column
|
|
127
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
class TemplateNode:
|
|
4
|
+
"""Base class for template syntax nodes."""
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class TextNode(TemplateNode):
|
|
9
|
+
content: str
|
|
10
|
+
line: int = 1
|
|
11
|
+
column: int = 1
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class VariableNode(TemplateNode):
|
|
15
|
+
path: list[str]
|
|
16
|
+
line: int = 1
|
|
17
|
+
column: int = 1
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class IfNode(TemplateNode):
|
|
21
|
+
condition: list[str]
|
|
22
|
+
body: list[TemplateNode] = field(default_factory=list)
|
|
23
|
+
else_body: list[TemplateNode] = field(default_factory=list)
|
|
24
|
+
line: int = 1
|
|
25
|
+
column: int = 1
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ForNode(TemplateNode):
|
|
29
|
+
variable_name: str
|
|
30
|
+
iterable: list[str]
|
|
31
|
+
body: list[TemplateNode] = field(default_factory=list)
|
|
32
|
+
line: int = 1
|
|
33
|
+
column: int = 1
|
|
34
|
+
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from ..exceptions import TemplateSyntaxError
|
|
4
|
+
from .lexer import TokenType
|
|
5
|
+
from .nodes import TextNode, VariableNode, IfNode, ForNode
|
|
6
|
+
|
|
7
|
+
VARIABLE_PATH_PATTERN = re.compile(
|
|
8
|
+
r"[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
FOR_STATEMENT_PATTERN = re.compile(
|
|
12
|
+
r"for\s+(?P<variable>[A-Za-z_]\w*)"
|
|
13
|
+
r"\s+in\s+(?P<iterable>.+)"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
def parse(tokens, template_name=None):
|
|
17
|
+
parser = Parser(tokens, template_name)
|
|
18
|
+
return parser.parse()
|
|
19
|
+
|
|
20
|
+
class Parser:
|
|
21
|
+
def __init__(self, tokens, template_name=None):
|
|
22
|
+
self.tokens = list(tokens)
|
|
23
|
+
self.template_name = template_name
|
|
24
|
+
self.position = 0
|
|
25
|
+
|
|
26
|
+
def parse(self):
|
|
27
|
+
nodes, closing_statment = self._parse_nodes(stop_statements=set())
|
|
28
|
+
|
|
29
|
+
if closing_statment is not None:
|
|
30
|
+
token = self.tokens[self.position]
|
|
31
|
+
|
|
32
|
+
self._raise_unexpected_statment(token)
|
|
33
|
+
|
|
34
|
+
return nodes
|
|
35
|
+
|
|
36
|
+
def _parse_nodes(self, stop_statements):
|
|
37
|
+
nodes = []
|
|
38
|
+
|
|
39
|
+
while self.position < len(self.tokens):
|
|
40
|
+
token = self.tokens[self.position]
|
|
41
|
+
|
|
42
|
+
if token.type == TokenType.TEXT:
|
|
43
|
+
nodes.append(
|
|
44
|
+
TextNode(
|
|
45
|
+
content=token.value,
|
|
46
|
+
line=token.line,
|
|
47
|
+
column=token.column
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
self.position += 1
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
if token.type == TokenType.VARIABLE:
|
|
54
|
+
nodes.append(self._parse_variable(token))
|
|
55
|
+
self.position += 1
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
if token.type == TokenType.STATEMENT:
|
|
59
|
+
statement = token.value
|
|
60
|
+
|
|
61
|
+
if statement in stop_statements:
|
|
62
|
+
return nodes, statement
|
|
63
|
+
|
|
64
|
+
keyword = self._get_keyword(statement)
|
|
65
|
+
|
|
66
|
+
if keyword == "if":
|
|
67
|
+
nodes.append(self._parse_if(opening_token=token))
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
if keyword == "for":
|
|
71
|
+
nodes.append(self._parse_for(opening_token=token))
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
if statement in {"endif", "else", "endfor"}:
|
|
75
|
+
self._raise_unexpected_statment(token)
|
|
76
|
+
|
|
77
|
+
self._raise_syntax_error(
|
|
78
|
+
message=f"Unknown template statement: {statement}",
|
|
79
|
+
token=token
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
self._raise_syntax_error(
|
|
83
|
+
message=f"Unkonwn template token type: {token.type}",
|
|
84
|
+
token=token
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return nodes, None
|
|
88
|
+
|
|
89
|
+
def _parse_variable(self, token):
|
|
90
|
+
path = self._parse_variable_path(expression=token.value, token=token, empty_message="Variable expression cannot be empty.")
|
|
91
|
+
|
|
92
|
+
return VariableNode(
|
|
93
|
+
path=path,
|
|
94
|
+
line=token.line,
|
|
95
|
+
column=token.column
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def _parse_if(self, opening_token):
|
|
99
|
+
parts = opening_token.value.split(maxsplit=1)
|
|
100
|
+
|
|
101
|
+
if len(parts) == 1:
|
|
102
|
+
self._raise_syntax_error(
|
|
103
|
+
message="If statement requires a condition.",
|
|
104
|
+
token=opening_token
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
condition_expression = parts[1].strip()
|
|
108
|
+
|
|
109
|
+
condition = self._parse_variable_path(
|
|
110
|
+
expression=condition_expression,
|
|
111
|
+
token=opening_token,
|
|
112
|
+
empty_message="If statement requires a condition."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
self.position += 1
|
|
116
|
+
|
|
117
|
+
body, closing_statement = self._parse_nodes(stop_statements={"else", "endif"})
|
|
118
|
+
|
|
119
|
+
if closing_statement is None:
|
|
120
|
+
self._raise_syntax_error(
|
|
121
|
+
message="Missing endif statement.",
|
|
122
|
+
token=opening_token
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
else_body = []
|
|
126
|
+
|
|
127
|
+
if closing_statement == "else":
|
|
128
|
+
self.position += 1
|
|
129
|
+
|
|
130
|
+
else_body, closing_statement = self._parse_nodes(stop_statements={"endif"})
|
|
131
|
+
|
|
132
|
+
if closing_statement is None:
|
|
133
|
+
self._raise_syntax_error(
|
|
134
|
+
message="Missing endif statement.",
|
|
135
|
+
token=opening_token
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
self.position += 1
|
|
139
|
+
|
|
140
|
+
return IfNode(
|
|
141
|
+
condition=condition,
|
|
142
|
+
body=body,
|
|
143
|
+
else_body=else_body,
|
|
144
|
+
line=opening_token.line,
|
|
145
|
+
column=opening_token.column
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def _parse_variable_path(self, expression, token, empty_message):
|
|
149
|
+
if not expression:
|
|
150
|
+
self._raise_syntax_error(
|
|
151
|
+
message=empty_message,
|
|
152
|
+
token=token
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if VARIABLE_PATH_PATTERN.fullmatch(expression) is None:
|
|
156
|
+
self._raise_syntax_error(
|
|
157
|
+
message=f"Invalid variable expression: {expression}",
|
|
158
|
+
token=token
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return expression.split(".")
|
|
162
|
+
|
|
163
|
+
def _get_keyword(self, statement):
|
|
164
|
+
parts = statement.split(maxsplit=1)
|
|
165
|
+
if not parts:
|
|
166
|
+
return ""
|
|
167
|
+
|
|
168
|
+
return parts[0]
|
|
169
|
+
|
|
170
|
+
def _raise_unexpected_statment(self, token):
|
|
171
|
+
self._raise_syntax_error(
|
|
172
|
+
message=f"Unexpected {token.value} statement.",
|
|
173
|
+
token=token
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def _raise_syntax_error(self, message, token):
|
|
177
|
+
raise TemplateSyntaxError(
|
|
178
|
+
message=message,
|
|
179
|
+
template_name=self.template_name,
|
|
180
|
+
line=token.line,
|
|
181
|
+
column=token.column
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def _parse_for(self, opening_token):
|
|
185
|
+
match = FOR_STATEMENT_PATTERN.fullmatch(opening_token.value)
|
|
186
|
+
|
|
187
|
+
if match is None:
|
|
188
|
+
self._raise_syntax_error(
|
|
189
|
+
message=f"Invalid for statement. Expected format: 'for <variable> in <iterable>'.",
|
|
190
|
+
token=opening_token
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
variable_name = match.group("variable")
|
|
194
|
+
|
|
195
|
+
iterable_expression = match.group("iterable").strip()
|
|
196
|
+
|
|
197
|
+
iterable = self._parse_variable_path(
|
|
198
|
+
expression=iterable_expression,
|
|
199
|
+
token=opening_token,
|
|
200
|
+
empty_message="Invalid for statement. Expected format: 'for <variable> in <iterable>'."
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
self.position += 1
|
|
204
|
+
|
|
205
|
+
body, closing_statement = self._parse_nodes(stop_statements={"endfor"})
|
|
206
|
+
|
|
207
|
+
if closing_statement is None:
|
|
208
|
+
self._raise_syntax_error(
|
|
209
|
+
message="Missing endfor statement.",
|
|
210
|
+
token=opening_token
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
self.position += 1
|
|
214
|
+
|
|
215
|
+
return ForNode(
|
|
216
|
+
variable_name=variable_name,
|
|
217
|
+
iterable=iterable,
|
|
218
|
+
body=body,
|
|
219
|
+
line=opening_token.line,
|
|
220
|
+
column=opening_token.column
|
|
221
|
+
)
|