edsl 0.1.53__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 (104) 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 +105 -7
  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 +10 -1
  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_pricing_estimation.py +127 -46
  48. edsl/jobs/jobs_remote_inference_logger.py +4 -0
  49. edsl/jobs/jobs_runner_status.py +30 -25
  50. edsl/jobs/progress_bar_manager.py +79 -0
  51. edsl/jobs/remote_inference.py +35 -1
  52. edsl/key_management/key_lookup_builder.py +6 -1
  53. edsl/language_models/language_model.py +102 -12
  54. edsl/language_models/model.py +10 -3
  55. edsl/language_models/price_manager.py +45 -75
  56. edsl/language_models/registry.py +5 -0
  57. edsl/language_models/utilities.py +2 -1
  58. edsl/notebooks/notebook.py +77 -10
  59. edsl/questions/VALIDATION_README.md +134 -0
  60. edsl/questions/__init__.py +24 -1
  61. edsl/questions/exceptions.py +21 -0
  62. edsl/questions/question_check_box.py +171 -149
  63. edsl/questions/question_dict.py +243 -51
  64. edsl/questions/question_multiple_choice_with_other.py +624 -0
  65. edsl/questions/question_registry.py +2 -1
  66. edsl/questions/templates/multiple_choice_with_other/__init__.py +0 -0
  67. edsl/questions/templates/multiple_choice_with_other/answering_instructions.jinja +15 -0
  68. edsl/questions/templates/multiple_choice_with_other/question_presentation.jinja +17 -0
  69. edsl/questions/validation_analysis.py +185 -0
  70. edsl/questions/validation_cli.py +131 -0
  71. edsl/questions/validation_html_report.py +404 -0
  72. edsl/questions/validation_logger.py +136 -0
  73. edsl/results/result.py +63 -16
  74. edsl/results/results.py +702 -171
  75. edsl/scenarios/construct_download_link.py +16 -3
  76. edsl/scenarios/directory_scanner.py +226 -226
  77. edsl/scenarios/file_methods.py +5 -0
  78. edsl/scenarios/file_store.py +117 -6
  79. edsl/scenarios/handlers/__init__.py +5 -1
  80. edsl/scenarios/handlers/mp4_file_store.py +104 -0
  81. edsl/scenarios/handlers/webm_file_store.py +104 -0
  82. edsl/scenarios/scenario.py +120 -101
  83. edsl/scenarios/scenario_list.py +800 -727
  84. edsl/scenarios/scenario_list_gc_test.py +146 -0
  85. edsl/scenarios/scenario_list_memory_test.py +214 -0
  86. edsl/scenarios/scenario_list_source_refactor.md +35 -0
  87. edsl/scenarios/scenario_selector.py +5 -4
  88. edsl/scenarios/scenario_source.py +1990 -0
  89. edsl/scenarios/tests/test_scenario_list_sources.py +52 -0
  90. edsl/surveys/survey.py +22 -0
  91. edsl/tasks/__init__.py +4 -2
  92. edsl/tasks/task_history.py +198 -36
  93. edsl/tests/scenarios/test_ScenarioSource.py +51 -0
  94. edsl/tests/scenarios/test_scenario_list_sources.py +51 -0
  95. edsl/utilities/__init__.py +2 -1
  96. edsl/utilities/decorators.py +121 -0
  97. edsl/utilities/memory_debugger.py +1010 -0
  98. {edsl-0.1.53.dist-info → edsl-0.1.55.dist-info}/METADATA +52 -76
  99. {edsl-0.1.53.dist-info → edsl-0.1.55.dist-info}/RECORD +102 -78
  100. edsl/jobs/jobs_runner_asyncio.py +0 -281
  101. edsl/language_models/unused/fake_openai_service.py +0 -60
  102. {edsl-0.1.53.dist-info → edsl-0.1.55.dist-info}/LICENSE +0 -0
  103. {edsl-0.1.53.dist-info → edsl-0.1.55.dist-info}/WHEEL +0 -0
  104. {edsl-0.1.53.dist-info → edsl-0.1.55.dist-info}/entry_points.txt +0 -0
edsl/base/base_class.py CHANGED
@@ -15,30 +15,47 @@ JSON/YAML serialization, file persistence, pretty printing, and object compariso
15
15
  from abc import ABC, abstractmethod, ABCMeta
16
16
  import gzip
17
17
  import json
18
- from typing import Any, Optional, Union
18
+ from typing import Any, Optional, Union, TYPE_CHECKING
19
19
  from uuid import UUID
20
20
  import difflib
21
- from typing import Dict, Tuple
21
+ from typing import Dict, Literal, List, Tuple
22
22
  from collections import UserList
23
23
  import inspect
24
+ import hashlib
24
25
 
25
26
  from .. import logger
26
27
 
28
+ if TYPE_CHECKING:
29
+ from ..coop.coop_objects import CoopObjects
30
+
31
+ VisibilityType = Literal["private", "public", "unlisted"]
32
+ RemoteJobStatus = Literal[
33
+ "queued",
34
+ "running",
35
+ "completed",
36
+ "failed",
37
+ "cancelled",
38
+ "cancelling",
39
+ "partial_failed",
40
+ ]
41
+
42
+
27
43
  class BaseException(Exception):
28
44
  """Base exception class for all EDSL exceptions.
29
-
45
+
30
46
  This class extends the standard Python Exception class to provide more helpful error messages
31
47
  by including links to relevant documentation and example notebooks when available.
32
-
48
+
33
49
  Attributes:
34
50
  relevant_doc: URL to documentation explaining this type of exception
35
51
  relevant_notebook: Optional URL to a notebook with usage examples
36
52
  """
