edsl 0.1.28__py3-none-any.whl → 0.1.29__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 (76) hide show
  1. edsl/Base.py +18 -18
  2. edsl/__init__.py +24 -24
  3. edsl/__version__.py +1 -1
  4. edsl/agents/Agent.py +77 -41
  5. edsl/agents/AgentList.py +35 -6
  6. edsl/agents/Invigilator.py +19 -1
  7. edsl/agents/InvigilatorBase.py +15 -10
  8. edsl/agents/PromptConstructionMixin.py +342 -100
  9. edsl/agents/descriptors.py +2 -1
  10. edsl/base/Base.py +289 -0
  11. edsl/config.py +2 -1
  12. edsl/conjure/InputData.py +39 -8
  13. edsl/coop/coop.py +188 -151
  14. edsl/coop/utils.py +43 -75
  15. edsl/data/Cache.py +19 -5
  16. edsl/data/SQLiteDict.py +11 -3
  17. edsl/jobs/Answers.py +15 -1
  18. edsl/jobs/Jobs.py +92 -47
  19. edsl/jobs/buckets/ModelBuckets.py +4 -2
  20. edsl/jobs/buckets/TokenBucket.py +1 -2
  21. edsl/jobs/interviews/Interview.py +3 -9
  22. edsl/jobs/interviews/InterviewStatusMixin.py +3 -3
  23. edsl/jobs/interviews/InterviewTaskBuildingMixin.py +15 -10
  24. edsl/jobs/runners/JobsRunnerAsyncio.py +21 -25
  25. edsl/jobs/tasks/TaskHistory.py +4 -3
  26. edsl/language_models/LanguageModel.py +5 -11
  27. edsl/language_models/ModelList.py +3 -3
  28. edsl/language_models/repair.py +8 -7
  29. edsl/notebooks/Notebook.py +40 -3
  30. edsl/prompts/Prompt.py +31 -19
  31. edsl/questions/QuestionBase.py +38 -13
  32. edsl/questions/QuestionBudget.py +5 -6
  33. edsl/questions/QuestionCheckBox.py +7 -3
  34. edsl/questions/QuestionExtract.py +5 -3
  35. edsl/questions/QuestionFreeText.py +3 -3
  36. edsl/questions/QuestionFunctional.py +0 -3
  37. edsl/questions/QuestionList.py +3 -4
  38. edsl/questions/QuestionMultipleChoice.py +16 -8
  39. edsl/questions/QuestionNumerical.py +4 -3
  40. edsl/questions/QuestionRank.py +5 -3
  41. edsl/questions/__init__.py +4 -3
  42. edsl/questions/descriptors.py +4 -2
  43. edsl/questions/question_registry.py +20 -31
  44. edsl/questions/settings.py +1 -1
  45. edsl/results/Dataset.py +31 -0
  46. edsl/results/DatasetExportMixin.py +493 -0
  47. edsl/results/Result.py +22 -74
  48. edsl/results/Results.py +105 -67
  49. edsl/results/ResultsDBMixin.py +7 -3
  50. edsl/results/ResultsExportMixin.py +22 -537
  51. edsl/results/ResultsGGMixin.py +3 -3
  52. edsl/results/ResultsToolsMixin.py +5 -5
  53. edsl/scenarios/FileStore.py +140 -0
  54. edsl/scenarios/Scenario.py +5 -6
  55. edsl/scenarios/ScenarioList.py +44 -15
  56. edsl/scenarios/ScenarioListExportMixin.py +32 -0
  57. edsl/scenarios/ScenarioListPdfMixin.py +2 -1
  58. edsl/scenarios/__init__.py +1 -0
  59. edsl/study/ObjectEntry.py +89 -13
  60. edsl/study/ProofOfWork.py +5 -2
  61. edsl/study/SnapShot.py +4 -8
  62. edsl/study/Study.py +21 -14
  63. edsl/study/__init__.py +2 -0
  64. edsl/surveys/MemoryPlan.py +11 -4
  65. edsl/surveys/Survey.py +46 -7
  66. edsl/surveys/SurveyExportMixin.py +4 -2
  67. edsl/surveys/SurveyFlowVisualizationMixin.py +6 -4
  68. edsl/tools/plotting.py +4 -2
  69. edsl/utilities/__init__.py +21 -21
  70. edsl/utilities/interface.py +66 -45
  71. edsl/utilities/utilities.py +11 -13
  72. {edsl-0.1.28.dist-info → edsl-0.1.29.dist-info}/METADATA +11 -10
  73. {edsl-0.1.28.dist-info → edsl-0.1.29.dist-info}/RECORD +75 -72
  74. edsl-0.1.28.dist-info/entry_points.txt +0 -3
  75. {edsl-0.1.28.dist-info → edsl-0.1.29.dist-info}/LICENSE +0 -0
  76. {edsl-0.1.28.dist-info → edsl-0.1.29.dist-info}/WHEEL +0 -0
