djc-core 1.1.1__cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.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.
djc_core/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ # This file is what maturin auto-generates. But it seems maturin omits it when we have a __init__.pyi file.
2
+ # So we have to manually include it here.
3
+ # Following block of code is what maturin would've generated
4
+
5
+ from .djc_core import *
6
+
7
+ __doc__ = djc_core.__doc__
8
+ if hasattr(djc_core, "__all__"):
9
+ __all__ = djc_core.__all__
10
+
11
+ # OVERRIDES START HERE
12
+ # Add here any additional public API that we defined purely in Python
13
+ from djc_core.djc_safe_eval import safe_eval, unsafe, SecurityError
14
+
15
+ if hasattr(djc_core, "__all__"):
16
+ __all__ += ["safe_eval", "unsafe", "SecurityError"]
djc_core/__init__.pyi ADDED
@@ -0,0 +1,2 @@
1
+ from djc_core.djc_html_transformer import *
2
+ from djc_core.djc_safe_eval import *
@@ -0,0 +1,36 @@
1
+ from typing import List, Dict, Optional
2
+
3
+ def set_html_attributes(
4
+ html: str,
5
+ root_attributes: List[str],
6
+ all_attributes: List[str],
7
+ check_end_names: Optional[bool] = None,
8
+ watch_on_attribute: Optional[str] = None,
9
+ ) -> tuple[str, Dict[str, List[str]]]:
10
+ """
11
+ Transform HTML by adding attributes to root and all elements.
12
+
13
+ Args:
14
+ html (str): The HTML string to transform. Can be a fragment or full document.
15
+ root_attributes (List[str]): List of attribute names to add to root elements only.
16
+ all_attributes (List[str]): List of attribute names to add to all elements.
17
+ check_end_names (Optional[bool]): Whether to validate matching of end tags. Defaults to None.
18
+ watch_on_attribute (Optional[str]): If set, captures which attributes were added to elements with this attribute.
19
+
20
+ Returns:
21
+ A tuple containing:
22
+ - The transformed HTML string
23
+ - A dictionary mapping captured attribute values to lists of attributes that were added
24
+ to those elements. Only returned if watch_on_attribute is set, otherwise empty dict.
25
+
26
+ Example:
27
+ >>> html = '<div><p>Hello</p></div>'
28
+ >>> set_html_attributes(html, ['data-root-id'], ['data-v-123'])
29
+ '<div data-root-id="" data-v-123=""><p data-v-123="">Hello</p></div>'
30
+
31
+ Raises:
32
+ ValueError: If the HTML is malformed or cannot be parsed.
33
+ """
34
+ ...
35
+
36
+ __all__ = ["set_html_attributes"]
@@ -0,0 +1,8 @@
1
+ from .sandbox import unsafe
2
+ from .eval import safe_eval, SecurityError
3
+
4
+ __all__ = [
5
+ "safe_eval",
6
+ "SecurityError",
7
+ "unsafe",
8
+ ]
@@ -0,0 +1,79 @@
1
+ # ruff: noqa
2
+ from typing import Any, Callable, Dict, Optional, TypeVar
3
+
4
+ F = TypeVar("F", bound=Callable[..., Any])
5
+
6
+ class SecurityError(Exception):
7
+ """An error raised when a security violation occurs."""
8
+
9
+
10
+ def safe_eval(
11
+ source: str,
12
+ *,
13
+ validate_variable: Optional[Callable[[str], bool]] = None,
14
+ validate_attribute: Optional[Callable[[Any, str], bool]] = None,
15
+ validate_subscript: Optional[Callable[[Any, Any], bool]] = None,
16
+ validate_callable: Optional[Callable[[Callable], bool]] = None,
17
+ validate_assign: Optional[Callable[[str, Any], bool]] = None,
18
+ ) -> Callable[[Dict[str, Any]], Any]:
19
+ """
20
+ Compile a Python expression string into a safe evaluation function.
21
+
22
+ This function takes a Python expression string and transforms it into safe code
23
+ by wrapping potentially unsafe operations (like variable access, function calls,
24
+ attribute access, etc.) with sandboxed function calls.
25
+
26
+ This is the re-implementation of Jinja's sandboxed evaluation logic.
27
+
28
+ Args:
29
+ source: The Python expression string to transform.
30
+ validate_variable: Optional extra validation for variable lookups.
31
+ validate_attribute: Optional extra validation for attribute access.
32
+ validate_subscript: Optional extra validation for subscript access.
33
+ validate_callable: Optional extra validation for function calls.
34
+ validate_assign: Optional extra validation for variable assignments.
35
+
36
+ Returns:
37
+ A compiled function that takes a context dictionary and evaluates the expression.
38
+ The function signature is: `func(context: Dict[str, Any]) -> Any`
39
+
40
+ The returned function may raise SecurityError if the expression is unsafe.
41
+
42
+ Raises:
43
+ SyntaxError: If the input is not valid Python syntax or contains forbidden constructs.
44
+
45
+ Example:
46
+ >>> compiled = safe_eval("my_var + 1")
47
+ >>> result = compiled({"my_var": 5})
48
+ >>> print(result)
49
+ 6
50
+
51
+ >>> compiled = safe_eval("lambda x: x + my_var")
52
+ >>> func = compiled({"my_var": 10})
53
+ >>> print(func(5))
54
+ 15
55
+
56
+ >>> compiled = safe_eval("unsafe_var + 1", validate_variable=lambda name: name != "unsafe_var")
57
+ >>> result = compiled({"unsafe_var": 5})
58
+ SecurityError: variable 'unsafe_var' is unsafe
59
+ """
60
+
61
+
62
+ def unsafe(f: F) -> F:
63
+ """
64
+ Marks a function or method as unsafe.
65
+
66
+ Example:
67
+ ```python
68
+ @unsafe
69
+ def delete(self):
70
+ pass
71
+ ```
72
+ """
73
+
74
+
75
+ __all__ = [
76
+ "safe_eval",
77
+ "SecurityError",
78
+ "unsafe",
79
+ ]
@@ -0,0 +1,155 @@
1
+ import functools
2
+ from typing import Any, Callable, TypeVar
3
+
4
+ T = TypeVar("T", bound=Callable)
5
+
6
+
7
+ def _format_error_with_context(
8
+ error: Exception,
9
+ source: str,
10
+ start_index: int,
11
+ end_index: int,
12
+ func_name: str,
13
+ add_prefix: bool = True,
14
+ ) -> None:
15
+ """
16
+ Format an error with underlined source code context.
17
+
18
+ Modifies the exception's message to include:
19
+ - Up to 2 preceding lines
20
+ - The lines containing the error (start_index to end_index)
21
+ - Up to 2 following lines
22
+ - Underlined code with ^^^ characters
23
+
24
+ Example:
25
+ ```
26
+ (a := 1 + my_var)
27
+ ^^^^^^
28
+ NameError: name 'my_var' is not defined
29
+ ```
30
+ """
31
+ # Convert source to lines with line numbers
32
+ lines = source.split("\n")
33
+
34
+ # Find which lines contain the error
35
+ line_starts = [0] # Cumulative character count at start of each line
36
+ for line in lines[:-1]:
37
+ line_starts.append(line_starts[-1] + len(line) + 1) # +1 for newline
38
+
39
+ # Find start and end line numbers (0-indexed)
40
+ start_line = 0
41
+ for i, line_start in enumerate(line_starts):
42
+ if line_start > start_index:
43
+ start_line = max(0, i - 1)
44
+ break
45
+ else:
46
+ start_line = len(lines) - 1
47
+
48
+ end_line = start_line
49
+ for i, line_start in enumerate(line_starts):
50
+ if line_start > end_index:
51
+ end_line = max(0, i - 1)
52
+ break
53
+ else:
54
+ end_line = len(lines) - 1
55
+
56
+ # Calculate column positions within each line
57
+ def get_column(line_num: int, char_index: int) -> int:
58
+ """Get column number (0-indexed) for a character index in a specific line."""
59
+ if line_num >= len(line_starts):
60
+ return 0
61
+ line_start = line_starts[line_num]
62
+ return max(0, char_index - line_start)
63
+
64
+ start_col = get_column(start_line, start_index)
65
+ end_col = get_column(end_line, end_index)
66
+
67
+ # Collect lines to show (up to 2 before and 2 after)
68
+ show_start = max(0, start_line - 2)
69
+ show_end = min(len(lines), end_line + 3) # +3 because end is inclusive
70
+
71
+ # Build the formatted error message
72
+ error_lines = []
73
+ if add_prefix:
74
+ error_lines.append(f"Error in {func_name}: {type(error).__name__}: {error}")
75
+ else:
76
+ # Just use the original error message
77
+ error_lines.append(str(error))
78
+ error_lines.append("")
79
+
80
+ # Add source lines with line numbers
81
+ for line_num in range(show_start, show_end):
82
+ line_content = lines[line_num]
83
+ line_display_num = line_num + 1 # 1-indexed for display
84
+
85
+ # Calculate underline range for this line
86
+ underline_start = 0
87
+ underline_end = len(line_content)
88
+
89
+ if line_num == start_line == end_line:
90
+ # Error spans single line
91
+ underline_start = start_col
92
+ underline_end = min(len(line_content), end_col)
93
+ elif line_num == start_line:
94
+ # Error starts on this line
95
+ underline_start = start_col
96
+ underline_end = len(line_content)
97
+ elif line_num == end_line:
98
+ # Error ends on this line
99
+ underline_start = 0
100
+ underline_end = min(len(line_content), end_col)
101
+ elif start_line < line_num < end_line:
102
+ # Error spans this entire line
103
+ underline_start = 0
104
+ underline_end = len(line_content)
105
+ else:
106
+ # No error on this line, don't underline
107
+ underline_start = -1
108
+ underline_end = -1
109
+
110
+ # Add line with number
111
+ line_prefix = f" {line_display_num:4d} | "
112
+ error_lines.append(line_prefix + line_content)
113
+
114
+ # Add underline if error is on this line
115
+ if underline_start >= 0:
116
+ # Create underline: prefix spaces + spaces to column + ^ characters
117
+ prefix_len = len(line_prefix) # " 4 | " = 9 characters
118
+ underline = " " * (prefix_len + underline_start) + "^" * max(
119
+ 1, underline_end - underline_start
120
+ )
121
+ error_lines.append(underline)
122
+
123
+ # Update exception message
124
+ error.args = ("\n".join(error_lines),)
125
+ # Mark that this error has been processed by error_context
126
+ error._safe_eval_error_processed = True # type: ignore[attr-defined]
127
+
128
+
129
+ def error_context(func_name: str) -> Callable[[T], T]:
130
+ """
131
+ Decorator that wraps functions to add error context reporting.
132
+
133
+ Extracts token tuple (start_index, end_index) from the third positional argument,
134
+ and on error, formats the error with underlined source code.
135
+ """
136
+
137
+ def decorator(func: T) -> T:
138
+ @functools.wraps(func)
139
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
140
+ # Functions that use this decorator have signature
141
+ # (context: Mapping[str, Any], source: str, token: tuple[int, int], *args: Any, **kwargs: Any) -> Any
142
+ source = args[1]
143
+ start_index, end_index = args[2]
144
+
145
+ try:
146
+ # On success return result normally
147
+ return func(*args, **kwargs)
148
+ except Exception as e:
149
+ # On error, modify the error message to include the source code context
150
+ _format_error_with_context(e, source, start_index, end_index, func_name)
151
+ raise
152
+
153
+ return wrapper
154
+
155
+ return decorator
@@ -0,0 +1,428 @@
1
+ import builtins
2
+ from typing import Any, Callable, Dict, Mapping, Optional, Tuple
3
+
4
+ from djc_core.djc_core import safe_eval as safe_eval_rust
5
+ from djc_core.djc_safe_eval.error import error_context, _format_error_with_context
6
+ from djc_core.djc_safe_eval.sandbox import (
7
+ is_safe_attribute,
8
+ is_safe_callable,
9
+ is_safe_variable,
10
+ )
11
+
12
+
13
+ class SecurityError(Exception):
14
+ """An error raised when a security violation occurs."""
15
+
16
+ pass
17
+
18
+
19
+ def safe_eval(
20
+ source: str,
21
+ *,
22
+ validate_variable: Optional[Callable[[str], bool]] = None,
23
+ validate_attribute: Optional[Callable[[Any, str], bool]] = None,
24
+ validate_subscript: Optional[Callable[[Any, Any], bool]] = None,
25
+ validate_callable: Optional[Callable[[Callable], bool]] = None,
26
+ validate_assign: Optional[Callable[[str, Any], bool]] = None,
27
+ ) -> Callable[[Dict[str, Any]], Any]:
28
+ """
29
+ Compile a Python expression string into a safe evaluation function.
30
+
31
+ This function takes a Python expression string and transforms it into safe code
32
+ by wrapping potentially unsafe operations (like variable access, function calls,
33
+ attribute access, etc.) with sandboxed function calls.
34
+
35
+ This is the re-implementation of Jinja's sandboxed evaluation logic.
36
+
37
+ Args:
38
+ source: The Python expression string to transform.
39
+ validate_variable: Optional extra validation for variable lookups.
40
+ validate_attribute: Optional extra validation for attribute access.
41
+ validate_subscript: Optional extra validation for subscript access.
42
+ validate_callable: Optional extra validation for function calls.
43
+ validate_assign: Optional extra validation for variable assignments.
44
+
45
+ Returns:
46
+ A compiled function that takes a context dictionary and evaluates the expression.
47
+ The function signature is: `func(context: Dict[str, Any]) -> Any`
48
+
49
+ The returned function may raise SecurityError if the expression is unsafe.
50
+
51
+ Raises:
52
+ SyntaxError: If the input is not valid Python syntax or contains forbidden constructs.
53
+
54
+ Example:
55
+ >>> compiled = safe_eval("my_var + 1")
56
+ >>> result = compiled({"my_var": 5})
57
+ >>> print(result)
58
+ 6
59
+
60
+ >>> compiled = safe_eval("lambda x: x + my_var")
61
+ >>> func = compiled({"my_var": 10})
62
+ >>> print(func(5))
63
+ 15
64
+
65
+ >>> compiled = safe_eval("unsafe_var + 1", validate_variable=lambda name: name != "unsafe_var")
66
+ >>> result = compiled({"unsafe_var": 5})
67
+ SecurityError: variable 'unsafe_var' is unsafe
68
+ """
69
+ # If user specified extra validation functions, wrap the original functions with them
70
+ if validate_variable is not None:
71
+
72
+ @error_context("variable")
73
+ def variable_fn(
74
+ __context: Mapping[str, Any],
75
+ __source: str,
76
+ __token: Tuple[int, int],
77
+ var_name: str,
78
+ ) -> Any:
79
+ if not validate_variable(var_name):
80
+ raise SecurityError(f"variable '{var_name}' is unsafe")
81
+ return variable(__context, __source, __token, var_name)
82
+ else:
83
+ variable_fn = variable
84
+
85
+ if validate_attribute is not None:
86
+
87
+ @error_context("attribute")
88
+ def attribute_fn(
89
+ __context: Mapping[str, Any],
90
+ __source: str,
91
+ __token: Tuple[int, int],
92
+ obj: Any,
93
+ attr_name: str,
94
+ ) -> Any:
95
+ if not validate_attribute(obj, attr_name):
96
+ raise SecurityError(
97
+ f"attribute '{attr_name}' on object '{type(obj)}' is unsafe"
98
+ )
99
+ return attribute(__context, __source, __token, obj, attr_name)
100
+ else:
101
+ attribute_fn = attribute
102
+
103
+ if validate_subscript is not None:
104
+
105
+ @error_context("subscript")
106
+ def subscript_fn(
107
+ __context: Mapping[str, Any],
108
+ __source: str,
109
+ __token: Tuple[int, int],
110
+ obj: Any,
111
+ key: Any,
112
+ ) -> Any:
113
+ if not validate_subscript(obj, key):
114
+ raise SecurityError(f"key '{key}' on object '{type(obj)}' is unsafe")
115
+ return subscript(__context, __source, __token, obj, key)
116
+ else:
117
+ subscript_fn = subscript
118
+
119
+ if validate_callable is not None:
120
+
121
+ @error_context("call")
122
+ def call_fn(
123
+ __context: Mapping[str, Any],
124
+ __source: str,
125
+ __token: Tuple[int, int],
126
+ func: Callable,
127
+ *args: Any,
128
+ **kwargs: Any,
129
+ ) -> Any:
130
+ if not validate_callable(func):
131
+ raise SecurityError(f"function '{func!r}' is unsafe")
132
+ return call(__context, __source, __token, func, *args, **kwargs)
133
+ else:
134
+ call_fn = call
135
+
136
+ if validate_assign is not None:
137
+
138
+ @error_context("assign")
139
+ def assign_fn(
140
+ __context: Mapping[str, Any],
141
+ __source: str,
142
+ __token: Tuple[int, int],
143
+ var_name: str,
144
+ value: Any,
145
+ ) -> Any:
146
+ if not validate_assign(var_name, value):
147
+ raise SecurityError(f"assignment to variable '{var_name}' is unsafe")
148
+ return assign(__context, __source, __token, var_name, value)
149
+ else:
150
+ assign_fn = assign
151
+
152
+ # Create evaluation namespace with wrapped functions
153
+ # These are captured in the closure of the lambda function we'll create
154
+ eval_namespace = {
155
+ "variable": variable_fn,
156
+ "attribute": attribute_fn,
157
+ "subscript": subscript_fn,
158
+ "call": call_fn,
159
+ "assign": assign_fn,
160
+ "slice": slice,
161
+ "interpolation": interpolation,
162
+ "template": template,
163
+ "format": format,
164
+ # Pass through the source string. This way we won't have to re-define
165
+ # the functions on each evaluation.
166
+ "source": source,
167
+ }
168
+
169
+ # Get transformed code from Rust
170
+ transformed_code = safe_eval_rust(source)
171
+
172
+ # Wrap the transformed code in a lambda that captures the helper functions
173
+ # This avoids the overhead of calling eval() and creating a dict on each evaluation
174
+ # NOTE: The lambda is assigned to a variable because we use `exec()` instead of `eval()`
175
+ # to support triple-quoted multi-line strings (which `eval()` doesn't handle).
176
+ # And while `eval()` returns its result directly, with `exec()` we need to assign it to a variable.
177
+ # We wrap with parentheses and newlines to allow multi-line expressions and trailing comments:
178
+ # the newlines ensure that trailing comments don't swallow the closing parenthesis.
179
+ lambda_code = f"_eval_expr = lambda context: (\n{transformed_code}\n)"
180
+ eval_locals = {}
181
+ try:
182
+ # Execute the function definition
183
+ exec(lambda_code, eval_namespace, eval_locals)
184
+ # Get the function from the namespace
185
+ eval_func = eval_locals["_eval_expr"]
186
+ except Exception as e:
187
+ # If the error hasn't been processed by error_context decorator,
188
+ # include the whole expression in the error message (without the "Error in..." prefix)
189
+ if not getattr(e, "_safe_eval_error_processed", False):
190
+ _format_error_with_context(
191
+ e, source, 0, len(source), "expression", add_prefix=False
192
+ )
193
+ # Mark it as processed to avoid double-formatting if re-raised
194
+ e._safe_eval_error_processed = True # type: ignore[attr-defined]
195
+ raise
196
+
197
+ # Return a function that calls the compiled function directly
198
+ # This is much faster than calling exec() on each evaluation
199
+ def evaluate(context: Dict[str, Any]) -> Any:
200
+ """
201
+ Evaluate the compiled expression with the given context.
202
+
203
+ Args:
204
+ context: Dictionary of variables to use in evaluation.
205
+
206
+ Returns:
207
+ The result of evaluating the expression.
208
+ """
209
+ try:
210
+ return eval_func(context)
211
+ except Exception as e:
212
+ # If the error hasn't been processed by error_context decorator,
213
+ # include the whole expression in the error message (without the "Error in..." prefix)
214
+ if not getattr(e, "_safe_eval_error_processed", False):
215
+ _format_error_with_context(
216
+ e, source, 0, len(source), "expression", add_prefix=False
217
+ )
218
+ # Mark it as processed to avoid double-formatting if re-raised
219
+ e._safe_eval_error_processed = True # type: ignore[attr-defined]
220
+ raise
221
+
222
+ return evaluate
223
+
224
+
225
+ # Following are the operations that we intercept. These functions are called by the transformed code.
226
+ #
227
+ # E.g.
228
+ # ```python
229
+ # my_var
230
+ # obj := {"attr": 2}
231
+ # ```
232
+ #
233
+ # is transformed into:
234
+ # ```python
235
+ # variable((0, 4), source, context, "my_var")
236
+ # assign((0, 18), source, context, "obj", {"attr": 2})
237
+ # ```
238
+ #
239
+ # Each interceptor function receives the same 3 first positional arguments:
240
+ # - __context: Mapping[str, Any] - The evaluation context
241
+ # - __source: str - The source code
242
+ # - __token: Tuple[int, int] - The token tuple (start_index, end_index)
243
+ #
244
+ # The __source and __token arguments are used by `@error_context` decorator to add the position
245
+ # where the error occurred to the error message.
246
+ # E.g.
247
+ # ```
248
+ # obj := eval("unsafe code")
249
+ # ^^^^^^^^^^^^^^^^^^^
250
+ # SecurityError: <built-in function eval> is unsafe
251
+ # ```
252
+
253
+
254
+ @error_context("variable")
255
+ def variable(
256
+ __context: Mapping[str, Any], __source: str, __token: Tuple[int, int], var_name: str
257
+ ) -> Any:
258
+ """Look up a variable in the evaluation context, e.g. `my_var`"""
259
+ if not is_safe_variable(var_name):
260
+ raise SecurityError(f"variable '{var_name}' is unsafe")
261
+ return __context[var_name]
262
+
263
+
264
+ @error_context("attribute")
265
+ def attribute(
266
+ __context: Mapping[str, Any],
267
+ __source: str,
268
+ __token: Tuple[int, int],
269
+ obj: Any,
270
+ attr_name: str,
271
+ ) -> Any:
272
+ """Access an attribute of an object, e.g. `obj.attr`"""
273
+ if not is_safe_attribute(obj, attr_name):
274
+ raise SecurityError(
275
+ f"attribute '{attr_name}' on object '{type(obj)}' is unsafe"
276
+ )
277
+ return getattr(obj, attr_name)
278
+
279
+
280
+ @error_context("subscript")
281
+ def subscript(
282
+ __context: Mapping[str, Any],
283
+ __source: str,
284
+ __token: Tuple[int, int],
285
+ obj: Any,
286
+ key: Any,
287
+ ) -> Any:
288
+ """Access a key of an object, e.g. `obj[key]`"""
289
+ # NOTE: Right now subscript uses the same logic as attribute
290
+ if not is_safe_attribute(obj, key):
291
+ raise SecurityError(f"key '{key}' on object '{type(obj)}' is unsafe")
292
+ return obj[key]
293
+
294
+
295
+ # NOTE: Our internal args are prefixed with `__` to avoid keyword argument conflicts with the original input.
296
+ @error_context("call")
297
+ def call(
298
+ __context: Mapping[str, Any],
299
+ __source: str,
300
+ __token: Tuple[int, int],
301
+ func: Callable,
302
+ *args: Any,
303
+ **kwargs: Any,
304
+ ) -> Any:
305
+ """Call a function, e.g. `func(arg1, arg2, ...)`"""
306
+ is_safe, replacement_message = is_safe_callable(func)
307
+ if not is_safe:
308
+ error_msg = f"function '{func!r}' is unsafe"
309
+ if replacement_message:
310
+ error_msg += f". {replacement_message}"
311
+ raise SecurityError(error_msg)
312
+ return func(*args, **kwargs)
313
+
314
+
315
+ @error_context("assign")
316
+ def assign(
317
+ __context: Mapping[str, Any],
318
+ __source: str,
319
+ __token: Tuple[int, int],
320
+ var_name: str,
321
+ value: Any,
322
+ ) -> Any:
323
+ """Assign a value to a variable in the evaluation context, e.g. `(x := 5)`"""
324
+ if not is_safe_variable(var_name):
325
+ raise SecurityError(f"variable '{var_name}' is unsafe")
326
+ __context[var_name] = value
327
+ return value
328
+
329
+
330
+ # NOTE: We don't need to validate the slice arguments as they are always safe.
331
+ # Slice had to be redefined from bracket syntax `obj[lower:upper:step]` to function call syntax
332
+ # `slice(lower, upper, step)` because we convert brackets to function calls `subscript(obj, key)`.
333
+ # But since we had to intercept it, this function ensures we show the correct position in the error message.
334
+ @error_context("slice")
335
+ def slice(
336
+ __context: Mapping[str, Any],
337
+ __source: str,
338
+ __token: Tuple[int, int],
339
+ lower: Any = None,
340
+ upper: Any = None,
341
+ step: Any = None,
342
+ ) -> builtins.slice:
343
+ """Create a slice object, e.g. `obj[lower:upper:step]`"""
344
+ return builtins.slice(lower, upper, step)
345
+
346
+
347
+ # For compatiblity with Python 3.14+:
348
+ # - on 3.14+, t-strings are created as normal
349
+ # - on >=3.13, using t-strings raises an error
350
+ # See: https://docs.python.org/3.14/library/string.templatelib.html#template-strings
351
+ @error_context("interpolation")
352
+ def interpolation(
353
+ __context: Mapping[str, Any],
354
+ __source: str,
355
+ __token: Tuple[int, int],
356
+ value: Any,
357
+ expression: str,
358
+ conversion: Optional[str],
359
+ format_spec: str,
360
+ ) -> Any:
361
+ """Process t-string interpolation."""
362
+ try:
363
+ from string.templatelib import Interpolation # type: ignore[import-untyped]
364
+ except ImportError:
365
+ raise NotImplementedError("t-string interpolation is not supported")
366
+ return Interpolation(value, expression, conversion, format_spec)
367
+
368
+
369
+ @error_context("template")
370
+ def template(
371
+ __context: Mapping[str, Any], __source: str, __token: Tuple[int, int], *parts: Any
372
+ ) -> Any:
373
+ """Construct a template from parts."""
374
+ try:
375
+ from string.templatelib import Template # type: ignore[import-untyped]
376
+ except ImportError:
377
+ raise NotImplementedError("t-string template construction is not supported")
378
+ return Template(*parts)
379
+
380
+
381
+ @error_context("format")
382
+ def format(
383
+ __context: Mapping[str, Any],
384
+ __source: str,
385
+ __token: Tuple[int, int],
386
+ template_string: str,
387
+ *args: Any,
388
+ ) -> str:
389
+ """
390
+ Format a template string with arguments.
391
+
392
+ Each argument is a tuple (value, conversion_flag, format_spec) where:
393
+ - value: The expression value to format
394
+ - conversion_flag: "r", "s", "a", or None
395
+ - format_spec: A string for static specs, or a tuple (template, *args) for dynamic specs
396
+
397
+ This wraps the built-in str.format() method so that errors inside f-strings get nice
398
+ error reporting with underlining via the `@error_context` decorator.
399
+ """
400
+ processed_args = []
401
+ for value, conversion_flag, format_spec in args:
402
+ # Apply conversion flag if present
403
+ if conversion_flag == "r":
404
+ value = repr(value)
405
+ elif conversion_flag == "s":
406
+ value = str(value)
407
+ elif conversion_flag == "a":
408
+ value = ascii(value)
409
+ # If None, keep value as-is
410
+
411
+ # Apply format spec if present (non-empty string or tuple)
412
+ if format_spec:
413
+ if isinstance(format_spec, tuple):
414
+ # Dynamic format spec: (template, *args)
415
+ spec_template, *spec_args = format_spec
416
+ # Format the spec template with its args
417
+ format_spec_str = spec_template.format(*spec_args)
418
+ else:
419
+ # Static format spec: already a string
420
+ format_spec_str = format_spec
421
+
422
+ # Only apply format spec if it's non-empty
423
+ if format_spec_str:
424
+ value = builtins.format(value, format_spec_str)
425
+
426
+ processed_args.append(value)
427
+
428
+ return template_string.format(*processed_args)
@@ -0,0 +1,227 @@
1
+ """
2
+ A sandbox layer that ensures unsafe operations cannot be performed.
3
+ Useful when the Python expression itself comes from an untrusted source.
4
+
5
+ Based on the Jinja v3.1.6 sandbox implementation.
6
+ See https://github.com/pallets/jinja/blob/5ef70112a1ff19c05324ff889dd30405b1002044/src/jinja2/sandbox.py
7
+
8
+ We do NOT support:
9
+ - Builtins. If you need to use `len()`, `str()`, `int()`, `list()`, `dict()`, etc.,
10
+ you'll have to pass them as variables.
11
+ - "safe" range. Jinja puts limit on the number of items a range may produce.
12
+ We don't expose `range()` function to the sandboxed code at all.
13
+ - "Immutable" sandbox (e.g. raising when mutating a list).
14
+ - Async functions, coroutines, etc.
15
+ - `str.format` and `str.format_map` are not allowed as they can be used to access unsafe variables.
16
+ Use f-strings instead.
17
+
18
+ We add these safety features not present in Jinja:
19
+ - Prevent users from calling unsafe builtins like `eval` even if they were passed as variables.
20
+
21
+ To mark custom functions as unsafe, use the `@unsafe` decorator.
22
+
23
+ Example:
24
+ ```python
25
+ @unsafe
26
+ def delete(self):
27
+ pass
28
+ ```
29
+ """
30
+
31
+ import builtins
32
+ import types
33
+ from typing import Any, Callable, Dict, Optional, Set, Tuple, TypeVar
34
+
35
+ F = TypeVar("F", bound=Callable[..., Any])
36
+
37
+ #: Unsafe function attributes.
38
+ UNSAFE_FUNCTION_ATTRIBUTES: Set[str] = set()
39
+
40
+ #: Unsafe method attributes. Function attributes are unsafe for methods too.
41
+ UNSAFE_METHOD_ATTRIBUTES: Set[str] = set()
42
+
43
+ #: unsafe generator attributes.
44
+ UNSAFE_GENERATOR_ATTRIBUTES = {"gi_frame", "gi_code"}
45
+
46
+ #: unsafe attributes on coroutines
47
+ UNSAFE_COROUTINE_ATTRIBUTES = {"cr_frame", "cr_code"}
48
+
49
+ #: unsafe attributes on async generators
50
+ UNSAFE_ASYNC_GENERATOR_ATTRIBUTES = {"ag_code", "ag_frame"}
51
+
52
+ # Builtin functions that users have no business calling in sandboxed code.
53
+ # We check for these as it may happen that these functons were passed as variables,
54
+ # and the user may try to call them.
55
+ # Dictionary mapping unsafe functions to replacement messages.
56
+ _UNSAFE_BUILTIN_FUNCTION_NAMES = {
57
+ "__build_class__": None,
58
+ "__import__": None,
59
+ "__loader__": None,
60
+ "aiter": None,
61
+ "anext": None,
62
+ "breakpoint": None,
63
+ "classmethod": None,
64
+ "compile": None,
65
+ "delattr": None,
66
+ "eval": None,
67
+ "exec": None,
68
+ "exit": None,
69
+ "getattr": None,
70
+ "globals": None,
71
+ "help": None,
72
+ "input": None,
73
+ "locals": None,
74
+ "memoryview": None,
75
+ "open": None,
76
+ "property": None,
77
+ "quit": None,
78
+ "setattr": None,
79
+ "staticmethod": None,
80
+ "super": None,
81
+ "vars": None,
82
+ }
83
+ UNSAFE_BUILTIN_FUNCTIONS: Dict[Any, Optional[str]] = {
84
+ getattr(builtins, attr): replacement
85
+ for attr, replacement in _UNSAFE_BUILTIN_FUNCTION_NAMES.items()
86
+ if hasattr(builtins, attr)
87
+ }
88
+
89
+ # These are not allowed as they can be used to access unsafe variables,
90
+ # e.g. `"a{0.b.__builtins__[__import__]}b".format({"b": 42})`
91
+ # Use f-strings instead.
92
+ UNSAFE_FUNCTIONS: Dict[Any, Optional[str]] = {
93
+ str.format: "Use f-strings instead.",
94
+ str.format_map: "Use f-strings instead.",
95
+ }
96
+
97
+
98
+ def unsafe(f: F) -> F:
99
+ """
100
+ Marks a function or method as unsafe.
101
+
102
+ Example:
103
+ ```python
104
+ @unsafe
105
+ def delete(self):
106
+ pass
107
+ ```
108
+ """
109
+ f.unsafe_callable = True # type: ignore
110
+ return f
111
+
112
+
113
+ def _is_internal_attribute(obj: Any, attr: str) -> bool:
114
+ """
115
+ Test if the attribute is an internal Python attribute.
116
+
117
+ >>> _is_internal_attribute(str, "mro")
118
+ True
119
+ >>> _is_internal_attribute(str, "upper")
120
+ False
121
+ """
122
+ if isinstance(obj, types.FunctionType):
123
+ if attr in UNSAFE_FUNCTION_ATTRIBUTES:
124
+ return True
125
+ elif isinstance(obj, types.MethodType):
126
+ if attr in UNSAFE_FUNCTION_ATTRIBUTES or attr in UNSAFE_METHOD_ATTRIBUTES:
127
+ return True
128
+ elif isinstance(obj, type):
129
+ if attr == "mro":
130
+ return True
131
+ elif isinstance(obj, (types.CodeType, types.TracebackType, types.FrameType)):
132
+ return True
133
+ elif isinstance(obj, types.GeneratorType):
134
+ if attr in UNSAFE_GENERATOR_ATTRIBUTES:
135
+ return True
136
+ elif hasattr(types, "CoroutineType") and isinstance(obj, types.CoroutineType):
137
+ if attr in UNSAFE_COROUTINE_ATTRIBUTES:
138
+ return True
139
+ elif hasattr(types, "AsyncGeneratorType") and isinstance(
140
+ obj, types.AsyncGeneratorType
141
+ ):
142
+ if attr in UNSAFE_ASYNC_GENERATOR_ATTRIBUTES:
143
+ return True
144
+ return attr.startswith("__")
145
+
146
+
147
+ def is_safe_attribute(obj: Any, attr: str) -> bool:
148
+ """
149
+ Check if the attribute of an object is safe to access.
150
+
151
+ Unsafe attributes are:
152
+ - Starting with an underscore `_`
153
+ - Internal attributes as set by `_is_internal_attribute`.
154
+ """
155
+ # Non-string subscripts should be fine, as they should be found only
156
+ # as list slices.
157
+ if not isinstance(attr, str):
158
+ return True
159
+ return not (attr.startswith("_") or _is_internal_attribute(obj, attr))
160
+
161
+
162
+ def is_safe_callable(obj: Any) -> Tuple[bool, Optional[str]]:
163
+ """
164
+ Check if an object is safely callable.
165
+
166
+ Returns:
167
+ Tuple of (is_safe, replacement_message)
168
+ - is_safe: True if safe to call, False otherwise
169
+ - replacement_message: Optional string suggesting a replacement, None if not applicable
170
+
171
+ Unsafe callables are:
172
+ - Decorated with `@unsafe`
173
+ - Marked with `obj.alters_data = True` (Django convention)
174
+ - Unsafe builtins (e.g. `eval`)
175
+ - `str.format` or `str.format_map` (use f-strings instead)
176
+ """
177
+ # Check for bound methods (e.g., "string".format())
178
+ # Handle both regular methods (types.MethodType) and built-in methods (builtin_function_or_method)
179
+ underlying_func = None
180
+ if isinstance(obj, types.MethodType):
181
+ # Regular Python method - has __func__ attribute
182
+ underlying_func = obj.__func__
183
+ elif (
184
+ hasattr(obj, "__self__")
185
+ and hasattr(obj, "__name__")
186
+ and not hasattr(obj, "__func__")
187
+ ):
188
+ # Built-in method descriptor (e.g., str.format, str.format_map)
189
+ # These are bound methods that don't have __func__, but we can get the original descriptor
190
+ try:
191
+ underlying_func = getattr(type(obj.__self__), obj.__name__)
192
+ except (AttributeError, TypeError):
193
+ pass
194
+
195
+ if underlying_func is not None:
196
+ # Check marks on inner function (decorated with @unsafe or alters_data)
197
+ if getattr(underlying_func, "unsafe_callable", False) or getattr(
198
+ underlying_func, "alters_data", False
199
+ ):
200
+ return (False, None)
201
+ # Check if the underlying function is in our unsafe dictionaries
202
+ if underlying_func in UNSAFE_FUNCTIONS:
203
+ return (False, UNSAFE_FUNCTIONS[underlying_func])
204
+ if underlying_func in UNSAFE_BUILTIN_FUNCTIONS:
205
+ return (False, UNSAFE_BUILTIN_FUNCTIONS[underlying_func])
206
+
207
+ # Check marks on the outer function (decorated with @unsafe or alters_data)
208
+ if getattr(obj, "unsafe_callable", False) or getattr(obj, "alters_data", False):
209
+ return (False, None)
210
+
211
+ # Check identity for unbound functions
212
+ if obj in UNSAFE_FUNCTIONS:
213
+ return (False, UNSAFE_FUNCTIONS[obj])
214
+ if obj in UNSAFE_BUILTIN_FUNCTIONS:
215
+ return (False, UNSAFE_BUILTIN_FUNCTIONS[obj])
216
+
217
+ return (True, None)
218
+
219
+
220
+ def is_safe_variable(var_name: str) -> bool:
221
+ """
222
+ Check if a variable is safe to access.
223
+
224
+ Unsafe variables are:
225
+ - Starting with an underscore `_`
226
+ """
227
+ return not var_name.startswith("_")
djc_core/py.typed ADDED
File without changes
@@ -0,0 +1,240 @@
1
+ Metadata-Version: 2.4
2
+ Name: djc_core
3
+ Version: 1.1.1
4
+ Classifier: Programming Language :: Python
5
+ Classifier: Programming Language :: Python :: 3
6
+ Classifier: Programming Language :: Python :: 3.8
7
+ Classifier: Programming Language :: Python :: 3.9
8
+ Classifier: Programming Language :: Python :: 3.10
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Classifier: Programming Language :: Rust
14
+ Classifier: Programming Language :: Python :: Implementation :: CPython
15
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
16
+ License-File: LICENSE
17
+ Summary: HTML parser used by django-components written in Rust.
18
+ Keywords: django,components,html
19
+ Author-email: Juro Oravec <juraj.oravec.josefson@gmail.com>
20
+ License: MIT
21
+ Requires-Python: >=3.8, <4.0
22
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
23
+ Project-URL: Changelog, https://github.com/django-components/djc-core/blob/main/CHANGELOG.md
24
+ Project-URL: Donate, https://github.com/sponsors/EmilStenstrom
25
+ Project-URL: Homepage, https://github.com/django-components/djc-core/
26
+ Project-URL: Issues, https://github.com/django-components/djc-core/issues
27
+
28
+ # djc-core
29
+
30
+ [![PyPI - Version](https://img.shields.io/pypi/v/djc-core)](https://pypi.org/project/djc-core/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/djc-core)](https://pypi.org/project/djc-core/) [![PyPI - License](https://img.shields.io/pypi/l/djc-core)](https://github.com/django-components/djc-core/blob/master/LICENSE/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/djc-core)](https://pypistats.org/packages/djc-core) [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/django-components/djc-core/tests.yml)](https://github.com/django-components/djc-core/actions/workflows/tests.yml)
31
+
32
+ Rust-based parsers and toolings used by [django-components](https://github.com/django-components/django-components). Exposed as a Python package with [maturin](https://www.maturin.rs/).
33
+
34
+ ## Installation
35
+
36
+ ```sh
37
+ pip install djc-core
38
+ ```
39
+
40
+ ## Packages
41
+
42
+ ### Safe eval
43
+
44
+ Re-implementation of Jinja2's sandboxed evaluation logic, built in Rust using the Ruff Python parser.
45
+
46
+ **Usage**
47
+
48
+ ```python
49
+ from djc_core import safe_eval
50
+
51
+ # Compile an expression
52
+ compiled = safe_eval("my_var + 1")
53
+
54
+ # Evaluate with a context
55
+ result = compiled({"my_var": 5})
56
+ print(result) # 6
57
+ ```
58
+
59
+ **Key Features**
60
+
61
+ - **Security**: Blocks unsafe operations like `eval()`, `exec()`, accessing private attributes (`_private`), and dangerous builtins
62
+ - **Variable tracking**: Reports which variables are used and which are assigned via walrus operator (`:=`)
63
+ - **Error reporting**: Provides detailed error messages with underlined source code indicating where errors occurred
64
+ - **Performance**: Implemented in Rust for fast parsing and transformation
65
+
66
+ **Supported Syntax**
67
+
68
+ Almost all Python expression features are supported:
69
+
70
+ - Literals, data structures, operators
71
+ - Comprehensions, lambdas, conditionals
72
+ - F-strings and t-strings
73
+ - Function calls, attribute/subscript access
74
+ - Walrus operator for assignments
75
+
76
+ **Security**
77
+
78
+ By default, `safe_eval` blocks:
79
+
80
+ - Unsafe builtins (`eval`, `exec`, `open`, etc.)
81
+ - Private attributes (starting with `_`)
82
+ - Dunder attributes (`__class__`, `__dict__`, etc.)
83
+ - Functions decorated with `@unsafe`
84
+ - Django methods marked with `alters_data = True`
85
+
86
+ For more details, examples, and advanced usage, see [`crates/djc-safe-eval/README.md`](crates/djc-safe-eval/README.md).
87
+
88
+ > **WARNING!** Just like Jinja2 and Django's templating, none of these are 100% bulletproof solutions!
89
+ >
90
+ > Because they work by blocking known unsafe scenarios. There can always be a new unknown scenario.
91
+ >
92
+ > If you expose a dangerous function to the template/expression, this can be potentially exploited.
93
+ >
94
+ > Safer approach would be to allow to call only those functions that have been explicitly tagged as safe.
95
+ >
96
+ > If you really need to render templates submitted from your users you should instead define the UI blocks yourself, and let your users pick and choose through JSON or similar:
97
+ >
98
+ > ```json
99
+ > {
100
+ > "template": "my_template",
101
+ > "user_id": 123,
102
+ > "blocks": [
103
+ > {"type": "header", "title": "Hello!"},
104
+ > {"type": "paragraph", "text": "This is my blog"},
105
+ > {"type": "table", "data": [[1, 2, 3], [3, 4, 5]]},
106
+ > ]
107
+ > }
108
+ > ```
109
+
110
+ ### HTML transfomer
111
+
112
+ Transform HTML in a single pass. This is a simple implementation.
113
+
114
+ This implementation was found to be 40-50x faster than our Python implementation, taking ~90ms to parse 5 MB of HTML.
115
+
116
+ **Usage**
117
+
118
+ ```python
119
+ from djc_core import set_html_attributes
120
+
121
+ html = '<div><p>Hello</p></div>'
122
+ result, _ = set_html_attributes(
123
+ html,
124
+ # Add attributes to the root elements
125
+ root_attributes=['data-root-id'],
126
+ # Add attributes to all elements
127
+ all_attributes=['data-v-123'],
128
+ )
129
+ ```
130
+
131
+ To save ourselves from re-parsing the HTML, `set_html_attributes` returns not just the transformed HTML, but also a dictionary as the second item.
132
+
133
+ This dictionary contains a record of which HTML attributes were written to which elemenents.
134
+
135
+ To populate this dictionary, you need set `watch_on_attribute` to an attribute name.
136
+
137
+ Then, during the HTML transformation, we check each element for this attribute. And if the element HAS this attribute, we:
138
+
139
+ 1. Get the value of said attribute
140
+ 2. Record the attributes that were added to the element, using the value of the watched attribute as the key.
141
+
142
+ ```python
143
+ from djc_core import set_html_attributes
144
+
145
+ html = """
146
+ <div data-watch-id="123">
147
+ <p data-watch-id="456">
148
+ Hello
149
+ </p>
150
+ </div>
151
+ """
152
+
153
+ result, captured = set_html_attributes(
154
+ html,
155
+ # Add attributes to the root elements
156
+ root_attributes=['data-root-id'],
157
+ # Add attributes to all elements
158
+ all_attributes=['data-djc-tag'],
159
+ # Watch for this attribute on elements
160
+ watch_on_attribute='data-watch-id',
161
+ )
162
+
163
+ print(captured)
164
+ # {
165
+ # '123': ['data-root-id', 'data-djc-tag'],
166
+ # '456': ['data-djc-tag'],
167
+ # }
168
+ ```
169
+
170
+ ## Architecture
171
+
172
+ This project uses a multi-crate Rust workspace structure to maintain clean separation of concerns:
173
+
174
+ ### Crate structure
175
+
176
+ - **`djc-html-transformer`**: Pure Rust library for HTML transformation
177
+ - **`djc-template-parser`**: Pure Rust library for Django template parsing
178
+ - **`djc-core`**: Python bindings that combines all other libraries
179
+
180
+ ### Design philosophy
181
+
182
+ To make sense of the code, the Python API and Rust logic are defined separately:
183
+
184
+ 1. Each crate (AKA Rust package) has `lib.rs` (which is like Python's `__init__.py`). These files do not define the main logic, but only the public API of the crate. So the API that's to be used by other crates.
185
+ 2. The `djc-core` crate imports other crates
186
+ 3. And it is only this `djc-core` where we define the Python API using PyO3.
187
+
188
+ ## Development
189
+
190
+ 1. Setup python env
191
+
192
+ ```sh
193
+ python -m venv .venv
194
+ ```
195
+
196
+ 2. Install dependencies
197
+
198
+ ```sh
199
+ pip install -r requirements-dev.txt
200
+ ```
201
+
202
+ The dev requirements also include `maturin` which is used packaging a Rust project
203
+ as Python package.
204
+
205
+ 3. Install Rust
206
+
207
+ See https://www.rust-lang.org/tools/install
208
+
209
+ 4. Run Rust tests
210
+
211
+ ```sh
212
+ cargo test
213
+ ```
214
+
215
+ 5. Build the Python package
216
+
217
+ ```sh
218
+ maturin develop
219
+ ```
220
+
221
+ To build the production-optimized package, use `maturin develop --release`.
222
+
223
+ 6. Run Python tests
224
+
225
+ ```sh
226
+ pytest
227
+ ```
228
+
229
+ > NOTE: When running Python tests, you need to run `maturin develop` first.
230
+
231
+ ## Deployment
232
+
233
+ Deployment is done automatically via GitHub Actions.
234
+
235
+ To publish a new version of the package, you need to:
236
+
237
+ 1. Bump the version in `pyproject.toml` and `Cargo.toml`
238
+ 2. Open a PR and merge it to `main`.
239
+ 3. Create a new tag on the `main` branch with the new version number (e.g. `1.0.0`), or create a new release in the GitHub UI.
240
+
@@ -0,0 +1,14 @@
1
+ djc_core/__init__.py,sha256=Ln1Cbt4owEeLpp3dEaqW3aittUD4NXRKubgRqRKgy5o,580
2
+ djc_core/__init__.pyi,sha256=Md_d2R3hcxDpRnUMp7j2aaiGJcb_MLlPGWnYgvAwfGs,81
3
+ djc_core/djc_core.cpython-314t-aarch64-linux-gnu.so,sha256=1aMA246JUNbIgHVGsPohTKBW2r2ZpkeZ32ZNi3_lcZs,22350952
4
+ djc_core/djc_html_transformer.pyi,sha256=HY7-gMgzNOqUptGtNPl1yIEZ5zSPXpxCim0hN846kks,1465
5
+ djc_core/djc_safe_eval/__init__.py,sha256=sd8eDySbeKk4Bu7d_cwMg6aQQcyOtD2IpAbJAZoOCiQ,137
6
+ djc_core/djc_safe_eval/__init__.pyi,sha256=-iJZ4LrvzAZf1HiaPqXAR7sr9fHyKl2Zqn5mXuLpq6g,2555
7
+ djc_core/djc_safe_eval/error.py,sha256=1iQRvbC6p9WNW33igCeP7qSEuCCPACRIrdH6CiC6Mmc,5306
8
+ djc_core/djc_safe_eval/eval.py,sha256=6-ViPj8ILkhtabG6T_b-zBjmOABbbRgsPnKI-pl7Feo,15232
9
+ djc_core/djc_safe_eval/sandbox.py,sha256=wFVPVgjK0Vcv1GgbuqAYo89bETl_ClibOWpDklI5K6g,7482
10
+ djc_core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ djc_core-1.1.1.dist-info/METADATA,sha256=dpZWI3e_TkLbw52YCSLWxQX_PUr2DBs-fapiPZGjyVY,7873
12
+ djc_core-1.1.1.dist-info/WHEEL,sha256=K42pU0cRVtxA8N2aGHpXHx_0G7KEA_mBkTgnb38YQKw,151
13
+ djc_core-1.1.1.dist-info/licenses/LICENSE,sha256=flYqsTDuObp3YA1SgILjCKp6YxKDC6FcMVBpJNicRq0,1074
14
+ djc_core-1.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.11.5)
3
+ Root-Is-Purelib: false
4
+ Tag: cp314-cp314t-manylinux_2_17_aarch64
5
+ Tag: cp314-cp314t-manylinux2014_aarch64
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 django-components
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.