edsl 0.1.27.dev2__tar.gz → 0.1.29__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 (220) hide show
  1. {edsl-0.1.27.dev2 → edsl-0.1.29}/PKG-INFO +42 -15
  2. edsl-0.1.29/README.md +55 -0
  3. edsl-0.1.29/edsl/Base.py +288 -0
  4. edsl-0.1.29/edsl/BaseDiff.py +260 -0
  5. edsl-0.1.29/edsl/__init__.py +42 -0
  6. edsl-0.1.29/edsl/__version__.py +1 -0
  7. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/agents/Agent.py +103 -46
  8. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/agents/AgentList.py +97 -13
  9. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/agents/Invigilator.py +23 -10
  10. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/agents/InvigilatorBase.py +19 -14
  11. edsl-0.1.29/edsl/agents/PromptConstructionMixin.py +376 -0
  12. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/agents/descriptors.py +5 -2
  13. {edsl-0.1.27.dev2/edsl → edsl-0.1.29/edsl/base}/Base.py +101 -23
  14. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/config.py +2 -1
  15. edsl-0.1.29/edsl/conjure/AgentConstructionMixin.py +152 -0
  16. edsl-0.1.29/edsl/conjure/Conjure.py +56 -0
  17. edsl-0.1.29/edsl/conjure/InputData.py +659 -0
  18. edsl-0.1.29/edsl/conjure/InputDataCSV.py +48 -0
  19. edsl-0.1.29/edsl/conjure/InputDataMixinQuestionStats.py +182 -0
  20. edsl-0.1.29/edsl/conjure/InputDataPyRead.py +91 -0
  21. edsl-0.1.29/edsl/conjure/InputDataSPSS.py +8 -0
  22. edsl-0.1.29/edsl/conjure/InputDataStata.py +8 -0
  23. edsl-0.1.29/edsl/conjure/QuestionOptionMixin.py +76 -0
  24. edsl-0.1.29/edsl/conjure/QuestionTypeMixin.py +23 -0
  25. edsl-0.1.29/edsl/conjure/RawQuestion.py +65 -0
  26. edsl-0.1.29/edsl/conjure/SurveyResponses.py +7 -0
  27. edsl-0.1.29/edsl/conjure/__init__.py +9 -0
  28. edsl-0.1.29/edsl/conjure/naming_utilities.py +263 -0
  29. edsl-0.1.29/edsl/conjure/utilities.py +201 -0
  30. edsl-0.1.29/edsl/conversation/Conversation.py +238 -0
  31. edsl-0.1.29/edsl/conversation/car_buying.py +58 -0
  32. edsl-0.1.29/edsl/conversation/mug_negotiation.py +81 -0
  33. edsl-0.1.29/edsl/conversation/next_speaker_utilities.py +93 -0
  34. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/coop/coop.py +337 -121
  35. edsl-0.1.29/edsl/coop/utils.py +123 -0
  36. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/data/Cache.py +74 -22
  37. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/data/CacheHandler.py +10 -9
  38. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/data/SQLiteDict.py +11 -3
  39. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/inference_services/AnthropicService.py +1 -0
  40. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/inference_services/DeepInfraService.py +20 -13
  41. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/inference_services/GoogleService.py +7 -1
  42. edsl-0.1.29/edsl/inference_services/InferenceServicesCollection.py +60 -0
  43. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/inference_services/OpenAIService.py +17 -10
  44. edsl-0.1.29/edsl/inference_services/models_available_cache.py +69 -0
  45. edsl-0.1.29/edsl/inference_services/rate_limits_cache.py +25 -0
  46. edsl-0.1.29/edsl/inference_services/write_available.py +10 -0
  47. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/Answers.py +15 -1
  48. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/Jobs.py +322 -73
  49. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/buckets/BucketCollection.py +9 -3
  50. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/buckets/ModelBuckets.py +4 -2
  51. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/buckets/TokenBucket.py +1 -2
  52. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/interviews/Interview.py +7 -10
  53. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/interviews/InterviewStatusMixin.py +3 -3
  54. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/interviews/InterviewTaskBuildingMixin.py +39 -20
  55. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/interviews/retry_management.py +4 -4
  56. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/runners/JobsRunnerAsyncio.py +103 -65
  57. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/runners/JobsRunnerStatusData.py +3 -3
  58. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/tasks/QuestionTaskCreator.py +4 -2
  59. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/tasks/TaskHistory.py +4 -3
  60. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/language_models/LanguageModel.py +42 -55
  61. edsl-0.1.29/edsl/language_models/ModelList.py +96 -0
  62. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/language_models/registry.py +14 -0
  63. edsl-0.1.29/edsl/language_models/repair.py +175 -0
  64. edsl-0.1.29/edsl/notebooks/Notebook.py +241 -0
  65. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/prompts/Prompt.py +31 -19
  66. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/questions/QuestionBase.py +145 -23
  67. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/questions/QuestionBudget.py +5 -6
  68. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/questions/QuestionCheckBox.py +7 -3
  69. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/questions/QuestionExtract.py +5 -3
  70. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/questions/QuestionFreeText.py +3 -3
  71. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/questions/QuestionFunctional.py +0 -3
  72. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/questions/QuestionList.py +3 -4
  73. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/questions/QuestionMultipleChoice.py +16 -8
  74. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/questions/QuestionNumerical.py +4 -3
  75. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/questions/QuestionRank.py +5 -3
  76. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/questions/__init__.py +4 -3
  77. edsl-0.1.29/edsl/questions/derived/__init__.py +0 -0
  78. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/questions/descriptors.py +9 -4
  79. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/questions/question_registry.py +27 -31
  80. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/questions/settings.py +1 -1
  81. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/results/Dataset.py +31 -0
  82. edsl-0.1.27.dev2/edsl/results/ResultsExportMixin.py → edsl-0.1.29/edsl/results/DatasetExportMixin.py +70 -104
  83. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/results/Result.py +42 -82
  84. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/results/Results.py +178 -66
  85. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/results/ResultsDBMixin.py +10 -9
  86. edsl-0.1.29/edsl/results/ResultsExportMixin.py +43 -0
  87. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/results/ResultsGGMixin.py +3 -3
  88. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/results/ResultsToolsMixin.py +9 -9
  89. edsl-0.1.29/edsl/scenarios/FileStore.py +140 -0
  90. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/scenarios/Scenario.py +59 -6
  91. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/scenarios/ScenarioList.py +138 -52
  92. edsl-0.1.29/edsl/scenarios/ScenarioListExportMixin.py +32 -0
  93. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/scenarios/ScenarioListPdfMixin.py +2 -1
  94. edsl-0.1.29/edsl/scenarios/__init__.py +2 -0
  95. edsl-0.1.29/edsl/study/ObjectEntry.py +173 -0
  96. edsl-0.1.29/edsl/study/ProofOfWork.py +113 -0
  97. edsl-0.1.29/edsl/study/SnapShot.py +73 -0
  98. edsl-0.1.29/edsl/study/Study.py +498 -0
  99. edsl-0.1.29/edsl/study/__init__.py +4 -0
  100. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/surveys/MemoryPlan.py +11 -4
  101. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/surveys/Survey.py +124 -37
  102. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/surveys/SurveyExportMixin.py +25 -5
  103. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/surveys/SurveyFlowVisualizationMixin.py +6 -4
  104. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/tools/plotting.py +4 -2
  105. edsl-0.1.29/edsl/utilities/__init__.py +22 -0
  106. edsl-0.1.29/edsl/utilities/gcp_bucket/__init__.py +0 -0
  107. edsl-0.1.29/edsl/utilities/gcp_bucket/cloud_storage.py +96 -0
  108. edsl-0.1.29/edsl/utilities/gcp_bucket/simple_example.py +9 -0
  109. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/utilities/interface.py +90 -73
  110. edsl-0.1.29/edsl/utilities/repair_functions.py +28 -0
  111. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/utilities/utilities.py +59 -6
  112. {edsl-0.1.27.dev2 → edsl-0.1.29}/pyproject.toml +3 -1
  113. edsl-0.1.27.dev2/README.md +0 -29
  114. edsl-0.1.27.dev2/edsl/__init__.py +0 -38
  115. edsl-0.1.27.dev2/edsl/__version__.py +0 -1
  116. edsl-0.1.27.dev2/edsl/agents/PromptConstructionMixin.py +0 -134
  117. edsl-0.1.27.dev2/edsl/conjure/RawResponseColumn.py +0 -327
  118. edsl-0.1.27.dev2/edsl/conjure/SurveyBuilder.py +0 -308
  119. edsl-0.1.27.dev2/edsl/conjure/SurveyBuilderCSV.py +0 -78
  120. edsl-0.1.27.dev2/edsl/conjure/SurveyBuilderSPSS.py +0 -118
  121. edsl-0.1.27.dev2/edsl/conjure/__init__.py +0 -4
  122. edsl-0.1.27.dev2/edsl/conjure/utilities.py +0 -64
  123. edsl-0.1.27.dev2/edsl/coop/utils.py +0 -137
  124. edsl-0.1.27.dev2/edsl/data/RemoteDict.py +0 -103
  125. edsl-0.1.27.dev2/edsl/inference_services/InferenceServicesCollection.py +0 -34
  126. edsl-0.1.27.dev2/edsl/language_models/repair.py +0 -103
  127. edsl-0.1.27.dev2/edsl/notebooks/Notebook.py +0 -116
  128. edsl-0.1.27.dev2/edsl/scenarios/__init__.py +0 -1
  129. edsl-0.1.27.dev2/edsl/utilities/__init__.py +0 -21
  130. {edsl-0.1.27.dev2 → edsl-0.1.29}/LICENSE +0 -0
  131. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/agents/__init__.py +0 -0
  132. /edsl-0.1.27.dev2/edsl/inference_services/__init__.py → /edsl-0.1.29/edsl/conjure/examples/placeholder.txt +0 -0
  133. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/coop/__init__.py +0 -0
  134. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/data/CacheEntry.py +0 -0
  135. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/data/__init__.py +0 -0
  136. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/data/orm.py +0 -0
  137. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/data_transfer_models.py +0 -0
  138. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/enums.py +0 -0
  139. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/exceptions/__init__.py +0 -0
  140. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/exceptions/agents.py +0 -0
  141. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/exceptions/configuration.py +0 -0
  142. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/exceptions/coop.py +0 -0
  143. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/exceptions/data.py +0 -0
  144. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/exceptions/general.py +0 -0
  145. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/exceptions/jobs.py +0 -0
  146. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/exceptions/language_models.py +0 -0
  147. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/exceptions/prompts.py +0 -0
  148. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/exceptions/questions.py +0 -0
  149. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/exceptions/results.py +0 -0
  150. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/exceptions/surveys.py +0 -0
  151. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/inference_services/InferenceServiceABC.py +0 -0
  152. {edsl-0.1.27.dev2/edsl/questions/derived → edsl-0.1.29/edsl/inference_services}/__init__.py +0 -0
  153. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/inference_services/registry.py +0 -0
  154. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/__init__.py +0 -0
  155. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/interviews/InterviewStatistic.py +0 -0
  156. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/interviews/InterviewStatisticsCollection.py +0 -0
  157. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/interviews/InterviewStatusDictionary.py +0 -0
  158. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/interviews/InterviewStatusLog.py +0 -0
  159. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/interviews/ReportErrors.py +0 -0
  160. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/interviews/interview_exception_tracking.py +0 -0
  161. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/interviews/interview_status_enum.py +0 -0
  162. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/runners/JobsRunnerStatusMixin.py +0 -0
  163. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/tasks/TaskCreators.py +0 -0
  164. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/tasks/TaskStatusLog.py +0 -0
  165. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/tasks/task_management.py +0 -0
  166. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/tasks/task_status_enum.py +0 -0
  167. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/tokens/InterviewTokenUsage.py +0 -0
  168. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/jobs/tokens/TokenUsage.py +0 -0
  169. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/language_models/RegisterLanguageModelsMeta.py +0 -0
  170. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/language_models/__init__.py +0 -0
  171. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/language_models/unused/ReplicateBase.py +0 -0
  172. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/notebooks/__init__.py +0 -0
  173. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/prompts/QuestionInstructionsBase.py +0 -0
  174. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/prompts/__init__.py +0 -0
  175. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/prompts/library/agent_instructions.py +0 -0
  176. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/prompts/library/agent_persona.py +0 -0
  177. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/prompts/library/question_budget.py +0 -0
  178. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/prompts/library/question_checkbox.py +0 -0
  179. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/prompts/library/question_extract.py +0 -0
  180. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/prompts/library/question_freetext.py +0 -0
  181. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/prompts/library/question_linear_scale.py +0 -0
  182. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/prompts/library/question_list.py +0 -0
  183. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/prompts/library/question_multiple_choice.py +0 -0
  184. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/prompts/library/question_numerical.py +0 -0
  185. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/prompts/library/question_rank.py +0 -0
  186. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/prompts/prompt_config.py +0 -0
  187. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/prompts/registry.py +0 -0
  188. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/questions/AnswerValidatorMixin.py +0 -0
  189. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/questions/RegisterQuestionsMeta.py +0 -0
  190. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/questions/SimpleAskMixin.py +0 -0
  191. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/questions/compose_questions.py +0 -0
  192. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/questions/derived/QuestionLikertFive.py +0 -0
  193. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/questions/derived/QuestionLinearScale.py +0 -0
  194. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/questions/derived/QuestionTopK.py +0 -0
  195. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/questions/derived/QuestionYesNo.py +0 -0
  196. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/results/ResultsFetchMixin.py +0 -0
  197. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/results/__init__.py +0 -0
  198. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/scenarios/ScenarioHtmlMixin.py +0 -0
  199. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/scenarios/ScenarioImageMixin.py +0 -0
  200. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/shared.py +0 -0
  201. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/surveys/DAG.py +0 -0
  202. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/surveys/Memory.py +0 -0
  203. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/surveys/Rule.py +0 -0
  204. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/surveys/RuleCollection.py +0 -0
  205. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/surveys/SurveyCSS.py +0 -0
  206. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/surveys/__init__.py +0 -0
  207. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/surveys/base.py +0 -0
  208. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/surveys/descriptors.py +0 -0
  209. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/tools/__init__.py +0 -0
  210. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/tools/clusters.py +0 -0
  211. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/tools/embeddings.py +0 -0
  212. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/tools/embeddings_plotting.py +0 -0
  213. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/tools/summarize.py +0 -0
  214. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/utilities/SystemInfo.py +0 -0
  215. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/utilities/ast_utilities.py +0 -0
  216. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/utilities/data/Registry.py +0 -0
  217. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/utilities/data/__init__.py +0 -0
  218. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/utilities/data/scooter_results.json +0 -0
  219. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/utilities/decorators.py +0 -0
  220. {edsl-0.1.27.dev2 → edsl-0.1.29}/edsl/utilities/restricted_python.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: edsl