@@ -0,0 +1,140 @@
1
+ from edsl import Scenario
2
+ import base64
3
+ import io
4
+ import tempfile
5
+ from typing import Optional
6
+
7
+
8
+ class FileStore(Scenario):
9
+ def __init__(
10
+ self,
11
+ filename: str,
12
+ binary: Optional[bool] = None,
13
+ suffix: Optional[str] = None,
14
+ base64_string: Optional[str] = None,
15
+ ):
16
+ self.filename = filename
17
+ self.suffix = suffix or "." + filename.split(".")[-1]
18
+ self.binary = binary or False
19
+ self.base64_string = base64_string or self.encode_file_to_base64_string(
20
+ filename
21
+ )
22
+ super().__init__(
23
+ {
24
+ "filename": self.filename,
25
+ "base64_string": self.base64_string,
26
+ "binary": self.binary,
27
+ "suffix": self.suffix,
28
+ }
29
+ )
30
+
31
+ @classmethod
32
+ def from_dict(cls, d):
33
+ return cls(d["filename"], d["binary"], d["suffix"], d["base64_string"])
34
+
35
+ def encode_file_to_base64_string(self, file_path):
36
+ try:
37
+ # Attempt to open the file in text mode
38
+ with open(file_path, "r") as text_file:
39
+ # Read the text data
40
+ text_data = text_file.read()
41
+ # Encode the text data to a base64 string
42
+ base64_encoded_data = base64.b64encode(text_data.encode("utf-8"))
43
+ except UnicodeDecodeError:
44
+ # If reading as text fails, open the file in binary mode
45
+ with open(file_path, "rb") as binary_file:
46
+ # Read the binary data
47
+ binary_data = binary_file.read()
48
+ # Encode the binary data to a base64 string
49
+ base64_encoded_data = base64.b64encode(binary_data)
50
+ self.binary = True
51
+ # Convert the base64 bytes to a string
52
+ base64_string = base64_encoded_data.decode("utf-8")
53
+
54
+ return base64_string
55
+
56
+ def open(self):
57
+ if self.binary:
58
+ return self.base64_to_file(self["base64_string"], is_binary=True)
59
+ else:
60
+ return self.base64_to_text_file(self["base64_string"])
61
+
62
+ @staticmethod
63
+ def base64_to_text_file(base64_string):
64
+ # Decode the base64 string to bytes
65
+ text_data_bytes = base64.b64decode(base64_string)
66
+
67
+ # Convert bytes to string
68
+ text_data = text_data_bytes.decode("utf-8")
69
+
70
+ # Create a StringIO object from the text data
71
+ text_file = io.StringIO(text_data)
72
+
73
+ return text_file
74
+
75
+ @staticmethod
76
+ def base64_to_file(base64_string, is_binary=True):
77
+ # Decode the base64 string to bytes
78
+ file_data = base64.b64decode(base64_string)
79
+
80
+ if is_binary:
81
+ # Create a BytesIO object for binary data
82
+ return io.BytesIO(file_data)
83
+ else:
84
+ # Convert bytes to string for text data
85
+ text_data = file_data.decode("utf-8")
86
+ # Create a StringIO object for text data
87
+ return io.StringIO(text_data)
88
+
89
+ def to_tempfile(self, suffix=None):
90
+ if suffix is None:
91
+ suffix = self.suffix
92
+ if self.binary:
93
+ file_like_object = self.base64_to_file(
94
+ self["base64_string"], is_binary=True
95
+ )
96
+ else:
97
+ file_like_object = self.base64_to_text_file(self["base64_string"])
98
+
99
+ # Create a named temporary file
100
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
101
+ temp_file.write(file_like_object.read())
102
+ temp_file.close()
103
+
104
+ return temp_file.name
105
+
106
+ def push(self, description=None):
107
+ scenario_version = Scenario.from_dict(self.to_dict())
108
+ if description is None:
109
+ description = "File: " + self["filename"]
110
+ info = scenario_version.push(description=description)
111
+ return info
112
+
113
+ @classmethod
114
+ def pull(cls, uuid):
115
+ scenario_version = Scenario.pull(uuid)
116
+ return cls.from_dict(scenario_version.to_dict())
117
+
118
+
119
+ class CSVFileStore(FileStore):
120
+ def __init__(self, filename):
121
+ super().__init__(filename, suffix=".csv")
122
+
123
+
124
+ class PDFFileStore(FileStore):
125
+ def __init__(self, filename):
126
+ super().__init__(filename, suffix=".pdf")
127
+
128
+
129
+ if __name__ == "__main__":
130
+ # file_path = "../conjure/examples/Ex11-2.sav"
131
+ # fs = FileStore(file_path)
132
+ # info = fs.push()
133
+ # print(info)
134
+
135
+ # from edsl import Conjure
136
+
137
+ # c = Conjure(datafile_name=fs.to_tempfile())
138
+ f = PDFFileStore("paper.pdf")
139
+ # print(f.to_tempfile())
140
+ f.push()
@@ -1,17 +1,12 @@
1
1
  """A Scenario is a dictionary with a key/value to parameterize a question."""