53
+
37
54
  relevant_doc = "https://docs.expectedparrot.com/"
38
55
 
39
56
  def __init__(self, message, *, show_docs=True, log_level="error"):
40
57
  """Initialize a new BaseException with formatted error message.
41
-
58
+
42
59
  Args:
43
60
  message: The primary error message
44
61
  show_docs: If True, append documentation links to the error message
@@ -61,7 +78,7 @@ class BaseException(Exception):
61
78
  # Join with double newlines for clear separation
62
79
  final_message = "\n\n".join(formatted_message)
63
80
  super().__init__(final_message)
64
-
81
+
65
82
  # Log the exception
66
83
  if log_level == "debug":
67
84
  logger.debug(f"{self.__class__.__name__}: {message}")
@@ -100,7 +117,7 @@ class DisplayYAML:
100
117
 
101
118
  class PersistenceMixin:
102
119
  """Mixin for saving and loading objects to and from files.
103
-
120
+
104
121
  This mixin provides methods for serializing objects to various formats (JSON, YAML),
105
122
  saving to and loading from files, and interacting with cloud storage. It enables
106
123
  persistence operations like duplicating objects and uploading/downloading from the
@@ -109,21 +126,21 @@ class PersistenceMixin:
109
126
 
110
127
  def duplicate(self, add_edsl_version=False):
111
128
  """Create and return a deep copy of the object.
112
-
129
+
113
130
  Args:
114
131
  add_edsl_version: Whether to include EDSL version information in the duplicated object
115
-
132
+
116
133
  Returns:
117
134
  A new instance of the same class with identical properties
118
135
  """
119
136
  return self.from_dict(self.to_dict(add_edsl_version=False))
120
-
137
+
121
138
  @classmethod
122
139
  def help(cls):
123
140
  """Display the class documentation string.
124
-
141
+
125
142
  This is a convenience method to quickly access the docstring of the class.
126
-
143
+
127
144
  Returns:
128
145
  None, but prints the class docstring to stdout
129
146
  """
@@ -137,16 +154,16 @@ class PersistenceMixin:
137
154
  expected_parrot_url: Optional[str] = None,
138
155
  ):
139
156
  """Upload this object to the EDSL cooperative platform.
140
-
157
+
141
158
  This method serializes the object and posts it to the EDSL coop service,
142
159
  making it accessible to others or for your own use across sessions.
143
-
160
+
144
161
  Args:
145
162
  description: Optional text description of the object
146
163
  alias: Optional human-readable identifier for the object
147
164
  visibility: Access level setting ("private", "unlisted", or "public")
148
165
  expected_parrot_url: Optional custom URL for the coop service
149
-
166
+
150
167
  Returns:
151
168
  The response from the coop service containing the object's unique identifier
152
169
  """
@@ -157,13 +174,13 @@ class PersistenceMixin:
157
174
 
158
175
  def to_yaml(self, add_edsl_version=False, filename: str = None) -> Union[str, None]:
159
176
  """Convert the object to YAML format.
160
-
177
+
161
178
  Serializes the object to YAML format and optionally writes it to a file.
162
-
179
+
163
180
  Args:
164
181
  add_edsl_version: Whether to include EDSL version information
165
182
  filename: If provided, write the YAML to this file path
166
-
183
+
167
184
  Returns:
168
185
  str: The YAML string representation if no filename is provided
169
186
  None: If written to file
@@ -180,16 +197,16 @@ class PersistenceMixin:
180
197
  @classmethod
181
198
  def from_yaml(cls, yaml_str: Optional[str] = None, filename: Optional[str] = None):
182
199
  """Create an instance from YAML data.
183
-
200
+
184
201
  Deserializes a YAML string or file into a new instance of the class.
185
-
202
+
186
203
  Args:
187
204
  yaml_str: YAML string containing object data
188
205
  filename: Path to a YAML file containing object data
189
-
206
+
190
207
  Returns:
191
208
  A new instance of the class populated with the deserialized data
192
-
209
+
193
210
  Raises:
194
211
  BaseValueError: If neither yaml_str nor filename is provided
195
212
  """
@@ -204,14 +221,15 @@ class PersistenceMixin:
204
221
  return cls.from_dict(d)
205
222
  else:
206
223
  from edsl.base.exceptions import BaseValueError
224
+
207
225
  raise BaseValueError("Either yaml_str or filename must be provided.")
208
226
 
209
227
  def create_download_link(self):
210
228
  """Generate a downloadable link for this object.
211
-
229
+
212
230
  Creates a temporary file containing the serialized object and generates
213
231
  a download link that can be shared with others.
214
-
232
+
215
233
  Returns:
216
234
  str: A URL that can be used to download the object
217
235
  """
@@ -236,12 +254,77 @@ class PersistenceMixin:
236
254
  """
237
255
  from edsl.coop import Coop
238
256
  from edsl.coop import ObjectRegistry
257
+ from edsl.jobs import Jobs
239
258
 
240
- object_type = ObjectRegistry.get_object_type_by_edsl_class(cls)
241
259
  coop = Coop()
242
260
 
261
+ if issubclass(cls, Jobs):
262
+ job_status = coop.remote_inference_get(
263
+ job_uuid=str(url_or_uuid), include_json_string=True
264
+ )
265
+ job_dict = json.loads(job_status.get("job_json_string"))
266
+ return cls.from_dict(job_dict)
267
+
268
+ object_type = ObjectRegistry.get_object_type_by_edsl_class(cls)
269
+
243
270
  return coop.get(url_or_uuid, expected_object_type=object_type)
