dapper-sqls 1.1.3__py3-none-any.whl → 1.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,35 +1,261 @@
1
- from pydantic import BaseModel, BaseConfig
2
- from abc import ABC
1
+ from pydantic import BaseModel, ConfigDict, PrivateAttr, Field, create_model
3
2
  from abc import ABC, abstractmethod
4
- from .result import Result
3
+ from typing import Set, Any, ClassVar, get_origin, get_args, Union, Optional, Literal, get_type_hints, List
4
+ from ..utils import get_dict_args
5
+ from dataclasses import asdict
6
+ import copy
7
+ import datetime
8
+
9
+ QUERY_FIELD_TYPES = {
10
+ 'StringQueryField',
11
+ 'NumericQueryField',
12
+ 'BoolQueryField',
13
+ 'DateQueryField',
14
+ 'BytesQueryField',
15
+ 'JoinStringCondition',
16
+ 'JoinNumericCondition',
17
+ 'JoinBooleanCondition',
18
+ 'JoinDateCondition',
19
+ 'JoinBytesCondition',
20
+ }
21
+
22
+ def convert_datetime_date_to_str(annotation):
23
+ """Convert datetime/date or their unions with str to just str."""
24
+ if annotation in (datetime.datetime, datetime.date):
25
+ return str
26
+ origin_inner = get_origin(annotation)
27
+ args_inner = get_args(annotation)
28
+ if origin_inner is Union:
29
+ set_args = set(args_inner)
30
+ if str in set_args and (datetime.datetime in set_args or datetime.date in set_args):
31
+ return str
32
+ return annotation
33
+
34
+ def remove_query_field_types(annotation):
35
+ """
36
+ Remove os tipos de QueryField (como StringQueryField) de uma Union ou substitui diretamente
37
+ """
38
+ origin = get_origin(annotation)
39
+ args = get_args(annotation)
40
+
41
+ def is_query_field(arg):
42
+ return getattr(arg, '__name__', '') in QUERY_FIELD_TYPES
43
+
44
+ if origin is Union:
45
+ new_args = tuple(arg for arg in args if not is_query_field(arg))
46
+ if len(new_args) == 1:
47
+ return new_args[0]
48
+ return Union[new_args]
49
+ elif is_query_field(annotation):
50
+ return str # fallback para str caso algum passe isolado
51
+ return annotation
52
+
53
+ def is_optional(annotation):
54
+ """Check if an annotation is Optional[...] or Union[..., None]."""
55
+ origin = get_origin(annotation)
56
+ args = get_args(annotation)
57
+ return origin is Union and type(None) in args
58
+
59
+ def remove_optional(annotation):
60
+ """Remove NoneType from Union[...]"""
61
+ args = tuple(arg for arg in get_args(annotation) if arg is not type(None))
62
+ if len(args) == 1:
63
+ return args[0]
64
+ return Union[args]
65
+
66
+ def make_optional(annotation):
67
+ """Make an annotation optional if not already."""
68
+ if is_optional(annotation):
69
+ return annotation
70
+ return Optional[annotation]
71
+
72
+ class SensitiveFields(object):
73
+
74
+ _sensitive_fields : Set[str] = set()
75
+
76
+ @classmethod
77
+ def set(cls, new_sensitive_filds : Set[str]):
78
+ cls._sensitive_fields = new_sensitive_filds
79
+
80
+ @classmethod
81
+ def get(cls):
82
+ return cls._sensitive_fields
83
+
5
84
 
6
85
  class TableBaseModel(BaseModel, ABC):
7
- class Config(BaseConfig):
86
+ class Config(ConfigDict):
8
87
  from_attributes = True
9
88
 
10
- _TABLE_NAME: str = ''
89
+ TABLE_NAME: ClassVar[str]
11
90
 