2
2
 
3
+ import time
3
4
  import copy
4
5
  from collections import UserDict
5
6
  from typing import Union, List, Optional, Generator
6
7
  import base64
7
8
  import hashlib
8
- import json
9
-
10
- import fitz # PyMuPDF
11
9
  import os
12
- import subprocess
13
-
14
- from rich.table import Table
15
10
 
16
11
  from edsl.Base import Base
17
12
  from edsl.scenarios.ScenarioImageMixin import ScenarioImageMixin
@@ -217,6 +212,8 @@ class Scenario(Base, UserDict, ScenarioImageMixin, ScenarioHtmlMixin):
217
212
 
218
213
  @classmethod
219
214
  def from_pdf(cls, pdf_path):
215
+ import fitz # PyMuPDF
216
+
220
217
  # Ensure the file exists
221
218
  if not os.path.exists(pdf_path):
222
219
  raise FileNotFoundError(f"The file {pdf_path} does not exist.")
@@ -404,6 +401,8 @@ class Scenario(Base, UserDict, ScenarioImageMixin, ScenarioHtmlMixin):
404
401
 
405
402
  def rich_print(self) -> "Table":
406
403
  """Display an object as a rich table."""
404
+ from rich.table import Table
405
+
407
406
  table_data, column_names = self._table()
408
407
  table = Table(title=f"{self.__class__.__name__} Attributes")
409
408
  for column in column_names:
@@ -5,25 +5,22 @@ import csv
5
5
  import random
6
6
  from collections import UserList, Counter
7
7
  from collections.abc import Iterable
8
-
9
8
  from typing import Any, Optional, Union, List
10
9
 
11
- from rich.table import Table
12
10
  from simpleeval import EvalWithCompoundTypes
13
11
 
14
- from edsl.scenarios.Scenario import Scenario
15
12
  from edsl.Base import Base
16
13
  from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
14
+ from edsl.scenarios.Scenario import Scenario
17
15
  from edsl.scenarios.ScenarioListPdfMixin import ScenarioListPdfMixin
16
+ from edsl.scenarios.ScenarioListExportMixin import ScenarioListExportMixin
18
17
 
19
- from edsl.utilities.interface import print_scenario_list
20
18
 