244
271
 
272
+ @classmethod
273
+ def list(
274
+ cls,
275
+ visibility: Union[VisibilityType, List[VisibilityType], None] = None,
276
+ job_status: Union[RemoteJobStatus, List[RemoteJobStatus], None] = None,
277
+ search_query: Union[str, None] = None,
278
+ page: int = 1,
279
+ page_size: int = 10,
280
+ sort_ascending: bool = False,
281
+ ) -> "CoopObjects":
282
+ """List objects from coop.
283
+
284
+ Notes:
285
+ - The visibility parameter is not supported for remote inference jobs.
286
+ - The job_status parameter is not supported for objects.
287
+ - search_query only works with the description field.
288
+ - If sort_ascending is False, then the most recently created objects are returned first.
289
+ """
290
+ from edsl.coop import Coop
291
+ from edsl.coop import ObjectRegistry
292
+ from edsl.jobs import Jobs
293
+
294
+ coop = Coop()
295
+ if issubclass(cls, Jobs):
296
+ if visibility is not None:
297
+ from edsl.base.exceptions import BaseValueError
298
+
299
+ raise BaseValueError(
300
+ "The visibility parameter is not supported for remote inference jobs."
301
+ )
302
+ return coop.remote_inference_list(
303
+ job_status,
304
+ search_query,
305
+ page,
306
+ page_size,
307
+ sort_ascending,
308
+ )
309
+
310
+ if job_status is not None:
311
+ from edsl.base.exceptions import BaseValueError
312
+
313
+ raise BaseValueError(
314
+ "The job_status parameter is not supported for objects."
315
+ )
316
+
317
+ object_type = ObjectRegistry.get_object_type_by_edsl_class(cls)
318
+
319
+ return coop.list(
320
+ object_type,
321
+ visibility,
322
+ search_query,
323
+ page,
324
+ page_size,
325
+ sort_ascending,
326
+ )
327
+
245
328
  @classmethod
246
329
  def delete(cls, url_or_uuid: Union[str, UUID]) -> None:
247
330
  """Delete the object from coop."""
@@ -369,20 +452,20 @@ class PersistenceMixin:
369
452
  Serializes the object to JSON and writes it to the specified file.
370
453
  By default, the file will be compressed using gzip. File extensions
371
454
  are handled automatically.
372
-
455
+
373
456
  Args:
374
457
  filename: Path where the file should be saved
375
458
  compress: If True, compress the file using gzip (default: True)
376
-
459
+
377
460
  Returns:
378
461
  None
379
-
462
+
380
463
  Examples:
381
464
  >>> obj.save("my_object.json.gz") # Compressed
382
465
  >>> obj.save("my_object.json", compress=False) # Uncompressed
383
466
  """
384
467
  logger.debug(f"Saving {self.__class__.__name__} to file: {filename}")
385
-
468
+
386
469
  if filename.endswith("json.gz"):
387
470
  filename = filename[:-8]
388
471
  if filename.endswith("json"):
@@ -397,20 +480,24 @@ class PersistenceMixin:
397
480
  full_file_name = filename + ".json"
398
481
  with open(filename + ".json", "w") as f:
399
482
  f.write(json.dumps(self.to_dict()))
400
-
401
- logger.info(f"Successfully saved {self.__class__.__name__} to {full_file_name}")
483
+
484
+ logger.info(
485
+ f"Successfully saved {self.__class__.__name__} to {full_file_name}"
486
+ )
402
487
  print("Saved to", full_file_name)
403
488
  except Exception as e:
404
- logger.error(f"Failed to save {self.__class__.__name__} to {filename}: {str(e)}")
489
+ logger.error(
490
+ f"Failed to save {self.__class__.__name__} to {filename}: {str(e)}"
491
+ )
405
492
  raise
406
493
 
407
494
  @staticmethod
408
495
  def open_compressed_file(filename):
409
496
  """Read and parse a compressed JSON file.
410
-
497
+
411
498
  Args:
412
499
  filename: Path to a gzipped JSON file
413
-
500
+
414
501
  Returns:
415
502
  dict: The parsed JSON content
416
503
  """
@@ -423,10 +510,10 @@ class PersistenceMixin:
423
510
  @staticmethod
424
511
  def open_regular_file(filename):
425
512
  """Read and parse an uncompressed JSON file.
426
-
513
+
427
514
  Args:
428
515
  filename: Path to a JSON file
429
-
516
+
430
517
  Returns:
431
518
  dict: The parsed JSON content
432
519
  """
@@ -437,21 +524,21 @@ class PersistenceMixin:
437
524
  @classmethod
438
525
  def load(cls, filename):
439
526
  """Load the object from a JSON file (compressed or uncompressed).
440
-
527
+
441
528
  This method deserializes an object from a file, automatically detecting
442
529
  whether the file is compressed with gzip or not.
443
-
530
+
444
531
  Args:
445
532
  filename: Path to the file to load
446
-
533
+
447
534
  Returns:
448
535
  An instance of the class populated with data from the file
449
-
536
+
450
537
  Raises:
451
538
  Various exceptions may be raised if the file doesn't exist or contains invalid data
