AIVLAD commited on
Commit
2179a0e
·
1 Parent(s): 6df794b

fix: replace ThreadPoolExecutor with SynchronousExecutor for SQLite compatibility

Browse files

- Implement SynchronousExecutor for same-thread execution
- Prevents SQLite database locking in Django test transactions
- Normalize all Decimal outputs to 2 decimal places using quantize()
- Update test expectations for consistent decimal formatting
- Fix average_cost_per_day calculation to use quantized decimals

SQLite database locking prevented ThreadPoolExecutor from working with
Django test transactions which require same-thread database access.

.coverage CHANGED
Binary files a/.coverage and b/.coverage differ
 
apps/profiles/__pycache__/repositories.cpython-312.pyc CHANGED
Binary files a/apps/profiles/__pycache__/repositories.cpython-312.pyc and b/apps/profiles/__pycache__/repositories.cpython-312.pyc differ
 
apps/profiles/__pycache__/views.cpython-312.pyc CHANGED
Binary files a/apps/profiles/__pycache__/views.cpython-312.pyc and b/apps/profiles/__pycache__/views.cpython-312.pyc differ
 
apps/profiles/repositories.py CHANGED
@@ -53,11 +53,12 @@ class DailyProgressRepository:
53
  record_count=Count("id"),
54
  )
55
 
56
- # Explicit None handling - no implicit defaults
 
57
  return {
58
- "total_feet": Decimal("0") if result["total_feet"] is None else result["total_feet"],
59
- "total_ice": Decimal("0") if result["total_ice"] is None else result["total_ice"],
60
- "total_cost": Decimal("0") if result["total_cost"] is None else result["total_cost"],
61
- "avg_feet": Decimal("0") if result["avg_feet"] is None else result["avg_feet"],
62
  "record_count": result["record_count"],
63
  }
 
53
  record_count=Count("id"),
54
  )
55
 
56
+ # Normalize all decimals to 2 decimal places
57
+ two_places = Decimal("0.01")
58
  return {
59
+ "total_feet": (Decimal("0") if result["total_feet"] is None else result["total_feet"]).quantize(two_places),
60
+ "total_ice": (Decimal("0") if result["total_ice"] is None else result["total_ice"]).quantize(two_places),
61
+ "total_cost": (Decimal("0") if result["total_cost"] is None else result["total_cost"]).quantize(two_places),
62
+ "avg_feet": (Decimal("0") if result["avg_feet"] is None else result["avg_feet"]).quantize(two_places),
63
  "record_count": result["record_count"],
64
  }
apps/profiles/services/__pycache__/aggregators.cpython-312.pyc CHANGED
Binary files a/apps/profiles/services/__pycache__/aggregators.cpython-312.pyc and b/apps/profiles/services/__pycache__/aggregators.cpython-312.pyc differ
 
apps/profiles/services/aggregators.py CHANGED
@@ -2,23 +2,45 @@
2
 
3
  from __future__ import annotations
4
 
5
- from concurrent.futures import ThreadPoolExecutor, as_completed
6
  from datetime import date, datetime
7
 
8
  from apps.profiles.repositories import DailyProgressRepository
9
 
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  class CostAggregatorService:
12
  """Service for cost calculations across multiple profiles.
13
 
14
- Uses ThreadPoolExecutor with single worker for serial execution
15
- to maintain proper resource cleanup semantics while ensuring
16
- SQLite database compatibility.
17
  """
18
 
19
  def __init__(self) -> None:
20
- """Initialize service with single-threaded executor."""
21
- self.executor = ThreadPoolExecutor(max_workers=1)
22
 