21
- from edsl.utilities import is_valid_variable_name
19
+ class ScenarioListMixin(ScenarioListPdfMixin, ScenarioListExportMixin):
20
+ pass
22
21
 
23
- from edsl.results.ResultsExportMixin import ResultsExportMixin
24
22
 
25
-
26
- class ScenarioList(Base, UserList, ScenarioListPdfMixin, ResultsExportMixin):
23
+ class ScenarioList(Base, UserList, ScenarioListMixin):
27
24
  """Class for creating a list of scenarios to be used in a survey."""
28
25
 
29
26
  def __init__(self, data: Optional[list] = None):
@@ -119,7 +116,7 @@ class ScenarioList(Base, UserList, ScenarioListPdfMixin, ResultsExportMixin):
119
116
 
120
117
  return ScenarioList(random.sample(self.data, n))
121
118
 
122
- def expand(self, expand_field: str) -> ScenarioList:
119
+ def expand(self, expand_field: str, number_field=False) -> ScenarioList:
123
120
  """Expand the ScenarioList by a field.
124
121
 
125
122
  Example:
@@ -133,9 +130,11 @@ class ScenarioList(Base, UserList, ScenarioListPdfMixin, ResultsExportMixin):
133
130
  values = scenario[expand_field]
134
131
  if not isinstance(values, Iterable) or isinstance(values, str):
135
132
  values = [values]
136
- for value in values:
133
+ for index, value in enumerate(values):
137
134
  new_scenario = scenario.copy()
138
135
  new_scenario[expand_field] = value
136
+ if number_field:
137
+ new_scenario[expand_field + "_number"] = index + 1
139
138
  new_scenarios.append(new_scenario)
140
139
  return ScenarioList(new_scenarios)
141
140
 
@@ -155,6 +154,8 @@ class ScenarioList(Base, UserList, ScenarioListPdfMixin, ResultsExportMixin):
155
154
  )
156
155
  raw_var_name, expression = new_var_string.split("=", 1)
157
156
  var_name = raw_var_name.strip()
157
+ from edsl.utilities.utilities import is_valid_variable_name
158
+
158
159
  if not is_valid_variable_name(var_name):
159
160
  raise Exception(f"{var_name} is not a valid variable name.")
160
161
 
@@ -178,16 +179,20 @@ class ScenarioList(Base, UserList, ScenarioListPdfMixin, ResultsExportMixin):
178
179
 
179
180
  return ScenarioList(new_data)
180
181
 
181
- def order_by(self, field: str, reverse: bool = False) -> ScenarioList:
182
- """Order the scenarios by a field.
182
+ def order_by(self, *fields: str, reverse: bool = False) -> ScenarioList:
183
+ """Order the scenarios by one or more fields.
183
184
 
184
185
  Example:
185
186
 
186
187
  >>> s = ScenarioList([Scenario({'a': 1, 'b': 2}), Scenario({'a': 1, 'b': 1})])
187
- >>> s.order_by('b')
188
+ >>> s.order_by('b', 'a')
188
189
  ScenarioList([Scenario({'a': 1, 'b': 1}), Scenario({'a': 1, 'b': 2})])
189
190
  """
190
- return ScenarioList(sorted(self, key=lambda x: x[field], reverse=reverse))
191
+
192
+ def get_sort_key(scenario: Any) -> tuple:
193
+ return tuple(scenario[field] for field in fields)
194
+
195
+ return ScenarioList(sorted(self, key=get_sort_key, reverse=reverse))
191
196
 
192
197
  def filter(self, expression: str) -> ScenarioList:
193
198
  """
@@ -337,6 +342,20 @@ class ScenarioList(Base, UserList, ScenarioListPdfMixin, ResultsExportMixin):
337
342
  """
338
343
  return cls([Scenario(row) for row in df.to_dict(orient="records")])
339
344
 
345
+ def to_key_value(self, field, value=None) -> Union[dict, set]:
346
+ """Return the set of values in the field.
347
+
348
+ Example:
349
+
350
+ >>> s = ScenarioList([Scenario({'name': 'Alice'}), Scenario({'name': 'Bob'})])
351
+ >>> s.to_key_value('name') == {'Alice', 'Bob'}
352
+ True
353
+ """
354
+ if value is None:
355
+ return {scenario[field] for scenario in self}
356
+ else:
357
+ return {scenario[field]: scenario[value] for scenario in self}
358
+
340
359
  @classmethod
