edsl 0.1.54__py3-none-any.whl → 0.1.55__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.
Files changed (101) hide show
  1. edsl/__init__.py +8 -1
  2. edsl/__init__original.py +134 -0
  3. edsl/__version__.py +1 -1
  4. edsl/agents/agent.py +29 -0
  5. edsl/agents/agent_list.py +36 -1
  6. edsl/base/base_class.py +281 -151
  7. edsl/buckets/__init__.py +8 -3
  8. edsl/buckets/bucket_collection.py +9 -3
  9. edsl/buckets/model_buckets.py +4 -2
  10. edsl/buckets/token_bucket.py +2 -2
  11. edsl/buckets/token_bucket_client.py +5 -3
  12. edsl/caching/cache.py +131 -62
  13. edsl/caching/cache_entry.py +70 -58
  14. edsl/caching/sql_dict.py +17 -0
  15. edsl/cli.py +99 -0
  16. edsl/config/config_class.py +16 -0
  17. edsl/conversation/__init__.py +31 -0
  18. edsl/coop/coop.py +276 -242
  19. edsl/coop/coop_jobs_objects.py +59 -0
  20. edsl/coop/coop_objects.py +29 -0
  21. edsl/coop/coop_regular_objects.py +26 -0
  22. edsl/coop/utils.py +24 -19
  23. edsl/dataset/dataset.py +338 -101
  24. edsl/db_list/sqlite_list.py +349 -0
  25. edsl/inference_services/__init__.py +40 -5
  26. edsl/inference_services/exceptions.py +11 -0
  27. edsl/inference_services/services/anthropic_service.py +5 -2
  28. edsl/inference_services/services/aws_bedrock.py +6 -2
  29. edsl/inference_services/services/azure_ai.py +6 -2
  30. edsl/inference_services/services/google_service.py +3 -2
  31. edsl/inference_services/services/mistral_ai_service.py +6 -2
  32. edsl/inference_services/services/open_ai_service.py +6 -2
  33. edsl/inference_services/services/perplexity_service.py +6 -2
  34. edsl/inference_services/services/test_service.py +94 -5
  35. edsl/interviews/answering_function.py +167 -59
  36. edsl/interviews/interview.py +124 -72
  37. edsl/interviews/interview_task_manager.py +10 -0
  38. edsl/invigilators/invigilators.py +9 -0
  39. edsl/jobs/async_interview_runner.py +146 -104
  40. edsl/jobs/data_structures.py +6 -4
  41. edsl/jobs/decorators.py +61 -0
  42. edsl/jobs/fetch_invigilator.py +61 -18
  43. edsl/jobs/html_table_job_logger.py +14 -2
  44. edsl/jobs/jobs.py +180 -104
  45. edsl/jobs/jobs_component_constructor.py +2 -2
  46. edsl/jobs/jobs_interview_constructor.py +2 -0
  47. edsl/jobs/jobs_remote_inference_logger.py +4 -0
  48. edsl/jobs/jobs_runner_status.py +30 -25
  49. edsl/jobs/progress_bar_manager.py +79 -0
  50. edsl/jobs/remote_inference.py +35 -1
  51. edsl/key_management/key_lookup_builder.py +6 -1
  52. edsl/language_models/language_model.py +86 -6
  53. edsl/language_models/model.py +10 -3
  54. edsl/language_models/price_manager.py +45 -75
  55. edsl/language_models/registry.py +5 -0
  56. edsl/notebooks/notebook.py +77 -10
  57. edsl/questions/VALIDATION_README.md +134 -0
  58. edsl/questions/__init__.py +24 -1
  59. edsl/questions/exceptions.py +21 -0
  60. edsl/questions/question_dict.py +201 -16
  61. edsl/questions/question_multiple_choice_with_other.py +624 -0
  62. edsl/questions/question_registry.py +2 -1
  63. edsl/questions/templates/multiple_choice_with_other/__init__.py +0 -0
  64. edsl/questions/templates/multiple_choice_with_other/answering_instructions.jinja +15 -0
  65. edsl/questions/templates/multiple_choice_with_other/question_presentation.jinja +17 -0
  66. edsl/questions/validation_analysis.py +185 -0
  67. edsl/questions/validation_cli.py +131 -0
  68. edsl/questions/validation_html_report.py +404 -0
  69. edsl/questions/validation_logger.py +136 -0
  70. edsl/results/result.py +63 -16
  71. edsl/results/results.py +702 -171
  72. edsl/scenarios/construct_download_link.py +16 -3
  73. edsl/scenarios/directory_scanner.py +226 -226
  74. edsl/scenarios/file_methods.py +5 -0
  75. edsl/scenarios/file_store.py +117 -6
  76. edsl/scenarios/handlers/__init__.py +5 -1
  77. edsl/scenarios/handlers/mp4_file_store.py +104 -0
  78. edsl/scenarios/handlers/webm_file_store.py +104 -0
  79. edsl/scenarios/scenario.py +120 -101
  80. edsl/scenarios/scenario_list.py +800 -727
  81. edsl/scenarios/scenario_list_gc_test.py +146 -0
  82. edsl/scenarios/scenario_list_memory_test.py +214 -0
  83. edsl/scenarios/scenario_list_source_refactor.md +35 -0
  84. edsl/scenarios/scenario_selector.py +5 -4
  85. edsl/scenarios/scenario_source.py +1990 -0
  86. edsl/scenarios/tests/test_scenario_list_sources.py +52 -0
  87. edsl/surveys/survey.py +22 -0
  88. edsl/tasks/__init__.py +4 -2
  89. edsl/tasks/task_history.py +198 -36
  90. edsl/tests/scenarios/test_ScenarioSource.py +51 -0
  91. edsl/tests/scenarios/test_scenario_list_sources.py +51 -0
  92. edsl/utilities/__init__.py +2 -1
  93. edsl/utilities/decorators.py +121 -0
  94. edsl/utilities/memory_debugger.py +1010 -0
  95. {edsl-0.1.54.dist-info → edsl-0.1.55.dist-info}/METADATA +51 -76
  96. {edsl-0.1.54.dist-info → edsl-0.1.55.dist-info}/RECORD +99 -75
  97. edsl/jobs/jobs_runner_asyncio.py +0 -281
  98. edsl/language_models/unused/fake_openai_service.py +0 -60
  99. {edsl-0.1.54.dist-info → edsl-0.1.55.dist-info}/LICENSE +0 -0
  100. {edsl-0.1.54.dist-info → edsl-0.1.55.dist-info}/WHEEL +0 -0
  101. {edsl-0.1.54.dist-info → edsl-0.1.55.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,146 @@
1
+ """
2
+ Test script to investigate garbage collection behavior with ScenarioList.
3
+ """
4
+
5
+ import gc
6
+ import os
7
+ import psutil
8
+ import sys
9
+ import pickle
10
+ import tracemalloc
11
+ from typing import Dict, List, Any
12
+
13
+ def get_memory_usage() -> float:
14
+ """Get current memory usage in MB"""
15
+ process = psutil.Process(os.getpid())
16
+ memory_info = process.memory_info()
17
+ memory_mb = memory_info.rss / (1024 * 1024)
18
+ return memory_mb
19
+
20
+ def display_memory_usage(label: str) -> float:
21
+ """Display and return current memory usage."""
22
+ mem = get_memory_usage()
23
+ print(f"{label}: {mem:.2f} MB")
24
+ return mem
25
+
26
+ def test_pickle_gc() -> None:
27
+ """Test if pickle.dumps objects are properly garbage collected."""
28
+ display_memory_usage("Initial memory")
29
+
30
+ # Force garbage collection
31
+ gc.collect()
32
+ start_mem = display_memory_usage("Memory after initial gc")
33
+
34
+ # Test with a string of 1MB
35
+ text = "x" * (1024 * 1024) # 1MB string
36
+ display_memory_usage("Memory after creating 1MB string")
37
+
38
+ # Create 100 large dictionaries
39
+ data_list: List[Dict[str, Any]] = []
40
+ for i in range(100):
41
+ data = {"id": i, "text": text, "value": i * 100}
42
+ data_list.append(data)
43
+
44
+ gc.collect()
45
+ display_memory_usage("Memory after creating 100 dicts with shared string")
46
+
47
+ # Now pickle each object
48
+ print("\nPickling objects one at a time:")
49
+ for i, data in enumerate(data_list):
50
+ # Pickle the object and immediately delete the reference
51
+ serialized = pickle.dumps(data)
52
+ # Explicitly delete to help garbage collection
53
+ del serialized
54
+
55
+ # Every 10 items, force garbage collection and check memory
56
+ if i % 10 == 9:
57
+ gc.collect()
58
+ display_memory_usage(f"Memory after pickling {i+1} objects and gc")
59
+
60
+ # Final garbage collection
61
+ gc.collect()
62
+ end_mem = display_memory_usage("Memory after final gc")
63
+
64
+ print(f"\nTotal memory increase: {end_mem - start_mem:.2f} MB")
65
+
66
+ # Now test with tracemalloc to see exactly where memory is allocated
67
+ print("\nDetailed memory tracing with tracemalloc:")
68
+ tracemalloc.start()
69
+
70
+ # Get snapshot before
71
+ data = {"id": 1, "text": "x" * (1024 * 1024), "value": 100}
72
+ snapshot1 = tracemalloc.take_snapshot()
73
+
74
+ # Do 10 pickling operations
75
+ for i in range(10):
76
+ serialized = pickle.dumps(data)
77
+ del serialized
78
+
79
+ # Get snapshot after
80
+ snapshot2 = tracemalloc.take_snapshot()
81
+
82
+ # Compare and show top differences
83
+ top_stats = snapshot2.compare_to(snapshot1, 'lineno')
84
+ print("\nTop 10 memory allocations:")
85
+ for stat in top_stats[:10]:
86
+ print(f"{stat.traceback.format()[0]} - {stat.size_diff / 1024:.2f} KB")
87
+
88
+ tracemalloc.stop()
89
+
90
+ def test_sqlite_insert() -> None:
91
+ """Test SQLite insertion memory behavior."""
92
+ import sqlite3
93
+ import tempfile
94
+
95
+ display_memory_usage("Initial memory")
96
+
97
+ # Force garbage collection
98
+ gc.collect()
99
+ start_mem = display_memory_usage("Memory after initial gc")
100
+
101
+ # Create a temporary database
102
+ with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp:
103
+ db_path = tmp.name
104
+
105
+ # Connect to the database
106
+ conn = sqlite3.connect(db_path)
107
+ conn.execute("CREATE TABLE IF NOT EXISTS items (idx INTEGER, value BLOB)")
108
+
109
+ # Create a large string and dictionary
110
+ text = "x" * (1024 * 1024) # 1MB string
111
+ display_memory_usage("Memory after creating 1MB string")
112
+
113
+ # Insert 100 large pickled objects
114
+ print("\nInserting objects one at a time:")
115
+ for i in range(100):
116
+ data = {"id": i, "text": text, "value": i * 100}
117
+ serialized = pickle.dumps(data)
118
+
119
+ # Insert into database
120
+ conn.execute("INSERT INTO items (idx, value) VALUES (?, ?)", (i, serialized))
121
+
122
+ # Explicitly delete serialized data
123
+ del serialized
124
+
125
+ # Every 10 items, force garbage collection and check memory
126
+ if i % 10 == 9:
127
+ conn.commit() # Commit to ensure SQLite releases memory
128
+ gc.collect()
129
+ display_memory_usage(f"Memory after inserting {i+1} objects and gc")
130
+
131
+ # Close connection and clean up
132
+ conn.close()
133
+ os.unlink(db_path)
134
+
135
+ # Final garbage collection
136
+ gc.collect()
137
+ end_mem = display_memory_usage("Memory after final gc")
138
+
139
+ print(f"\nTotal memory increase: {end_mem - start_mem:.2f} MB")
140
+
141
+ if __name__ == "__main__":
142
+ print("===== Testing pickle garbage collection =====")
143
+ test_pickle_gc()
144
+
145
+ print("\n\n===== Testing SQLite insertion memory behavior =====")
146
+ test_sqlite_insert()
@@ -0,0 +1,214 @@
1
+ """
2
+ Memory usage test for ScenarioList with different data sizes.
3
+ """
4
+
5
+ import gc
6
+ import os
7
+ import psutil
8
+ import time
9
+ import json
10
+ from typing import Dict, List, Any, Tuple
11
+ import matplotlib.pyplot as plt
12
+
13
+ def get_memory_usage() -> float:
14
+ """Get current memory usage in MB"""
15
+ process = psutil.Process(os.getpid())
16
+ memory_info = process.memory_info()
17
+ memory_mb = memory_info.rss / (1024 * 1024)
18
+ return memory_mb
19
+
20
+ def log_memory(label: str) -> float:
21
+ """Log and return current memory usage."""
22
+ mem = get_memory_usage()
23
+ print(f"{label}: {mem:.2f} MB")
24
+ return mem
25
+
26
+ def run_memory_test(sizes: List[int], item_size_kb: int = 10) -> Dict[str, Dict[int, float]]:
27
+ """
28
+ Test memory usage for ScenarioList with different dataset sizes.
29
+
30
+ Args:
31
+ sizes: List of dataset sizes to test
32
+ item_size_kb: Size of text data in each scenario (in KB)
33
+
34
+ Returns:
35
+ Dictionary with memory usage metrics for each size
36
+ """
37
+ from edsl.scenarios import ScenarioList
38
+
39
+ results = {
40
+ "creation": {},
41
+ "filter": {},
42
+ "baseline": {},
43
+ "total": {}
44
+ }
45
+
46
+ for size in sizes:
47
+ print(f"\n{'='*50}")
48
+ print(f"Testing with {size} scenarios (each with {item_size_kb}KB text)")
49
+ print(f"{'='*50}")
50
+
51
+ # Force garbage collection before starting
52
+ gc.collect()
53
+ gc.collect()
54
+ time.sleep(1) # Give system time to stabilize
55
+
56
+ baseline_mem = log_memory(f"Baseline memory")
57
+ results["baseline"][size] = baseline_mem
58
+
59
+ # Create test data
60
+ text_size = item_size_kb * 1024 # Convert KB to bytes
61
+ text = "x" * text_size
62
+
63
+ # Create scenarios
64
+ print(f"Creating {size} scenarios...")
65
+ scenarios = []
66
+ for i in range(size):
67
+ scenarios.append({
68
+ "id": i,
69
+ "text": text,
70
+ "category": "A" if i % 2 == 0 else "B",
71
+ "value": i * 10
72
+ })
73
+
74
+ # Measure memory after creating raw data
75
+ after_raw_mem = log_memory("Memory after creating raw data")
76
+
77
+ # Create ScenarioList
78
+ print(f"Creating ScenarioList...")
79
+ start_time = time.time()
80
+ sl = ScenarioList(scenarios)
81
+ creation_time = time.time() - start_time
82
+
83
+ # Measure memory after creating ScenarioList
84
+ after_creation_mem = log_memory("Memory after creating ScenarioList")
85
+ creation_mem_diff = after_creation_mem - after_raw_mem
86
+ results["creation"][size] = creation_mem_diff
87
+ print(f"Creation memory increase: {creation_mem_diff:.2f} MB")
88
+ print(f"Creation time: {creation_time:.2f} seconds")
89
+
90
+ # Filter ScenarioList
91
+ print(f"Filtering ScenarioList...")
92
+ start_time = time.time()
93
+ filtered = sl.filter("id % 2 == 0")
94
+ filter_time = time.time() - start_time
95
+
96
+ # Measure memory after filtering
97
+ after_filter_mem = log_memory("Memory after filtering")
98
+ filter_mem_diff = after_filter_mem - after_creation_mem
99
+ results["filter"][size] = filter_mem_diff
100
+ results["total"][size] = after_filter_mem - baseline_mem
101
+ print(f"Filter memory increase: {filter_mem_diff:.2f} MB")
102
+ print(f"Filter time: {filter_time:.2f} seconds")
103
+ print(f"Total memory increase: {after_filter_mem - baseline_mem:.2f} MB")
104
+
105
+ # Clean up to prepare for next iteration
106
+ del scenarios
107
+ del sl
108
+ del filtered
109
+ gc.collect()
110
+ gc.collect()
111
+ time.sleep(1) # Give system time to stabilize
112
+
113
+ return results
114
+
115
+ def plot_results(results: Dict[str, Dict[int, float]],
116
+ output_path: str = "memory_usage_plot.png") -> None:
117
+ """
118
+ Plot memory usage results.
119
+
120
+ Args:
121
+ results: Dictionary with memory usage metrics
122
+ output_path: Path to save the plot
123
+ """
124
+ sizes = sorted(results["creation"].keys())
125
+
126
+ # Extract data for plotting
127
+ creation_memory = [results["creation"][size] for size in sizes]
128
+ filter_memory = [results["filter"][size] for size in sizes]
129
+ total_memory = [results["total"][size] for size in sizes]
130
+
131
+ # Create figure and axis
132
+ plt.figure(figsize=(12, 8))
133
+
134
+ # Plot memory usage
135
+ plt.subplot(2, 1, 1)
136
+ plt.plot(sizes, creation_memory, 'o-', label='ScenarioList Creation')
137
+ plt.plot(sizes, filter_memory, 's-', label='Filter Operation')
138
+ plt.plot(sizes, total_memory, '^-', label='Total Memory Usage')
139
+ plt.xlabel('Number of Scenarios')
140
+ plt.ylabel('Memory Usage (MB)')
141
+ plt.title('Memory Usage vs. Dataset Size')
142
+ plt.grid(True)
143
+ plt.legend()
144
+
145
+ # Plot memory usage per scenario
146
+ plt.subplot(2, 1, 2)
147
+ mem_per_scenario_creation = [mem/size for mem, size in zip(creation_memory, sizes)]
148
+ mem_per_scenario_filter = [mem/size for mem, size in zip(filter_memory, sizes)]
149
+ mem_per_scenario_total = [mem/size for mem, size in zip(total_memory, sizes)]
150
+
151
+ plt.plot(sizes, mem_per_scenario_creation, 'o-', label='Creation (per scenario)')
152
+ plt.plot(sizes, mem_per_scenario_filter, 's-', label='Filter (per scenario)')
153
+ plt.plot(sizes, mem_per_scenario_total, '^-', label='Total (per scenario)')
154
+ plt.xlabel('Number of Scenarios')
155
+ plt.ylabel('Memory Usage per Scenario (MB)')
156
+ plt.title('Memory Efficiency vs. Dataset Size')
157
+ plt.grid(True)
158
+ plt.legend()
159
+
160
+ # Set log scale for x-axis to better visualize the trend
161
+ plt.xscale('log')
162
+
163
+ plt.tight_layout()
164
+ plt.savefig(output_path)
165
+ print(f"Plot saved to {output_path}")
166
+
167
+ def main():
168
+ """Run memory tests and plot results."""
169
+ # Test with increasing dataset sizes
170
+ sizes = [100, 500, 1000, 2000, 5000, 10000]
171
+
172
+ # Each scenario will have a 10KB text field
173
+ item_size_kb = 10
174
+
175
+ # Create output directory if it doesn't exist
176
+ os.makedirs("benchmark_logs/memory_reports", exist_ok=True)
177
+
178
+ # Run tests
179
+ results = run_memory_test(sizes, item_size_kb)
180
+
181
+ # Save results as JSON
182
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
183
+ results_path = f"benchmark_logs/memory_reports/memory_test_results_{timestamp}.json"
184
+ with open(results_path, 'w') as f:
185
+ json.dump(results, f, indent=2)
186
+ print(f"\nResults saved to {results_path}")
187
+
188
+ # Plot results
189
+ plot_path = f"benchmark_logs/memory_reports/memory_usage_plot_{timestamp}.png"
190
+ plot_results(results, plot_path)
191
+
192
+
193
+ def test_scenario_list_memory():
194
+ """
195
+ Simple test function for pytest to run a small memory test.
196
+ This is a simplified version of the main benchmarking function.
197
+ """
198
+ # Use very small sample size for pytest
199
+ sizes = [10, 20]
200
+ item_size_kb = 1
201
+
202
+ # Run a minimal test
203
+ results = run_memory_test(sizes, item_size_kb)
204
+
205
+ # Verify we got results
206
+ assert isinstance(results, dict)
207
+ assert "creation" in results
208
+ assert "filter" in results
209
+ assert "total" in results
210
+
211
+ # No need to return anything for pytest
212
+
213
+ if __name__ == "__main__":
214
+ main()
@@ -0,0 +1,35 @@
1
+ # ScenarioList Source Refactoring Checklist
2
+
3
+ This document outlines the refactoring process to move the `from_X` methods from `ScenarioList` to child classes of `Source`.
4
+
5
+ ## Refactoring Process
6
+
7
+ For each source type, follow these steps:
8
+
9
+ 1. Create a new child class of `Source` in `scenario_source.py`
10
+ 2. Add a deprecated classmethod in `ScenarioList` that references the new source class
11
+ 3. Run pytest to confirm everything works correctly
12
+ 4. Move to the next source type
13
+
14
+ ## Source Types Checklist
15
+
16
+ - [x] `urls` - Already implemented as `URLSource`
17
+ - [x] `list` - Already implemented as `ListSource`
18
+ - [x] `directory` - Implemented as `DirectorySource`
19
+ - [x] `list_of_tuples` - Implemented as `TuplesSource`
20
+ - [x] `sqlite` - Implemented as `SQLiteSource`
21
+ - [x] `latex` - Implemented as `LaTeXSource`
22
+ - [x] `google_doc` - Implemented as `GoogleDocSource`
23
+ - [x] `pandas` - Implemented as `PandasSource`
24
+ - [x] `dta` - Implemented as `StataSource`
25
+ - [x] `wikipedia` - Implemented as `WikipediaSource`
26
+ - [x] `excel` - Implemented as `ExcelSource`
27
+ - [x] `google_sheet` - Implemented as `GoogleSheetSource`
28
+ - [x] `delimited_file` - Implemented as `DelimitedFileSource`
29
+ - [x] `csv` - Implemented as `CSVSource` (extending `DelimitedFileSource`)
30
+ - [x] `tsv` - Implemented as `TSVSource` (extending `DelimitedFileSource`)
31
+ - [ ] `dict` - Implement as `DictSource`
32
+ - [ ] `nested_dict` - Implement as `NestedDictSource`
33
+ - [x] `parquet` - Implemented as `ParquetSource`
34
+ - [x] `pdf` - Implemented as `PDFSource`
35
+ - [x] `pdf_to_image` - Implemented as `PDFImageSource`
@@ -131,10 +131,11 @@ class ScenarioSelector:
131
131
  f"No fields matched the given patterns: {patterns}. "
132
132
  f"Available fields are: {self.available_fields}"
133
133
  )
134
-
135
- return self.scenario_list.__class__(
136
- [scenario.select(fields_to_select) for scenario in self.scenario_list.data]
137
- )
134
+
135
+ new_sl = self.scenario_list.__class__(data=[], codebook=self.scenario_list.codebook)
136
+ for scenario in self.scenario_list:
137
+ new_sl.append(scenario.select(fields_to_select))
138
+ return new_sl
138
139
 
139
140
  def get_available_fields(self) -> list[str]:
140
141
  """