12
- @property
13
- def TABLE_NAME(cls) -> str:
14
- return cls._TABLE_NAME
91
+ TABLE_ALIAS: ClassVar[str]
92
+
93
+ DESCRIPTION : ClassVar[str]
94
+
95
+ IDENTITIES : ClassVar[Set[str]]
96
+
97
+ PRIMARY_KEYs : ClassVar[Set[str]]
98
+
99
+ OPTIONAL_FIELDS : ClassVar[Set[str]]
100
+
101
+ MAX_LENGTH_FIELDS: ClassVar[dict[str, int]] = {}
102
+
103
+ _explicit_fields: Set[str] = PrivateAttr(default_factory=set)
104
+ _pending_updates: dict[str, Any] = PrivateAttr(default_factory=dict)
105
+ _initial_values: dict[str, Any] = PrivateAttr(default_factory=dict)
106
+
107
+
108
+ def __init__(self, **data):
109
+ sensitive = SensitiveFields.get()
110
+ filtered_data = {k: v for k, v in data.items() if k not in sensitive}
111
+
112
+ super().__init__(**filtered_data)
113
+ self._explicit_fields = set(filtered_data.keys())
114
+ self._initial_values = copy.deepcopy(self.model_dump())
115
+
116
+ def _reset_defaults(self):
117
+ for field_name, model_field in self.model_fields.items():
118
+ if field_name not in self._explicit_fields:
119
+ setattr(self, field_name, None)
120
+
121
+ def reset_to_initial_values(self):
122
+ for key, value in self._initial_values.items():
123
+ setattr(self, key, copy.deepcopy(value))
124
+ self.clear_updates()
125
+
126
+ def equals(self, other: "TableBaseModel") -> bool:
127
+ return self.model_dump() == other.model_dump()
128
+
129
+ def clear_updates(self):
130
+ self._pending_updates.clear()
15
131
 
132
+ def has_updates(self) -> bool:
133
+ for key, new_value in self._pending_updates.items():
134
+ if key in self.model_fields:
135
+ current_value = getattr(self, key, None)
136
+
137
+ if isinstance(current_value, BaseModel) and isinstance(new_value, BaseModel):
138
+ if current_value.model_dump() != new_value.model_dump():
139
+ return True
140
+
141
+ elif hasattr(current_value, "__dataclass_fields__") and hasattr(new_value, "__dataclass_fields__"):
142
+ if asdict(current_value) != asdict(new_value):
143
+ return True
144
+
145
+ elif hasattr(current_value, "__dict__") and hasattr(new_value, "__dict__"):
146
+ if vars(current_value) != vars(new_value):
147
+ return True
148
+
149
+ elif new_value != current_value:
150
+ return True
151
+ return False
152
+
153
+ @staticmethod
154
+ def queue_update(self : 'TableBaseModel', **fields):
155
+ fields = get_dict_args(fields)
156
+ for key, value in fields.items():
157
+ if value != None and key in self.model_fields:
158
+ self._pending_updates[key] = value
159
+
160
+ def apply_updates(self):
161
+ for key, value in self._pending_updates.items():
162
+ if key in self.model_fields:
163
+ setattr(self, key, value)
164
+ self.clear_updates()
165
+
166
+ def alter_model_class(self, remove_fields: tuple[str] = (), mode: Literal['all_optional', 'all_required', 'original'] = 'all_optional', query_field = False):
167
+ fields = {}
168
+
169
+ for field_name, field in self.model_fields.items():
170
+ if field_name in remove_fields:
171
+ continue
172
+
173
+ ann = convert_datetime_date_to_str(field.annotation)
174
+ if not query_field:
175
+ ann = remove_query_field_types(ann)
176
+
177
+ max_length = None
178
+ if mode in ('all_required', 'original'):
179
+ max_length = self.MAX_LENGTH_FIELDS.get(field_name)
180
+ if isinstance(max_length, int) and max_length < 1:
181
+ max_length = None
182
+
183
+ default = field.default
184
+
185
+ if mode == 'all_optional':
186
+ ann = make_optional(ann)
187
+ default = None
188
+
189
+ elif mode == 'all_required':
190
+ if is_optional(ann):
191
+ ann = remove_optional(ann)
192
+ default = ...
193
+
194
+ elif mode == 'original':
195
+ if field_name in self.OPTIONAL_FIELDS:
196
+ ann = make_optional(ann)
197
+ default = None
198
+ else:
199
+ if is_optional(ann):
200
+ ann = remove_optional(ann)
201
+ default = ...
202
+
203
+ fields[field_name] = (ann, Field(default=default, description=field.description, max_length=max_length))
204
+
205
+ new_model_class = create_model(
206
+ self.__name__,
207
+ __config__=ConfigDict(extra='forbid'),
208
+ **fields
209
+ )
210
+ return new_model_class
211
+
212
+ @classmethod
213
+ def get_field_type_names(cls) -> dict[str, set[str]]:
214
+ result = {}
215
+ type_hints = get_type_hints(cls, include_extras=True)
216
+
217
+ for field_name, hint in type_hints.items():
218
+ if field_name.startswith('_') or get_origin(hint) is ClassVar:
219
+ continue
220
+
221
+ args = get_args(hint)
222
+ if not args:
223
+ args = (hint,)
224
+
225
+ types = {
226
+ t.__name__ if hasattr(t, '__name__') else t._name if hasattr(t, '_name') else str(t)
227
+ for t in args
228
+ if t is not type(None)
229
+ }
230
+
231
+ result[field_name] = types
232
+
233
+ return result
234
+
235
+ class SearchTable(BaseModel):
236
+ model: TableBaseModel
237
+ include: Optional[List[str]] = Field(default_factory=list)
238
+
239
+ class JoinSearchTable(SearchTable):
240
+ join_type: Literal["INNER", "LEFT", "RIGHT", "FULL"] = "LEFT"
241
+
16
242
  class BaseUpdate(ABC):