341
360
  def from_csv(cls, filename: str) -> ScenarioList:
342
361
  """Create a ScenarioList from a CSV file.
@@ -356,6 +375,8 @@ class ScenarioList(Base, UserList, ScenarioListPdfMixin, ResultsExportMixin):
356
375
  >>> scenario_list[1]['age']
357
376
  '25'
358
377
  """
378
+ from edsl.scenarios.Scenario import Scenario
379
+
359
380
  observations = []
360
381
  with open(filename, "r") as f:
361
382
  reader = csv.reader(f)
@@ -393,12 +414,16 @@ class ScenarioList(Base, UserList, ScenarioListPdfMixin, ResultsExportMixin):
393
414
  ScenarioList([Scenario({'name': 'Alice'}), Scenario({'name': 'Bob'})])
394
415
 
395
416
  """
417
+ from edsl.scenarios.Scenario import Scenario
418
+
396
419
  return cls([Scenario(s) for s in scenario_dicts_list])
397
420
 
398
421
  @classmethod
399
422
  @remove_edsl_version
400
423
  def from_dict(cls, data) -> ScenarioList:
401
424
  """Create a `ScenarioList` from a dictionary."""
425
+ from edsl.scenarios.Scenario import Scenario
426
+
402
427
  return cls([Scenario.from_dict(s) for s in data["scenarios"]])
403
428
 
404
429
  def code(self) -> str:
@@ -423,6 +448,8 @@ class ScenarioList(Base, UserList, ScenarioListPdfMixin, ResultsExportMixin):
423
448
 
424
449
  def rich_print(self) -> None:
425
450
  """Display an object as a table."""
451
+ from rich.table import Table
452
+
426
453
  table = Table(title="ScenarioList")
427
454
  table.add_column("Index", style="bold")
428
455
  table.add_column("Scenario")
@@ -437,7 +464,9 @@ class ScenarioList(Base, UserList, ScenarioListPdfMixin, ResultsExportMixin):
437
464
  pretty_labels: Optional[dict] = None,
438
465
  filename: str = None,
439
466
  ):
440
- print_scenario_list(self)
467
+ from edsl.utilities.interface import print_scenario_list
468
+
469
+ print_scenario_list(self[:max_rows])
441
470
 
442
471
  def __getitem__(self, key: Union[int, slice]) -> Any:
443
472
  """Return the item at the given index.
@@ -0,0 +1,32 @@
1
+ """Mixin class for exporting results."""
2
+
3
+ from functools import wraps
4
+ from edsl.results.DatasetExportMixin import DatasetExportMixin
5
+
6
+
7
+ def to_dataset(func):
8
+ """Convert the Results object to a Dataset object before calling the function."""
9
+
10
+ @wraps(func)
11
+ def wrapper(self, *args, **kwargs):
12
+ """Return the function with the Results object converted to a Dataset object."""
13
+ if self.__class__.__name__ == "ScenarioList":
14
+ return func(self.to_dataset(), *args, **kwargs)
15
+ else:
16
+ raise Exception(
17
+ f"Class {self.__class__.__name__} not recognized as a Results or Dataset object."
18
+ )
19
+
20
+ return wrapper
21
+
22
+
23
+ def decorate_all_methods(cls):
24
+ for attr_name, attr_value in cls.__dict__.items():
25
+ if callable(attr_value):
26
+ setattr(cls, attr_name, to_dataset(attr_value))
27
+ return cls
28
+
29
+
30
+ @decorate_all_methods
31
+ class ScenarioListExportMixin(DatasetExportMixin):
32
+ """Mixin class for exporting Results objects."""
@@ -2,7 +2,7 @@ import fitz # PyMuPDF
2
2
  import os
3
3
  import subprocess
4
4
 
5
- from edsl import Scenario
5
+ # from edsl import Scenario
6
6
 
7
7
 