452
539
  """
453
540
  logger.debug(f"Loading {cls.__name__} from file: {filename}")
454
-
541
+
455
542
  try:
456
543
  if filename.endswith("json.gz"):
457
544
  d = cls.open_compressed_file(filename)
@@ -461,10 +548,14 @@ class PersistenceMixin:
461
548
  logger.debug(f"Loaded regular file {filename}")
462
549
  else:
463
550
  try:
464
- logger.debug(f"Attempting to load as compressed file: {filename}.json.gz")
551
+ logger.debug(
552
+ f"Attempting to load as compressed file: {filename}.json.gz"
553
+ )
465
554
  d = cls.open_compressed_file(filename + ".json.gz")
466
555
  except Exception as e:
467
- logger.debug(f"Failed to load as compressed file, trying regular: {e}")
556
+ logger.debug(
557
+ f"Failed to load as compressed file, trying regular: {e}"
558
+ )
468
559
  d = cls.open_regular_file(filename + ".json")
469
560
  # finally:
470
561
  # raise ValueError("File must be a json or json.gz file")
@@ -478,7 +569,7 @@ class PersistenceMixin:
478
569
 
479
570
  class RegisterSubclassesMeta(ABCMeta):
480
571
  """Metaclass for automatically registering all subclasses.
481
-
572
+
482
573
  This metaclass maintains a registry of all classes that inherit from Base,
483
574
  allowing for dynamic discovery of available classes and capabilities like
484
575
  automatic deserialization. When a new class is defined with Base as its
@@ -489,7 +580,7 @@ class RegisterSubclassesMeta(ABCMeta):
489
580
 
490
581
  def __init__(cls, name, bases, nmspc):
491
582
  """Register the class in the registry upon creation.
492
-
583
+
493
584
  Args:
494
585
  name: The name of the class being created
495
586
  bases: The base classes of the class being created
@@ -502,10 +593,10 @@ class RegisterSubclassesMeta(ABCMeta):
502
593
  @staticmethod
503
594
  def get_registry(exclude_classes: Optional[list] = None):
504
595
  """Get the registry of all registered subclasses.
505
-
596
+
506
597
  Args:
507
598
  exclude_classes: Optional list of class names to exclude from the result
508
-
599
+
509
600
  Returns:
510
601
  dict: A dictionary mapping class names to class objects
511
602
  """
@@ -520,20 +611,20 @@ class RegisterSubclassesMeta(ABCMeta):
520
611
 
521
612
  class DiffMethodsMixin:
522
613
  """Mixin that adds the ability to compute differences between objects.
523
-
614
+
524
615
  This mixin provides operator overloads that enable convenient comparison and
525
616
  differencing between objects of the same class.
526
617
  """
527
-
618
+
528
619
  def __sub__(self, other):
529
620
  """Calculate the difference between this object and another.
530
-
621
+
531
622
  This overloads the subtraction operator (-) to provide an intuitive way
532
623
  to compare objects and find their differences.
533
-
624
+
534
625
  Args:
535
626
  other: Another object to compare against this one
536
-
627
+
537
628
  Returns:
538
629
  BaseDiff: An object representing the differences between the two objects
539
630
  """
@@ -544,10 +635,10 @@ class DiffMethodsMixin:
544
635
 
545
636
  def is_iterable(obj):
546
637
  """Check if an object is iterable.
547
-
638
+
548
639
  Args:
549
640
  obj: The object to check
550
-
641
+
551
642
  Returns:
552
643
  bool: True if the object is iterable, False otherwise
553
644
  """
@@ -560,15 +651,15 @@ def is_iterable(obj):
560
651
 
561
652
  class RepresentationMixin:
562
653
  """Mixin that provides rich display and representation capabilities.
563
-
654
+
564
655
  This mixin enhances objects with methods for displaying their contents in various
565
656
  formats including JSON, HTML tables, and rich terminal output. It improves the
566
657
  user experience when working with EDSL objects in notebooks and terminals.
567
658
  """
568
-
659
+
569
660
  def json(self):
570
661
  """Get a parsed JSON representation of this object.
571
-
662
+
572
663
  Returns:
573
664
  dict: The object's data as a Python dictionary
574
665
  """
@@ -576,7 +667,7 @@ class RepresentationMixin:
576
667
 
577
668
  def to_dataset(self):
578
669
  """Convert this object to a Dataset for advanced data operations.
579
-
670
+
580
671
  Returns:
581
672
  Dataset: A Dataset object containing this object's data
582
673
  """
@@ -586,7 +677,7 @@ class RepresentationMixin:
586
677
 
587
678
  def view(self):
588
679
  """Display an interactive visualization of this object.
589
-
680
+
590
681
  Returns:
591
682
  The result of the dataset's view method
592
683
  """
@@ -597,10 +688,10 @@ class RepresentationMixin:
597
688
 
598
689
  def display_dict(self):
599
690
  """Create a flattened dictionary representation for display purposes.
600
-
691
+
601
692
  This method creates a flattened view of nested structures using colon notation
602
693
  in keys to represent hierarchy.
603
-
694
+
604
695
  Returns:
605
696
  dict: A flattened dictionary suitable for display
606
697
  """
@@ -619,10 +710,10 @@ class RepresentationMixin:
619
710
 
620
711
  def print(self, format="rich"):
621
712
  """Print a formatted table representation of this object.
622
-
713
+
623
714
  Args:
624
715
  format: The output format (currently only 'rich' is supported)
625
-
716
+
626
717
  Returns:
627
718
  None, but prints a formatted table to the console
628
719
  """
@@ -641,15 +732,15 @@ class RepresentationMixin:
641
732
 
642
733
  def _repr_html_(self):
643
734
  """Generate an HTML representation for Jupyter notebooks.
644
-
735
+
645
736
  This method is automatically called by Jupyter to render the object
646
737
  as HTML in notebook cells.
647
-
738
+
648
739
  Returns:
649
740
  str: HTML representation of the object