17
243
 
18
- def __init__(self, executor , model):
19
- self._set_data = model
20
- self._executor = executor
244
+ def __init__(self, executor , model):
245
+ self._set_data = model
246
+ self._executor = executor
21
247
 
22
- @property
23
- def set_data(self):
24
- return self._set_data
248
+ @property
249
+ def set_data(self):
250
+ return self._set_data
25
251
 
26
- @property
27
- def executor(self):
28
- return self._executor
252
+ @property
253
+ def executor(self):
254
+ return self._executor
29
255
 
30
- @abstractmethod
31
- def where(self, *args) -> Result.Send:
32
- pass
256
+ @abstractmethod
257
+ def where(self, *args):
258
+ pass
33
259
 
34
260
 
35
261
 
@@ -34,7 +34,7 @@ class ConnectionStringData(object):
34
34
  @username.setter
35
35
  def username(self, value: str):
36
36
  if not isinstance(value, str):
37
- raise ValueError("O nome de usu�rio deve ser uma string.")
37
+ raise ValueError("O nome de usuário deve ser uma string.")
38
38
  self._username = value
39
39
 
40
40
  @property
@@ -0,0 +1,214 @@
1
+ from typing import Union, List, Literal, Any, Optional
2
+ from pydantic import BaseModel, Field, create_model
3
+ from datetime import datetime, date
4
+ from abc import ABC, abstractmethod
5
+
6
+ class QueryFieldBase(BaseModel, ABC):
7
+
8
+ class Config:
9
+ extra = "forbid"
10
+
11
+ prefix: Optional[str] = Field(
12
+ default=...,
13
+ description="Optional prefix to be prepended to the SQL condition (e.g., for parentheses or NOT)"
14
+ )
15
+ suffix: Optional[str] = Field(
16
+ default=...,
17
+ description="Optional suffix to be appended to the SQL condition (e.g., for closing parentheses)"
18
+ )
19
+
20
+ def quote(self, val):
21
+ if isinstance(val, str):
22
+ val = val.replace("'", "''")
23
+ return f"'{val}'"
24
+ elif isinstance(val, bool):
25
+ return '1' if val else '0'
26
+ elif isinstance(val, datetime):
27
+ return f"'{val.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]}'"
28
+ elif isinstance(val, date):
29
+ return f"'{val.strftime('%Y-%m-%d')}'"
30
+ return str(val)
31
+
32
+ def format_sql(self, field_name: str, value_expr: str, operator: str) -> str:
33
+ prefix = self.prefix if isinstance(self.prefix, str) else ""
34
+ suffix = self.suffix if isinstance(self.suffix, str) else ""
35
+ return f"{prefix}{field_name} {operator} {value_expr}{suffix}"
36
+
37
+ @abstractmethod
38
+ def to_sql(self, field_name: str):
39
+ ...
40
+
41
+
42
+ class StringQueryField(QueryFieldBase):
43
+ value: Union[str, List[str]] = Field(
44
+ default=...,
45
+ description="The value or list of values to compare against the string column"
46
+ )
47
+ operator: Literal['=', '!=', 'LIKE', 'IN', 'NOT IN'] = Field(
48
+ default=...,
49
+ description="SQL operator used for comparison"
50
+ )
51
+
52
+ case_insensitive: bool = Field(
53
+ default=...,
54
+ description="Whether to apply case-insensitive matching (uses UPPER() on field and value)"
55
+ )
56
+
57
+ def apply_like_pattern(self, v: str) -> str:
58
+ if self.operator == 'LIKE':
59
+ return f"%{v}%"
60
+ return v
61
+
62
+ def to_sql(self, field_name: str) -> str:
63
+ field_expr = f"UPPER({field_name})" if self.case_insensitive else field_name
64
+
65
+ if isinstance(self.value, list):
66
+ values = [self.apply_like_pattern(v) for v in self.value]
67
+ values = [v.upper() if self.case_insensitive else v for v in values]
68
+ value_expr = "(" + ", ".join(self.quote(v) for v in values) + ")"
69
+ else:
70
+ val = self.apply_like_pattern(self.value)
71
+ val = val.upper() if self.case_insensitive else val
72
+ value_expr = self.quote(val)
73
+
74
+ return self.format_sql(field_expr, value_expr, self.operator)
75
+
76
+ class NumericQueryField(QueryFieldBase):
77
+ value: Union[int, float, List[Union[int, float]]] = Field(
78
+ default=...,
79
+ description="The numeric value or list of values to compare against the column"
80
+ )
81
+ operator: Literal['=', '!=', '>', '<', '>=', '<=', 'IN', 'NOT IN'] = Field(
82
+ default=...,
83
+ description="SQL operator used for numeric comparison"
84
+ )
85
+
86
+ def to_sql(self, field_name: str) -> str:
87
+ if isinstance(self.value, list):
88
+ value_expr = "(" + ", ".join(str(v) for v in self.value) + ")"
89
+ else:
90
+ value_expr = str(self.value)
91
+
92
+ return self.format_sql(field_name, value_expr, self.operator)
93
+
94
+ class BoolQueryField(QueryFieldBase):
95
+ value: bool = Field(
96
+ default=...,
97
+ description="Boolean value to compare against the column"
98
+ )
99
+ operator: Literal['=', '!='] = Field(
100
+ default=...,
101
+ description="SQL operator used for boolean comparison"
102
+ )
103
+
104
+ def to_sql(self, field_name: str) -> str:
105
+ value_expr = '1' if self.value else '0'
106
+ return self.format_sql(field_name, value_expr, self.operator)
107
+
108
+
109
+ class DateQueryField(QueryFieldBase):
110
+ value:Union[str, datetime, date] = Field(
111
+ default=...,
112
+ description="Date or datetime value to compare (can also be a string in ISO format)"
113
+ )
114
+ operator: Literal['=', '!=', '>', '<', '>=', '<='] = Field(
115
+ default=...,
116
+ description="SQL operator used for date/time comparison"
117
+ )
118
+
119
+ def to_sql(self, field_name: str) -> str:
120
+ if isinstance(self.value, str):
121
+ value_expr = f"'{self.value}'"
122
+ else:
123
+ value_expr = self.quote(self.value)
124
+ return self.format_sql(field_name, value_expr, self.operator)
125
+
126
+
127
+ class BytesQueryField(QueryFieldBase):
128
+ value: Union[bytes, List[bytes]] = Field(
129
+ default=...,
130
+ description="The bytes value or list of byte values to compare against the column"
131
+ )
132
+ operator: Literal['=', '!=', 'IN', 'NOT IN'] = Field(
133
+ default=...,
134
+ description="SQL operator used for byte comparison"
135
+ )
136
+
137
+ def to_sql(self, field_name: str) -> str:
138
+ def format_byte(b: bytes) -> str:
139
+ return "0x" + b.hex() # SQL Server format
140
+
141
+ if isinstance(self.value, list):
142
+ value_expr = "(" + ", ".join(format_byte(v) for v in self.value) + ")"
143
+ else:
144
+ value_expr = format_byte(self.value)
145
+
146
+ return self.format_sql(field_name, value_expr, self.operator)
147
+
148
+ class BaseJoinConditionField(BaseModel):
149
+ class Config:
150
+ extra = "forbid"
151
+
152
+ join_table_column: str = Field(
153
+ ...,
154
+ description="Join table column"
155
+ )
156
+ operator: Literal['=', '!=', '>', '<', '>=', '<=', 'IN', 'NOT IN', 'LIKE'] = Field(
157
+ default=...,
158
+ description="SQL operator used for join condition"
159
+ )
160
+
161
+ def to_sql(self, alias_table : str, field_name: str) -> str:
162
+ right = f"{alias_table}.{self.join_table_column}"
163
+ return f"{field_name} {self.operator} {right}"
164
+
165
+ @classmethod
166
+ def with_join_table_column_type(cls, join_table_column_type: Any):
167
+ new_model = create_model(
168
+ cls.__name__,
169
+ __base__=cls,
170
+ join_table_column=(
171
+ join_table_column_type,
172
+ Field(
173
+ ...,
174
+ description=cls.model_fields['join_table_column'].description
175
+ )
176
+ ),
177
+ )
178
+ return new_model
179
+
180
+ class JoinNumericCondition(BaseJoinConditionField):
181
+ operator: Literal['=', '!=', '>', '<', '>=', '<=', 'IN', 'NOT IN'] = Field(
182
+ default=...,
183
+ description="SQL operator used for numeric comparison"
184
+ )
185
+
186
+ class JoinNumericCondition(BaseJoinConditionField):
187
+ operator: Literal['=', '!=', '>', '<', '>=', '<=', 'IN', 'NOT IN'] = Field(
188
+ default=...,
189
+ description="SQL operator used for numeric comparison in a join condition"
190
+ )
191
+
192
+ class JoinStringCondition(BaseJoinConditionField):
193
+ operator: Literal['=', '!=', 'LIKE', 'IN', 'NOT IN'] = Field(
194
+ default=...,
195
+ description="SQL operator used for string comparison in a join condition"
196
+ )
197
+
198
+ class JoinBooleanCondition(BaseJoinConditionField):
199
+ operator: Literal['=', '!='] = Field(
200
+ default=...,
201
+ description="SQL operator used for boolean comparison in a join condition"
202
+ )
203
+
204
+ class JoinDateCondition(BaseJoinConditionField):
205
+ operator: Literal['=', '!=', '>', '<', '>=', '<='] = Field(
206
+ default=...,
207
+ description="SQL operator used for date/time comparison in a join condition"
208
+ )
209
+
210
+ class JoinBytesCondition(BaseJoinConditionField):
211
+ operator: Literal['=', '!=', 'IN', 'NOT IN'] = Field(
212
+ default=...,
213
+ description="SQL operator used for byte comparison in a join condition"
214
+ )