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.
Files changed (34) hide show
  1. {t_sql-4.4.2 → t_sql-4.5.0}/PKG-INFO +1 -1
  2. {t_sql-4.4.2 → t_sql-4.5.0}/pyproject.toml +1 -1
  3. {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_type_processor.py +10 -6
  4. {t_sql-4.4.2 → t_sql-4.5.0}/tsql/__init__.py +2 -0
  5. {t_sql-4.4.2 → t_sql-4.5.0}/tsql/query_builder.py +15 -9
  6. t_sql-4.5.0/tsql/row.py +39 -0
  7. {t_sql-4.4.2 → t_sql-4.5.0}/.dockerignore +0 -0
  8. {t_sql-4.4.2 → t_sql-4.5.0}/.github/workflows/publish.yml +0 -0
  9. {t_sql-4.4.2 → t_sql-4.5.0}/.github/workflows/test.yml +0 -0
  10. {t_sql-4.4.2 → t_sql-4.5.0}/.gitignore +0 -0
  11. {t_sql-4.4.2 → t_sql-4.5.0}/Dockerfile +0 -0
  12. {t_sql-4.4.2 → t_sql-4.5.0}/LICENSE +0 -0
  13. {t_sql-4.4.2 → t_sql-4.5.0}/README.md +0 -0
  14. {t_sql-4.4.2 → t_sql-4.5.0}/compose.yaml +0 -0
  15. {t_sql-4.4.2 → t_sql-4.5.0}/context7.json +0 -0
  16. {t_sql-4.4.2 → t_sql-4.5.0}/pytest.ini +0 -0
  17. {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_alembic_integration.py +0 -0
  18. {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_asyncpg_integration.py +0 -0
  19. {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_different_object_types.py +0 -0
  20. {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_escaped.py +0 -0
  21. {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_escaped_binary_hex.py +0 -0
  22. {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_helper_functions.py +0 -0
  23. {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_injection_edge_cases.py +0 -0
  24. {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_injection_protection_validation.py +0 -0
  25. {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_injections_for_escaped.py +0 -0
  26. {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_mysql_integration.py +0 -0
  27. {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_parameter_names.py +0 -0
  28. {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_query_builder.py +0 -0
  29. {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_sqlalchemy_integration.py +0 -0
  30. {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_sqlite_integration.py +0 -0
  31. {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_styles.py +0 -0
  32. {t_sql-4.4.2 → t_sql-4.5.0}/tests/test_tsql.py +0 -0
  33. {t_sql-4.4.2 → t_sql-4.5.0}/tsql/styles.py +0 -0
  34. {t_sql-4.4.2 → t_sql-4.5.0}/tsql/type_processor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: t-sql
3
- Version: 4.4.2
3
+ Version: 4.5.0
4
4
  Summary: Safe SQL. SQL queries for python t-strings (PEP 750)
5
5
  Project-URL: Homepage, https://github.com/nhumrich/t-sql
6
6
  License-File: LICENSE
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "t-sql"
7
- version = "4.4.2"
7
+ version = "4.5.0"
8
8
  description = "Safe SQL. SQL queries for python t-strings (PEP 750)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.14"
@@ -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 rows[0]["social"] == "123-45-6789"
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 rows[0]["metadata_col"] == {"key": "value"}
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 rows_star[0]["ssn"] == "987-65-4321"
318
- assert rows_star[0]["metadata_col"] == {"foo": "bar"}
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[Dict[str, Any]]:
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
- Modifies row objects in place to preserve driver-specific row types
570
- (e.g., asyncpg Record objects).
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
- The same list of rows with transformed values (mutated in place)
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) # ssn decrypted, metadata deserialized (in place)
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
- # Apply processors to rows in place
608
+ # Convert to Row objects and apply processors
609
+ results = []
606
610
  for row in rows:
607
- for col_name in list(row.keys()):
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
- row[col_name] = processors[col_name].process_result_value(row[col_name])
614
+ row_dict[col_name] = processors[col_name].process_result_value(row_dict[col_name])
615
+ results.append(row_dict)
610
616
 
611
- return rows
617
+ return results
612
618
 
613
619
 
614
620
  class InsertBuilder(QueryBuilder):
@@ -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