Skip to content

Commit 34e790c

Browse files
committed
Update cython to optimize for larger objects
1 parent a158b41 commit 34e790c

31 files changed

+3953
-42
lines changed

.claude/settings.local.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(docker exec:*)"
5+
],
6+
"deny": [],
7+
"ask": []
8+
}
9+
}

docs/source/changelog.rst

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,7 @@ Changelog
33

44
All notable changes to this project will be documented in this file.
55

6-
[0.6.4] - 2025-08-12
7-
--------------------
8-
9-
Changed
10-
^^^^^^^
11-
- Increased freelist sizes for ``InputFilter``, ``FieldModel``, and ``ExternalApiConfig`` to reduce allocations
12-
- Replaced Python generators with C-style loops and added early exit conditions in filter/validator chains
13-
- Optimized dictionary operations and string lookups with extended interning
14-
- Improved ``has_unknown_fields`` with adaptive strategies based on data size
15-
16-
17-
[0.6.3] - 2025-08-11
6+
[0.6.3] - 2025-08-12
187
--------------------
198

209
Added
@@ -28,6 +17,10 @@ Changed
2817
- Updated Dockerfile base image from ``debian:buster-slim`` to ``debian:bookworm-slim`` to fix package repository issues after Debian Buster reached end-of-life.
2918
- Optimized Cython compilation with enhanced compiler flags (``-O3``, ``-march=native``, etc.) for better performance.
3019
- Enhanced ``ValidationMixin`` to support lazy evaluation when filter chains exceed the configured threshold.
20+
- Increased freelist sizes for ``InputFilter``, ``FieldModel``, and ``ExternalApiConfig`` to reduce allocations
21+
- Replaced Python generators with C-style loops and added early exit conditions in filter/validator chains
22+
- Optimized dictionary operations and string lookups with extended interning
23+
- Improved ``has_unknown_fields`` with adaptive strategies based on data size
3124

3225

3326
[0.6.2] - 2025-07-03

flask_inputfilter/_input_filter.pxd

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@ from typing import Any
33

44
from flask_inputfilter.models.cimports cimport BaseCondition, BaseFilter, BaseValidator, ExternalApiConfig, FieldModel
55

6-
from libcpp.vector cimport vector
6+
from libcpp.unordered_set cimport unordered_set
77
from libcpp.string cimport string
88

99

1010
cdef extern from "helper.h":
11-
vector[string] make_default_methods()
11+
unordered_set[string] make_default_methods_set()
1212

1313

1414
cdef class InputFilter:
1515
cdef readonly:
16-
vector[string] methods
16+
unordered_set[string] methods
1717
dict[str, FieldModel] fields
1818
list[BaseCondition] conditions
1919
list[BaseFilter] global_filters

flask_inputfilter/_input_filter.pyx

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
# cython: boundscheck=False
44
# cython: wraparound=False
55
# cython: cdivision=True
6+
# cython: nonecheck=False
7+
# cython: initializedcheck=False
8+
# cython: overflowcheck=False
69

10+
import cython
711
import json
812
import logging
913
import sys
@@ -16,35 +20,52 @@ from flask_inputfilter.exceptions import ValidationError
1620
from flask_inputfilter.mixins.cimports cimport DataMixin
1721
from flask_inputfilter.models.cimports cimport BaseCondition, BaseFilter, BaseValidator, ExternalApiConfig, FieldModel
1822

19-
from libcpp.vector cimport vector
23+
from libcpp.unordered_set cimport unordered_set
2024
from libcpp.string cimport string
2125