3
- Version: 0.1.27.dev2
3
+ Version: 0.1.29
4
4
  Summary: Create and analyze LLM-based surveys
5
5
  Home-page: https://www.expectedparrot.com/
6
6
  License: MIT
@@ -30,6 +30,7 @@ Requires-Dist: pandas (>=2.1.4,<3.0.0)
30
30
  Requires-Dist: pydot (>=2.0.0,<3.0.0)
31
31
  Requires-Dist: pygments (>=2.17.2,<3.0.0)
32
32
  Requires-Dist: pymupdf (>=1.24.4,<2.0.0)
33
+ Requires-Dist: pyreadstat (>=1.2.7,<2.0.0)
33
34
  Requires-Dist: python-docx (>=1.1.0,<2.0.0)
34
35
  Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
35
36
  Requires-Dist: restrictedpython (>=7.1,<8.0)
@@ -45,28 +46,54 @@ Description-Content-Type: text/markdown
45
46
  <img src="https://github.com/expectedparrot/edsl/blob/main/static/logo.png?raw=true" alt="edsl.png" width="100"/>
46
47
  </p>
47
48
 
48
- The Expected Parrot Domain-Specific Language (EDSL) package lets you conduct computational social science and market research with AI. Use it to design surveys and experiments, simulate responses with large language models, and perform data labeling and other research tasks. EDSL comes with built-in methods for analyzing, visualizing and sharing your results.
49
+ The Expected Parrot Domain-Specific Language (EDSL) package lets you conduct computational social science and market research with AI. Use it to design surveys and experiments, simulate responses with large language models, and perform data labeling and other research tasks. Results are formatted as specified datasets and come with built-in methods for analyzing, visualizing, and sharing.
49
50
 
