├── .github └── workflows │ └── CI.yml ├── .gitignore ├── LICENSE ├── README.md ├── fastapi_sa_orm_filter ├── __init__.py ├── dto.py ├── exceptions.py ├── main.py ├── operators.py ├── parsers.py └── sa_expression_builder.py ├── poetry.lock ├── pyproject.toml ├── pytest.ini └── tests ├── conftest.py ├── schemas.py ├── test_filter.py ├── test_join_filter.py └── utils.py /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | test: 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | max-parallel: 4 12 | matrix: 13 | os: [ubuntu-latest] 14 | python-version: ['3.10', '3.11', '3.12'] 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Setup Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v3 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | python3 -m pip install --upgrade pip 25 | python3 -m pip install poetry 26 | poetry config virtualenvs.create false 27 | poetry lock 28 | poetry install --no-root 29 | - name: Run tests 30 | run: | 31 | poetry run pytest -s 32 | - name: Run flake8 33 | run: | 34 | poetry run flake8 --max-line-length=120 35 | - name: Upload coverage reports to Codecov 36 | uses: codecov/codecov-action@v3 37 | env: 38 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | __pycache__/ 3 | .env 4 | dist 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Oleksandr Zhydyk 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## FastAPI SQLAlchemy Filter 2 | ![ci_badge](https://github.com/OleksandrZhydyk/FastAPI-SQLAlchemy-Filters/actions/workflows/CI.yml/badge.svg) 3 | [![Downloads](https://static.pepy.tech/badge/fastapi_sa_orm_filter)](https://pepy.tech/project/fastapi_sa_orm_filter) 4 | [![PyPI version](https://img.shields.io/pypi/v/fastapi-sa-orm-filter.svg)](https://pypi.org/project/fastapi-sa-orm-filter/) 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | 7 | Package that helps to implement easy objects filtering and sorting for applications 8 | build on FastAPI and SQLAlchemy. 9 | For using you just need to define your custom filter with filtered fields and applied operators. 10 | Supported operators, datatypes and example of work you can find below. 11 | 12 | ### Installation 13 | ```shell 14 | pip install fastapi-sa-orm-filter 15 | ``` 16 | ### Compatibility 17 | v 0.2.1 18 | - Python: >= 3.10 19 | - Fastapi: >= 0.100 20 | - Pydantic: >= 2.0.0 21 | - SQLAlchemy: >= 1.4.36, < 2.1.0 22 | 23 | v 0.1.5 24 | - Python: >= 3.8 25 | - Fastapi: <= 0.100 26 | - Pydantic: < 2.0.0 27 | - SQLAlchemy: == 1.4 28 | 29 | ### Quickstart 30 | 31 | ```shell 32 | from fastapi import FastAPI 33 | from fastapi.params import Query 34 | from fastapi_sa_orm_filter import FilterCore, ops 35 | 36 | from db.base import get_session 37 | from db.models import MyModel 38 | 39 | 40 | app = FastAPI() 41 | 42 | # Define fields and operators for filter 43 | my_objects_filter = { 44 | 'my_model_field_name': [ops.eq, ops.in_], 45 | 'my_model_field_name': [ops.between, ops.eq, ops.gt, ops.lt, ops.in_], 46 | 'my_model_field_name': [ops.like, ops.startswith, ops.contains, ops.in_], 47 | 'my_model_field_name': [ops.between, ops.not_eq, ops.gte, ops.lte] 48 | } 49 | 50 | @app.get("/") 51 | async def get_filtered_objects( 52 | filter_query: str = Query(default=''), 53 | db: AsyncSession = Depends(get_session) 54 | ) -> List[MyModel]: 55 | my_filter = FilterCore(MyModel, my_objects_filter) 56 | query = my_filter.get_query(filter_query) 57 | res = await db.execute(query) 58 | return res.scalars().all() 59 | ``` 60 | 61 | ### Examples of usage 62 | 63 | ```shell 64 | 65 | # Input query string 66 | ''' 67 | salary_from__in_=60,70,80& 68 | created_at__between=2023-05-01,2023-05-05| 69 | category__eq=Medicine& 70 | order_by=-id,category 71 | ''' 72 | 73 | 74 | # Returned SQLAlchemy orm query exact as: 75 | 76 | select(model) 77 | .where( 78 | or_( 79 | and_( 80 | model.salary_from.in_(60,70,80), 81 | model.created_at.between(2023-05-01, 2023-05-05) 82 | ), 83 | model.category == 'Medicine' 84 | ).order_by(model.id.desc(), model.category.asc()) 85 | ``` 86 | 87 | ```shell 88 | # Filter by joined model 89 | 90 | # Input query string 91 | '''vacancies.salary_from__gte=100''' 92 | 93 | allowed_filter_fields = { 94 | "id": [ops.eq], 95 | "title": [ops.startswith, ops.eq, ops.contains], 96 | "salary_from": [ops.eq, ops.gt, ops.lte, ops.gte] 97 | } 98 | 99 | company_filter = FilterCore( 100 | Company, 101 | allowed_filter_fields, 102 | select(Company).join(Vacancy).options(joinedload(Company.vacancies)) 103 | ) 104 | 105 | @app.get("/") 106 | async def get_filtered_company( 107 | filter_query: str = "title__eq=MyCompany&vacancies.salary_from__gte=100", 108 | db: AsyncSession = Depends(get_session) 109 | ) -> List[Company]: 110 | 111 | query = company_filter.get_query(filter_query) 112 | res = await db.execute(query) 113 | return res.scalars().all() 114 | 115 | # Returned SQLAlchemy query 116 | select(Company) 117 | .join(Vacancy) 118 | .options(joinedload(Company.vacancies)) 119 | .where( 120 | and_( 121 | Company.title == "MyCompany", 122 | Vacancy.salary_from >= 100 123 | ) 124 | ) 125 | 126 | ``` 127 | 128 | ### Supported query string format 129 | 130 | * field_name__eq=value 131 | * field_name__in_=value1,value2 132 | * field_name__eq=value&field_name__in_=value1,value2 133 | * field_name__eq=value&field_name__in_=value1,value2&order_by=-field_name 134 | 135 | ### Modify query for custom selection 136 | ```shell 137 | # Create a class inherited from FilterCore and rewrite 'get_unordered_query' method. 138 | # ^0.2.0 Version 139 | 140 | class CustomFilter(FilterCore): 141 | 142 | def get_select_query_part(self): 143 | custom_select = select( 144 | self.model.id, 145 | self.model.is_active, 146 | func.sum(self.model.salary_from).label("sum_salary_from"), 147 | self.model.category 148 | ) 149 | return custom_select 150 | 151 | def get_group_by_query_part(self): 152 | return [self.model.is_active] 153 | 154 | 155 | # 0.1.5 Version 156 | # Original method is: 157 | def get_unordered_query(self, conditions): 158 | unordered_query = select(self._model).filter(or_(*conditions)) 159 | return unordered_query 160 | 161 | # Rewrite example: 162 | class CustomFilter(FilterCore): 163 | 164 | def get_unordered_query(self, conditions): 165 | unordered_query = select( 166 | self.model.field_name1, 167 | self.model.field_name2, 168 | func.sum(self.model.field_name3).label("field_name3"), 169 | self.model.field_name4 170 | ).filter(or_(*conditions)).group_by(self.model.field_name2) 171 | return unordered_query 172 | 173 | ``` 174 | 175 | ### Supported SQLAlchemy datatypes: 176 | * DATETIME 177 | * DATE 178 | * INTEGER 179 | * FLOAT 180 | * TEXT 181 | * VARCHAR 182 | * Enum(VARCHAR()) 183 | * BOOLEAN 184 | 185 | ### Available filter operators: 186 | * __eq__ 187 | * __gt__ 188 | * __lt__ 189 | * __gte__ 190 | * __lte__ 191 | * __in___ 192 | * __startswith__ 193 | * __endswith__ 194 | * __between__ 195 | * __like__ 196 | * __ilike__ 197 | * __contains__ 198 | * __icontains__ 199 | * __not_eq__ 200 | * __not_in__ 201 | * __not_like__ 202 | * __not_between__ 203 | -------------------------------------------------------------------------------- /fastapi_sa_orm_filter/__init__.py: -------------------------------------------------------------------------------- 1 | """FastAPI-SQLAlchemy filter, transform request query string to SQLAlchemy orm query""" 2 | from fastapi_sa_orm_filter.main import FilterCore # noqa 3 | from fastapi_sa_orm_filter.operators import Operators as ops # noqa 4 | 5 | __version__ = "0.2.2" 6 | -------------------------------------------------------------------------------- /fastapi_sa_orm_filter/dto.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class ParsedFilter: 6 | field_name: str 7 | operator: str 8 | value: str 9 | relation: str | None 10 | 11 | @property 12 | def has_relation(self) -> bool: 13 | return bool(self.relation) 14 | -------------------------------------------------------------------------------- /fastapi_sa_orm_filter/exceptions.py: -------------------------------------------------------------------------------- 1 | class SAFilterOrmException(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /fastapi_sa_orm_filter/main.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Type 2 | 3 | from fastapi import HTTPException 4 | from sqlalchemy import select 5 | from sqlalchemy.orm import DeclarativeBase 6 | from sqlalchemy.sql.elements import BinaryExpression, UnaryExpression 7 | from sqlalchemy.sql.expression import or_ 8 | from starlette import status 9 | from sqlalchemy.sql import Select 10 | 11 | from fastapi_sa_orm_filter.exceptions import SAFilterOrmException 12 | from fastapi_sa_orm_filter.operators import Operators as ops 13 | from fastapi_sa_orm_filter.parsers import FilterQueryParser, OrderByQueryParser 14 | from fastapi_sa_orm_filter.sa_expression_builder import SAFilterExpressionBuilder 15 | 16 | 17 | class FilterCore: 18 | """ 19 | Class serves of SQLAlchemy orm query creation. 20 | Convert parsed query data to python data types and form SQLAlchemy query. 21 | """ 22 | 23 | def __init__( 24 | self, 25 | model: Type[DeclarativeBase], 26 | allowed_filters: dict[str, list[ops]], 27 | select_query_part: Select[Any] | None = None 28 | ) -> None: 29 | """ 30 | Produce a class:`FilterCore` object against a function 31 | 32 | :param model: declared SQLAlchemy db model 33 | :param allowed_filters: dict with allowed model fields and operators 34 | for filter, like: 35 | { 36 | 'field_name': [startswith, eq, in_], 37 | 'field_name': [contains, like] 38 | } 39 | """ 40 | self.model = model 41 | self._allowed_filters = allowed_filters 42 | self.select_query_part = select_query_part 43 | 44 | def get_query(self, custom_filter: str) -> Select[Any]: 45 | """ 46 | Construct the SQLAlchemy orm query from request query string 47 | 48 | :param custom_filter: request query string with fields and filter conditions 49 | salary_from__in_=60,70,80& 50 | created_at__between=2023-05-01,2023-05-05| 51 | category__eq=Medicine& 52 | order_by=-id 53 | :param select_query_part: custom select query part (select(model).join(model1)) 54 | 55 | :return: 56 | select(model) 57 | .where( 58 | or_( 59 | and_( 60 | model.salary_from.in_(60,70,80), 61 | model.created_at.between(2023-05-01, 2023-05-05) 62 | ), 63 | model.category == 'Medicine' 64 | ).order_by(model.id.desc()) 65 | """ 66 | split_query = self._split_by_order_by(custom_filter) 67 | try: 68 | complete_query = self._get_complete_query(*split_query) 69 | except SAFilterOrmException as e: 70 | raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=e.args[0]) 71 | return complete_query 72 | 73 | def _get_complete_query(self, filter_query_str: str, order_by_query_str: str | None = None) -> Select[Any]: 74 | select_query_part = self.get_select_query_part() 75 | filter_query_part = self._get_filter_query_part(filter_query_str) 76 | complete_query = select_query_part.filter(*filter_query_part) 77 | group_query_part = self.get_group_by_query_part() 78 | if group_query_part: 79 | complete_query = complete_query.group_by(*group_query_part) 80 | if order_by_query_str is not None: 81 | order_by_query = self.get_order_by_query_part(order_by_query_str) 82 | complete_query = complete_query.order_by(*order_by_query) 83 | return complete_query 84 | 85 | def get_select_query_part(self) -> Select[Any]: 86 | if self.select_query_part is not None: 87 | return self.select_query_part 88 | return select(self.model) 89 | 90 | def _get_filter_query_part(self, filter_query_str: str) -> list[Any]: 91 | conditions = self._get_filter_query(filter_query_str) 92 | if len(conditions) == 0: 93 | return conditions 94 | return [or_(*conditions)] 95 | 96 | def get_group_by_query_part(self) -> list: 97 | return [] 98 | 99 | def get_order_by_query_part(self, order_by_query_str: str) -> list[UnaryExpression]: 100 | order_by_parser = OrderByQueryParser(self.model) 101 | return order_by_parser.get_order_by_query(order_by_query_str) 102 | 103 | def _get_filter_query(self, custom_filter: str) -> list[BinaryExpression]: 104 | filter_conditions = [] 105 | if custom_filter == "": 106 | return filter_conditions 107 | 108 | parser = FilterQueryParser(custom_filter, self._allowed_filters) 109 | parsed_filters = parser.get_parsed_query() 110 | sa_builder = SAFilterExpressionBuilder(self.model) 111 | return sa_builder.get_expressions(parsed_filters) 112 | 113 | @staticmethod 114 | def _split_by_order_by(query) -> list: 115 | split_query = [query_part.strip("&") for query_part in query.split("order_by=")] 116 | if len(split_query) > 2: 117 | raise SAFilterOrmException("Use only one order_by directive") 118 | return split_query 119 | -------------------------------------------------------------------------------- /fastapi_sa_orm_filter/operators.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class Operators(str, Enum): 5 | eq = "__eq__" 6 | gt = "__gt__" 7 | lt = "__lt__" 8 | gte = "__ge__" 9 | lte = "__le__" 10 | in_ = "in_" 11 | startswith = "startswith" 12 | endswith = "endswith" 13 | between = "between" 14 | like = "like" 15 | ilike = "ilike" 16 | contains = "contains" 17 | icontains = "icontains" 18 | not_eq = "__ne__" 19 | not_in = "not_in" 20 | not_like = "not_like" 21 | not_between = "not_between" 22 | 23 | 24 | class OrderSequence(str, Enum): 25 | desc = "desc" 26 | asc = "asc" 27 | -------------------------------------------------------------------------------- /fastapi_sa_orm_filter/parsers.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import DeclarativeBase 2 | from sqlalchemy.sql.elements import UnaryExpression 3 | 4 | from fastapi_sa_orm_filter.dto import ParsedFilter 5 | from fastapi_sa_orm_filter.exceptions import SAFilterOrmException 6 | from fastapi_sa_orm_filter.operators import Operators as ops 7 | from fastapi_sa_orm_filter.operators import OrderSequence 8 | 9 | 10 | class OrderByQueryParser: 11 | """ 12 | Class parse order by part of request query string. 13 | """ 14 | def __init__(self, model: type[DeclarativeBase]) -> None: 15 | self._model = model 16 | 17 | def get_order_by_query(self, order_by_query_str: str) -> list[UnaryExpression]: 18 | order_by_fields = self._validate_order_by_fields(order_by_query_str) 19 | order_by_query = [] 20 | for field in order_by_fields: 21 | if '-' in field: 22 | column = getattr(self._model, field.strip('-')) 23 | order_by_query.append(getattr(column, OrderSequence.desc)()) 24 | else: 25 | column = getattr(self._model, field.strip('+')) 26 | order_by_query.append(getattr(column, OrderSequence.asc)()) 27 | return order_by_query 28 | 29 | def _validate_order_by_fields(self, order_by_query_str: str) -> list[str]: 30 | """ 31 | :return: 32 | [ 33 | +field_name, 34 | -field_name 35 | ] 36 | """ 37 | order_by_fields = order_by_query_str.split(",") 38 | model_fields = self._model.__table__.columns.keys() 39 | for field in order_by_fields: 40 | field = field.strip('+').strip('-') 41 | if field in model_fields: 42 | continue 43 | raise SAFilterOrmException(f"Incorrect order_by field name {field} for model {self._model.__name__}") 44 | return order_by_fields 45 | 46 | 47 | class FilterQueryParser: 48 | """ 49 | Class parse filter part of request query string. 50 | """ 51 | 52 | def __init__( 53 | self, query: str, 54 | allowed_filters: dict[str, list[ops]] 55 | ) -> None: 56 | self._query = query 57 | self._allowed_filters = allowed_filters 58 | 59 | def get_parsed_query(self) -> list[list[ParsedFilter]]: 60 | """ 61 | :return: 62 | [ 63 | [ParsedFilter, ParsedFilter, ParsedFilter] 64 | ] 65 | """ 66 | and_blocks = self._parse_by_conjunctions() 67 | parsed_query = [] 68 | for and_block in and_blocks: 69 | parsed_and_blocks = [] 70 | for expression in and_block: 71 | parsed_filter = self._parse_expression(expression) 72 | self._validate_query_params(parsed_filter.field_name, parsed_filter.operator) 73 | parsed_and_blocks.append(parsed_filter) 74 | parsed_query.append(parsed_and_blocks) 75 | return parsed_query 76 | 77 | def _parse_by_conjunctions(self) -> list[list[str]]: 78 | """ 79 | Split request query string by 'OR' and 'AND' conjunctions 80 | to divide query string to field's conditions 81 | 82 | :return: [ 83 | ['field_name__operator=value', 'field_name__operator=value'], 84 | ['field_name__operator=value'] 85 | ] 86 | """ 87 | and_blocks = [block.split("&") for block in self._query.split("|")] 88 | return and_blocks 89 | 90 | def _parse_expression( 91 | self, expression: str 92 | ) -> ParsedFilter: 93 | relation = None 94 | try: 95 | field_name, condition = expression.split("__") 96 | if "." in field_name: 97 | relation, field_name = self._get_relation_model(field_name) 98 | operator, value = condition.split("=") 99 | except ValueError: 100 | raise SAFilterOrmException( 101 | "Incorrect filter request syntax," 102 | " please use pattern :" 103 | "'{field_name}__{condition}={value}{conjunction}' " 104 | "or '{relation}.{field_name}__{condition}={value}{conjunction}'", 105 | ) 106 | 107 | return ParsedFilter(field_name=field_name, operator=operator, value=value, relation=relation) 108 | 109 | def _get_relation_model(self, field_name: str) -> list[str]: 110 | return field_name.split(".") 111 | 112 | def _validate_query_params( 113 | self, field_name: str, operator: str 114 | ) -> None: 115 | """ 116 | Check expression on valid and allowed field_name and operator 117 | """ 118 | if field_name not in self._allowed_filters: 119 | raise SAFilterOrmException(f"Forbidden filter field '{field_name}'") 120 | for allow_filter in self._allowed_filters[field_name]: 121 | if operator == allow_filter.name: 122 | return 123 | raise SAFilterOrmException(f"Forbidden filter '{operator}' for '{field_name}'") 124 | -------------------------------------------------------------------------------- /fastapi_sa_orm_filter/sa_expression_builder.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any 3 | 4 | import pydantic 5 | from pydantic import create_model 6 | from pydantic._internal._model_construction import ModelMetaclass 7 | from sqlalchemy import inspect, BinaryExpression, and_ 8 | from sqlalchemy.orm import DeclarativeBase, InstrumentedAttribute 9 | from sqlalchemy_to_pydantic import sqlalchemy_to_pydantic 10 | 11 | from fastapi_sa_orm_filter.exceptions import SAFilterOrmException 12 | from fastapi_sa_orm_filter.operators import Operators as ops 13 | 14 | 15 | class SAFilterExpressionBuilder: 16 | 17 | def __init__(self, model: type[DeclarativeBase]) -> None: 18 | self.model = model 19 | self._relationships = inspect(model).relationships.items() 20 | self._model_serializers = self.create_pydantic_serializers() 21 | 22 | def get_expressions(self, parsed_filters) -> list[BinaryExpression]: 23 | model = self.model 24 | table = self.model.__tablename__ 25 | 26 | or_expr = [] 27 | 28 | for and_parsed_filter in parsed_filters: 29 | and_expr = [] 30 | for and_filter in and_parsed_filter: 31 | if and_filter.has_relation: 32 | model = self.get_relation_model(and_filter.relation) 33 | table = model.__tablename__ 34 | column = self.get_column(model, and_filter.field_name) 35 | serialized_dict = self.serialize_expression_value(table, column, and_filter.operator, and_filter.value) 36 | value = serialized_dict[column.name] 37 | expr = self.get_orm_for_field(column, and_filter.operator, value) 38 | and_expr.append(expr) 39 | or_expr.append(and_(*and_expr)) 40 | return or_expr 41 | 42 | def get_relation_model(self, relation: str) -> DeclarativeBase: 43 | for relationship in self._relationships: 44 | if relationship[0] == relation: 45 | return relationship[1].mapper.class_ 46 | raise SAFilterOrmException(f"Can not find relation {relation} in {self.model.__name__} model") 47 | 48 | def get_column(self, model: type[DeclarativeBase], field_name: str) -> InstrumentedAttribute: 49 | column = getattr(model, field_name, None) 50 | 51 | if not column: 52 | raise SAFilterOrmException(f"DB model {model.__name__} doesn't have field '{field_name}'") 53 | return column 54 | 55 | def create_pydantic_serializers(self) -> dict[str, dict[str, ModelMetaclass]]: 56 | """ 57 | Create two pydantic models (optional and list field types) 58 | for value: str serialization 59 | 60 | :return: { 61 | 'optional_model': 62 | class model.__name__(BaseModel): 63 | field: Optional[type] 64 | 'list_model': 65 | class model.__name__(BaseModel): 66 | field: Optional[List[type]] 67 | } 68 | """ 69 | 70 | models = [self.model] 71 | models.extend(self.get_relations_classes()) 72 | 73 | serializers = {} 74 | 75 | for model in models: 76 | pydantic_serializer = sqlalchemy_to_pydantic(model) 77 | optional_model = self.get_optional_pydantic_model(model, pydantic_serializer) 78 | optional_list_model = self.get_optional_pydantic_model(model, pydantic_serializer, is_list=True) 79 | 80 | serializers[model.__tablename__] = { 81 | "optional_model": optional_model, "optional_list_model": optional_list_model 82 | } 83 | 84 | return serializers 85 | 86 | def get_relations_classes(self) -> list: 87 | return [relation[1].mapper.class_ for relation in self._relationships] 88 | 89 | def get_orm_for_field( 90 | self, column: InstrumentedAttribute, operator: str, value: Any 91 | ) -> BinaryExpression: 92 | """ 93 | Create SQLAlchemy orm expression for the field 94 | """ 95 | if operator in [ops.between]: 96 | return getattr(column, ops[operator].value)(*value) 97 | return getattr(column, ops[operator].value)(value) 98 | 99 | def serialize_expression_value( 100 | self, table: str, column: InstrumentedAttribute, operator: str, value: str 101 | ) -> dict[str, Any]: 102 | """ 103 | Serialize expression value from string to python type value, 104 | according to db model types 105 | 106 | :return: {'field_name': [value, value]} 107 | """ 108 | value = value.split(",") 109 | try: 110 | if operator not in [ops.between, ops.in_]: 111 | value = value[0] 112 | model_serializer = self._model_serializers[table]["optional_model"] 113 | else: 114 | model_serializer = self._model_serializers[table]["optional_list_model"] 115 | return model_serializer(**{column.name: value}).model_dump(exclude_none=True) 116 | except pydantic.ValidationError as e: 117 | raise SAFilterOrmException(json.loads(e.json())) 118 | except ValueError: 119 | raise SAFilterOrmException(f"Incorrect filter value '{value}'") 120 | 121 | @staticmethod 122 | def get_optional_pydantic_model(model, pydantic_serializer, is_list: bool = False): 123 | fields = {} 124 | for k, v in pydantic_serializer.model_fields.items(): 125 | origin_annotation = getattr(v, 'annotation') 126 | if is_list: 127 | fields[k] = (list[origin_annotation], None) 128 | else: 129 | fields[k] = (origin_annotation, None) 130 | pydantic_model = create_model(model.__name__, **fields) 131 | return pydantic_model 132 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "aiosqlite" 5 | version = "0.20.0" 6 | description = "asyncio bridge to the standard sqlite3 module" 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6"}, 11 | {file = "aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7"}, 12 | ] 13 | 14 | [package.dependencies] 15 | typing_extensions = ">=4.0" 16 | 17 | [package.extras] 18 | dev = ["attribution (==1.7.0)", "black (==24.2.0)", "coverage[toml] (==7.4.1)", "flake8 (==7.0.0)", "flake8-bugbear (==24.2.6)", "flit (==3.9.0)", "mypy (==1.8.0)", "ufmt (==2.3.0)", "usort (==1.0.8.post1)"] 19 | docs = ["sphinx (==7.2.6)", "sphinx-mdinclude (==0.5.3)"] 20 | 21 | [[package]] 22 | name = "annotated-types" 23 | version = "0.7.0" 24 | description = "Reusable constraint types to use with typing.Annotated" 25 | optional = false 26 | python-versions = ">=3.8" 27 | files = [ 28 | {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, 29 | {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, 30 | ] 31 | 32 | [[package]] 33 | name = "anyio" 34 | version = "4.7.0" 35 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 36 | optional = false 37 | python-versions = ">=3.9" 38 | files = [ 39 | {file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"}, 40 | {file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"}, 41 | ] 42 | 43 | [package.dependencies] 44 | exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} 45 | idna = ">=2.8" 46 | sniffio = ">=1.1" 47 | typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} 48 | 49 | [package.extras] 50 | doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] 51 | test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] 52 | trio = ["trio (>=0.26.1)"] 53 | 54 | [[package]] 55 | name = "certifi" 56 | version = "2024.12.14" 57 | description = "Python package for providing Mozilla's CA Bundle." 58 | optional = false 59 | python-versions = ">=3.6" 60 | files = [ 61 | {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, 62 | {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, 63 | ] 64 | 65 | [[package]] 66 | name = "charset-normalizer" 67 | version = "3.4.0" 68 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 69 | optional = false 70 | python-versions = ">=3.7.0" 71 | files = [ 72 | {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, 73 | {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, 74 | {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, 75 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, 76 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, 77 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, 78 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, 79 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, 80 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, 81 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, 82 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, 83 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, 84 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, 85 | {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, 86 | {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, 87 | {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, 88 | {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, 89 | {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, 90 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, 91 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, 92 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, 93 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, 94 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, 95 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, 96 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, 97 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, 98 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, 99 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, 100 | {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, 101 | {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, 102 | {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, 103 | {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, 104 | {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, 105 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, 106 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, 107 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, 108 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, 109 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, 110 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, 111 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, 112 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, 113 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, 114 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, 115 | {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, 116 | {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, 117 | {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, 118 | {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, 119 | {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, 120 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, 121 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, 122 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, 123 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, 124 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, 125 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, 126 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, 127 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, 128 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, 129 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, 130 | {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, 131 | {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, 132 | {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, 133 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, 134 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, 135 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, 136 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, 137 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, 138 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, 139 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, 140 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, 141 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, 142 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, 143 | {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, 144 | {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, 145 | {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, 146 | {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, 147 | {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, 148 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, 149 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, 150 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, 151 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, 152 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, 153 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, 154 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, 155 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, 156 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, 157 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, 158 | {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, 159 | {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, 160 | {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, 161 | {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, 162 | {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, 163 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, 164 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, 165 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, 166 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, 167 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, 168 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, 169 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, 170 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, 171 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, 172 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, 173 | {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, 174 | {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, 175 | {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, 176 | {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, 177 | ] 178 | 179 | [[package]] 180 | name = "colorama" 181 | version = "0.4.6" 182 | description = "Cross-platform colored terminal text." 183 | optional = false 184 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 185 | files = [ 186 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 187 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 188 | ] 189 | 190 | [[package]] 191 | name = "docutils" 192 | version = "0.21.2" 193 | description = "Docutils -- Python Documentation Utilities" 194 | optional = false 195 | python-versions = ">=3.9" 196 | files = [ 197 | {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, 198 | {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, 199 | ] 200 | 201 | [[package]] 202 | name = "exceptiongroup" 203 | version = "1.2.2" 204 | description = "Backport of PEP 654 (exception groups)" 205 | optional = false 206 | python-versions = ">=3.7" 207 | files = [ 208 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 209 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 210 | ] 211 | 212 | [package.extras] 213 | test = ["pytest (>=6)"] 214 | 215 | [[package]] 216 | name = "fastapi" 217 | version = "0.115.6" 218 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 219 | optional = false 220 | python-versions = ">=3.8" 221 | files = [ 222 | {file = "fastapi-0.115.6-py3-none-any.whl", hash = "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305"}, 223 | {file = "fastapi-0.115.6.tar.gz", hash = "sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654"}, 224 | ] 225 | 226 | [package.dependencies] 227 | pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" 228 | starlette = ">=0.40.0,<0.42.0" 229 | typing-extensions = ">=4.8.0" 230 | 231 | [package.extras] 232 | all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] 233 | standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] 234 | 235 | [[package]] 236 | name = "flake8" 237 | version = "7.1.1" 238 | description = "the modular source code checker: pep8 pyflakes and co" 239 | optional = false 240 | python-versions = ">=3.8.1" 241 | files = [ 242 | {file = "flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"}, 243 | {file = "flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38"}, 244 | ] 245 | 246 | [package.dependencies] 247 | mccabe = ">=0.7.0,<0.8.0" 248 | pycodestyle = ">=2.12.0,<2.13.0" 249 | pyflakes = ">=3.2.0,<3.3.0" 250 | 251 | [[package]] 252 | name = "flit" 253 | version = "3.10.1" 254 | description = "A simple packaging tool for simple packages." 255 | optional = false 256 | python-versions = ">=3.8" 257 | files = [ 258 | {file = "flit-3.10.1-py3-none-any.whl", hash = "sha256:d79c19c2caae73cc486d3d827af6a11c1a84b9efdfab8d9683b714ec8d1dc1f1"}, 259 | {file = "flit-3.10.1.tar.gz", hash = "sha256:9c6258ae76d218ce60f9e39a43ca42006a3abcc5c44ea6bb2a1daa13857a8f1a"}, 260 | ] 261 | 262 | [package.dependencies] 263 | docutils = "*" 264 | flit_core = ">=3.10.1" 265 | pip = "*" 266 | requests = "*" 267 | tomli-w = "*" 268 | 269 | [package.extras] 270 | doc = ["pygments-github-lexers", "sphinx", "sphinxcontrib_github_alt"] 271 | test = ["pytest (>=2.7.3)", "pytest-cov", "responses", "testpath", "tomli"] 272 | 273 | [[package]] 274 | name = "flit-core" 275 | version = "3.10.1" 276 | description = "Distribution-building parts of Flit. See flit package for more information" 277 | optional = false 278 | python-versions = ">=3.6" 279 | files = [ 280 | {file = "flit_core-3.10.1-py3-none-any.whl", hash = "sha256:cb31a76e8b31ad3351bb89e531f64ef2b05d1e65bd939183250bf81ddf4922a8"}, 281 | {file = "flit_core-3.10.1.tar.gz", hash = "sha256:66e5b87874a0d6e39691f0e22f09306736b633548670ad3c09ec9db03c5662f7"}, 282 | ] 283 | 284 | [[package]] 285 | name = "greenlet" 286 | version = "3.1.1" 287 | description = "Lightweight in-process concurrent programming" 288 | optional = false 289 | python-versions = ">=3.7" 290 | files = [ 291 | {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, 292 | {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, 293 | {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0"}, 294 | {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120"}, 295 | {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc"}, 296 | {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617"}, 297 | {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7"}, 298 | {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6"}, 299 | {file = "greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80"}, 300 | {file = "greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70"}, 301 | {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159"}, 302 | {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e"}, 303 | {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1"}, 304 | {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383"}, 305 | {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a"}, 306 | {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511"}, 307 | {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395"}, 308 | {file = "greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39"}, 309 | {file = "greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d"}, 310 | {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79"}, 311 | {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa"}, 312 | {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441"}, 313 | {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36"}, 314 | {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9"}, 315 | {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0"}, 316 | {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942"}, 317 | {file = "greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01"}, 318 | {file = "greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1"}, 319 | {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff"}, 320 | {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a"}, 321 | {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e"}, 322 | {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4"}, 323 | {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e"}, 324 | {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1"}, 325 | {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c"}, 326 | {file = "greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761"}, 327 | {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011"}, 328 | {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13"}, 329 | {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475"}, 330 | {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b"}, 331 | {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822"}, 332 | {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01"}, 333 | {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6"}, 334 | {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291"}, 335 | {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981"}, 336 | {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803"}, 337 | {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc"}, 338 | {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de"}, 339 | {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa"}, 340 | {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af"}, 341 | {file = "greenlet-3.1.1-cp37-cp37m-win32.whl", hash = "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798"}, 342 | {file = "greenlet-3.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef"}, 343 | {file = "greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9"}, 344 | {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111"}, 345 | {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81"}, 346 | {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba"}, 347 | {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8"}, 348 | {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1"}, 349 | {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd"}, 350 | {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7"}, 351 | {file = "greenlet-3.1.1-cp38-cp38-win32.whl", hash = "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef"}, 352 | {file = "greenlet-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d"}, 353 | {file = "greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3"}, 354 | {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42"}, 355 | {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f"}, 356 | {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437"}, 357 | {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145"}, 358 | {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c"}, 359 | {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e"}, 360 | {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e"}, 361 | {file = "greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c"}, 362 | {file = "greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22"}, 363 | {file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"}, 364 | ] 365 | 366 | [package.extras] 367 | docs = ["Sphinx", "furo"] 368 | test = ["objgraph", "psutil"] 369 | 370 | [[package]] 371 | name = "idna" 372 | version = "3.10" 373 | description = "Internationalized Domain Names in Applications (IDNA)" 374 | optional = false 375 | python-versions = ">=3.6" 376 | files = [ 377 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 378 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 379 | ] 380 | 381 | [package.extras] 382 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 383 | 384 | [[package]] 385 | name = "iniconfig" 386 | version = "2.0.0" 387 | description = "brain-dead simple config-ini parsing" 388 | optional = false 389 | python-versions = ">=3.7" 390 | files = [ 391 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 392 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 393 | ] 394 | 395 | [[package]] 396 | name = "mccabe" 397 | version = "0.7.0" 398 | description = "McCabe checker, plugin for flake8" 399 | optional = false 400 | python-versions = ">=3.6" 401 | files = [ 402 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 403 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 404 | ] 405 | 406 | [[package]] 407 | name = "packaging" 408 | version = "24.2" 409 | description = "Core utilities for Python packages" 410 | optional = false 411 | python-versions = ">=3.8" 412 | files = [ 413 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, 414 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, 415 | ] 416 | 417 | [[package]] 418 | name = "pip" 419 | version = "24.3.1" 420 | description = "The PyPA recommended tool for installing Python packages." 421 | optional = false 422 | python-versions = ">=3.8" 423 | files = [ 424 | {file = "pip-24.3.1-py3-none-any.whl", hash = "sha256:3790624780082365f47549d032f3770eeb2b1e8bd1f7b2e02dace1afa361b4ed"}, 425 | {file = "pip-24.3.1.tar.gz", hash = "sha256:ebcb60557f2aefabc2e0f918751cd24ea0d56d8ec5445fe1807f1d2109660b99"}, 426 | ] 427 | 428 | [[package]] 429 | name = "pluggy" 430 | version = "1.5.0" 431 | description = "plugin and hook calling mechanisms for python" 432 | optional = false 433 | python-versions = ">=3.8" 434 | files = [ 435 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 436 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 437 | ] 438 | 439 | [package.extras] 440 | dev = ["pre-commit", "tox"] 441 | testing = ["pytest", "pytest-benchmark"] 442 | 443 | [[package]] 444 | name = "pycodestyle" 445 | version = "2.12.1" 446 | description = "Python style guide checker" 447 | optional = false 448 | python-versions = ">=3.8" 449 | files = [ 450 | {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, 451 | {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, 452 | ] 453 | 454 | [[package]] 455 | name = "pydantic" 456 | version = "2.10.4" 457 | description = "Data validation using Python type hints" 458 | optional = false 459 | python-versions = ">=3.8" 460 | files = [ 461 | {file = "pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d"}, 462 | {file = "pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06"}, 463 | ] 464 | 465 | [package.dependencies] 466 | annotated-types = ">=0.6.0" 467 | pydantic-core = "2.27.2" 468 | typing-extensions = ">=4.12.2" 469 | 470 | [package.extras] 471 | email = ["email-validator (>=2.0.0)"] 472 | timezone = ["tzdata"] 473 | 474 | [[package]] 475 | name = "pydantic-core" 476 | version = "2.27.2" 477 | description = "Core functionality for Pydantic validation and serialization" 478 | optional = false 479 | python-versions = ">=3.8" 480 | files = [ 481 | {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, 482 | {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, 483 | {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, 484 | {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, 485 | {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, 486 | {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, 487 | {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, 488 | {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, 489 | {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, 490 | {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, 491 | {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, 492 | {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, 493 | {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, 494 | {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, 495 | {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, 496 | {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, 497 | {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, 498 | {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, 499 | {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, 500 | {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, 501 | {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, 502 | {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, 503 | {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, 504 | {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, 505 | {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, 506 | {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, 507 | {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, 508 | {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, 509 | {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, 510 | {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, 511 | {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, 512 | {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, 513 | {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, 514 | {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, 515 | {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, 516 | {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, 517 | {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, 518 | {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, 519 | {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, 520 | {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, 521 | {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, 522 | {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, 523 | {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, 524 | {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, 525 | {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, 526 | {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, 527 | {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, 528 | {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, 529 | {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, 530 | {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, 531 | {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, 532 | {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, 533 | {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, 534 | {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, 535 | {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, 536 | {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, 537 | {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, 538 | {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, 539 | {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, 540 | {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, 541 | {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, 542 | {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, 543 | {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, 544 | {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, 545 | {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, 546 | {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, 547 | {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, 548 | {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, 549 | {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, 550 | {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, 551 | {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, 552 | {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, 553 | {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, 554 | {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, 555 | {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, 556 | {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, 557 | {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, 558 | {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, 559 | {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, 560 | {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, 561 | {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, 562 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, 563 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, 564 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, 565 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, 566 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, 567 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, 568 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, 569 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, 570 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, 571 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, 572 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, 573 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, 574 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, 575 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, 576 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, 577 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, 578 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, 579 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, 580 | {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, 581 | ] 582 | 583 | [package.dependencies] 584 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 585 | 586 | [[package]] 587 | name = "pyflakes" 588 | version = "3.2.0" 589 | description = "passive checker of Python programs" 590 | optional = false 591 | python-versions = ">=3.8" 592 | files = [ 593 | {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, 594 | {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, 595 | ] 596 | 597 | [[package]] 598 | name = "pytest" 599 | version = "8.3.4" 600 | description = "pytest: simple powerful testing with Python" 601 | optional = false 602 | python-versions = ">=3.8" 603 | files = [ 604 | {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, 605 | {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, 606 | ] 607 | 608 | [package.dependencies] 609 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 610 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 611 | iniconfig = "*" 612 | packaging = "*" 613 | pluggy = ">=1.5,<2" 614 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 615 | 616 | [package.extras] 617 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 618 | 619 | [[package]] 620 | name = "pytest-asyncio" 621 | version = "0.24.0" 622 | description = "Pytest support for asyncio" 623 | optional = false 624 | python-versions = ">=3.8" 625 | files = [ 626 | {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, 627 | {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, 628 | ] 629 | 630 | [package.dependencies] 631 | pytest = ">=8.2,<9" 632 | 633 | [package.extras] 634 | docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] 635 | testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] 636 | 637 | [[package]] 638 | name = "requests" 639 | version = "2.32.3" 640 | description = "Python HTTP for Humans." 641 | optional = false 642 | python-versions = ">=3.8" 643 | files = [ 644 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 645 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 646 | ] 647 | 648 | [package.dependencies] 649 | certifi = ">=2017.4.17" 650 | charset-normalizer = ">=2,<4" 651 | idna = ">=2.5,<4" 652 | urllib3 = ">=1.21.1,<3" 653 | 654 | [package.extras] 655 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 656 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 657 | 658 | [[package]] 659 | name = "sniffio" 660 | version = "1.3.1" 661 | description = "Sniff out which async library your code is running under" 662 | optional = false 663 | python-versions = ">=3.7" 664 | files = [ 665 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 666 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 667 | ] 668 | 669 | [[package]] 670 | name = "sqlalchemy" 671 | version = "2.0.36" 672 | description = "Database Abstraction Library" 673 | optional = false 674 | python-versions = ">=3.7" 675 | files = [ 676 | {file = "SQLAlchemy-2.0.36-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59b8f3adb3971929a3e660337f5dacc5942c2cdb760afcabb2614ffbda9f9f72"}, 677 | {file = "SQLAlchemy-2.0.36-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37350015056a553e442ff672c2d20e6f4b6d0b2495691fa239d8aa18bb3bc908"}, 678 | {file = "SQLAlchemy-2.0.36-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8318f4776c85abc3f40ab185e388bee7a6ea99e7fa3a30686580b209eaa35c08"}, 679 | {file = "SQLAlchemy-2.0.36-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c245b1fbade9c35e5bd3b64270ab49ce990369018289ecfde3f9c318411aaa07"}, 680 | {file = "SQLAlchemy-2.0.36-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:69f93723edbca7342624d09f6704e7126b152eaed3cdbb634cb657a54332a3c5"}, 681 | {file = "SQLAlchemy-2.0.36-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f9511d8dd4a6e9271d07d150fb2f81874a3c8c95e11ff9af3a2dfc35fe42ee44"}, 682 | {file = "SQLAlchemy-2.0.36-cp310-cp310-win32.whl", hash = "sha256:c3f3631693003d8e585d4200730616b78fafd5a01ef8b698f6967da5c605b3fa"}, 683 | {file = "SQLAlchemy-2.0.36-cp310-cp310-win_amd64.whl", hash = "sha256:a86bfab2ef46d63300c0f06936bd6e6c0105faa11d509083ba8f2f9d237fb5b5"}, 684 | {file = "SQLAlchemy-2.0.36-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fd3a55deef00f689ce931d4d1b23fa9f04c880a48ee97af488fd215cf24e2a6c"}, 685 | {file = "SQLAlchemy-2.0.36-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f5e9cd989b45b73bd359f693b935364f7e1f79486e29015813c338450aa5a71"}, 686 | {file = "SQLAlchemy-2.0.36-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ddd9db6e59c44875211bc4c7953a9f6638b937b0a88ae6d09eb46cced54eff"}, 687 | {file = "SQLAlchemy-2.0.36-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2519f3a5d0517fc159afab1015e54bb81b4406c278749779be57a569d8d1bb0d"}, 688 | {file = "SQLAlchemy-2.0.36-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59b1ee96617135f6e1d6f275bbe988f419c5178016f3d41d3c0abb0c819f75bb"}, 689 | {file = "SQLAlchemy-2.0.36-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:39769a115f730d683b0eb7b694db9789267bcd027326cccc3125e862eb03bfd8"}, 690 | {file = "SQLAlchemy-2.0.36-cp311-cp311-win32.whl", hash = "sha256:66bffbad8d6271bb1cc2f9a4ea4f86f80fe5e2e3e501a5ae2a3dc6a76e604e6f"}, 691 | {file = "SQLAlchemy-2.0.36-cp311-cp311-win_amd64.whl", hash = "sha256:23623166bfefe1487d81b698c423f8678e80df8b54614c2bf4b4cfcd7c711959"}, 692 | {file = "SQLAlchemy-2.0.36-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7b64e6ec3f02c35647be6b4851008b26cff592a95ecb13b6788a54ef80bbdd4"}, 693 | {file = "SQLAlchemy-2.0.36-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46331b00096a6db1fdc052d55b101dbbfc99155a548e20a0e4a8e5e4d1362855"}, 694 | {file = "SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53"}, 695 | {file = "SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9dfa18ff2a67b09b372d5db8743c27966abf0e5344c555d86cc7199f7ad83a"}, 696 | {file = "SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:90812a8933df713fdf748b355527e3af257a11e415b613dd794512461eb8a686"}, 697 | {file = "SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1bc330d9d29c7f06f003ab10e1eaced295e87940405afe1b110f2eb93a233588"}, 698 | {file = "SQLAlchemy-2.0.36-cp312-cp312-win32.whl", hash = "sha256:79d2e78abc26d871875b419e1fd3c0bca31a1cb0043277d0d850014599626c2e"}, 699 | {file = "SQLAlchemy-2.0.36-cp312-cp312-win_amd64.whl", hash = "sha256:b544ad1935a8541d177cb402948b94e871067656b3a0b9e91dbec136b06a2ff5"}, 700 | {file = "SQLAlchemy-2.0.36-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5cc79df7f4bc3d11e4b542596c03826063092611e481fcf1c9dfee3c94355ef"}, 701 | {file = "SQLAlchemy-2.0.36-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3c01117dd36800f2ecaa238c65365b7b16497adc1522bf84906e5710ee9ba0e8"}, 702 | {file = "SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bc633f4ee4b4c46e7adcb3a9b5ec083bf1d9a97c1d3854b92749d935de40b9b"}, 703 | {file = "SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e46ed38affdfc95d2c958de328d037d87801cfcbea6d421000859e9789e61c2"}, 704 | {file = "SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b2985c0b06e989c043f1dc09d4fe89e1616aadd35392aea2844f0458a989eacf"}, 705 | {file = "SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a121d62ebe7d26fec9155f83f8be5189ef1405f5973ea4874a26fab9f1e262c"}, 706 | {file = "SQLAlchemy-2.0.36-cp313-cp313-win32.whl", hash = "sha256:0572f4bd6f94752167adfd7c1bed84f4b240ee6203a95e05d1e208d488d0d436"}, 707 | {file = "SQLAlchemy-2.0.36-cp313-cp313-win_amd64.whl", hash = "sha256:8c78ac40bde930c60e0f78b3cd184c580f89456dd87fc08f9e3ee3ce8765ce88"}, 708 | {file = "SQLAlchemy-2.0.36-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:be9812b766cad94a25bc63bec11f88c4ad3629a0cec1cd5d4ba48dc23860486b"}, 709 | {file = "SQLAlchemy-2.0.36-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50aae840ebbd6cdd41af1c14590e5741665e5272d2fee999306673a1bb1fdb4d"}, 710 | {file = "SQLAlchemy-2.0.36-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4557e1f11c5f653ebfdd924f3f9d5ebfc718283b0b9beebaa5dd6b77ec290971"}, 711 | {file = "SQLAlchemy-2.0.36-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07b441f7d03b9a66299ce7ccf3ef2900abc81c0db434f42a5694a37bd73870f2"}, 712 | {file = "SQLAlchemy-2.0.36-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:28120ef39c92c2dd60f2721af9328479516844c6b550b077ca450c7d7dc68575"}, 713 | {file = "SQLAlchemy-2.0.36-cp37-cp37m-win32.whl", hash = "sha256:b81ee3d84803fd42d0b154cb6892ae57ea6b7c55d8359a02379965706c7efe6c"}, 714 | {file = "SQLAlchemy-2.0.36-cp37-cp37m-win_amd64.whl", hash = "sha256:f942a799516184c855e1a32fbc7b29d7e571b52612647866d4ec1c3242578fcb"}, 715 | {file = "SQLAlchemy-2.0.36-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3d6718667da04294d7df1670d70eeddd414f313738d20a6f1d1f379e3139a545"}, 716 | {file = "SQLAlchemy-2.0.36-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:72c28b84b174ce8af8504ca28ae9347d317f9dba3999e5981a3cd441f3712e24"}, 717 | {file = "SQLAlchemy-2.0.36-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b11d0cfdd2b095e7b0686cf5fabeb9c67fae5b06d265d8180715b8cfa86522e3"}, 718 | {file = "SQLAlchemy-2.0.36-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e32092c47011d113dc01ab3e1d3ce9f006a47223b18422c5c0d150af13a00687"}, 719 | {file = "SQLAlchemy-2.0.36-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6a440293d802d3011028e14e4226da1434b373cbaf4a4bbb63f845761a708346"}, 720 | {file = "SQLAlchemy-2.0.36-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c54a1e53a0c308a8e8a7dffb59097bff7facda27c70c286f005327f21b2bd6b1"}, 721 | {file = "SQLAlchemy-2.0.36-cp38-cp38-win32.whl", hash = "sha256:1e0d612a17581b6616ff03c8e3d5eff7452f34655c901f75d62bd86449d9750e"}, 722 | {file = "SQLAlchemy-2.0.36-cp38-cp38-win_amd64.whl", hash = "sha256:8958b10490125124463095bbdadda5aa22ec799f91958e410438ad6c97a7b793"}, 723 | {file = "SQLAlchemy-2.0.36-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc022184d3e5cacc9579e41805a681187650e170eb2fd70e28b86192a479dcaa"}, 724 | {file = "SQLAlchemy-2.0.36-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b817d41d692bf286abc181f8af476c4fbef3fd05e798777492618378448ee689"}, 725 | {file = "SQLAlchemy-2.0.36-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4e46a888b54be23d03a89be510f24a7652fe6ff660787b96cd0e57a4ebcb46d"}, 726 | {file = "SQLAlchemy-2.0.36-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4ae3005ed83f5967f961fd091f2f8c5329161f69ce8480aa8168b2d7fe37f06"}, 727 | {file = "SQLAlchemy-2.0.36-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03e08af7a5f9386a43919eda9de33ffda16b44eb11f3b313e6822243770e9763"}, 728 | {file = "SQLAlchemy-2.0.36-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3dbb986bad3ed5ceaf090200eba750b5245150bd97d3e67343a3cfed06feecf7"}, 729 | {file = "SQLAlchemy-2.0.36-cp39-cp39-win32.whl", hash = "sha256:9fe53b404f24789b5ea9003fc25b9a3988feddebd7e7b369c8fac27ad6f52f28"}, 730 | {file = "SQLAlchemy-2.0.36-cp39-cp39-win_amd64.whl", hash = "sha256:af148a33ff0349f53512a049c6406923e4e02bf2f26c5fb285f143faf4f0e46a"}, 731 | {file = "SQLAlchemy-2.0.36-py3-none-any.whl", hash = "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e"}, 732 | {file = "sqlalchemy-2.0.36.tar.gz", hash = "sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5"}, 733 | ] 734 | 735 | [package.dependencies] 736 | greenlet = {version = "!=0.4.17", markers = "python_version < \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} 737 | typing-extensions = ">=4.6.0" 738 | 739 | [package.extras] 740 | aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] 741 | aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] 742 | aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] 743 | asyncio = ["greenlet (!=0.4.17)"] 744 | asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] 745 | mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] 746 | mssql = ["pyodbc"] 747 | mssql-pymssql = ["pymssql"] 748 | mssql-pyodbc = ["pyodbc"] 749 | mypy = ["mypy (>=0.910)"] 750 | mysql = ["mysqlclient (>=1.4.0)"] 751 | mysql-connector = ["mysql-connector-python"] 752 | oracle = ["cx_oracle (>=8)"] 753 | oracle-oracledb = ["oracledb (>=1.0.1)"] 754 | postgresql = ["psycopg2 (>=2.7)"] 755 | postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] 756 | postgresql-pg8000 = ["pg8000 (>=1.29.1)"] 757 | postgresql-psycopg = ["psycopg (>=3.0.7)"] 758 | postgresql-psycopg2binary = ["psycopg2-binary"] 759 | postgresql-psycopg2cffi = ["psycopg2cffi"] 760 | postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] 761 | pymysql = ["pymysql"] 762 | sqlcipher = ["sqlcipher3_binary"] 763 | 764 | [[package]] 765 | name = "sqlalchemy-to-pydantic" 766 | version = "0.0.8" 767 | description = "Tools to convert SQLAlchemy models to Pydantic models" 768 | optional = false 769 | python-versions = ">=3.10,<4.0" 770 | files = [ 771 | {file = "sqlalchemy_to_pydantic-0.0.8-py3-none-any.whl", hash = "sha256:e2b13b793b983cc43ec2291bd0dadc731c278017814b98140df8f1c468c4f837"}, 772 | {file = "sqlalchemy_to_pydantic-0.0.8.tar.gz", hash = "sha256:7af94ecd04c3ca1243975bba5e8e2b2e3928faf9f5ad98a555b0a1b90eec344e"}, 773 | ] 774 | 775 | [package.dependencies] 776 | pydantic = ">=2.3.0,<3.0.0" 777 | sqlalchemy = ">=2.0.20,<3.0.0" 778 | 779 | [[package]] 780 | name = "starlette" 781 | version = "0.41.3" 782 | description = "The little ASGI library that shines." 783 | optional = false 784 | python-versions = ">=3.8" 785 | files = [ 786 | {file = "starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7"}, 787 | {file = "starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835"}, 788 | ] 789 | 790 | [package.dependencies] 791 | anyio = ">=3.4.0,<5" 792 | 793 | [package.extras] 794 | full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] 795 | 796 | [[package]] 797 | name = "tomli" 798 | version = "2.2.1" 799 | description = "A lil' TOML parser" 800 | optional = false 801 | python-versions = ">=3.8" 802 | files = [ 803 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, 804 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, 805 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, 806 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, 807 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, 808 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, 809 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, 810 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, 811 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, 812 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, 813 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, 814 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, 815 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, 816 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, 817 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, 818 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, 819 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, 820 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, 821 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, 822 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, 823 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, 824 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, 825 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, 826 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, 827 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, 828 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, 829 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, 830 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, 831 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, 832 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, 833 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, 834 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, 835 | ] 836 | 837 | [[package]] 838 | name = "tomli-w" 839 | version = "1.1.0" 840 | description = "A lil' TOML writer" 841 | optional = false 842 | python-versions = ">=3.9" 843 | files = [ 844 | {file = "tomli_w-1.1.0-py3-none-any.whl", hash = "sha256:1403179c78193e3184bfaade390ddbd071cba48a32a2e62ba11aae47490c63f7"}, 845 | {file = "tomli_w-1.1.0.tar.gz", hash = "sha256:49e847a3a304d516a169a601184932ef0f6b61623fe680f836a2aa7128ed0d33"}, 846 | ] 847 | 848 | [[package]] 849 | name = "typing-extensions" 850 | version = "4.12.2" 851 | description = "Backported and Experimental Type Hints for Python 3.8+" 852 | optional = false 853 | python-versions = ">=3.8" 854 | files = [ 855 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 856 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 857 | ] 858 | 859 | [[package]] 860 | name = "urllib3" 861 | version = "2.2.3" 862 | description = "HTTP library with thread-safe connection pooling, file post, and more." 863 | optional = false 864 | python-versions = ">=3.8" 865 | files = [ 866 | {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, 867 | {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, 868 | ] 869 | 870 | [package.extras] 871 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 872 | h2 = ["h2 (>=4,<5)"] 873 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 874 | zstd = ["zstandard (>=0.18.0)"] 875 | 876 | [metadata] 877 | lock-version = "2.0" 878 | python-versions = ">=3.10,<4.0" 879 | content-hash = "e91b5753f2c720775fa184dde16ac3b14f503acf838d065645bd3079463c86c6" 880 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "fastapi-sqlalchemy-filters" 3 | version = "0.2.2" 4 | description = "" 5 | authors = ["Alexandr Zhydyk "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = ">=3.10,<4.0" 10 | fastapi = "^0.115.5" 11 | sqlalchemy = "^2.0.36" 12 | sqlalchemy-to-pydantic = "^0.0.8" 13 | 14 | 15 | [tool.poetry.group.dev.dependencies] 16 | pytest = "^8.3.3" 17 | pytest-asyncio = "^0.24.0" 18 | aiosqlite = "^0.20.0" 19 | flit = "^3.10.1" 20 | flake8 = "^7.1.1" 21 | 22 | 23 | [build-system] 24 | requires = ["flit_core >=3.2,<4"] 25 | build-backend = "flit_core.buildapi" 26 | 27 | [project] 28 | name = "fastapi-sa-orm-filter" 29 | authors = [{name = "Oleksandr Zhydyk", email = "zhydykalex@ukr.net"}] 30 | dynamic = ["version", "description"] 31 | readme = "README.md" 32 | license = {file = "LICENSE"} 33 | classifiers = ["License :: OSI Approved :: MIT License"] 34 | 35 | requires-python = ">=3.10" 36 | 37 | dependencies = [ 38 | "fastapi", 39 | "sqlalchemy", 40 | "pydantic", 41 | "sqlalchemy-to-pydantic" 42 | ] 43 | 44 | [project.urls] 45 | Home = "https://github.com/OleksandrZhydyk/FastAPI-SQLAlchemy-Filters" 46 | Issues = "https://github.com/OleksandrZhydyk/FastAPI-SQLAlchemy-Filters/issues" 47 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | 3 | pythonpath = . 4 | 5 | asyncio_mode = auto 6 | addopts = -v --tb=short 7 | python_classes = Test* *Test *Custom 8 | python_files = test_* *_test check_* 9 | asyncio_default_fixture_loop_scope = session 10 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from datetime import date, datetime 2 | 3 | import pytest 4 | import pytest_asyncio 5 | 6 | from sqlalchemy import select, func, ForeignKey 7 | from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker 8 | from sqlalchemy.orm import declarative_base, relationship, joinedload 9 | from sqlalchemy.orm import Mapped 10 | from sqlalchemy.orm import mapped_column 11 | 12 | from fastapi_sa_orm_filter.main import FilterCore 13 | from fastapi_sa_orm_filter.operators import Operators as ops 14 | from tests.utils import JobCategory 15 | 16 | Base = declarative_base() 17 | 18 | 19 | class Vacancy(Base): 20 | __tablename__ = "vacancy" 21 | id: Mapped[int] = mapped_column(primary_key=True) 22 | title: Mapped[str] 23 | description: Mapped[str] 24 | is_active: Mapped[bool] 25 | created_at: Mapped[date] 26 | updated_at: Mapped[datetime] 27 | salary_from: Mapped[int] 28 | salary_up_to: Mapped[float] 29 | category: Mapped[JobCategory] = mapped_column(nullable=False) 30 | company_id: Mapped[int] = mapped_column(ForeignKey("company.id")) 31 | company: Mapped["Company"] = relationship(back_populates="vacancies") 32 | 33 | 34 | class Company(Base): 35 | __tablename__ = "company" 36 | id: Mapped[int] = mapped_column(primary_key=True) 37 | title: Mapped[str] 38 | vacancies: Mapped[list["Vacancy"]] = relationship(back_populates="company") 39 | 40 | 41 | @pytest.fixture(scope="session") 42 | def sqlite_file_path(tmp_path_factory): 43 | file_path = tmp_path_factory.mktemp("data") 44 | yield file_path 45 | 46 | 47 | @pytest.fixture(scope="session") 48 | def database_url(sqlite_file_path) -> str: 49 | return f"sqlite+aiosqlite:///{sqlite_file_path}.db" 50 | 51 | 52 | @pytest.fixture(scope="session") 53 | def create_engine(database_url): 54 | return create_async_engine(database_url, echo=False, future=True) 55 | 56 | 57 | @pytest.fixture(scope="session") 58 | def create_session(create_engine): 59 | return async_sessionmaker(create_engine, expire_on_commit=False) 60 | 61 | 62 | @pytest_asyncio.fixture(autouse=True, scope="function") 63 | async def db_models(create_engine): 64 | async with create_engine.begin() as conn: 65 | await conn.run_sync(Base.metadata.drop_all) 66 | await conn.run_sync(Base.metadata.create_all) 67 | yield 68 | async with create_engine.begin() as conn: 69 | await conn.run_sync(Base.metadata.drop_all) 70 | 71 | 72 | @pytest.fixture 73 | async def session(create_session) -> AsyncSession: 74 | async with create_session() as session: 75 | yield session 76 | 77 | 78 | @pytest.fixture 79 | async def create_companies(session): 80 | companies = [] 81 | for i in range(1, 3): 82 | company_instance = Company(title=f"MyCompany{i}") 83 | session.add(company_instance) 84 | await session.commit() 85 | await session.refresh(company_instance) 86 | companies.append(company_instance) 87 | return companies 88 | 89 | 90 | @pytest.fixture(scope="function") 91 | async def create_vacancies(session, create_companies): 92 | vacancy_instances = [] 93 | enum_category = [member.name for member in JobCategory] 94 | for i in range(1, 11): 95 | vacancy = Vacancy( 96 | title=f"title{i}", 97 | description=f"description{i}", 98 | salary_from=50 + i * 10, 99 | salary_up_to=100.725 + i * 10, 100 | created_at=date(2023, 5, i), 101 | updated_at=datetime(2023, i, 5, 15, 15, 15), 102 | category=JobCategory[enum_category[i - 1]], 103 | is_active=bool(i % 2), 104 | company=create_companies[(50 + i * 10)//100] 105 | ) 106 | vacancy_instances.append(vacancy) 107 | session.add_all(vacancy_instances) 108 | await session.commit() 109 | 110 | 111 | @pytest.fixture 112 | def get_vacancy_restriction() -> dict: 113 | return { 114 | 'title': [ops.startswith, ops.eq, ops.endswith], 115 | 'category': [ops.startswith, ops.eq, ops.in_], 116 | 'salary_from': [ops.between, ops.eq, ops.gt, ops.lt, ops.in_, ops.gte], 117 | 'salary_up_to': [ops.eq, ops.gt, ops.lt], 118 | 'description': [ops.like, ops.not_like, ops.contains, ops.eq, ops.in_], 119 | 'created_at': [ops.between, ops.in_, ops.eq, ops.gt, ops.lt, ops.not_eq], 120 | 'updated_at': [ops.between, ops.in_, ops.eq, ops.gt, ops.lt], 121 | 'is_active': [ops.eq], 122 | 'salary': [ops.eq] 123 | } 124 | 125 | 126 | @pytest.fixture 127 | def get_vacancy_filter(get_vacancy_restriction): 128 | return FilterCore(Vacancy, get_vacancy_restriction) 129 | 130 | 131 | @pytest.fixture 132 | def get_vacancy_filter_with_join(get_vacancy_restriction): 133 | return FilterCore(Vacancy, get_vacancy_restriction, select(Vacancy).join(Company)) 134 | 135 | 136 | @pytest.fixture 137 | def get_custom_vacancy_filter(get_vacancy_restriction): 138 | 139 | class CustomFilter(FilterCore): 140 | 141 | def get_select_query_part(self): 142 | custom_select = select( 143 | self.model.id, 144 | self.model.is_active, 145 | func.sum(self.model.salary_from).label("sum_salary_from"), 146 | self.model.category 147 | ) 148 | return custom_select 149 | 150 | def get_group_by_query_part(self): 151 | return [self.model.is_active] 152 | 153 | return CustomFilter(Vacancy, get_vacancy_restriction) 154 | 155 | 156 | @pytest.fixture 157 | def get_company_restriction() -> dict: 158 | return { 159 | "id": [ops.eq], 160 | "title": [ops.startswith, ops.eq, ops.contains], 161 | "salary_from": [ops.eq, ops.gt, ops.lte, ops.gte] 162 | } 163 | 164 | 165 | @pytest.fixture 166 | def get_company_filter(get_company_restriction): 167 | return FilterCore(Company, get_company_restriction) 168 | 169 | 170 | @pytest.fixture 171 | def get_custom_company_filter(get_company_restriction): 172 | 173 | class CustomFilter(FilterCore): 174 | def get_query(self, custom_filter): 175 | query = super().get_query(custom_filter) 176 | return query.join(Vacancy).options(joinedload(Company.vacancies)) 177 | 178 | return CustomFilter(Company, get_company_restriction) 179 | 180 | 181 | @pytest.fixture 182 | def get_filter_passed_in_init(get_company_restriction): 183 | return FilterCore( 184 | Company, 185 | get_company_restriction, 186 | select(Company).join(Vacancy) 187 | ) 188 | -------------------------------------------------------------------------------- /tests/schemas.py: -------------------------------------------------------------------------------- 1 | from pydantic import ConfigDict, BaseModel 2 | from sqlalchemy_to_pydantic import sqlalchemy_to_pydantic 3 | 4 | from tests.conftest import Vacancy 5 | from tests.utils import JobCategory 6 | 7 | PydanticVacancy = sqlalchemy_to_pydantic(Vacancy) 8 | 9 | 10 | class CustomPydanticVacancy(BaseModel): 11 | id: int 12 | is_active: bool 13 | sum_salary_from: float 14 | category: JobCategory 15 | 16 | model_config = ConfigDict(from_attributes=True) 17 | 18 | 19 | class ListPydanticVacancy(BaseModel): 20 | vacancies: list[PydanticVacancy] 21 | 22 | 23 | class ListCustomPydanticVacancy(BaseModel): 24 | vacancies: list[CustomPydanticVacancy] 25 | -------------------------------------------------------------------------------- /tests/test_filter.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from datetime import datetime, date 4 | 5 | from fastapi import HTTPException 6 | from starlette.status import HTTP_400_BAD_REQUEST 7 | from tests.utils import JobCategory 8 | from tests.schemas import ListPydanticVacancy, ListCustomPydanticVacancy 9 | 10 | 11 | async def test_empty_query(session, get_vacancy_filter, create_vacancies): 12 | query = get_vacancy_filter.get_query("") 13 | res = await session.execute(query) 14 | data = ListPydanticVacancy( 15 | vacancies=list(res.scalars().all()) 16 | ).model_dump(exclude={"created_at", "updated_at", "company_id"}) 17 | 18 | assert len(data["vacancies"]) == 10 19 | assert isinstance(data["vacancies"][0]["created_at"], date) 20 | assert isinstance(data["vacancies"][0]["updated_at"], datetime) 21 | check_data = data["vacancies"][0].copy() 22 | del check_data["created_at"] 23 | del check_data["updated_at"] 24 | del check_data["company_id"] 25 | assert check_data == { 26 | "id": 1, 27 | "title": "title1", 28 | "description": "description1", 29 | "is_active": True, 30 | "salary_from": 60, 31 | "salary_up_to": 110.725, 32 | "category": JobCategory.finance 33 | } 34 | 35 | 36 | async def test_eq_with_int(session, get_vacancy_filter, create_vacancies): 37 | query = get_vacancy_filter.get_query("salary_from__eq=60") 38 | res = await session.execute(query) 39 | data = ListPydanticVacancy( 40 | vacancies=list(res.scalars().all()) 41 | ).model_dump(exclude={"created_at", "updated_at", "company_id"}) 42 | 43 | assert len(data["vacancies"]) == 1 44 | assert isinstance(data["vacancies"][0]["created_at"], date) 45 | assert isinstance(data["vacancies"][0]["updated_at"], datetime) 46 | check_data = data["vacancies"][0].copy() 47 | del check_data["created_at"] 48 | del check_data["updated_at"] 49 | del check_data["company_id"] 50 | assert check_data == { 51 | "id": 1, 52 | "title": "title1", 53 | "description": "description1", 54 | "is_active": True, 55 | "salary_from": 60, 56 | "salary_up_to": 110.725, 57 | "category": JobCategory.finance 58 | } 59 | 60 | 61 | async def test_eq_with_float(session, get_vacancy_filter, create_vacancies): 62 | query = get_vacancy_filter.get_query("salary_up_to__eq=120.725") 63 | res = await session.execute(query) 64 | data = ListPydanticVacancy( 65 | vacancies=list(res.scalars().all()) 66 | ).model_dump(exclude={"created_at", "updated_at", "company_id"}) 67 | 68 | assert len(data["vacancies"]) == 1 69 | assert isinstance(data["vacancies"][0]["created_at"], date) 70 | assert isinstance(data["vacancies"][0]["updated_at"], datetime) 71 | check_data = data["vacancies"][0].copy() 72 | del check_data["created_at"] 73 | del check_data["updated_at"] 74 | del check_data["company_id"] 75 | assert check_data == { 76 | "id": 2, 77 | "title": "title2", 78 | "description": "description2", 79 | "is_active": False, 80 | "salary_from": 70, 81 | "salary_up_to": 120.725, 82 | "category": JobCategory.marketing 83 | } 84 | 85 | 86 | async def test_in_with_str(session, get_vacancy_filter, create_vacancies): 87 | query = get_vacancy_filter.get_query("description__in_=description1,description2") 88 | res = await session.execute(query) 89 | data = ListPydanticVacancy(vacancies=list(res.scalars().all())).model_dump() 90 | assert len(data["vacancies"]) == 2 91 | assert isinstance(data["vacancies"][0]["created_at"], date) 92 | assert isinstance(data["vacancies"][0]["updated_at"], datetime) 93 | check_data = data["vacancies"][0].copy() 94 | del check_data["created_at"] 95 | del check_data["updated_at"] 96 | del check_data["company_id"] 97 | assert check_data == { 98 | "id": 1, 99 | "title": "title1", 100 | "description": "description1", 101 | "is_active": True, 102 | "salary_from": 60, 103 | "salary_up_to": 110.725, 104 | "category": JobCategory.finance 105 | } 106 | 107 | 108 | async def test_contains_with_str(session, get_vacancy_filter, create_vacancies): 109 | query = get_vacancy_filter.get_query("description__contains=tion1") 110 | res = await session.execute(query) 111 | data = ListPydanticVacancy(vacancies=list(res.scalars().all())).model_dump() 112 | assert len(data["vacancies"]) == 2 113 | assert isinstance(data["vacancies"][0]["created_at"], date) 114 | assert isinstance(data["vacancies"][0]["updated_at"], datetime) 115 | check_data = data["vacancies"][-1].copy() 116 | del check_data["created_at"] 117 | del check_data["updated_at"] 118 | del check_data["company_id"] 119 | assert check_data == { 120 | "id": 10, 121 | "title": "title10", 122 | "description": "description10", 123 | "is_active": False, 124 | "salary_from": 150, 125 | "salary_up_to": 200.725, 126 | "category": JobCategory.miscellaneous 127 | } 128 | 129 | 130 | async def test_endswith_with_str(session, get_vacancy_filter, create_vacancies): 131 | query = get_vacancy_filter.get_query("title__endswith=le1") 132 | res = await session.execute(query) 133 | data = ListPydanticVacancy(vacancies=list(res.scalars().all())).model_dump() 134 | assert len(data["vacancies"]) == 1 135 | assert isinstance(data["vacancies"][0]["created_at"], date) 136 | assert isinstance(data["vacancies"][0]["updated_at"], datetime) 137 | check_data = data["vacancies"][0].copy() 138 | del check_data["created_at"] 139 | del check_data["updated_at"] 140 | del check_data["company_id"] 141 | assert check_data == { 142 | "id": 1, 143 | "title": "title1", 144 | "description": "description1", 145 | "is_active": True, 146 | "salary_from": 60, 147 | "salary_up_to": 110.725, 148 | "category": JobCategory.finance 149 | } 150 | 151 | 152 | async def test_in_with_int(session, get_vacancy_filter, create_vacancies): 153 | query = get_vacancy_filter.get_query("salary_from__in_=60,70,80") 154 | res = await session.execute(query) 155 | data = ListPydanticVacancy(vacancies=list(res.scalars().all())).model_dump() 156 | assert len(data["vacancies"]) == 3 157 | assert isinstance(data["vacancies"][0]["created_at"], date) 158 | assert isinstance(data["vacancies"][0]["updated_at"], datetime) 159 | check_data = data["vacancies"][0].copy() 160 | del check_data["created_at"] 161 | del check_data["updated_at"] 162 | del check_data["company_id"] 163 | assert check_data == { 164 | "id": 1, 165 | "title": "title1", 166 | "description": "description1", 167 | "is_active": True, 168 | "salary_from": 60, 169 | "salary_up_to": 110.725, 170 | "category": JobCategory.finance 171 | } 172 | 173 | 174 | async def test_gte_with_int(session, get_vacancy_filter, create_vacancies): 175 | query = get_vacancy_filter.get_query("salary_from__gte=120") 176 | res = await session.execute(query) 177 | data = ListPydanticVacancy(vacancies=list(res.scalars().all())).model_dump() 178 | assert len(data["vacancies"]) == 4 179 | assert isinstance(data["vacancies"][0]["created_at"], date) 180 | assert isinstance(data["vacancies"][0]["updated_at"], datetime) 181 | check_data = data["vacancies"][0].copy() 182 | del check_data["created_at"] 183 | del check_data["updated_at"] 184 | del check_data["company_id"] 185 | assert check_data == { 186 | "id": 7, 187 | "title": "title7", 188 | "description": "description7", 189 | "is_active": True, 190 | "salary_from": 120, 191 | "salary_up_to": 170.725, 192 | "category": JobCategory.construction 193 | } 194 | 195 | 196 | async def test_not_eq_with_date(session, get_vacancy_filter, create_vacancies): 197 | query = get_vacancy_filter.get_query("created_at__not_eq=2023-05-01") 198 | res = await session.execute(query) 199 | data = ListPydanticVacancy(vacancies=list(res.scalars().all())).model_dump() 200 | assert len(data["vacancies"]) == 9 201 | assert isinstance(data["vacancies"][0]["created_at"], date) 202 | assert isinstance(data["vacancies"][0]["updated_at"], datetime) 203 | check_data = data["vacancies"][0].copy() 204 | del check_data["created_at"] 205 | del check_data["updated_at"] 206 | del check_data["company_id"] 207 | assert check_data == { 208 | "id": 2, 209 | "title": "title2", 210 | "description": "description2", 211 | "is_active": False, 212 | "salary_from": 70, 213 | "salary_up_to": 120.725, 214 | "category": JobCategory.marketing 215 | } 216 | 217 | 218 | async def test_eq_with_bool(session, get_vacancy_filter, create_vacancies): 219 | query = get_vacancy_filter.get_query("is_active__eq=true") 220 | res = await session.execute(query) 221 | data = ListPydanticVacancy(vacancies=list(res.scalars().all())).model_dump() 222 | assert len(data["vacancies"]) == 5 223 | assert isinstance(data["vacancies"][0]["created_at"], date) 224 | assert isinstance(data["vacancies"][0]["updated_at"], datetime) 225 | check_data = data["vacancies"][0].copy() 226 | del check_data["created_at"] 227 | del check_data["updated_at"] 228 | del check_data["company_id"] 229 | assert check_data == { 230 | "id": 1, 231 | "title": "title1", 232 | "description": "description1", 233 | "is_active": True, 234 | "salary_from": 60, 235 | "salary_up_to": 110.725, 236 | "category": JobCategory.finance 237 | } 238 | 239 | 240 | async def test_between_with_int(session, get_vacancy_filter, create_vacancies): 241 | query = get_vacancy_filter.get_query("salary_from__between=50,90") 242 | res = await session.execute(query) 243 | data = ListPydanticVacancy(vacancies=list(res.scalars().all())).model_dump() 244 | assert len(data["vacancies"]) == 4 245 | assert isinstance(data["vacancies"][0]["created_at"], date) 246 | assert isinstance(data["vacancies"][0]["updated_at"], datetime) 247 | check_data = data["vacancies"][0].copy() 248 | del check_data["created_at"] 249 | del check_data["updated_at"] 250 | del check_data["company_id"] 251 | assert check_data == { 252 | "id": 1, 253 | "title": "title1", 254 | "description": "description1", 255 | "is_active": True, 256 | "salary_from": 60, 257 | "salary_up_to": 110.725, 258 | "category": JobCategory.finance 259 | } 260 | 261 | 262 | async def test_between_with_datetime(session, get_vacancy_filter, create_vacancies): 263 | query = get_vacancy_filter.get_query("updated_at__between=2023-01-01 00:00:00,2023-05-01 00:00:00") 264 | res = await session.execute(query) 265 | data = ListPydanticVacancy(vacancies=list(res.scalars().all())).model_dump() 266 | assert len(data["vacancies"]) == 4 267 | assert isinstance(data["vacancies"][0]["created_at"], date) 268 | assert isinstance(data["vacancies"][0]["updated_at"], datetime) 269 | check_data = data["vacancies"][-1].copy() 270 | del check_data["created_at"] 271 | del check_data["updated_at"] 272 | del check_data["company_id"] 273 | assert check_data == { 274 | "id": 4, 275 | "title": "title4", 276 | "description": "description4", 277 | "is_active": False, 278 | "salary_from": 90, 279 | "salary_up_to": 140.725, 280 | "category": JobCategory.it 281 | } 282 | 283 | 284 | async def test_between_with_date(session, get_vacancy_filter, create_vacancies): 285 | query = get_vacancy_filter.get_query("created_at__between=2023-05-01,2023-05-05") 286 | res = await session.execute(query) 287 | data = ListPydanticVacancy(vacancies=list(res.scalars().all())).model_dump() 288 | assert len(data["vacancies"]) == 5 289 | assert isinstance(data["vacancies"][0]["created_at"], date) 290 | assert isinstance(data["vacancies"][0]["updated_at"], datetime) 291 | check_data = data["vacancies"][-1].copy() 292 | del check_data["created_at"] 293 | del check_data["updated_at"] 294 | del check_data["company_id"] 295 | assert check_data == { 296 | "id": 5, 297 | "title": "title5", 298 | "description": "description5", 299 | "is_active": True, 300 | "salary_from": 100, 301 | "salary_up_to": 150.725, 302 | "category": JobCategory.metallurgy 303 | } 304 | 305 | 306 | async def test_gt_with_int(session, get_vacancy_filter, create_vacancies): 307 | query = get_vacancy_filter.get_query("salary_from__gt=100") 308 | res = await session.execute(query) 309 | data = ListPydanticVacancy(vacancies=list(res.scalars().all())).model_dump() 310 | assert len(data["vacancies"]) == 5 311 | assert isinstance(data["vacancies"][0]["created_at"], date) 312 | assert isinstance(data["vacancies"][0]["updated_at"], datetime) 313 | check_data = data["vacancies"][0].copy() 314 | del check_data["created_at"] 315 | del check_data["updated_at"] 316 | del check_data["company_id"] 317 | assert check_data == { 318 | "id": 6, 319 | "title": "title6", 320 | "description": "description6", 321 | "is_active": False, 322 | "salary_from": 110, 323 | "salary_up_to": 160.725, 324 | "category": JobCategory.medicine 325 | } 326 | 327 | 328 | async def test_enum_with_str(session, get_vacancy_filter, create_vacancies): 329 | query = get_vacancy_filter.get_query("category__eq=Medicine") 330 | res = await session.execute(query) 331 | data = ListPydanticVacancy(vacancies=list(res.scalars().all())).model_dump() 332 | assert len(data["vacancies"]) == 1 333 | assert isinstance(data["vacancies"][0]["created_at"], date) 334 | assert isinstance(data["vacancies"][0]["updated_at"], datetime) 335 | check_data = data["vacancies"][0].copy() 336 | del check_data["created_at"] 337 | del check_data["updated_at"] 338 | del check_data["company_id"] 339 | assert check_data == { 340 | "id": 6, 341 | "title": "title6", 342 | "description": "description6", 343 | "is_active": False, 344 | "salary_from": 110, 345 | "salary_up_to": 160.725, 346 | "category": JobCategory.medicine 347 | } 348 | 349 | 350 | async def test_complex_query(session, get_vacancy_filter, create_vacancies): 351 | query = get_vacancy_filter.get_query( 352 | "created_at__between=2023-05-01,2023-05-05&" 353 | "updated_at__in_=2023-01-05 15:15:15,2023-05-05 15:15:15|" 354 | "salary_from__gt=100" 355 | ) 356 | res = await session.execute(query) 357 | data = ListPydanticVacancy(vacancies=list(res.scalars().all())).model_dump() 358 | assert len(data["vacancies"]) == 7 359 | assert isinstance(data["vacancies"][0]["created_at"], date) 360 | assert isinstance(data["vacancies"][0]["updated_at"], datetime) 361 | check_data = data["vacancies"][1].copy() 362 | del check_data["created_at"] 363 | del check_data["updated_at"] 364 | del check_data["company_id"] 365 | assert check_data == { 366 | "id": 5, 367 | "title": "title5", 368 | "description": "description5", 369 | "is_active": True, 370 | "salary_from": 100, 371 | "salary_up_to": 150.725, 372 | "category": JobCategory.metallurgy 373 | } 374 | 375 | 376 | async def test_several_or_in_query(session, get_vacancy_filter, create_vacancies): 377 | created_between = ["2023-05-01", "2023-05-05"] 378 | created_pattern = "%Y-%m-%d" 379 | updated_in = ["2023-01-05 15:15:15", "2023-05-05 15:15:15"] 380 | updated_pattern = "%Y-%m-%d %H:%M:%S" 381 | salary = 120 382 | 383 | query = get_vacancy_filter.get_query( 384 | f"created_at__between={created_between[0]},{created_between[1]}|" 385 | f"updated_at__in_={updated_in[0]},{updated_in[1]}|" 386 | f"salary_up_to__lt={salary}" 387 | ) 388 | res = await session.execute(query) 389 | data = ListPydanticVacancy(vacancies=list(res.scalars().all())).model_dump() 390 | assert len(data["vacancies"]) == 5 391 | 392 | for vacancy in data["vacancies"]: 393 | assert ( 394 | datetime.strptime(created_between[0], created_pattern).date() < 395 | vacancy["created_at"] < 396 | datetime.strptime(created_between[1], created_pattern).date() 397 | or vacancy["salary_up_to"] < salary 398 | or vacancy["updated_at"] == datetime.strptime(updated_in[0], updated_pattern) 399 | or vacancy["updated_at"] == datetime.strptime(updated_in[1], updated_pattern) 400 | ) 401 | 402 | 403 | async def test_complex_query_with_order_by(session, get_vacancy_filter, create_vacancies): 404 | query = get_vacancy_filter.get_query( 405 | "created_at__between=2023-05-01,2023-05-05&" 406 | "updated_at__in_=2023-01-05 15:15:15,2023-05-05 15:15:15|" 407 | "salary_from__gt=100&" 408 | "order_by=-id" 409 | ) 410 | res = await session.execute(query) 411 | data = ListPydanticVacancy(vacancies=list(res.scalars().all())).model_dump() 412 | assert len(data["vacancies"]) == 7 413 | assert isinstance(data["vacancies"][0]["created_at"], date) 414 | assert isinstance(data["vacancies"][0]["updated_at"], datetime) 415 | check_data = data["vacancies"][0].copy() 416 | del check_data["created_at"] 417 | del check_data["updated_at"] 418 | del check_data["company_id"] 419 | assert check_data == { 420 | "id": 10, 421 | "title": "title10", 422 | "description": "description10", 423 | "is_active": False, 424 | "salary_from": 150, 425 | "salary_up_to": 200.725, 426 | "category": JobCategory.miscellaneous 427 | } 428 | 429 | 430 | async def test_order_by_id(session, get_vacancy_filter, create_vacancies): 431 | query = get_vacancy_filter.get_query("order_by=-id") 432 | res = await session.execute(query) 433 | data = ListPydanticVacancy(vacancies=list(res.scalars().all())).model_dump() 434 | assert len(data["vacancies"]) == 10 435 | assert isinstance(data["vacancies"][0]["created_at"], date) 436 | assert isinstance(data["vacancies"][0]["updated_at"], datetime) 437 | check_data = data["vacancies"][0].copy() 438 | del check_data["created_at"] 439 | del check_data["updated_at"] 440 | del check_data["company_id"] 441 | assert check_data == { 442 | "id": 10, 443 | "title": "title10", 444 | "description": "description10", 445 | "is_active": False, 446 | "salary_from": 150, 447 | "salary_up_to": 200.725, 448 | "category": JobCategory.miscellaneous 449 | } 450 | 451 | 452 | async def test_custom_query(session, get_custom_vacancy_filter, create_vacancies): 453 | query = get_custom_vacancy_filter.get_query("") 454 | res = await session.execute(query) 455 | data = ListCustomPydanticVacancy(vacancies=res.all()).model_dump(exclude_none=True) 456 | assert len(data["vacancies"]) == 2 457 | check_data = data["vacancies"][0].copy() 458 | assert check_data == { 459 | "id": 2, 460 | "is_active": False, 461 | "sum_salary_from": 550, 462 | "category": JobCategory.marketing 463 | } 464 | 465 | 466 | async def test_custom_complex_query(session, get_custom_vacancy_filter, create_vacancies): 467 | query = get_custom_vacancy_filter.get_query( 468 | "created_at__between=2023-05-01,2023-05-05&" 469 | "updated_at__in_=2023-01-05 15:15:15,2023-05-05 15:15:15|" 470 | "salary_from__gt=100&" 471 | "order_by=-salary_from" 472 | ) 473 | res = await session.execute(query) 474 | data = ListCustomPydanticVacancy(vacancies=res.all()).model_dump(exclude_none=True) 475 | assert len(data["vacancies"]) == 2 476 | check_data = data["vacancies"][0].copy() 477 | assert check_data == { 478 | "id": 6, 479 | "is_active": False, 480 | "sum_salary_from": 390, 481 | "category": JobCategory.medicine 482 | } 483 | 484 | 485 | @pytest.mark.parametrize( 486 | "bad_filter, expected_status_code, expected_detail", 487 | ( 488 | ( 489 | "salary_from__qt=100", 490 | HTTP_400_BAD_REQUEST, 491 | "Forbidden filter 'qt' for 'salary_from'" 492 | ), 493 | ( 494 | "id__gt=100", 495 | HTTP_400_BAD_REQUEST, 496 | "Forbidden filter field 'id'" 497 | ), 498 | ( 499 | "salary_from__eq=", 500 | HTTP_400_BAD_REQUEST, 501 | [ 502 | { 503 | "type": "int_parsing", 504 | "loc": ["salary_from"], 505 | "msg": "Input should be a valid integer, unable to parse string as an integer", 506 | "input": "" 507 | } 508 | ] 509 | ), 510 | ( 511 | "salary__eq=100", 512 | HTTP_400_BAD_REQUEST, 513 | "DB model Vacancy doesn't have field 'salary'" 514 | ), 515 | ( 516 | "salary_from_eq=100", 517 | HTTP_400_BAD_REQUEST, 518 | "Incorrect filter request syntax, " 519 | "please use pattern :'{field_name}__{condition}={value}{conjunction}' " 520 | "or '{relation}.{field_name}__{condition}={value}{conjunction}'" 521 | ), 522 | ( 523 | "salary_from__eq-100", 524 | HTTP_400_BAD_REQUEST, 525 | "Incorrect filter request syntax, " 526 | "please use pattern :'{field_name}__{condition}={value}{conjunction}' " 527 | "or '{relation}.{field_name}__{condition}={value}{conjunction}'" 528 | ), 529 | ( 530 | "category__eq=Unknown_category", 531 | HTTP_400_BAD_REQUEST, 532 | [ 533 | { 534 | "type": "enum", 535 | "loc": ["category"], 536 | "msg": "Input should be 'Finance', 'Marketing', 'Agriculture', 'IT', " 537 | "'Metallurgy', 'Medicine', 'Construction', 'Building', 'Services' or 'Miscellaneous'", 538 | "input": "Unknown_category", 539 | "ctx": { 540 | "expected": "'Finance', 'Marketing', 'Agriculture', 'IT', 'Metallurgy', 'Medicine', " 541 | "'Construction', 'Building', 'Services' or 'Miscellaneous'" 542 | } 543 | } 544 | 545 | ] 546 | ), 547 | ( 548 | "created_at__between=2023-05-01,2023-05-05" 549 | "updated_at__in_=2023-01-05 15:15:15,2023-05-05 15:15:15|" 550 | "salary_from__gt=100", 551 | HTTP_400_BAD_REQUEST, 552 | "Incorrect filter request syntax, " 553 | "please use pattern :'{field_name}__{condition}={value}{conjunction}' " 554 | "or '{relation}.{field_name}__{condition}={value}{conjunction}'" 555 | ), 556 | ( 557 | "is_active__eq=100", 558 | HTTP_400_BAD_REQUEST, 559 | [ 560 | { 561 | "type": "bool_parsing", 562 | "loc": ["is_active"], 563 | "msg": "Input should be a valid boolean, unable to interpret input", 564 | "input": "100" 565 | } 566 | ] 567 | ), 568 | ( 569 | "salary_up_to__eq=100/12", 570 | HTTP_400_BAD_REQUEST, 571 | [ 572 | { 573 | "type": "float_parsing", 574 | "loc": ["salary_up_to"], 575 | "msg": "Input should be a valid number, unable to parse string as a number", 576 | "input": "100/12" 577 | } 578 | ] 579 | ), 580 | ) 581 | ) 582 | def test_fail_filter(get_vacancy_filter, bad_filter, expected_status_code, expected_detail): 583 | with pytest.raises(HTTPException) as e: 584 | get_vacancy_filter.get_query(bad_filter) 585 | assert e.type == HTTPException 586 | assert e.value.status_code == expected_status_code 587 | 588 | errors_details = e.value.detail 589 | 590 | if isinstance(errors_details, list): 591 | for detail in errors_details: 592 | detail.pop("url") 593 | 594 | assert errors_details == expected_detail 595 | -------------------------------------------------------------------------------- /tests/test_join_filter.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from fastapi import HTTPException 3 | from starlette.status import HTTP_400_BAD_REQUEST 4 | 5 | 6 | async def test_relation_search(session, get_custom_company_filter, create_vacancies): 7 | query = get_custom_company_filter.get_query("title__eq=MyCompany2&vacancies.salary_from__gte=100") 8 | res = await session.execute(query) 9 | companies = res.unique().scalars().all() 10 | 11 | assert len(companies) == 1 12 | assert len(companies[0].vacancies) == 6 13 | 14 | company = companies[0] 15 | assert company.id == 2 16 | assert company.title == "MyCompany2" 17 | 18 | 19 | async def test_pass_custom_select_into_init(session, get_filter_passed_in_init, create_vacancies): 20 | query = get_filter_passed_in_init.get_query("title__eq=MyCompany2&vacancies.salary_from__gte=100") 21 | res = await session.execute(query) 22 | companies = res.unique().scalars().all() 23 | 24 | assert len(companies) == 1 25 | 26 | company = companies[0] 27 | assert company.id == 2 28 | assert company.title == "MyCompany2" 29 | 30 | 31 | @pytest.mark.parametrize( 32 | "bad_filter, expected_status_code, expected_detail", 33 | ( 34 | ( 35 | "unknown.vacancies.salary_from__gte=100", 36 | HTTP_400_BAD_REQUEST, 37 | "Incorrect filter request syntax, " 38 | "please use pattern :'{field_name}__{condition}={value}{conjunction}' " 39 | "or '{relation}.{field_name}__{condition}={value}{conjunction}'" 40 | ), 41 | ( 42 | "unknown.salary_from__gte=100", 43 | HTTP_400_BAD_REQUEST, 44 | "Can not find relation unknown in Company model" 45 | ), 46 | ) 47 | ) 48 | def test_fail_relation_filter(get_custom_company_filter, bad_filter, expected_status_code, expected_detail): 49 | with pytest.raises(HTTPException) as e: 50 | get_custom_company_filter.get_query(bad_filter) 51 | assert e.type == HTTPException 52 | assert e.value.status_code == expected_status_code 53 | 54 | errors_details = e.value.detail 55 | 56 | if isinstance(errors_details, list): 57 | for detail in errors_details: 58 | detail.pop("url") 59 | 60 | assert errors_details == expected_detail 61 | 62 | 63 | async def test_reverse_relation(session, get_vacancy_filter_with_join, create_vacancies): 64 | query = get_vacancy_filter_with_join.get_query("company.title__eq=MyCompany1") 65 | res = await session.execute(query) 66 | vacancies = res.unique().scalars().all() 67 | 68 | assert len(vacancies) == 4 69 | vacancy = vacancies[0] 70 | assert vacancy.id == 1 71 | assert vacancy.company_id == 1 72 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class JobCategory(Enum): 5 | finance = "Finance" 6 | marketing = "Marketing" 7 | agro = "Agriculture" 8 | it = "IT" 9 | metallurgy = "Metallurgy" 10 | medicine = "Medicine" 11 | construction = "Construction" 12 | building = "Building" 13 | services = "Services" 14 | miscellaneous = "Miscellaneous" 15 | --------------------------------------------------------------------------------