criteria-pattern 0.1.1__tar.gz

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.
@@ -0,0 +1,12 @@
1
+ # linting files
2
+ .coverage
3
+
4
+ # cache files/folders
5
+ **cache**
6
+
7
+ # build and distribution files/folders
8
+ **dist**
9
+ **build**
10
+
11
+ # environment folders
12
+ **.venv**
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) (2024-2025) adriamontoto
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.
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: criteria-pattern
3
+ Version: 0.1.1
4
+ Summary: The Criteria Pattern is a Python package that simplifies and standardizes criteria based filtering, validation and selection.
5
+ Project-URL: Homepage, https://github.com/adriamontoto/criteria-pattern
6
+ Project-URL: Repository, https://github.com/adriamontoto/criteria-pattern
7
+ Project-URL: Issues, https://github.com/adriamontoto/criteria-pattern/issues
8
+ Author: Adria Montoto
9
+ License-Expression: MIT
10
+ License-File: LICENSE.md
11
+ Keywords: criteria,development,filtering,pattern,python,selection,tools,utilities,validation
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: value-object-pattern>=0.2.0
23
+ Description-Content-Type: text/markdown
24
+
25
+ <a name="readme-top"></a>
26
+
27
+ # 🤏🏻 Criteria Pattern
28
+
29
+ <p align="center">
30
+ <a href="https://github.com/adriamontoto/criteria-pattern/actions/workflows/ci.yaml?event=push&branch=master" target="_blank">
31
+ <img src="https://github.com/adriamontoto/criteria-pattern/actions/workflows/ci.yaml/badge.svg?event=push&branch=master" alt="CI Pipeline">
32
+ </a>
33
+ <a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/adriamontoto/criteria-pattern" target="_blank">
34
+ <img src="https://coverage-badge.samuelcolvin.workers.dev/adriamontoto/criteria-pattern.svg" alt="Coverage Pipeline">
35
+ </a>
36
+ <a href="https://pypi.org/project/criteria-pattern" target="_blank">
37
+ <img src="https://img.shields.io/pypi/v/criteria-pattern?color=%2334D058&label=pypi%20package" alt="Package Version">
38
+ </a>
39
+ <a href="https://pypi.org/project/criteria-pattern/" target="_blank">
40
+ <img src="https://img.shields.io/pypi/pyversions/criteria-pattern.svg?color=%2334D058" alt="Supported Python Versions">
41
+ </a>
42
+ </p>
43
+
44
+ The **Criteria Pattern** is a Python 🐍 package that simplifies and standardizes criteria based filtering 🤏🏻, validation and selection. This package provides a set of prebuilt 👷🏻 objects and utilities that you can drop into your existing projects and not have to implement yourself.
45
+
46
+ These utilities 🛠️ are useful when you need complex filtering logic. It also enforces 👮🏻 best practices so all your filtering processes follow a uniform standard.
47
+
48
+ Easy to install and integrate, this is a must have for any Python developer looking to simplify their workflow, enforce design patterns and use the full power of modern ORMs and SQL 🗄️ in their projects 🚀.
49
+ <br><br>
50
+
51
+ ## Table of Contents
52
+
53
+ - [📥 Installation](#installation)
54
+ - [💻 Utilization](#utilization)
55
+ - [🤝 Contributing](#contributing)
56
+ - [🔑 License](#license)
57
+
58
+ <p align="right">
59
+ <a href="#readme-top">🔼 Back to top</a>
60
+ </p><br><br>
61
+
62
+ <a name="installation"></a>
63
+
64
+ ## 📥 Installation
65
+
66
+ You can install **Criteria Pattern** using `pip`:
67
+
68
+ ```bash
69
+ pip install criteria-pattern
70
+ ```
71
+
72
+ <p align="right">
73
+ <a href="#readme-top">🔼 Back to top</a>
74
+ </p><br><br>
75
+
76
+ <a name="utilization"></a>
77
+
78
+ ## 💻 Utilization
79
+
80
+ ```python
81
+ from criteria_pattern import Criteria, Filter, FilterOperator
82
+ from criteria_pattern.converter import SqlConverter
83
+
84
+ is_adult = Criteria(filters=[Filter('age', FilterOperator.GREATER_OR_EQUAL, 18)])
85
+ email_is_gmail = Criteria(filters=[Filter('email', FilterOperator.ENDS_WITH, '@gmail.com')])
86
+ email_is_yahoo = Criteria(filters=[Filter('email', FilterOperator.ENDS_WITH, '@yahoo.com')])
87
+
88
+ query, parameters = SqlConverter.convert(criteria=is_adult & (email_is_gmail | email_is_yahoo), table='user')
89
+ print(query)
90
+ print(parameters)
91
+ # >>> SELECT * FROM user WHERE (age >= %(parameter_0)s AND (email LIKE '%%' || %(parameter_1)s OR email LIKE '%%' || %(parameter_2)s));
92
+ # >>> {'parameter_0': 18, 'parameter_1': '@gmail.com', 'parameter_2': '@yahoo.com'}
93
+ ```
94
+
95
+ <p align="right">
96
+ <a href="#readme-top">🔼 Back to top</a>
97
+ </p><br><br>
98
+
99
+ <a name="contributing"></a>
100
+
101
+ ## 🤝 Contributing
102
+
103
+ We love community help! Before you open an issue or pull request, please read:
104
+
105
+ - [`🤝 How to Contribute`](https://github.com/adriamontoto/criteria-pattern/blob/master/.github/CONTRIBUTING.md)
106
+ - [`🧭 Code of Conduct`](https://github.com/adriamontoto/criteria-pattern/blob/master/.github/CODE_OF_CONDUCT.md)
107
+ - [`🔐 Security Policy`](https://github.com/adriamontoto/criteria-pattern/blob/master/.github/SECURITY.md)
108
+
109
+ _Thank you for helping make **🤏🏻 Criteria Pattern** package awesome! 🌟_
110
+
111
+ <p align="right">
112
+ <a href="#readme-top">🔼 Back to top</a>
113
+ </p><br><br>
114
+
115
+ <a name="license"></a>
116
+
117
+ ## 🔑 License
118
+
119
+ This project is licensed under the terms of the [`MIT license`](https://github.com/adriamontoto/criteria-pattern/blob/master/LICENSE.md).
120
+
121
+ <p align="right">
122
+ <a href="#readme-top">🔼 Back to top</a>
123
+ </p>
@@ -0,0 +1,99 @@
1
+ <a name="readme-top"></a>
2
+
3
+ # 🤏🏻 Criteria Pattern
4
+
5
+ <p align="center">
6
+ <a href="https://github.com/adriamontoto/criteria-pattern/actions/workflows/ci.yaml?event=push&branch=master" target="_blank">
7
+ <img src="https://github.com/adriamontoto/criteria-pattern/actions/workflows/ci.yaml/badge.svg?event=push&branch=master" alt="CI Pipeline">
8
+ </a>
9
+ <a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/adriamontoto/criteria-pattern" target="_blank">
10
+ <img src="https://coverage-badge.samuelcolvin.workers.dev/adriamontoto/criteria-pattern.svg" alt="Coverage Pipeline">
11
+ </a>
12
+ <a href="https://pypi.org/project/criteria-pattern" target="_blank">
13
+ <img src="https://img.shields.io/pypi/v/criteria-pattern?color=%2334D058&label=pypi%20package" alt="Package Version">
14
+ </a>
15
+ <a href="https://pypi.org/project/criteria-pattern/" target="_blank">
16
+ <img src="https://img.shields.io/pypi/pyversions/criteria-pattern.svg?color=%2334D058" alt="Supported Python Versions">
17
+ </a>
18
+ </p>
19
+
20
+ The **Criteria Pattern** is a Python 🐍 package that simplifies and standardizes criteria based filtering 🤏🏻, validation and selection. This package provides a set of prebuilt 👷🏻 objects and utilities that you can drop into your existing projects and not have to implement yourself.
21
+
22
+ These utilities 🛠️ are useful when you need complex filtering logic. It also enforces 👮🏻 best practices so all your filtering processes follow a uniform standard.
23
+
24
+ Easy to install and integrate, this is a must have for any Python developer looking to simplify their workflow, enforce design patterns and use the full power of modern ORMs and SQL 🗄️ in their projects 🚀.
25
+ <br><br>
26
+
27
+ ## Table of Contents
28
+
29
+ - [📥 Installation](#installation)
30
+ - [💻 Utilization](#utilization)
31
+ - [🤝 Contributing](#contributing)
32
+ - [🔑 License](#license)
33
+
34
+ <p align="right">
35
+ <a href="#readme-top">🔼 Back to top</a>
36
+ </p><br><br>
37
+
38
+ <a name="installation"></a>
39
+
40
+ ## 📥 Installation
41
+
42
+ You can install **Criteria Pattern** using `pip`:
43
+
44
+ ```bash
45
+ pip install criteria-pattern
46
+ ```
47
+
48
+ <p align="right">
49
+ <a href="#readme-top">🔼 Back to top</a>
50
+ </p><br><br>
51
+
52
+ <a name="utilization"></a>
53
+
54
+ ## 💻 Utilization
55
+
56
+ ```python
57
+ from criteria_pattern import Criteria, Filter, FilterOperator
58
+ from criteria_pattern.converter import SqlConverter
59
+
60
+ is_adult = Criteria(filters=[Filter('age', FilterOperator.GREATER_OR_EQUAL, 18)])
61
+ email_is_gmail = Criteria(filters=[Filter('email', FilterOperator.ENDS_WITH, '@gmail.com')])
62
+ email_is_yahoo = Criteria(filters=[Filter('email', FilterOperator.ENDS_WITH, '@yahoo.com')])
63
+
64
+ query, parameters = SqlConverter.convert(criteria=is_adult & (email_is_gmail | email_is_yahoo), table='user')
65
+ print(query)
66
+ print(parameters)
67
+ # >>> SELECT * FROM user WHERE (age >= %(parameter_0)s AND (email LIKE '%%' || %(parameter_1)s OR email LIKE '%%' || %(parameter_2)s));
68
+ # >>> {'parameter_0': 18, 'parameter_1': '@gmail.com', 'parameter_2': '@yahoo.com'}
69
+ ```
70
+
71
+ <p align="right">
72
+ <a href="#readme-top">🔼 Back to top</a>
73
+ </p><br><br>
74
+
75
+ <a name="contributing"></a>
76
+
77
+ ## 🤝 Contributing
78
+
79
+ We love community help! Before you open an issue or pull request, please read:
80
+
81
+ - [`🤝 How to Contribute`](https://github.com/adriamontoto/criteria-pattern/blob/master/.github/CONTRIBUTING.md)
82
+ - [`🧭 Code of Conduct`](https://github.com/adriamontoto/criteria-pattern/blob/master/.github/CODE_OF_CONDUCT.md)
83
+ - [`🔐 Security Policy`](https://github.com/adriamontoto/criteria-pattern/blob/master/.github/SECURITY.md)
84
+
85
+ _Thank you for helping make **🤏🏻 Criteria Pattern** package awesome! 🌟_
86
+
87
+ <p align="right">
88
+ <a href="#readme-top">🔼 Back to top</a>
89
+ </p><br><br>
90
+
91
+ <a name="license"></a>
92
+
93
+ ## 🔑 License
94
+
95
+ This project is licensed under the terms of the [`MIT license`](https://github.com/adriamontoto/criteria-pattern/blob/master/LICENSE.md).
96
+
97
+ <p align="right">
98
+ <a href="#readme-top">🔼 Back to top</a>
99
+ </p>
@@ -0,0 +1,11 @@
1
+ __version__ = '0.1.1'
2
+
3
+ from .models import Criteria, Filter, FilterOperator, Order, OrderDirection
4
+
5
+ __all__ = (
6
+ 'Criteria',
7
+ 'Filter',
8
+ 'FilterOperator',
9
+ 'Order',
10
+ 'OrderDirection',
11
+ )
@@ -0,0 +1,3 @@
1
+ from .sql_converter import SqlConverter
2
+
3
+ __all__ = ('SqlConverter',)
@@ -0,0 +1,387 @@
1
+ """
2
+ Raw SQL converter module.
3
+ """
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ from typing import Any, assert_never
7
+
8
+ from criteria_pattern import Criteria, FilterOperator, OrderDirection
9
+ from criteria_pattern.exceptions import InvalidColumnError, InvalidTableError
10
+ from criteria_pattern.models.criteria import AndCriteria, NotCriteria, OrCriteria
11
+
12
+
13
+ class SqlConverter:
14
+ """
15
+ Raw SQL converter.
16
+
17
+ Example:
18
+ ```python
19
+ from criteria_pattern import Criteria, Filter, FilterOperator
20
+ from criteria_pattern.converter import SqlConverter
21
+
22
+ is_adult = Criteria(filters=[Filter('age', FilterOperator.GREATER_OR_EQUAL, 18)])
23
+ email_is_gmail = Criteria(filters=[Filter('email', FilterOperator.ENDS_WITH, '@gmail.com')])
24
+ email_is_yahoo = Criteria(filters=[Filter('email', FilterOperator.ENDS_WITH, '@yahoo.com')])
25
+
26
+ query, parameters = SqlConverter.convert(criteria=is_adult & (email_is_gmail | email_is_yahoo), table='user')
27
+ print(query)
28
+ print(parameters)
29
+ # >>> SELECT * FROM user WHERE (age >= %(parameter_0)s AND (email LIKE '%%' || %(parameter_1)s OR email LIKE '%%' || %(parameter_2)s));
30
+ # >>> {'parameter_0': 18, 'parameter_1': '@gmail.com', 'parameter_2': '@yahoo.com'}
31
+ ```
32
+ """ # noqa: E501
33
+
34
+ @classmethod
35
+ def convert(
36
+ cls,
37
+ criteria: Criteria,
38
+ table: str,
39
+ columns: Sequence[str] | None = None,
40
+ columns_mapping: Mapping[str, str] | None = None,
41
+ check_table_injection: bool = False,
42
+ check_column_injection: bool = False,
43
+ check_criteria_injection: bool = False,
44
+ valid_tables: Sequence[str] | None = None,
45
+ valid_columns: Sequence[str] | None = None,
46
+ ) -> tuple[str, dict[str, Any]]:
47
+ """
48
+ Convert the Criteria object to a raw SQL query.
49
+
50
+ Args:
51
+ criteria (Criteria): Criteria to convert.
52
+ table (str): Name of the table to query.
53
+ columns (Sequence[str], optional): Columns of the table to select. Default to *. Default to *.
54
+ columns_mapping (Mapping[str, str], optional): Mapping of column names to aliases. Default to empty dict.
55
+ check_criteria_injection (bool, optional): Raise an error if the criteria field is not in the list of valid
56
+ columns. Default to False.
57
+ check_table_injection (bool, optional): Raise an error if the table is not in the list of valid tables.
58
+ Default to False.
59
+ check_column_injection (bool, optional): Raise an error if the column is not in the list of valid columns.
60
+ Default to False.
61
+ valid_tables (Sequence[str], optional): List of valid tables to query. Default to empty list.
62
+ valid_columns (Sequence[str], optional): List of valid columns to select. Default to empty list.
63
+
64
+ Raises:
65
+ InvalidTableError: If the table is not in the list of valid tables (only if check_table_injection=True).
66
+ InvalidColumnError: If the column is not in the list of valid columns (only if check_column_injection=True).
67
+
68
+ Returns:
69
+ tuple[str, dict[str, Any]]: The raw SQL query string and the query parameters.
70
+
71
+ Example:
72
+ ```python
73
+ from criteria_pattern import Criteria, Filter, FilterOperator
74
+ from criteria_pattern.converter import SqlConverter
75
+
76
+ is_adult = Criteria(filters=[Filter('age', FilterOperator.GREATER_OR_EQUAL, 18)])
77
+ email_is_gmail = Criteria(filters=[Filter('email', FilterOperator.ENDS_WITH, '@gmail.com')])
78
+ email_is_yahoo = Criteria(filters=[Filter('email', FilterOperator.ENDS_WITH, '@yahoo.com')])
79
+
80
+ query, parameters = SqlConverter.convert(criteria=is_adult & (email_is_gmail | email_is_yahoo), table='user')
81
+ print(query)
82
+ print(parameters)
83
+ # >>> SELECT * FROM user WHERE (age >= %(parameter_0)s AND (email LIKE '%%' || %(parameter_1)s OR email LIKE '%%' || %(parameter_2)s));
84
+ # >>> {'parameter_0': 18, 'parameter_1': '@gmail.com', 'parameter_2': '@yahoo.com'}
85
+ ```
86
+ """ # noqa: E501
87
+ columns = columns or ['*']
88
+ columns_mapping = columns_mapping or {}
89
+ valid_tables = valid_tables or []
90
+ valid_columns = valid_columns or []
91
+
92
+ if check_table_injection:
93
+ cls._validate_table(table=table, valid_tables=valid_tables)
94
+
95
+ if check_column_injection:
96
+ cls._validate_columns(columns=columns, columns_mapping=columns_mapping, valid_columns=valid_columns)
97
+
98
+ if check_criteria_injection:
99
+ cls._validate_criteria(criteria=criteria, valid_columns=valid_columns)
100
+
101
+ query = f'SELECT {", ".join(columns)} FROM {table}' # noqa: S608 # nosec
102
+ parameters: dict[str, Any] = {}
103
+
104
+ if criteria.has_filters():
105
+ where_clause, parameters = cls._process_filters(criteria=criteria, columns_mapping=columns_mapping)
106
+ query += f' WHERE {where_clause}'
107
+
108
+ if criteria.has_orders():
109
+ order_clause = cls._process_orders(criteria=criteria, columns_mapping=columns_mapping)
110
+ query += f' ORDER BY {order_clause}'
111
+
112
+ return f'{query};', parameters
113
+
114
+ @classmethod
115
+ def _validate_table(cls, table: str, valid_tables: Sequence[str]) -> None:
116
+ """
117
+ Validate the table name to prevent SQL injection.
118
+
119
+ Args:
120
+ table (str): Name of the table to query.
121
+ valid_tables (Sequence[str]): List of valid tables to query.
122
+
123
+ Raises:
124
+ InvalidTableError: If the table is not in the list of valid tables.
125
+ """
126
+ if table not in valid_tables:
127
+ raise InvalidTableError(table=table, valid_tables=valid_tables)
128
+
129
+ @classmethod
130
+ def _validate_columns(
131
+ cls,
132
+ columns: Sequence[str],
133
+ columns_mapping: Mapping[str, str],
134
+ valid_columns: Sequence[str],
135
+ ) -> None:
136
+ """
137
+ Validate the column names to prevent SQL injection.
138
+
139
+ Args:
140
+ columns (Sequence[str]): Columns of the table to select.
141
+ columns_mapping (Mapping[str, str]): Mapping of column names to aliases.
142
+ valid_columns (Sequence[str]): List of valid columns to select.
143
+
144
+ Raises:
145
+ InvalidColumnError: If the column is not in the list of valid columns.
146
+ """
147
+ for column in columns:
148
+ if column not in valid_columns:
149
+ raise InvalidColumnError(column=column, valid_columns=valid_columns)
150
+
151
+ for column in columns_mapping.values():
152
+ if column not in valid_columns:
153
+ raise InvalidColumnError(column=column, valid_columns=valid_columns)
154
+
155
+ @classmethod
156
+ def _validate_criteria(cls, criteria: Criteria, valid_columns: Sequence[str]) -> None:
157
+ """
158
+ Validate the Criteria object to prevent SQL injection.
159
+
160
+ Args:
161
+ criteria (Criteria): Criteria to validate.
162
+ valid_columns (Sequence[str]): List of valid columns to select.
163
+
164
+ Raises:
165
+ InvalidColumnError: If the column is not in the list of valid columns.
166
+ """
167
+ for filter in criteria.filters:
168
+ if filter.field not in valid_columns:
169
+ raise InvalidColumnError(column=filter.field, valid_columns=valid_columns)
170
+
171
+ for order in criteria.orders:
172
+ if order.field not in valid_columns:
173
+ raise InvalidColumnError(column=order.field, valid_columns=valid_columns)
174
+
175
+ @classmethod
176
+ def _process_filters(cls, criteria: Criteria, columns_mapping: Mapping[str, str]) -> tuple[str, dict[str, Any]]:
177
+ """
178
+ Process the Criteria object to return an SQL WHERE clause.
179
+
180
+ Args:
181
+ criteria (Criteria): Criteria to process.
182
+ columns_mapping (Mapping[str, str]): Mapping of column names to aliases.
183
+
184
+ Returns:
185
+ tuple[str, dict[str, Any]]: Processed filter string for SQL WHERE clause and parameters for the SQL query.
186
+ """
187
+ return cls._process_filters_recursive(criteria=criteria, columns_mapping=columns_mapping)
188
+
189
+ @classmethod
190
+ def _process_filters_recursive( # noqa: C901
191
+ cls,
192
+ criteria: Criteria,
193
+ columns_mapping: Mapping[str, str],
194
+ parameters_counter: int = 0,
195
+ ) -> tuple[str, dict[str, Any]]:
196
+ """
197
+ Process the Criteria object to return an SQL WHERE clause.
198
+
199
+ Args:
200
+ criteria (Criteria): Criteria to process.
201
+ columns_mapping (Mapping[str, str]): Mapping of column names to aliases.
202
+ parameters_counter (int): Counter for parameter names to ensure uniqueness.
203
+
204
+ Returns:
205
+ tuple[str, dict[str, Any]]: Processed filter string for SQL WHERE clause and parameters for the SQL query.
206
+ """
207
+ filters = ''
208
+ parameters: dict[str, Any] = {}
209
+
210
+ if isinstance(criteria, AndCriteria):
211
+ left_conditions, left_parameters = cls._process_filters_recursive(
212
+ criteria=criteria.left,
213
+ columns_mapping=columns_mapping,
214
+ parameters_counter=parameters_counter,
215
+ )
216
+ parameters_counter += len(left_parameters)
217
+ parameters.update(left_parameters)
218
+
219
+ right_conditions, right_parameters = cls._process_filters_recursive(
220
+ criteria=criteria.right,
221
+ columns_mapping=columns_mapping,
222
+ parameters_counter=parameters_counter,
223
+ )
224
+ parameters_counter += len(right_parameters)
225
+ parameters.update(right_parameters)
226
+
227
+ filters += f'({left_conditions} AND {right_conditions})'
228
+
229
+ return filters, parameters
230
+
231
+ if isinstance(criteria, OrCriteria):
232
+ left_conditions, left_parameters = cls._process_filters_recursive(
233
+ criteria=criteria.left,
234
+ columns_mapping=columns_mapping,
235
+ parameters_counter=parameters_counter,
236
+ )
237
+ parameters_counter += len(left_parameters)
238
+ parameters.update(left_parameters)
239
+
240
+ right_conditions, right_parameters = cls._process_filters_recursive(
241
+ criteria=criteria.right,
242
+ columns_mapping=columns_mapping,
243
+ parameters_counter=parameters_counter,
244
+ )
245
+ parameters_counter += len(right_parameters)
246
+ parameters.update(right_parameters)
247
+
248
+ filters += f'({left_conditions} OR {right_conditions})'
249
+
250
+ return filters, parameters
251
+
252
+ if isinstance(criteria, NotCriteria):
253
+ not_conditions, not_parameters = cls._process_filters_recursive(
254
+ criteria=criteria.criteria,
255
+ columns_mapping=columns_mapping,
256
+ parameters_counter=parameters_counter,
257
+ )
258
+ parameters_counter += len(not_parameters)
259
+ parameters.update(not_parameters)
260
+
261
+ filters += f'NOT ({not_conditions})'
262
+
263
+ return filters, parameters
264
+
265
+ for filter in criteria.filters:
266
+ filter_field = columns_mapping.get(filter.field, filter.field)
267
+ parameter_name = f'parameter_{parameters_counter}'
268
+ parameters[parameter_name] = filter.value
269
+ placeholder = f'%({parameter_name})s'
270
+ parameters_counter += 1
271
+
272
+ match filter.operator:
273
+ case FilterOperator.EQUAL:
274
+ filters += f'{filter_field} = {placeholder}'
275
+
276
+ case FilterOperator.NOT_EQUAL:
277
+ filters += f'{filter_field} != {placeholder}'
278
+
279
+ case FilterOperator.GREATER:
280
+ filters += f'{filter_field} > {placeholder}'
281
+
282
+ case FilterOperator.GREATER_OR_EQUAL:
283
+ filters += f'{filter_field} >= {placeholder}'
284
+
285
+ case FilterOperator.LESS:
286
+ filters += f'{filter_field} < {placeholder}'
287
+
288
+ case FilterOperator.LESS_OR_EQUAL:
289
+ filters += f'{filter_field} <= {placeholder}'
290
+
291
+ case FilterOperator.LIKE:
292
+ filters += f'{filter_field} LIKE {placeholder}'
293
+
294
+ case FilterOperator.NOT_LIKE:
295
+ filters += f'{filter_field} NOT LIKE {placeholder}'
296
+
297
+ case FilterOperator.CONTAINS:
298
+ filters += f"{filter_field} LIKE '%%' || {placeholder} || '%%'"
299
+
300
+ case FilterOperator.NOT_CONTAINS:
301
+ filters += f"{filter_field} NOT LIKE '%%' || {placeholder} || '%%'"
302
+
303
+ case FilterOperator.STARTS_WITH:
304
+ filters += f"{filter_field} LIKE {placeholder} || '%%'"
305
+
306
+ case FilterOperator.NOT_STARTS_WITH:
307
+ filters += f"{filter_field} NOT LIKE {placeholder} || '%%'"
308
+
309
+ case FilterOperator.ENDS_WITH:
310
+ filters += f"{filter_field} LIKE '%%' || {placeholder}"
311
+
312
+ case FilterOperator.NOT_ENDS_WITH:
313
+ filters += f"{filter_field} NOT LIKE '%%' || {placeholder}"
314
+
315
+ case FilterOperator.BETWEEN:
316
+ parameters.pop(parameter_name)
317
+ parameters_counter -= 1
318
+
319
+ start_parameter_name = f'parameter_{parameters_counter}'
320
+ end_parameter_name = f'parameter_{parameters_counter + 1}'
321
+ parameters[start_parameter_name] = filter.value[0]
322
+ parameters[end_parameter_name] = filter.value[1]
323
+ start_placeholder = f'%({start_parameter_name})s'
324
+ end_placeholder = f'%({end_parameter_name})s'
325
+ parameters_counter += 2
326
+
327
+ filters += f'{filter_field} BETWEEN {start_placeholder} AND {end_placeholder}'
328
+
329
+ case FilterOperator.NOT_BETWEEN:
330
+ parameters.pop(parameter_name)
331
+ parameters_counter -= 1
332
+
333
+ start_parameter_name = f'parameter_{parameters_counter}'
334
+ end_parameter_name = f'parameter_{parameters_counter + 1}'
335
+ parameters[start_parameter_name] = filter.value[0]
336
+ parameters[end_parameter_name] = filter.value[1]
337
+ start_placeholder = f'%({start_parameter_name})s'
338
+ end_placeholder = f'%({end_parameter_name})s'
339
+ parameters_counter += 2
340
+
341
+ filters += f'{filter_field} NOT BETWEEN {start_placeholder} AND {end_placeholder}'
342
+
343
+ case FilterOperator.IS_NULL:
344
+ parameters.pop(parameter_name)
345
+ parameters_counter -= 1
346
+
347
+ filters += f'{filter_field} IS NULL'
348
+
349
+ case FilterOperator.IS_NOT_NULL:
350
+ parameters.pop(parameter_name)
351
+ parameters_counter -= 1
352
+
353
+ filters += f'{filter_field} IS NOT NULL'
354
+
355
+ case _: # pragma: no cover
356
+ assert_never(filter.operator)
357
+
358
+ return filters, parameters
359
+
360
+ @classmethod
361
+ def _process_orders(cls, criteria: Criteria, columns_mapping: Mapping[str, str]) -> str:
362
+ """
363
+ Process the Criteria object to return an SQL ORDER BY clause.
364
+
365
+ Args:
366
+ criteria (Criteria): Criteria to process.
367
+ columns_mapping (Mapping[str, str]): Mapping of column names to aliases.
368
+
369
+ Returns:
370
+ str: Processed order string for SQL ORDER BY clause.
371
+ """
372
+ orders = ''
373
+
374
+ for order in criteria.orders:
375
+ order_field = columns_mapping.get(order.field, order.field)
376
+
377
+ match order.direction:
378
+ case OrderDirection.ASC:
379
+ orders += f'{order_field} ASC, '
380
+
381
+ case OrderDirection.DESC:
382
+ orders += f'{order_field} DESC, '
383
+
384
+ case _: # pragma: no cover
385
+ assert_never(order.direction)
386
+
387
+ return orders.rstrip(', ')
@@ -0,0 +1,9 @@
1
+ from .invalid_column_error import InvalidColumnError
2
+ from .invalid_table_error import InvalidTableError
3
+ from .sql_converter_error import SqlConverterError
4
+
5
+ __all__ = (
6
+ 'InvalidColumnError',
7
+ 'InvalidTableError',
8
+ 'SqlConverterError',
9
+ )