23
  def calculate_multi_profile_costs(
24
  self,
@@ -66,8 +88,8 @@ class CostAggregatorService:
66
  return results
67
 
68
  def shutdown(self) -> None:
69
- """Shutdown the thread pool executor."""
70
- self.executor.shutdown(wait=True)
71
 
72
  def _calculate_profile_cost(
73
  self,
 
2
 
3
  from __future__ import annotations
4
 
5
+ from concurrent.futures import Future, as_completed
6
  from datetime import date, datetime
7
 
8
  from apps.profiles.repositories import DailyProgressRepository
9
 
10
 
11
+ class SynchronousExecutor:
12
+ """Executor that runs tasks synchronously in the same thread.
13
+
14
+ Provides ThreadPoolExecutor-compatible interface for SQLite compatibility.
15
+ SQLite with Django test transactions requires same-thread execution.
16
+ """
17
+
18
+ def submit(self, fn, *args, **kwargs) -> Future[dict[str, int | str]]: # type: ignore[no-untyped-def]
19
+ """Submit a callable to be executed synchronously."""
20
+ future: Future[dict[str, int | str]] = Future()
21
+ try:
22
+ result = fn(*args, **kwargs)
23
+ future.set_result(result)
24
+ except Exception as e:
25
+ future.set_exception(e)
26
+ return future
27
+
28
+ def shutdown(self) -> None:
29
+ """Shutdown executor - synchronous execution requires no cleanup."""
30
+ return
31
+
32
+
33
  class CostAggregatorService:
34
  """Service for cost calculations across multiple profiles.
35
 
36
+ Uses synchronous executor for SQLite database compatibility.
37
+ Processes profiles serially in the same thread to avoid database
38
+ locking issues with SQLite transactions in Django tests.
39
  """
40
 
41
  def __init__(self) -> None:
42
+ """Initialize service with synchronous executor."""
43
+ self.executor = SynchronousExecutor()
44
 
45
  def calculate_multi_profile_costs(
46
  self,
 
88
  return results
89
 
90
  def shutdown(self) -> None:
91
+ """Shutdown the executor."""
92
+ self.executor.shutdown()
93
 
94
  def _calculate_profile_cost(
95
  self,
apps/profiles/views.py CHANGED
@@ -131,9 +131,9 @@ class ProfileViewSet(viewsets.ModelViewSet[Profile]):
131
  total_days = (end_date - start_date).days + 1
132
 
133
  # Calculate average cost per day
134
- total_cost = aggregates["total_cost"]
135
  record_count = aggregates["record_count"]
136
- average_cost_per_day = total_cost / record_count if record_count > 0 else Decimal("0")
137
 
138
  # Build daily breakdown - only include days with actual progress
139
  daily_breakdown = []
 
131
  total_days = (end_date - start_date).days + 1
132
 
133
  # Calculate average cost per day
134
+ total_cost = Decimal(str(aggregates["total_cost"]))
135
  record_count = aggregates["record_count"]
136
+ average_cost_per_day = (total_cost / record_count).quantize(Decimal("0.01")) if record_count > 0 else Decimal("0.00")
137
 
138
  # Build daily breakdown - only include days with actual progress
139
  daily_breakdown = []
config/settings/__pycache__/test.cpython-312.pyc CHANGED
Binary files a/config/settings/__pycache__/test.cpython-312.pyc and b/config/settings/__pycache__/test.cpython-312.pyc differ
 
tests/unit/__pycache__/test_aggregators.cpython-312-pytest-8.4.2.pyc CHANGED
Binary files a/tests/unit/__pycache__/test_aggregators.cpython-312-pytest-8.4.2.pyc and b/tests/unit/__pycache__/test_aggregators.cpython-312-pytest-8.4.2.pyc differ
 
tests/unit/test_aggregators.py CHANGED
@@ -92,6 +92,6 @@ class TestCostAggregatorService:
92
  aggregator.shutdown()
93
 
94
  assert len(results) == 1
95
- assert results[0]["total_feet_built"] == "0"
96
- assert results[0]["total_ice_cubic_yards"] == "0"
97
- assert results[0]["total_cost_gold_dragons"] == "0"
 
92
  aggregator.shutdown()
93
 
94
  assert len(results) == 1
95
+ assert results[0]["total_feet_built"] == "0.00"
96
+ assert results[0]["total_ice_cubic_yards"] == "0.00"
97
+ assert results[0]["total_cost_gold_dragons"] == "0.00"