50
51
  ## 🔗 Links
51
- - PyPI: https://pypi.org/project/edsl/
52
- - Documentation: https://docs.expectedparrot.com
53
- - Getting started: https://docs.expectedparrot.com/en/latest/starter_tutorial.html
54
- - Discord: https://discord.com/invite/mxAYkjfy9m
52
+ - [PyPI](https://pypi.org/project/edsl/)
53
+ - [Documentation](https://docs.expectedparrot.com)
54
+ - [Getting started](https://docs.expectedparrot.com/en/latest/starter_tutorial.html)
55
+ - [Discord](https://discord.com/invite/mxAYkjfy9m)
56
+ - [Twitter](https://x.com/ExpectedParrot)
57
+ - [LinkedIn](https://www.linkedin.com/company/expectedparrot/)
58
+ - [Blog](https://blog.expectedparrot.com)
55
59
 
60
+ ## 🌎 Hello, World!
61
+ A quick example:
56
62
 
57
- ## 💡 Contributions, Feature Requests & Bugs
58
- Interested in contributing? Want us to add a new feature? Found a nasty bug that you would like us to squash? Please send us an email at info@expectedparrot.com or message us at our Discord server.
63
+ ```python
64
+ # Import a question type
65
+ from edsl.questions import QuestionMultipleChoice
59
66
 
67
+ # Construct a question using the question type template
68
+ q = QuestionMultipleChoice(
69
+ question_name="example_question",
70
+ question_text="How do you feel today?",
71
+ question_options=["Bad", "OK", "Good"]
72
+ )
60
73
 
61
- ## 💻 Getting started
62
- EDSL is compatible with Python 3.9 - 3.12.
74
+ # Run it with the default language model
75
+ results = q.run()
76
+
77
+ # Inspect the results in a dataset
78
+ results.select("example_question").print()
63
79
  ```
64
- pip install edsl
80
+
81
+ Output:
82
+ ```python
83
+ ┏━━━━━━━━━━━━━━━━━━━┓
84
+ ┃ answer ┃
85
+ ┃ .example_question ┃
86
+ ┡━━━━━━━━━━━━━━━━━━━┩
87
+ │ Good │
88
+ └───────────────────┘
65
89
  ```
66
90
 
67
- - Read the docs at http://docs.expectedparrot.com.
68
- - See https://docs.expectedparrot.com/en/latest/starter_tutorial.html for examples and tutorials.
91
+ ## 💻 Requirements
92
+ * EDSL is compatible with Python 3.9 - 3.12.
93
+ * API keys for large language models that you want to use, stored in a `.env` file.
94
+ See instructions on [storing API keys](https://docs.expectedparrot.com/en/latest/api_keys.html).
69
95
 
70
- ## 🔧 Dependencies
71
- API keys for LLMs that you want to use, stored in a `.env` file
96
+ ## 💡 Contributions, feature requests & bugs
97
+ Interested in contributing? Want us to add a new feature? Found a bug for us to squash?
98
+ Please send us an email at [info@expectedparrot.com](mailto:info@expectedparrot.com) or message us at our [Discord channel](https://discord.com/invite/mxAYkjfy9m).
72
99
 
edsl-0.1.29/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # Expected Parrot Domain-Specific Language
2
+ <p align="center">
3
+ <img src="https://github.com/expectedparrot/edsl/blob/main/static/logo.png?raw=true" alt="edsl.png" width="100"/>
4
+ </p>
5
+
6
+ The Expected Parrot Domain-Specific Language (EDSL) package lets you conduct computational social science and market research with AI. Use it to design surveys and experiments, simulate responses with large language models, and perform data labeling and other research tasks. Results are formatted as specified datasets and come with built-in methods for analyzing, visualizing, and sharing.
7
+
8
+ ## 🔗 Links
9
+ - [PyPI](https://pypi.org/project/edsl/)
10
+ - [Documentation](https://docs.expectedparrot.com)
11
+ - [Getting started](https://docs.expectedparrot.com/en/latest/starter_tutorial.html)
12
+ - [Discord](https://discord.com/invite/mxAYkjfy9m)
13
+ - [Twitter](https://x.com/ExpectedParrot)
14
+ - [LinkedIn](https://www.linkedin.com/company/expectedparrot/)
15
+ - [Blog](https://blog.expectedparrot.com)
16
+
17
+ ## 🌎 Hello, World!
18
+ A quick example:
19
+
20
+ ```python
21
+ # Import a question type
22
+ from edsl.questions import QuestionMultipleChoice
23
+
24
+ # Construct a question using the question type template
25
+ q = QuestionMultipleChoice(
26
+ question_name="example_question",
27
+ question_text="How do you feel today?",
28
+ question_options=["Bad", "OK", "Good"]
29
+ )
30
+
31
+ # Run it with the default language model
32
+ results = q.run()
33
+
34
+ # Inspect the results in a dataset
35
+ results.select("example_question").print()
36
+ ```
37
+
38
+ Output:
39
+ ```python
40
+ ┏━━━━━━━━━━━━━━━━━━━┓
41
+ ┃ answer ┃
42
+ ┃ .example_question ┃
43
+ ┡━━━━━━━━━━━━━━━━━━━┩
44
+ │ Good │
45
+ └───────────────────┘
46
+ ```
47
+
48
+ ## 💻 Requirements
49
+ * EDSL is compatible with Python 3.9 - 3.12.
50
+ * API keys for large language models that you want to use, stored in a `.env` file.
51
+ See instructions on [storing API keys](https://docs.expectedparrot.com/en/latest/api_keys.html).
52
+
53
+ ## 💡 Contributions, feature requests & bugs
54
+ Interested in contributing? Want us to add a new feature? Found a bug for us to squash?
55
+ Please send us an email at [info@expectedparrot.com](mailto:info@expectedparrot.com) or message us at our [Discord channel](https://discord.com/invite/mxAYkjfy9m).
@@ -0,0 +1,288 @@
1
+ """Base class for all classes in the package. It provides rich printing and persistence of objects."""
2
+
3
+ from abc import ABC, abstractmethod, ABCMeta
4
+ import gzip
5
+ import io
6
+ import json
7
+ from typing import Any, Optional, Union
8
+ from uuid import UUID
9
+
10
+
11
+ class RichPrintingMixin:
12
+ """Mixin for rich printing and persistence of objects."""
13
+
14
+ def _for_console(self):
15
+ """Return a string representation of the object for console printing."""
16
+ from rich.console import Console
17
+
18
+ with io.StringIO() as buf:
19
+ console = Console(file=buf, record=True)
20
+ table = self.rich_print()
21
+ console.print(table)
22
+ return console.export_text()
23
+
24
+ def __str__(self):
25
+ """Return a string representation of the object for console printing."""
26
+ return self._for_console()
27
+
28
+ def print(self):
29
+ """Print the object to the console."""
30
+ from edsl.utilities.utilities import is_notebook
31
+
32
+ if is_notebook():
33
+ from IPython.display import display
34
+
35
+ display(self.rich_print())
36
+ else:
37
+ from rich.console import Console
38
+
39
+ console = Console()
40
+ console.print(self.rich_print())
41
+
42
+
43
+ class PersistenceMixin:
44
+ """Mixin for saving and loading objects to and from files."""
45
+
46
+ def push(
47
+ self,
48
+ description: Optional[str] = None,
49
+ visibility: Optional[str] = "unlisted",
50
+ ):
51
+ """Post the object to coop."""
52
+ from edsl.coop import Coop
53
+
54
+ c = Coop()
55
+ return c.create(self, description, visibility)
56
+
57
+ @classmethod
58
+ def pull(cls, uuid: Optional[Union[str, UUID]] = None, url: Optional[str] = None):
59
+ """Pull the object from coop."""
60
+ from edsl.coop import Coop
61
+ from edsl.coop.utils import ObjectRegistry
62
+
63
+ object_type = ObjectRegistry.get_object_type_by_edsl_class(cls)
64
+ coop = Coop()
65
+ return coop.get(uuid, url, object_type)
66
+
67
+ @classmethod
68
+ def delete(cls, uuid: Optional[Union[str, UUID]] = None, url: Optional[str] = None):
69
+ """Delete the object from coop."""
70
+ from edsl.coop import Coop
71
+
72
+ coop = Coop()
73
+ return coop.delete(uuid, url)
74
+
75
+ @classmethod
76
+ def patch(
77
+ cls,
78
+ uuid: Optional[Union[str, UUID]] = None,
79
+ url: Optional[str] = None,
80
+ description: Optional[str] = None,
81
+ value: Optional[Any] = None,
82
+ visibility: Optional[str] = None,
83
+ ):
84
+ """
85
+ Patch an uploaded objects attributes.
86
+ - `description` changes the description of the object on Coop
87
+ - `value` changes the value of the object on Coop. **has to be an EDSL object**
88
+ - `visibility` changes the visibility of the object on Coop
89
+ """
90
+ from edsl.coop import Coop
91
+
92
+ coop = Coop()
93
+ return coop.patch(uuid, url, description, value, visibility)
94
+
95
+ @classmethod
96
+ def search(cls, query):
97
+ """Search for objects on coop."""
98
+ from edsl.coop import Coop
99
+
100
+ c = Coop()
101
+ return c.search(cls, query)
102
+
103
+ def save(self, filename, compress=True):
104
+ """Save the object to a file as zippped JSON.
105
+
106
+ >>> obj.save("obj.json.gz")
107
+
108
+ """
109
+ if filename.endswith("json.gz"):
110
+ import warnings
111
+
112
+ warnings.warn(
113
+ "Do not apply the file extensions. The filename should not end with 'json.gz'."
114
+ )
115
+ filename = filename[:-7]
116
+ if filename.endswith("json"):
117
+ filename = filename[:-4]
118
+ warnings.warn(
119
+ "Do not apply the file extensions. The filename should not end with 'json'."
120
+ )
121
+
122
+ if compress:
123
+ with gzip.open(filename + ".json.gz", "wb") as f:
124
+ f.write(json.dumps(self.to_dict()).encode("utf-8"))
125
+ else:
126
+ with open(filename + ".json", "w") as f:
127
+ f.write(json.dumps(self.to_dict()))
128
+
129
+ @staticmethod
130
+ def open_compressed_file(filename):
131
+ with gzip.open(filename, "rb") as f:
132
+ file_contents = f.read()
133
+ file_contents_decoded = file_contents.decode("utf-8")
134
+ d = json.loads(file_contents_decoded)
135
+ return d
136
+
137
+ @staticmethod
138
+ def open_regular_file(filename):
139
+ with open(filename, "r") as f:
140
+ d = json.loads(f.read())
141
+ return d
142
+
143
+ @classmethod
144
+ def load(cls, filename):
145
+ """Load the object from a file.
146
+
147
+ >>> obj = cls.load("obj.json.gz")
148
+
149
+ """
150
+
151
+ if filename.endswith("json.gz"):
152
+ d = cls.open_compressed_file(filename)
153
+ elif filename.endswith("json"):
154
+ d = cls.open_regular_file(filename)
155
+ else:
156
+ try:
157
+ d = cls.open_compressed_file(filename)
158
+ except:
159
+ d = cls.open_regular_file(filename)
160
+ finally:
161
+ raise ValueError("File must be a json or json.gz file")
162
+
163
+ return cls.from_dict(d)
164
+
165
+
166
+ class RegisterSubclassesMeta(ABCMeta):
167
+ """Metaclass for registering subclasses."""
168
+
169
+ _registry = {}
170
+
171
+ def __init__(cls, name, bases, nmspc):
172
+ """Register the class in the registry upon creation."""
173
+ super(RegisterSubclassesMeta, cls).__init__(name, bases, nmspc)
174
+ if cls.__name__ != "Base":
175
+ RegisterSubclassesMeta._registry[cls.__name__] = cls
176
+
177
+ @staticmethod
178
+ def get_registry():
179
+ """Return the registry of subclasses."""
180
+ return dict(RegisterSubclassesMeta._registry)
181
+
182
+
183
+ class DiffMethodsMixin:
184
+ def __sub__(self, other):
185
+ """Return the difference between two objects."""
186
+ from edsl.BaseDiff import BaseDiff
187
+
188
+ return BaseDiff(self, other)
189
+
190
+
191
+ class Base(
192
+ RichPrintingMixin,
193
+ PersistenceMixin,
194
+ DiffMethodsMixin,
195
+ ABC,
196
+ metaclass=RegisterSubclassesMeta,
197
+ ):
198
+ """Base class for all classes in the package."""
199
+
200
+ # def __getitem__(self, key):
201
+ # return getattr(self, key)
202
+
203
+ # @abstractmethod
204
+ # def _repr_html_(self) -> str:
205
+ # raise NotImplementedError("This method is not implemented yet.")
206
+
207
+ # @abstractmethod
208
+ # def _repr_(self) -> str:
209
+ # raise NotImplementedError("This method is not implemented yet.")
210
+
211
+ def keys(self):
212
+ """Return the keys of the object."""
213
+ _keys = list(self.to_dict().keys())
214
+ if "edsl_version" in _keys:
215
+ _keys.remove("edsl_version")
216
+ if "edsl_class_name" in _keys:
217
+ _keys.remove("edsl_class_name")
218
+ return _keys
219
+
220
+ def values(self):
221
+ """Return the values of the object."""
222
+ data = self.to_dict()
223
+ keys = self.keys()
224
+ return {data[key] for key in keys}
225
+
226
+ def _repr_html_(self):
227
+ from edsl.utilities.utilities import data_to_html
228
+
229
+ return data_to_html(self.to_dict())
230
+
231
+ # def html(self):
232
+ # html_string = self._repr_html_()
233
+ # import tempfile
234
+ # import webbrowser
235
+
236
+ # with tempfile.NamedTemporaryFile("w", delete=False, suffix=".html") as f:
237
+ # # print("Writing HTML to", f.name)
238
+ # f.write(html_string)
239
+ # webbrowser.open(f.name)
240
+
241
+ def __eq__(self, other):
242
+ """Return whether two objects are equal."""
243
+ import inspect
244
+
245
+ if not isinstance(other, self.__class__):
246
+ return False
247
+ if "sort" in inspect.signature(self._to_dict).parameters:
248
+ return self._to_dict(sort=True) == other._to_dict(sort=True)
249
+ else:
250
+ return self._to_dict() == other._to_dict()
251
+
252
+ @abstractmethod
253
+ def example():
254
+ """This method should be implemented by subclasses."""
255
+ raise NotImplementedError("This method is not implemented yet.")
256
+
257
+ @abstractmethod
258
+ def rich_print():
259
+ """This method should be implemented by subclasses."""
260
+ raise NotImplementedError("This method is not implemented yet.")
261
+
262
+ @abstractmethod
263
+ def to_dict():
264
+ """This method should be implemented by subclasses."""
265
+ raise NotImplementedError("This method is not implemented yet.")
266
+
267
+ @abstractmethod
268
+ def from_dict():
269
+ """This method should be implemented by subclasses."""
270
+ raise NotImplementedError("This method is not implemented yet.")
271
+
272
+ @abstractmethod
273
+ def code():
274
+ """This method should be implemented by subclasses."""
275
+ raise NotImplementedError("This method is not implemented yet.")
276
+
277
+ def show_methods(self, show_docstrings=True):
278
+ """Show the methods of the object."""
279
+ public_methods_with_docstrings = [
280
+ (method, getattr(self, method).__doc__)
281
+ for method in dir(self)
282
+ if callable(getattr(self, method)) and not method.startswith("_")
283
+ ]
284
+ if show_docstrings:
285
+ for method, documentation in public_methods_with_docstrings:
286
+ print(f"{method}: {documentation}")
287
+ else:
288
+ return [x[0] for x in public_methods_with_docstrings]
@@ -0,0 +1,260 @@
1
+ import difflib
2
+ import json
3
+ from typing import Any, Dict, Tuple
4
+ from collections import UserList
5
+ import inspect
6
+
7
+
8
+ class BaseDiffCollection(UserList):
9
+ def __init__(self, diffs=None):
10
+ if diffs is None:
11
+ diffs = []
12
+ super().__init__(diffs)
13
+
14
+ def apply(self, obj: Any):
15
+ for diff in self:
16
+ obj = diff.apply(obj)
17
+ return obj
18
+
19
+ def add_diff(self, diff) -> "BaseDiffCollection":
20
+ self.append(diff)
21
+ return self
22
+
23
+
24
+ class DummyObject:
25
+ def __init__(self, object_dict):
26
+ self.object_dict = object_dict
27
+
28
+ def _to_dict(self):
29
+ return self.object_dict
30
+
31
+
32
+ class BaseDiff:
33
+ def __init__(
34
+ self, obj1: Any, obj2: Any, added=None, removed=None, modified=None, level=0
35
+ ):
36
+ self.level = level
37
+
38
+ self.obj1 = obj1
39
+ self.obj2 = obj2
40
+
41
+ if "sort" in inspect.signature(obj1._to_dict).parameters:
42
+ self._dict1 = obj1._to_dict(sort=True)
43
+ self._dict2 = obj2._to_dict(sort=True)
44
+ else:
45
+ self._dict1 = obj1._to_dict()
46
+ self._dict2 = obj2._to_dict()
47
+ self._obj_class = type(obj1)
48
+
49
+ self.added = added
50
+ self.removed = removed
51
+ self.modified = modified
52
+
53
+ def __bool__(self):
54
+ return bool(self.added or self.removed or self.modified)
55
+
56
+ @property
57
+ def added(self):
58
+ if self._added is None:
59
+ self._added = self._find_added()
60
+ return self._added
61
+
62
+ def __add__(self, other):
63
+ return self.apply(other)
64
+
65
+ @added.setter
66
+ def added(self, value):
67
+ self._added = value if value is not None else self._find_added()
68
+
69
+ @property
70
+ def removed(self):
71
+ if self._removed is None:
72
+ self._removed = self._find_removed()
73
+ return self._removed
74
+
75
+ @removed.setter
76
+ def removed(self, value):
77
+ self._removed = value if value is not None else self._find_removed()
78
+
79
+ @property
80
+ def modified(self):
81
+ if self._modified is None:
82
+ self._modified = self._find_modified()
83
+ return self._modified
84
+
85
+ @modified.setter
86
+ def modified(self, value):
87
+ self._modified = value if value is not None else self._find_modified()
88
+
89
+ def _find_added(self) -> Dict[Any, Any]:
90
+ return {k: self._dict2[k] for k in self._dict2 if k not in self._dict1}
91
+
92
+ def _find_removed(self) -> Dict[Any, Any]:
93
+ return {k: self._dict1[k] for k in self._dict1 if k not in self._dict2}
94
+
95
+ def _find_modified(self) -> Dict[Any, Tuple[Any, Any, str]]:
96
+ modified = {}
97
+ for k in self._dict1:
98
+ if k in self._dict2 and self._dict1[k] != self._dict2[k]:
99
+ if isinstance(self._dict1[k], str) and isinstance(self._dict2[k], str):
100
+ diff = self._diff_strings(self._dict1[k], self._dict2[k])
101
+ modified[k] = (self._dict1[k], self._dict2[k], diff)
102
+ elif isinstance(self._dict1[k], dict) and isinstance(
103
+ self._dict2[k], dict
104
+ ):
105
+ diff = self._diff_dicts(self._dict1[k], self._dict2[k])
106
+ modified[k] = (self._dict1[k], self._dict2[k], diff)
107
+ elif isinstance(self._dict1[k], list) and isinstance(
108
+ self._dict2[k], list
109
+ ):
110
+ d1 = dict(zip(range(len(self._dict1[k])), self._dict1[k]))
111
+ d2 = dict(zip(range(len(self._dict2[k])), self._dict2[k]))
112
+ diff = BaseDiff(
113
+ DummyObject(d1), DummyObject(d2), level=self.level + 1
114
+ )
115
+ modified[k] = (self._dict1[k], self._dict2[k], diff)
116
+ else:
117
+ modified[k] = (self._dict1[k], self._dict2[k], "")
118
+ return modified
119
+
120
+ @staticmethod
121
+ def is_json(string_that_could_be_json: str) -> bool:
122
+ try:
123
+ json.loads(string_that_could_be_json)
124
+ return True
125
+ except json.JSONDecodeError:
126
+ return False
127
+
128
+ def _diff_dicts(self, dict1: Dict[str, Any], dict2: Dict[str, Any]) -> str:
129
+ diff = BaseDiff(DummyObject(dict1), DummyObject(dict2), level=self.level + 1)
130
+ return diff
131
+
132
+ def _diff_strings(self, str1: str, str2: str) -> str:
133
+ if self.is_json(str1) and self.is_json(str2):
134
+ diff = self._diff_dicts(json.loads(str1), json.loads(str2))
135
+ return diff
136
+ diff = difflib.ndiff(str1.splitlines(), str2.splitlines())
137
+ return diff
138
+
139
+ def apply(self, obj: Any):
140
+ """Apply the diff to the object."""
141
+
142
+ new_obj_dict = obj._to_dict()
143
+ for k, v in self.added.items():
144
+ new_obj_dict[k] = v
145
+ for k in self.removed.keys():
146
+ del new_obj_dict[k]
147
+ for k, (v1, v2, diff) in self.modified.items():
148
+ new_obj_dict[k] = v2
149
+
150
+ return obj.from_dict(new_obj_dict)
151
+
152
+ def to_dict(self) -> Dict[str, Any]:
153
+ return {
154
+ "added": self.added,
155
+ "removed": self.removed,
156
+ "modified": self.modified,
157
+ "obj1": self._dict1,
158
+ "obj2": self._dict2,
159
+ "obj_class": self._obj_class.__name__,
160
+ "level": self.level,
161
+ }
162
+
163
+ @classmethod
164
+ def from_dict(cls, diff_dict: Dict[str, Any], obj1: Any, obj2: Any):
165
+ return cls(
166
+ obj1=obj1,
167
+ obj2=obj2,
168
+ added=diff_dict["added"],
169
+ removed=diff_dict["removed"],
170
+ modified=diff_dict["modified"],
171
+ level=diff_dict["level"],
172
+ )
173
+
174
+ class Results(UserList):
175
+ def __init__(self, prepend=" ", level=0):
176
+ super().__init__()
177
+ self.prepend = prepend
178
+ self.level = level
179
+
180
+ def append(self, item):
181
+ super().append(self.prepend * self.level + item)
182
+
183
+ def __str__(self):
184
+ prepend = " "
185
+ result = self.Results(level=self.level, prepend="\t")
186
+ if self.added:
187
+ result.append("Added keys and values:")
188
+ for k, v in self.added.items():
189
+ result.append(prepend + f" {k}: {v}")
190
+ if self.removed:
191
+ result.append("Removed keys and values:")
192
+ for k, v in self.removed.items():
193
+ result.append(f" {k}: {v}")
194
+ if self.modified:
195
+ result.append("Modified keys and values:")
196
+ for k, (v1, v2, diff) in self.modified.items():
197
+ result.append(f"Key: {k}:")
198
+ result.append(f" Old value: {v1}")
199
+ result.append(f" New value: {v2}")
200
+ if diff:
201
+ result.append(f" Diff:")
202
+ try:
203
+ for line in diff:
204
+ result.append(f" {line}")
205
+ except:
206
+ result.append(f" {diff}")
207
+ return "\n".join(result)
208
+
209
+ def __repr__(self):
210
+ return (
211
+ f"BaseDiff(obj1={self.obj1!r}, obj2={self.obj2!r}, added={self.added!r}, "
212
+ f"removed={self.removed!r}, modified={self.modified!r})"
213
+ )
214
+
215
+ def add_diff(self, diff) -> "BaseDiffCollection":
216
+ return BaseDiffCollection([self, diff])
217
+
218
+
219
+ if __name__ == "__main__":
220
+ from edsl import Question
221
+
222
+ q_ft = Question.example("free_text")
223
+ q_mc = Question.example("multiple_choice")
224
+
225
+ diff1 = q_ft - q_mc
226
+ assert q_ft == q_mc + diff1
227
+ assert q_ft == diff1.apply(q_mc)
228
+ # new_q_mc = diff1.apply(q_ft)
229
+ # assert new_q_mc == q_mc
230
+
231
+ # new_q_mc = q_ft + diff1
232
+ # assert new_q_mc == q_mc
233
+
234
+ # new_q_mc = diff1 + q_ft
235
+ # assert new_q_mc == q_mc
236
+
237
+ # ## Test chain of diffs
238
+ q0 = Question.example("free_text")
239
+ q1 = q0.copy()
240
+ q1.question_text = "Why is Buzzard's Bay so named?"
241
+ diff1 = q1 - q0
242
+ q2 = q1.copy()
243
+ q2.question_name = "buzzard_bay"
244
+ diff2 = q2 - q1
245
+
246
+ diff_chain = diff1.add_diff(diff2)
247
+
248
+ new_q2 = diff_chain.apply(q0)
249
+ assert new_q2 == q2
250
+
251
+ new_q2 = diff_chain + q0
252
+ assert new_q2 == q2
253
+
254
+ # new_diffs = diff1.add_diff(diff1).add_diff(diff1)
255
+ # assert len(new_diffs) == 3
256
+
257
+ # q0 = Question.example("free_text")
258
+ # q1 = Question.example("free_text")
259
+ # q1.question_text = "Why is Buzzard's Bay so named?"
260
+ # q2 = q1.copy()