2226
cdef dict _INTERNED_STRINGS = {
2327
"_condition": sys.intern("_condition"),
2428
"_error": sys.intern("_error"),
29+
"apply": sys.intern("apply"),
30+
"Authorization": sys.intern("Authorization"),
31+
"cache": sys.intern("cache"),
32+
"check": sys.intern("check"),
2533
"copy": sys.intern("copy"),
34+
"data": sys.intern("data"),
35+
"data_key": sys.intern("data_key"),
2636
"default": sys.intern("default"),
2737
"DELETE": sys.intern("DELETE"),
38+
"errors": sys.intern("errors"),
2839
"external_api": sys.intern("external_api"),
2940
"fallback": sys.intern("fallback"),
41+
"fields": sys.intern("fields"),
3042
"filters": sys.intern("filters"),
3143
"GET": sys.intern("GET"),
44+
"get": sys.intern("get"),
45+
"headers": sys.intern("headers"),
46+
"items": sys.intern("items"),
47+
"json": sys.intern("json"),
48+
"keys": sys.intern("keys"),
49+
"method": sys.intern("method"),
50+
"name": sys.intern("name"),
51+
"params": sys.intern("params"),
3252
"PATCH": sys.intern("PATCH"),
3353
"POST": sys.intern("POST"),
3454
"PUT": sys.intern("PUT"),
3555
"required": sys.intern("required"),
56+
"status_code": sys.intern("status_code"),
3657
"steps": sys.intern("steps"),
37-
"validators": sys.intern("validators"),
38-
"data": sys.intern("data"),
39-
"errors": sys.intern("errors"),
58+
"text": sys.intern("text"),
59+
"url": sys.intern("url"),
60+
"validate": sys.intern("validate"),
4061
"validated_data": sys.intern("validated_data"),
41-
"fields": sys.intern("fields"),
42-
"name": sys.intern("name"),
62+
"validators": sys.intern("validators"),
4363
"value": sys.intern("value"),
64+
"values": sys.intern("values"),
4465
}
4566

4667
cdef extern from "helper.h":
47-
vector[string] make_default_methods()
68+
unordered_set[string] make_default_methods_set()
4869

4970
T = TypeVar("T")
5071

@@ -55,7 +76,7 @@ cdef class InputFilter:
5576
"""
5677

5778
def __cinit__(self) -> None:
58-
self.methods = make_default_methods()
79+
self.methods = make_default_methods_set()
5980
self.fields = {}
6081
self.conditions = []
6182
self.global_filters = []
@@ -66,9 +87,11 @@ cdef class InputFilter:
6687
self.model_class: Optional[Type[T]] = None
6788

6889
def __init__(self, methods: Optional[list[str]] = None) -> None:
90+
cdef str method
6991
if methods is not None:
7092
self.methods.clear()
71-
[self.methods.push_back(method.encode()) for method in methods]
93+
for method in methods:
94+
self.methods.insert(method.encode())
7295

7396
cpdef bint is_valid(self):
7497
"""
@@ -134,13 +157,7 @@ cdef class InputFilter:
134157
cdef:
135158
InputFilter input_filter = cls()
136159
string request_method = request.method.encode()
137-
Py_ssize_t i, n = input_filter.methods.size()
138-
bint method_allowed = False
139-
140-
for i in range(n):
141-
if request_method == input_filter.methods[i]:
142-
method_allowed = True
143-
break
160+
bint method_allowed = input_filter.methods.count(request_method) > 0
144161

145162
if not method_allowed:
146163
return Response(status=405, response="Method Not Allowed")
@@ -332,8 +349,8 @@ cdef class InputFilter:
332349
object value
333350

334351
for i in range(n):
335-
field = field_names[i]
336-
value = self.data.get(field)
352+
field = <str>field_names[i]
353+
value = self.data.get(field, None)
337354
if value is not None:
338355
result[field] = value
339356
return result

flask_inputfilter/conditions/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22