650
741
  """
651
742
  from edsl.dataset.display.table_display import TableDisplay
652
-
743
+
653
744
  if hasattr(self, "_summary"):
654
745
  summary_dict = self._summary()
655
746
  summary_line = "".join([f" {k}: {v};" for k, v in summary_dict.items()])
@@ -665,7 +756,9 @@ class RepresentationMixin:
665
756
  else:
666
757
  class_name = self.__class__.__name__
667
758
  documentation = getattr(self, "__documentation__", "")
668
- summary_line = "<p>" + f"<a href='{documentation}'>{class_name}</a>" + "</p>"
759
+ summary_line = (
760
+ "<p>" + f"<a href='{documentation}'>{class_name}</a>" + "</p>"
761
+ )
669
762
  display_dict = self.display_dict()
670
763
  return (
671
764
  summary_line
@@ -674,7 +767,7 @@ class RepresentationMixin:
674
767
 
675
768
  def __str__(self):
676
769
  """Return the string representation of the object.
677
-
770
+
678
771
  Returns:
679
772
  str: String representation of the object
680
773
  """
@@ -683,18 +776,18 @@ class RepresentationMixin:
683
776
 
684
777
  class HashingMixin:
685
778
  """Mixin that provides consistent hashing and equality operations.
686
-
779
+
687
780
  This mixin implements __hash__ and __eq__ methods to enable using EDSL objects
688
781
  in sets and as dictionary keys. The hash is based on the object's serialized content,
689
782
  so two objects with identical content will be considered equal.
690
783
  """
691
-
784
+
692
785
  def __hash__(self) -> int:
693
786
  """Generate a hash value for this object based on its content.
694
-
787
+
695
788
  The hash is computed from the serialized dictionary representation of the object,
696
789
  excluding any version information.
697
-
790
+
698
791
  Returns:
699
792
  int: A hash value for the object
700
793
  """
@@ -702,15 +795,23 @@ class HashingMixin:
702
795
 
703
796
  return dict_hash(self.to_dict(add_edsl_version=False))
704
797
 
798
+ def get_hash(self) -> str:
799
+ """Get a string hash representation of this object based on its content.
800
+
801
+ Returns:
802
+ str: A string representation of the hash value
803
+ """
804
+ return str(self.__hash__())
805
+
705
806
  def __eq__(self, other):
706
807
  """Compare this object with another for equality.
707
-
808
+
708
809
  Two objects are considered equal if they have the same hash value,
709
810
  which means they have identical content.
710
-
811
+
711
812
  Args:
712
813
  other: Another object to compare with this one
713
-
814
+
714
815
  Returns:
715
816
  bool: True if the objects are equal, False otherwise
716
817
  """
@@ -726,21 +827,45 @@ class Base(
726
827
  metaclass=RegisterSubclassesMeta,
727
828
  ):
728
829
  """Base class for all classes in the EDSL package.
729
-
830
+
730
831
  This abstract base class combines several mixins to provide a rich set of functionality
731
832
  to all EDSL objects. It defines the core interface that all EDSL objects must implement,
732
833
  including serialization, deserialization, and code generation.
733
-
834
+
734
835
  All EDSL classes should inherit from this class to ensure consistent behavior
735
836
  and capabilities across the framework.
736
837
  """
737
838
 
839
+ def get_uuid(self) -> str:
840
+ """
841
+ Get the UUID of this object from the Expected Parrot cloud service based on its hash.
842
+
843
+ This method calculates the hash of the object and queries the cloud service
844
+ to find if there's an uploaded version with the same content. If found,
845
+ it returns the UUID of that object.
846
+
847
+ Returns:
848
+ str: The UUID of the object in the cloud service if found
849
+
850
+ Raises:
851
+ CoopServerResponseError: If the object is not found or there's an error
852
+ communicating with the server
853
+ """
854
+ from edsl.coop import Coop
855
+
856
+ # Calculate the hash of the object
857
+ object_hash = self.get_hash()
858
+
859
+ # Query the cloud service to get the UUID based on the hash
860
+ coop = Coop()
861
+ return coop.get_uuid_from_hash(object_hash)
862
+
738
863
  def keys(self):
739
864
  """Get the key names in the object's dictionary representation.
740
-
865
+
741
866
  This method returns all the keys in the serialized form of the object,
742
867
  excluding metadata keys like version information.
743
-
868
+
744
869
  Returns:
745
870
  list: A list of key names
746
871
  """
@@ -753,7 +878,7 @@ class Base(
753
878
 
754
879
  def values(self):
755
880
  """Get the values in the object's dictionary representation.
756
-
881
+
757
882
  Returns:
758
883
  set: A set containing all the values in the object
759
884
  """
@@ -764,19 +889,20 @@ class Base(
764
889
  @abstractmethod
765
890
  def example():
766
891
  """Create an example instance of this class.
767
-
892
+
768
893
  This method should be implemented by all subclasses to provide
769
894
  a convenient way to create example objects for testing and demonstration.
770
-
895
+
771
896
  Returns:
772
897
  An instance of the class with sample data
773
898
  """
774
899
  from edsl.base.exceptions import BaseNotImplementedError
900
+
775
901
  raise BaseNotImplementedError("This method is not implemented yet.")
776
-
902
+
777
903
  def json(self):
778
904
  """Get a formatted JSON representation of this object.
779
-
905
+
780
906
  Returns:
781
907
  DisplayJSON: A displayable JSON representation
782
908
  """
@@ -784,30 +910,30 @@ class Base(
784
910
 
785
911
  def yaml(self):
786
912
  """Get a formatted YAML representation of this object.
787
-
913
+
788
914
  Returns:
789
915
  DisplayYAML: A displayable YAML representation