8
8
  class ScenarioListPdfMixin:
@@ -22,6 +22,7 @@ class ScenarioListPdfMixin:
22
22
  """
23
23
  import tempfile
24
24
  from pdf2image import convert_from_path
25
+ from edsl.scenarios import Scenario
25
26
 
26
27
  with tempfile.TemporaryDirectory() as output_folder:
27
28
  # Convert PDF to images
@@ -1 +1,2 @@
1
1
  from edsl.scenarios.Scenario import Scenario
2
+ from edsl.scenarios.ScenarioList import ScenarioList
edsl/study/ObjectEntry.py CHANGED
@@ -1,5 +1,8 @@
1
1
  import time
2
- from typing import Optional, Dict, Any, Type
2
+ import webbrowser
3
+ from typing import Any, Dict, Optional, Type
4
+ from edsl import QuestionBase
5
+ from edsl.Base import RegisterSubclassesMeta
3
6
 
4
7
 
5
8
  class ObjectEntry:
@@ -12,6 +15,16 @@ class ObjectEntry:
12
15
  created_at: Optional[float] = None,
13
16
  edsl_class_name: Optional[str] = None,
14
17
  ):
18
+ """
19
+ Initialize an ObjectEntry instance.
20
+
21
+ :param variable_name: The name of the variable.
22
+ :param object: The object being wrapped.
23
+ :param description: A description of the object.
24
+ :param coop_info: Optional Coop information dictionary.
25
+ :param created_at: Optional creation timestamp. Defaults to current time.
26
+ :param edsl_class_name: Optional EDSL class name. Defaults to object's class name.
27
+ """
15
28
  self.created_at = created_at or time.time()
16
29
  self.variable_name = variable_name
17
30
  self.object = object
@@ -20,22 +33,33 @@ class ObjectEntry:
20
33
  self.coop_info = coop_info
21
34
 
22
35
  @classmethod
23
- def _get_class(self, obj_dict: Dict[str, Any]) -> Type:
24
- "Get the class of an object from its dictionary representation."
25
- class_name = obj_dict["edsl_class_name"]
36
+ def _get_class(cls, object_dict: Dict[str, Any]) -> Type:
37
+ """
38
+ Get the class of an object from its dictionary representation.
39
+
40
+ :param object_dict: The dictionary representation of the object.
41
+ :return: The class of the object.
42
+ """
43
+ class_name = object_dict["edsl_class_name"]
26
44
  if class_name == "QuestionBase":
27
- from edsl import QuestionBase
28
-
29
45
  return QuestionBase
30
46
  else:
31
- from edsl.Base import RegisterSubclassesMeta
32
-
33
47
  return RegisterSubclassesMeta._registry[class_name]
34
48
 
35
49
  def __repr__(self) -> str:
50
+ """
51
+ Return a string representation of the ObjectEntry instance.
52
+
53
+ :return: A string representation of the ObjectEntry instance.
54
+ """
36
55
  return f"ObjectEntry(variable_name='{self.variable_name}', object={self.object!r}, description='{self.description}', coop_info={self.coop_info}, created_at={self.created_at}, edsl_class_name='{self.edsl_class_name}')"
37
56
 
38
57
  def to_dict(self) -> Dict[str, Any]:
58
+ """
59
+ Convert the ObjectEntry instance to a dictionary.
60
+
61
+ :return: A dictionary representation of the ObjectEntry instance.
62
+ """
39
63
  return {
40
64
  "created_at": self.created_at,
41
65
  "variable_name": self.variable_name,
@@ -47,34 +71,65 @@ class ObjectEntry:
47
71
 
48
72
  @classmethod
49
73
  def from_dict(cls, d: Dict[str, Any]) -> "ObjectEntry":
74
+ """
75
+ Create an ObjectEntry instance from a dictionary.
76
+
77
+ :param d: The dictionary representation of the ObjectEntry instance.
78
+ :return: An ObjectEntry instance.
79
+ """
50
80
  d["object"] = cls._get_class(d["object"]).from_dict(d["object"])
51
81
  return cls(**d)
52
82
 
53
83
  @property
54
84
  def hash(self) -> str:
85
+ """
86
+ Compute the hash of the object.
87
+
88
+ :return: The hash of the object as a string.
89
+ """
55
90
  return str(hash(self.object))
56
91
 
57
92
  def add_to_namespace(self) -> None:
93
+ """
94
+ Add the object to the global namespace using its variable name.
95
+ """
58
96
  globals()[self.variable_name] = self.object
59
97
 
60
98
  @property
61
99
  def coop_info(self) -> Optional[Dict[str, Any]]:
100
+ """
101
+ Get the Coop information for the object.
102
+
103
+ :return: The Coop information dictionary, if available.
104
+ """
62
105
  return self._coop_info
63
106
 
64
107
  @coop_info.setter
65
108
  def coop_info(self, coop_info: Optional[Dict[str, Any]]) -> None:
109
+ """
110
+ Set the Coop information for the object.
111
+
112
+ :param coop_info: The Coop information dictionary.
113
+ """
66
114
  self._coop_info = coop_info
67
115
 
68
116
  def view_on_coop(self) -> None:
117
+ """
118
+ Open the object's Coop URL in a web browser.
119
+ """
69
120
  if self.coop_info is None:
70
121
  print("Object not pushed to coop")
71
122
  return
72
- url = self.coop_info["url"]
73
- import webbrowser
74
-
123
+ url = self.coop_info.get("url")
75
124
  webbrowser.open(url)
76
125
 
77
- def push(self, refresh: bool = False) -> Dict[str, Any]:
126
+ def push(self, refresh: Optional[bool] = False) -> Dict[str, Any]:
127
+ """
128
+ Push the object to the Coop.
129
+
130
+ :param refresh: Whether to refresh the Coop entry for the object.
131
+ :return: The Coop info dictionary.
132
+ """
78
133
  if self.coop_info is None or refresh:
79
134
  self.coop_info = self.object.push(description=self.description)
80
135
  print(
@@ -85,13 +140,34 @@ class ObjectEntry:
85
140
  f"Object {self.variable_name} already pushed to coop with info: {self._coop_info}"
86
141
  )
87
142
 
143
+ def __eq__(self, other: "ObjectEntry") -> bool:
144
+ """
145
+ Check if two ObjectEntry instances are equal.
146
+
147
+ :param other: The other ObjectEntry instance.
148
+ :return: True if the two instances are equal, False otherwise.
149
+ """
150
+ # if the other item is not "ObjectEntry" type, return False
151
+ if not isinstance(other, ObjectEntry):
152
+ return False
153
+
154
+ return (
155
+ self.variable_name == other.variable_name
156
+ and self.object == other.object
157
+ and self.description == other.description
158
+ and self.coop_info == other.coop_info
159
+ and self.created_at == other.created_at
160
+ and self.edsl_class_name == other.edsl_class_name
161
+ )
162
+
88
163
 
89
164
  if __name__ == "__main__":
90
165
  from edsl import QuestionFreeText
166
+ from edsl.study import ObjectEntry
91
167
 
92
168
  q = QuestionFreeText.example()
93
169
 
94
170
  oe = ObjectEntry("q", q, "This is a question")
95
171
  d = oe.to_dict()
96
172
  new_oe = ObjectEntry.from_dict(d)
97
- # print(oe.coop_info)
173
+ new_oe == oe
edsl/study/ProofOfWork.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import hashlib
2
2
  import time
3
- from typing import Optional, Any, Dict, List
3
+ from typing import Any, Dict, List, Optional
4
4
 
5
5
 
6
6
  class ProofOfWork:
@@ -16,7 +16,10 @@ class ProofOfWork:
16
16
  self.input_data = input_data
17
17
 
18
18
  def to_dict(self) -> Dict[str, Any]:
19
- return {"input_data": self.input_data, "proof": self.proof}
19
+ return {
20
+ "input_data": self.input_data,
21
+ "proof": self.proof,
22
+ }
20
23
 
21
24
  @classmethod
22
25
  def from_dict(cls, data: Dict[str, Any]) -> "ProofOfWork":
edsl/study/SnapShot.py CHANGED
@@ -1,16 +1,11 @@
1
- from typing import Generator
2
1
  import inspect
2
+ from typing import Generator, List, Optional
3
3
 
4
4
 
5
5
  class SnapShot:
6
- def __init__(self, namespace, exclude=None):
6
+ def __init__(self, namespace, exclude: Optional[List] = None):
7
7
  self.namespace = namespace
8
-
9
- if exclude is None:
10
- self.exclude = []
11
- else:
12
- self.exclude = exclude
13
-
8
+ self.exclude = exclude or []
14
9
  self.edsl_objects = dict(self._get_edsl_objects(namespace=self.namespace))
15
10
  self.edsl_classes = dict(self._get_edsl_classes(namespace=self.namespace))
16
11
 
@@ -63,6 +58,7 @@ class SnapShot:
63
58
  from edsl.study.Study import Study
64
59
 
65
60
  for name, value in namespace.items():
61
+ # TODO check this code logic (if there are other objects with to_dict method that are not from edsl)
66
62
  if (
67
63
  hasattr(value, "to_dict")
68
64
  and not inspect.isclass(value)
edsl/study/Study.py CHANGED
@@ -1,23 +1,19 @@
1
- import os
2
- import platform
3
- import socket
4
1
  import copy
5
2
  import inspect
6
3
  import json
7
- from typing import Optional, List, Dict
4
+ import os
5
+ import platform
6
+ import socket
8
7
  from datetime import datetime
9
-
10
- # from edsl.Base import Base
8
+ from typing import Dict, Optional, Union
11
9
  from edsl import Cache, set_session_cache, unset_session_cache
12
10
  from edsl.utilities.utilities import dict_hash
13
-
14
11
  from edsl.study.ObjectEntry import ObjectEntry
15
12
  from edsl.study.ProofOfWork import ProofOfWork
16
13
  from edsl.study.SnapShot import SnapShot
14
+ from uuid import UUID
17
15
 
18
-
19
- class _StudyFrameMarker:
20
- pass
16
+ # from edsl.Base import Base
21
17
 
22
18
 
23
19
  class Study:
@@ -58,7 +54,7 @@ class Study:
58
54
  proof_of_work=None,
59
55
  proof_of_work_difficulty: int = None,
60
56
  namespace: Optional[dict] = None,
61
- verbose=True,
57
+ verbose: Optional[bool] = True,
62
58
  ):
63
59
  """
