t-sql 4.4.2__tar.gz → 4.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {t_sql-4.4.2 → t_sql-4.5.0}/PKG-INFO +1 -1
- {t_sql-4.4.2 → t_sql-4.5.0}/pyproject.toml +1 -1
- {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_type_processor.py +10 -6
- {t_sql-4.4.2 → t_sql-4.5.0}/tsql/__init__.py +2 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/tsql/query_builder.py +15 -9
- t_sql-4.5.0/tsql/row.py +39 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/.dockerignore +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/.github/workflows/publish.yml +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/.github/workflows/test.yml +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/.gitignore +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/Dockerfile +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/LICENSE +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/README.md +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/compose.yaml +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/context7.json +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/pytest.ini +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_alembic_integration.py +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_asyncpg_integration.py +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_different_object_types.py +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_escaped.py +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_escaped_binary_hex.py +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_helper_functions.py +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_injection_edge_cases.py +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_injection_protection_validation.py +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_injections_for_escaped.py +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_mysql_integration.py +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_parameter_names.py +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_query_builder.py +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_sqlalchemy_integration.py +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_sqlite_integration.py +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_styles.py +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_tsql.py +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/tsql/styles.py +0 -0
- {t_sql-4.4.2 → t_sql-4.5.0}/tsql/type_processor.py +0 -0
|
@@ -291,12 +291,14 @@ def test_map_results_with_joins_and_aliases():
|
|
|
291
291
|
}
|
|
292
292
|
]
|
|
293
293
|
|
|
294
|
-
query.map_results(rows)
|
|
294
|
+
results = query.map_results(rows)
|
|
295
295
|
|
|
296
296
|
# Check aliased column was decrypted
|
|
297
|
-
assert
|
|
297
|
+
assert results[0]["social"] == "123-45-6789"
|
|
298
|
+
assert results[0].social == "123-45-6789" # Test attribute access
|
|
298
299
|
# Check joined table column was deserialized
|
|
299
|
-
assert
|
|
300
|
+
assert results[0]["metadata_col"] == {"key": "value"}
|
|
301
|
+
assert results[0].metadata_col == {"key": "value"} # Test attribute access
|
|
300
302
|
|
|
301
303
|
# Test with SELECT * (should process all columns from all tables)
|
|
302
304
|
query_star = User.select().join(Profile, on=User.id == Profile.user_id)
|
|
@@ -311,8 +313,10 @@ def test_map_results_with_joins_and_aliases():
|
|
|
311
313
|
}
|
|
312
314
|
]
|
|
313
315
|
|
|
314
|
-
query_star.map_results(rows_star)
|
|
316
|
+
results_star = query_star.map_results(rows_star)
|
|
315
317
|
|
|
316
318
|
# Both processors should be applied
|
|
317
|
-
assert
|
|
318
|
-
assert
|
|
319
|
+
assert results_star[0]["ssn"] == "987-65-4321"
|
|
320
|
+
assert results_star[0].ssn == "987-65-4321" # Test attribute access
|
|
321
|
+
assert results_star[0]["metadata_col"] == {"foo": "bar"}
|
|
322
|
+
assert results_star[0].metadata_col == {"foo": "bar"} # Test attribute access
|
|
@@ -362,6 +362,7 @@ def delete(table: str, id: str | int) -> TSQL:
|
|
|
362
362
|
|
|
363
363
|
from tsql.query_builder import UnsafeQueryError
|
|
364
364
|
from tsql.type_processor import TypeProcessor
|
|
365
|
+
from tsql.row import Row
|
|
365
366
|
|
|
366
367
|
__all__ = [
|
|
367
368
|
'TSQL',
|
|
@@ -375,5 +376,6 @@ __all__ = [
|
|
|
375
376
|
'set_style',
|
|
376
377
|
'UnsafeQueryError',
|
|
377
378
|
'TypeProcessor',
|
|
379
|
+
'Row',
|
|
378
380
|
]
|
|
379
381
|
|
|
@@ -4,6 +4,7 @@ from datetime import datetime
|
|
|
4
4
|
from abc import ABC, abstractmethod
|
|
5
5
|
|
|
6
6
|
from tsql import TSQL, t_join
|
|
7
|
+
from tsql.row import Row
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class UnsafeQueryError(Exception):
|
|
@@ -559,26 +560,28 @@ class QueryBuilder(ABC):
|
|
|
559
560
|
"""
|
|
560
561
|
return self.to_tsql().render(style)
|
|
561
562
|
|
|
562
|
-
def map_results(self, rows: List[Dict[str, Any]]) -> List[
|
|
563
|
+
def map_results(self, rows: List[Dict[str, Any]]) -> List[Row]:
|
|
563
564
|
"""Transform database rows with type processors applied.
|
|
564
565
|
|
|
565
566
|
This method applies process_result_value from type processors to convert
|
|
566
567
|
database values back to Python values (e.g., decrypt encrypted fields,
|
|
567
568
|
deserialize JSON, etc.).
|
|
568
569
|
|
|
569
|
-
|
|
570
|
-
(
|
|
570
|
+
Returns Row objects (dict subclass with attribute access) for consistent
|
|
571
|
+
API regardless of input type (dict, asyncpg Record, etc.).
|
|
571
572
|
|
|
572
573
|
Args:
|
|
573
574
|
rows: List of row objects from database query results
|
|
574
575
|
|
|
575
576
|
Returns:
|
|
576
|
-
|
|
577
|
+
List of Row objects with transformed values
|
|
577
578
|
|
|
578
579
|
Example:
|
|
579
580
|
query = User.select().where(User.id == 1)
|
|
580
581
|
rows = await conn.fetch(*query.render())
|
|
581
|
-
query.map_results(rows)
|
|
582
|
+
results = query.map_results(rows)
|
|
583
|
+
print(results[0].name) # Attribute access
|
|
584
|
+
print(results[0]['name']) # Dict access
|
|
582
585
|
"""
|
|
583
586
|
if not hasattr(self, 'base_table'):
|
|
584
587
|
raise AttributeError("map_results requires a base_table attribute")
|
|
@@ -602,13 +605,16 @@ class QueryBuilder(ABC):
|
|
|
602
605
|
for table in tables:
|
|
603
606
|
processors.update(table._type_processors)
|
|
604
607
|
|
|
605
|
-
#
|
|
608
|
+
# Convert to Row objects and apply processors
|
|
609
|
+
results = []
|
|
606
610
|
for row in rows:
|
|
607
|
-
|
|
611
|
+
row_dict = Row(row) # Converts dict/Record to Row
|
|
612
|
+
for col_name in list(row_dict.keys()):
|
|
608
613
|
if col_name in processors:
|
|
609
|
-
|
|
614
|
+
row_dict[col_name] = processors[col_name].process_result_value(row_dict[col_name])
|
|
615
|
+
results.append(row_dict)
|
|
610
616
|
|
|
611
|
-
return
|
|
617
|
+
return results
|
|
612
618
|
|
|
613
619
|
|
|
614
620
|
class InsertBuilder(QueryBuilder):
|
t_sql-4.5.0/tsql/row.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Row object - dict with attribute access support."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Row(dict):
|
|
5
|
+
"""A dict subclass that supports attribute-style access.
|
|
6
|
+
|
|
7
|
+
Provides a nicer API for accessing row data while maintaining
|
|
8
|
+
full dict compatibility.
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
row = Row({'id': 1, 'name': 'Alice'})
|
|
12
|
+
print(row.id) # 1 (attribute access)
|
|
13
|
+
print(row['name']) # Alice (dict access)
|
|
14
|
+
row.age = 30 # Set via attribute
|
|
15
|
+
print(row['age']) # 30
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __getattr__(self, key: str):
|
|
19
|
+
"""Allow attribute-style access to dict keys."""
|
|
20
|
+
try:
|
|
21
|
+
return self[key]
|
|
22
|
+
except KeyError:
|
|
23
|
+
raise AttributeError(f"Row has no attribute {key!r}") from None
|
|
24
|
+
|
|
25
|
+
def __setattr__(self, key: str, value):
|
|
26
|
+
"""Allow attribute-style setting of dict keys."""
|
|
27
|
+
self[key] = value
|
|
28
|
+
|
|
29
|
+
def __delattr__(self, key: str):
|
|
30
|
+
"""Allow attribute-style deletion of dict keys."""
|
|
31
|
+
try:
|
|
32
|
+
del self[key]
|
|
33
|
+
except KeyError:
|
|
34
|
+
raise AttributeError(f"Row has no attribute {key!r}") from None
|
|
35
|
+
|
|
36
|
+
def __repr__(self) -> str:
|
|
37
|
+
"""Nice repr for debugging."""
|
|
38
|
+
items = ', '.join(f'{k}={v!r}' for k, v in self.items())
|
|
39
|
+
return f"Row({items})"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|