33
from .array_length_equal_condition import ArrayLengthEqualCondition
44
from .array_longer_than_condition import ArrayLongerThanCondition
5+
6+
# New conditions
7+
from .at_least_one_required_condition import AtLeastOneRequiredCondition
58
from .custom_condition import CustomCondition
69
from .equal_condition import EqualCondition
710
from .exactly_n_of_condition import ExactlyNOfCondition
811
from .exactly_n_of_matches_condition import ExactlyNOfMatchesCondition
912
from .exactly_one_of_condition import ExactlyOneOfCondition
1013
from .exactly_one_of_matches_condition import ExactlyOneOfMatchesCondition
1114
from .integer_bigger_than_condition import IntegerBiggerThanCondition
15+
from .mutually_exclusive_condition import MutuallyExclusiveCondition
1216
from .n_of_condition import NOfCondition
1317
from .n_of_matches_condition import NOfMatchesCondition
1418
from .not_equal_condition import NotEqualCondition
@@ -37,4 +41,7 @@
3741
"RequiredIfCondition",
3842
"StringLongerThanCondition",
3943
"TemporalOrderCondition",
44+
# New conditions
45+
"AtLeastOneRequiredCondition",
46+
"MutuallyExclusiveCondition",
4047
]
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
from __future__ import annotations
2+
3+
"""At least one field required condition."""
4+
5+
from typing import Any, Dict, List, Optional
6+
7+
from flask_inputfilter.models import BaseCondition
8+
9+
10+
class AtLeastOneRequiredCondition(BaseCondition):
11+
"""
12+
Ensures that at least one field from a list is present.
13+
14+
This condition validates that at least one field from a specified list of
15+
fields is present and has a non-empty value.
16+
"""
17+
18+
def __init__(
19+
self,
20+
fields: List[str],
21+
min_required: int = 1,
22+
check_empty: bool = True,
23+
custom_message: Optional[str] = None,
24+
) -> None:
25+
"""
26+
Initialize the at least one required condition.
27+
28+
Args:
29+
fields: List of field names to check
30+
min_required: Minimum number of fields that must be present
31+
check_empty: Whether to consider empty strings as absent
32+
custom_message: Custom error message
33+
34+
Example:
35+
# At least one contact method required
36+
AtLeastOneRequiredCondition(['email', 'phone', 'address'])
37+
"""
38+
self.fields = fields
39+
self.min_required = min_required
40+
self.check_empty = check_empty
41+
self.custom_message = custom_message
42+
43+
def check(self, data: Dict[str, Any]) -> bool:
44+
"""
45+
Check if at least the minimum required fields are present.
46+
47+
Args:
48+
data: The validated data dictionary
49+
50+
Returns:
51+
True if condition is met, False otherwise
52+
"""
53+
if not isinstance(data, dict):
54+
return False
55+
56+
present_count = 0
57+
58+
for field in self.fields:
59+
if field in data and data[field] is not None:
60+
if self.check_empty and isinstance(data[field], str):
61+
if data[field].strip() == "":
62+
continue
63+
64+
if self.check_empty and isinstance(data[field], (list, dict)):
65+
if len(data[field]) == 0:
66+
continue
67+
68+
present_count += 1
69+
70+
return present_count >= self.min_required
71+
72+
def get_error_message(self, data: Dict[str, Any]) -> str:
73+
"""
74+
Get a descriptive error message.
75+
76+
Args:
77+
data: The validated data dictionary
78+
79+
Returns:
80+
Error message string
81+
"""
82+
if self.custom_message:
83+
return self.custom_message
84+
85+
present_fields = []
86+
missing_fields = []
87+
88+
for field in self.fields:
89+
if field in data and data[field] is not None:
90+
if self.check_empty:
91+
if (
92+
isinstance(data[field], str)
93+
and data[field].strip() == ""
94+
) or (
95+
isinstance(data[field], (list, dict))
96+
and len(data[field]) == 0
97+
):
98+
missing_fields.append(field)
99+
else:
100+
present_fields.append(field)
101+
else:
102+
present_fields.append(field)
103+
else:
104+
missing_fields.append(field)
105+
106+
if self.min_required == 1:
107+
return (
108+
f"At least one of the following fields is required: {', '.join(self.fields)}. "
109+
f"None were provided or all were empty."
110+
)
111+
return (
112+
f"At least {self.min_required} of the following fields are required: "
113+
f"{', '.join(self.fields)}. Only {len(present_fields)} provided."
114+
)

0 commit comments

Comments
 (0)