64
60
  :param name: The name of the study.
@@ -469,11 +465,22 @@ class Study:
469
465
 
470
466
  def push(self, refresh=False) -> None:
471
467
  """Push the objects to coop."""
472
- for obj_entry in self.objects.values():
473
- obj_entry.push(refresh=refresh)
468
+
469
+ from edsl import Coop
470
+
471
+ coop = Coop()
472
+ coop.create(self, description=self.description)
473
+
474
+ @classmethod
475
+ def pull(cls, uuid: Optional[Union[str, UUID]] = None, url: Optional[str] = None):
476
+ """Pull the object from coop."""
477
+ from edsl.coop import Coop
478
+
479
+ coop = Coop()
480
+ return coop.get(uuid, url, "study")
474
481
 
475
482
  def __repr__(self):
476
- return f"""Study(name = {self.name}, description = {self.description}, objects = {self.objects}, cache = {self.cache}, filename = {self.filename}, coop = {self.coop}, use_study_cache = {self.use_study_cache}, overwrite_on_change = {self.overwrite_on_change})"""
483
+ return f"""Study(name = "{self.name}", description = "{self.description}", objects = {self.objects}, cache = {self.cache}, filename = "{self.filename}", coop = {self.coop}, use_study_cache = {self.use_study_cache}, overwrite_on_change = {self.overwrite_on_change})"""
477
484
 
478
485
 
479
486
  if __name__ == "__main__":
edsl/study/__init__.py CHANGED
@@ -1,2 +1,4 @@
1
1
  from edsl.study.ObjectEntry import ObjectEntry
2
2
  from edsl.study.ProofOfWork import ProofOfWork
3
+ from edsl.study.SnapShot import SnapShot
4
+ from edsl.study.Study import Study