790
916
  """
791
917
  return DisplayYAML(self.to_dict(add_edsl_version=False))
792
918
 
793
-
794
919
  @abstractmethod
795
920
  def to_dict():
796
921
  """Serialize this object to a dictionary.
797
-
922
+
798
923
  This method must be implemented by all subclasses to provide a
799
924
  standard way to serialize objects to dictionaries. The dictionary
800
925
  should contain all the data needed to reconstruct the object.
801
-
926
+
802
927
  Returns:
803
928
  dict: A dictionary representation of the object
804
929
  """
805
930
  from edsl.base.exceptions import BaseNotImplementedError
931
+
806
932
  raise BaseNotImplementedError("This method is not implemented yet.")
807
933
 
808
934
  def to_json(self):
809
935
  """Serialize this object to a JSON string.
810
-
936
+
811
937
  Returns:
812
938
  str: A JSON string representation of the object
813
939
  """
@@ -815,11 +941,11 @@ class Base(
815
941
 
816
942
  def store(self, d: dict, key_name: Optional[str] = None):
817
943
  """Store this object in a dictionary with an optional key.
818
-
944
+
819
945
  Args:
820
946
  d: The dictionary in which to store the object
821
947
  key_name: Optional key to use (defaults to the length of the dictionary)
822
-
948
+
823
949
  Returns:
824
950
  None
825
951
  """
@@ -832,39 +958,41 @@ class Base(
832
958
  @abstractmethod
833
959
  def from_dict():
834
960
  """Create an instance from a dictionary.
835
-
961
+
836
962
  This class method must be implemented by all subclasses to provide a
837
963
  standard way to deserialize objects from dictionaries.
838
-
964
+
839
965
  Returns:
840
966
  An instance of the class populated with data from the dictionary
841
967
  """
842
968
  from edsl.base.exceptions import BaseNotImplementedError
969
+
843
970
  raise BaseNotImplementedError("This method is not implemented yet.")
844
971
 
845
972
  @abstractmethod
846
973
  def code():
847
974
  """Generate Python code that recreates this object.
848
-
975
+
849
976
  This method must be implemented by all subclasses to provide a way to
850
977
  generate executable Python code that can recreate the object.
851
-
978
+
852
979
  Returns:
853
980
  str: Python code that, when executed, creates an equivalent object
854
981
  """
855
982
  from edsl.base.exceptions import BaseNotImplementedError
983
+
856
984
  raise BaseNotImplementedError("This method is not implemented yet.")
857
985
 
858
986
  def show_methods(self, show_docstrings=True):
859
987
  """Display all public methods available on this object.
860
-
988
+
861
989
  This utility method helps explore the capabilities of an object by listing
862
990
  all its public methods and optionally their documentation.
863
-
991
+
864
992
  Args:
865
993
  show_docstrings: If True, print method names with docstrings;
866
994
  if False, return the list of method names
867
-
995
+
868
996
  Returns:
869
997
  None or list: If show_docstrings is True, prints methods and returns None.
870
998
  If show_docstrings is False, returns a list of method names.
@@ -883,14 +1011,14 @@ class Base(
883
1011
 
884
1012
  class BaseDiffCollection(UserList):
885
1013
  """A collection of difference objects that can be applied in sequence.
886
-
1014
+
887
1015
  This class represents a series of differences between objects that can be
888
1016
  applied sequentially to transform one object into another through several steps.
889
1017
  """
890
-
1018
+
891
1019
  def __init__(self, diffs=None):
892
1020
  """Initialize a new BaseDiffCollection.
893
-
1021
+
894
1022
  Args:
895
1023
  diffs: Optional list of BaseDiff objects to include in the collection
896
1024
  """
@@ -900,10 +1028,10 @@ class BaseDiffCollection(UserList):
900
1028
 
901
1029
  def apply(self, obj: Any):
902
1030
  """Apply all diffs in the collection to an object in sequence.
903
-
1031
+
904
1032
  Args:
905
1033
  obj: The object to transform
906
-
1034
+
907
1035
  Returns:
908
1036
  The transformed object after applying all diffs
909
1037
  """
@@ -913,10 +1041,10 @@ class BaseDiffCollection(UserList):
913
1041
 
914
1042
  def add_diff(self, diff) -> "BaseDiffCollection":
915
1043
  """Add a new diff to the collection.
916
-
1044
+
917
1045
  Args:
918
1046
  diff: The BaseDiff object to add
919
-
1047
+
920
1048
  Returns:
921
1049
  BaseDiffCollection: self, for method chaining
922
1050
  """
@@ -926,14 +1054,14 @@ class BaseDiffCollection(UserList):
926
1054
 
927
1055
  class DummyObject:
928
1056
  """A simple class that can be used to wrap a dictionary for diffing purposes.
929
-
1057
+
930
1058
  This utility class is used internally to compare dictionaries by adapting them
931
1059
  to the same interface as EDSL objects.
932
1060
  """
933
-
1061
+
934
1062
  def __init__(self, object_dict):
935
1063
  """Initialize a new DummyObject.
936
-
1064
+
937
1065
  Args:
938
1066
  object_dict: A dictionary to wrap
939
1067
  """
@@ -941,7 +1069,7 @@ class DummyObject:
941
1069
 
942
1070
  def to_dict(self):
943
1071
  """Get the wrapped dictionary.
944
-
1072
+
945
1073
  Returns:
946
1074
  dict: The wrapped dictionary
947
1075
  """
@@ -950,20 +1078,20 @@ class DummyObject:
950
1078
 
951
1079
  class BaseDiff:
952
1080
  """Represents the differences between two EDSL objects.
953
-
1081
+
954
1082
  This class computes and stores the differences between two objects in terms of:
955
1083
  - Added keys/values (present in obj2 but not in obj1)
956
1084
  - Removed keys/values (present in obj1 but not in obj2)
957
1085
  - Modified keys/values (present in both but with different values)
958
-
1086
+
959
1087
  The differences can be displayed for inspection or applied to transform objects.
960
1088
  """
961
-
1089
+
962
1090
  def __init__(
963
1091
  self, obj1: Any, obj2: Any, added=None, removed=None, modified=None, level=0
964
1092
  ):
965
1093
  """Initialize a new BaseDiff between two objects.
966
-
1094
+
967
1095
  Args:
968
1096
  obj1: The first object (considered the "from" object)
969
1097
  obj2: The second object (considered the "to" object)
@@ -991,7 +1119,7 @@ class BaseDiff:
991
1119
 
992
1120
  def __bool__(self):
993
1121
  """Determine if there are any differences between the objects.
994
-
1122
+
995
1123
  Returns:
996
1124
  bool: True if there are differences, False if objects are identical
997
1125
  """
@@ -1000,7 +1128,7 @@ class BaseDiff:
1000
1128
  @property
1001
1129
  def added(self):
1002
1130
  """Get keys and values present in obj2 but not in obj1.
1003
-
1131
+
1004
1132
  Returns:
1005
1133
  dict: Keys and values that were added
1006
1134
  """
@@ -1010,12 +1138,12 @@ class BaseDiff:
1010
1138
 
1011
1139
  def __add__(self, other):
1012
1140
  """Apply this diff to another object.
1013
-
1141
+
1014
1142
  This overloads the + operator to allow applying diffs with a natural syntax.
1015
-
1143
+
1016
1144
  Args:
1017
1145
  other: The object to apply the diff to
1018
-
1146
+
1019
1147
  Returns:
1020
1148
  The transformed object
1021
1149
  """
@@ -1024,7 +1152,7 @@ class BaseDiff:
1024
1152
  @added.setter
1025
1153
  def added(self, value):
1026
1154
  """Set the added keys/values.
1027
-
1155
+
1028
1156
  Args:
1029
1157
  value: Dict of added keys/values or None to compute automatically
1030
1158
  """
@@ -1033,7 +1161,7 @@ class BaseDiff:
1033
1161
  @property
1034
1162
  def removed(self):
1035
1163
  """Get keys and values present in obj1 but not in obj2.
1036
-
1164
+
1037
1165
  Returns:
1038
1166
  dict: Keys and values that were removed
1039
1167
  """
@@ -1044,7 +1172,7 @@ class BaseDiff:
1044
1172
  @removed.setter
1045
1173
  def removed(self, value):
1046
1174
  """Set the removed keys/values.
1047
-
1175
+
1048
1176
  Args:
1049
1177
  value: Dict of removed keys/values or None to compute automatically
1050
1178
  """
@@ -1053,7 +1181,7 @@ class BaseDiff:
1053
1181
  @property
1054
1182
  def modified(self):
1055
1183
  """Get keys present in both objects but with different values.
1056
-
1184
+
1057
1185
  Returns:
1058
1186
  dict: Keys and their old/new values that were modified
1059
1187
  """
@@ -1064,7 +1192,7 @@ class BaseDiff:
1064
1192
  @modified.setter
1065
1193
  def modified(self, value):
1066
1194
  """Set the modified keys/values.
1067
-
1195
+
1068
1196
  Args:
1069
1197
  value: Dict of modified keys/values or None to compute automatically
1070
1198
  """
@@ -1072,7 +1200,7 @@ class BaseDiff:
1072
1200
 
1073
1201
  def _find_added(self) -> Dict[Any, Any]:
1074
1202
  """Find keys that exist in obj2 but not in obj1.
1075
-
1203
+
1076
1204
  Returns:
1077
1205
  dict: Keys and values that were added
1078
1206
  """
@@ -1080,7 +1208,7 @@ class BaseDiff:
1080
1208
 
1081
1209
  def _find_removed(self) -> Dict[Any, Any]:
1082
1210
  """Find keys that exist in obj1 but not in obj2.
1083
-
1211
+
1084
1212
  Returns:
1085
1213
  dict: Keys and values that were removed
1086
1214
  """
@@ -1088,10 +1216,10 @@ class BaseDiff:
1088
1216
 
1089
1217
  def _find_modified(self) -> Dict[Any, Tuple[Any, Any, str]]:
1090
1218
  """Find keys that exist in both objects but have different values.
1091
-
1219
+
1092
1220
  The difference calculation is type-aware and handles strings, dictionaries,
1093
1221
  and lists specially to provide more detailed difference information.
1094
-
1222
+
1095
1223
  Returns:
1096
1224
  dict: Keys mapped to tuples of (old_value, new_value, diff_details)
1097
1225
  """
@@ -1122,10 +1250,10 @@ class BaseDiff:
1122
1250
  @staticmethod
1123
1251
  def is_json(string_that_could_be_json: str) -> bool:
1124
1252
  """Check if a string is valid JSON.
1125
-
1253
+
1126
1254
  Args:
1127
1255
  string_that_could_be_json: The string to check
1128
-
1256
+
1129
1257
  Returns:
1130
1258
  bool: True if the string is valid JSON, False otherwise
1131
1259
  """
@@ -1137,11 +1265,11 @@ class BaseDiff:
1137
1265
 
1138
1266
  def _diff_dicts(self, dict1: Dict[str, Any], dict2: Dict[str, Any]) -> "BaseDiff":
1139
1267
  """Calculate the differences between two dictionaries.
1140
-
1268
+
1141
1269
  Args:
1142
1270
  dict1: The first dictionary
1143
1271
  dict2: The second dictionary
1144
-
1272
+
1145
1273
  Returns:
1146
1274
  BaseDiff: A difference object between the dictionaries
1147
1275
  """
@@ -1150,14 +1278,14 @@ class BaseDiff:
1150
1278
 
1151
1279
  def _diff_strings(self, str1: str, str2: str) -> str:
1152
1280
  """Calculate the differences between two strings.
1153
-
1281
+
1154
1282
  If both strings are valid JSON, they are compared as dictionaries.
1155
1283
  Otherwise, they are compared line by line.
1156
-
1284
+
1157
1285
  Args:
1158
1286
  str1: The first string
1159
1287
  str2: The second string
1160
-
1288
+
1161
1289
  Returns:
1162
1290
  Union[BaseDiff, Iterable[str]]: A diff object or line-by-line differences
1163
1291
  """
@@ -1169,13 +1297,13 @@ class BaseDiff:
1169
1297
 
1170
1298
  def apply(self, obj: Any):
1171
1299
  """Apply this diff to transform an object.
1172
-
1300
+
1173
1301
  This method applies the computed differences to an object, adding new keys,
1174
1302
  removing deleted keys, and updating modified values.
1175
-
1303
+
1176
1304
  Args:
1177
1305
  obj: The object to transform
1178
-
1306
+
1179
1307
  Returns:
1180
1308
  The transformed object
1181
1309
  """
@@ -1191,7 +1319,7 @@ class BaseDiff:
1191
1319
 
1192
1320
  def to_dict(self) -> Dict[str, Any]:
1193
1321
  """Serialize this difference object to a dictionary.
1194
-
1322
+
1195
1323
  Returns:
1196
1324
  dict: A dictionary representation of the differences
1197
1325
  """
@@ -1208,12 +1336,12 @@ class BaseDiff:
1208
1336
  @classmethod
1209
1337
  def from_dict(cls, diff_dict: Dict[str, Any], obj1: Any, obj2: Any):
1210
1338
  """Create a BaseDiff from a dictionary representation.
1211
-
1339
+
1212
1340
  Args:
1213
1341
  diff_dict: Dictionary containing the difference data
1214
1342
  obj1: The first object
1215
1343
  obj2: The second object
1216
-
1344
+
1217
1345
  Returns:
1218
1346
  BaseDiff: A new difference object
1219
1347
  """
@@ -1228,14 +1356,14 @@ class BaseDiff:
1228
1356
 
1229
1357
  class Results(UserList):
1230
1358
  """Helper class for storing and formatting difference results.
1231
-
1359
+
1232
1360
  This class extends UserList to provide indentation and formatting
1233
1361
  capabilities when displaying differences.
1234
1362
  """
1235
-
1363
+
1236
1364
  def __init__(self, prepend=" ", level=0):
1237
1365
  """Initialize a new Results collection.
1238
-
1366
+
1239
1367
  Args:
1240
1368
  prepend: The string to use for indentation
1241
1369
  level: The nesting level
@@ -1246,7 +1374,7 @@ class BaseDiff:
1246
1374
 
1247
1375
  def append(self, item):
1248
1376
  """Add an item to the results with proper indentation.
1249
-
1377
+
1250
1378
  Args:
1251
1379
  item: The string to add
1252
1380
  """
@@ -1254,7 +1382,7 @@ class BaseDiff:
1254
1382
 
1255
1383
  def __str__(self):
1256
1384
  """Generate a human-readable string representation of the differences.
1257
-
1385
+
1258
1386
  Returns:
1259
1387
  str: A formatted string showing the differences
1260
1388
  """
@@ -1285,7 +1413,7 @@ class BaseDiff:
1285
1413
 
1286
1414
  def __repr__(self):
1287
1415
  """Generate a developer-friendly string representation.
1288
-
1416
+
1289
1417
  Returns:
1290
1418
  str: A representation that can be used to recreate the object
1291
1419
  """
@@ -1296,19 +1424,21 @@ class BaseDiff:
1296
1424
 
1297
1425
  def add_diff(self, diff) -> "BaseDiffCollection":
1298
1426
  """Combine this diff with another into a collection.
1299
-
1427
+
1300
1428
  Args:
1301
1429
  diff: Another BaseDiff object
1302
-
1430
+
1303
1431
  Returns:
1304
1432
  BaseDiffCollection: A collection containing both diffs
1305
1433
  """
1306
1434
  from edsl.base import BaseDiffCollection
1435
+
1307
1436
  return BaseDiffCollection([self, diff])
1308
1437
 
1309
1438
 
1310
1439
  if __name__ == "__main__":
1311
- import doctest
1440
+ import doctest
1441
+
1312
1442
  doctest.testmod()
1313
1443
 
1314
1444
  from edsl import Question
@@ -1319,7 +1449,7 @@ if __name__ == "__main__":
1319
1449
  diff1 = q_ft - q_mc
1320
1450
  assert q_ft == q_mc + diff1
1321
1451
  assert q_ft == diff1.apply(q_mc)
1322
-
1452
+
1323
1453
  # ## Test chain of diffs
1324
1454
  q0 = Question.example("free_text")
1325
1455
  q1 = q0.copy()
@@ -1335,4 +1465,4 @@ if __name__ == "__main__":
1335
1465
  assert new_q2 == q2
1336
1466
 
1337
1467
  new_q2 = diff_chain + q0
1338
- assert new_q2 == q2
1468
+ assert new_q2 == q2