edsl 0.1.61__py3-none-any.whl → 0.1.62__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.
edsl/__version__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.61"
1
+ __version__ = "0.1.62"
edsl/base/base_class.py CHANGED
@@ -331,9 +331,17 @@ class PersistenceMixin:
331
331
  """
332
332
  from edsl.coop import Coop
333
333
  from edsl.coop import ObjectRegistry
334
+ from edsl.jobs import Jobs
334
335
 
335
336
  coop = Coop(url=expected_parrot_url)
336
337
 
338
+ if issubclass(cls, Jobs):
339
+ job_data = coop.new_remote_inference_get(
340
+ str(url_or_uuid), include_json_string=True
341
+ )
342
+ job_dict = json.loads(job_data.get("job_json_string"))
343
+ return cls.from_dict(job_dict)
344
+
337
345
  object_type = ObjectRegistry.get_object_type_by_edsl_class(cls)
338
346
 
339
347
  return coop.pull(url_or_uuid, object_type)
edsl/coop/coop.py CHANGED
@@ -598,7 +598,7 @@ class Coop(CoopFunctionsMixin):
598
598
  else:
599
599
  from .exceptions import CoopResponseError
600
600
 
601
- raise CoopResponseError("No signed url was provided received")
601
+ raise CoopResponseError("No signed url was provided.")
602
602
 
603
603
  response = requests.put(
604
604
  signed_url, data=json_data.encode(), headers=headers
@@ -945,18 +945,31 @@ class Coop(CoopFunctionsMixin):
945
945
 
946
946
  obj_uuid, owner_username, obj_alias = self._resolve_uuid_or_alias(url_or_uuid)
947
947
 
948
- # If we have a UUID and are updating the value, check the storage format first
949
- if obj_uuid and value:
950
- # Check if object is in new format (GCS)
951
- format_check_response = self._send_server_request(
952
- uri="api/v0/object/check-format",
953
- method="POST",
954
- payload={"object_uuid": str(obj_uuid)},
955
- )
956
- self._resolve_server_response(format_check_response)
957
- format_data = format_check_response.json()
948
+ # If we're updating the value, we need to check the storage format
949
+ if value:
950
+ # If we don't have a UUID but have an alias, get the UUID and format info first
951
+ if not obj_uuid and owner_username and obj_alias:
952
+ # Get object info including UUID and format
953
+ info_response = self._send_server_request(
954
+ uri="api/v0/object/alias/info",
955
+ method="GET",
956
+ params={"owner_username": owner_username, "alias": obj_alias},
957
+ )
958
+ self._resolve_server_response(info_response)
959
+ info_data = info_response.json()
958
960
 
959
- is_new_format = format_data.get("is_new_format", False)
961
+ obj_uuid = info_data.get("uuid")
962
+ is_new_format = info_data.get("is_new_format", False)
963
+ else:
964
+ # We have a UUID, check the format
965
+ format_check_response = self._send_server_request(
966
+ uri="api/v0/object/check-format",
967
+ method="POST",
968
+ payload={"object_uuid": str(obj_uuid)},
969
+ )
970
+ self._resolve_server_response(format_check_response)
971
+ format_data = format_check_response.json()
972
+ is_new_format = format_data.get("is_new_format", False)
960
973
 
961
974
  if is_new_format:
962
975
  # Handle new format objects: update metadata first, then upload content
@@ -1052,10 +1065,20 @@ class Coop(CoopFunctionsMixin):
1052
1065
  f"Failed to upload object to GCS: {gcs_response.status_code}"
1053
1066
  )
1054
1067
 
1068
+ # Step 4: Confirm upload and trigger queue worker processing
1069
+ confirm_response = self._send_server_request(
1070
+ uri="api/v0/object/confirm-upload",
1071
+ method="POST",
1072
+ payload={"object_uuid": str(obj_uuid)},
1073
+ )
1074
+ self._resolve_server_response(confirm_response)
1075
+ confirm_data = confirm_response.json()
1076
+
1055
1077
  return {
1056
1078
  "status": "success",
1057
- "message": "Object updated successfully (new format - uploaded to GCS)",
1079
+ "message": "Object updated successfully (new format - uploaded to GCS and processing triggered)",
1058
1080
  "object_uuid": str(obj_uuid),
1081
+ "processing_started": confirm_data.get("processing_started", False),
1059
1082
  }
1060
1083
 
1061
1084
  ################
@@ -1195,7 +1218,7 @@ class Coop(CoopFunctionsMixin):
1195
1218
  if not upload_signed_url:
1196
1219
  from .exceptions import CoopResponseError
1197
1220
 
1198
- raise CoopResponseError("No signed url was provided received")
1221
+ raise CoopResponseError("No signed url was provided.")
1199
1222
 
1200
1223
  response = requests.put(
1201
1224
  upload_signed_url,
@@ -1431,6 +1454,160 @@ class Coop(CoopFunctionsMixin):
1431
1454
  }
1432
1455
  )
1433
1456
 
1457
+ def new_remote_inference_get(
1458
+ self,
1459
+ job_uuid: Optional[str] = None,
1460
+ results_uuid: Optional[str] = None,
1461
+ include_json_string: Optional[bool] = False,
1462
+ ) -> RemoteInferenceResponse:
1463
+ """
1464
+ Get the status and details of a remote inference job.
1465
+
1466
+ This method retrieves the current status and information about a remote job,
1467
+ including links to results if the job has completed successfully.
1468
+
1469
+ Parameters:
1470
+ job_uuid (str, optional): The UUID of the remote job to check
1471
+ results_uuid (str, optional): The UUID of the results associated with the job
1472
+ (can be used if you only have the results UUID)
1473
+ include_json_string (bool, optional): If True, include the json string for the job in the response
1474
+
1475
+ Returns:
1476
+ RemoteInferenceResponse: Information about the job including:
1477
+ job_uuid: The unique identifier for the job
1478
+ results_uuid: The UUID of the results
1479
+ results_url: URL to access the results
1480
+ status: Current status ("queued", "running", "completed", "failed")
1481
+ version: EDSL version used for the job
1482
+ job_json_string: The json string for the job (if include_json_string is True)
1483
+ latest_job_run_details: Metadata about the job status
1484
+ interview_details: Metadata about the job interview status (for jobs that have reached running status)
1485
+ total_interviews: The total number of interviews in the job
1486
+ completed_interviews: The number of completed interviews
1487
+ interviews_with_exceptions: The number of completed interviews that have exceptions
1488
+ exception_counters: A list of exception counts for the job
1489
+ exception_type: The type of exception
1490
+ inference_service: The inference service
1491
+ model: The model
1492
+ question_name: The name of the question
1493
+ exception_count: The number of exceptions
1494
+ failure_reason: The reason the job failed (failed jobs only)
1495
+ failure_description: The description of the failure (failed jobs only)
1496
+ error_report_uuid: The UUID of the error report (partially failed jobs only)
1497
+ cost_credits: The cost of the job run in credits
1498
+ cost_usd: The cost of the job run in USD
1499
+ expenses: The expenses incurred by the job run
1500
+ service: The service
1501
+ model: The model
1502
+ token_type: The type of token (input or output)
1503
+ price_per_million_tokens: The price per million tokens
1504
+ tokens_count: The number of tokens consumed
1505
+ cost_credits: The cost of the service/model/token type combination in credits
1506
+ cost_usd: The cost of the service/model/token type combination in USD
1507
+
1508
+ Raises:
1509
+ ValueError: If neither job_uuid nor results_uuid is provided
1510
+ CoopServerResponseError: If there's an error communicating with the server
1511
+
1512
+ Notes:
1513
+ - Either job_uuid or results_uuid must be provided
1514
+ - If both are provided, job_uuid takes precedence
1515
+ - For completed jobs, you can use the results_url to view or download results
1516
+ - For failed jobs, check the latest_error_report_url for debugging information
1517
+
1518
+ Example:
1519
+ >>> job_status = coop.new_remote_inference_get("9f8484ee-b407-40e4-9652-4133a7236c9c")
1520
+ >>> print(f"Job status: {job_status['status']}")
1521
+ >>> if job_status['status'] == 'completed':
1522
+ ... print(f"Results available at: {job_status['results_url']}")
1523
+ """
1524
+ if job_uuid is None and results_uuid is None:
1525
+ from .exceptions import CoopValueError
1526
+
1527
+ raise CoopValueError("Either job_uuid or results_uuid must be provided.")
1528
+ elif job_uuid is not None:
1529
+ params = {"job_uuid": job_uuid}
1530
+ else:
1531
+ params = {"results_uuid": results_uuid}
1532
+ if include_json_string:
1533
+ params["include_json_string"] = include_json_string
1534
+
1535
+ response = self._send_server_request(
1536
+ uri="api/v0/remote-inference",
1537
+ method="GET",
1538
+ params=params,
1539
+ )
1540
+ self._resolve_server_response(response)
1541
+ data = response.json()
1542
+
1543
+ results_uuid = data.get("results_uuid")
1544
+
1545
+ if results_uuid is None:
1546
+ results_url = None
1547
+ else:
1548
+ results_url = f"{self.url}/content/{results_uuid}"
1549
+
1550
+ latest_job_run_details = data.get("latest_job_run_details", {})
1551
+ if data.get("status") == "partial_failed":
1552
+ latest_error_report_uuid = latest_job_run_details.get("error_report_uuid")
1553
+ if latest_error_report_uuid is None:
1554
+ latest_job_run_details["error_report_url"] = None
1555
+ else:
1556
+ latest_error_report_url = (
1557
+ f"{self.url}/home/remote-inference/error/{latest_error_report_uuid}"
1558
+ )
1559
+ latest_job_run_details["error_report_url"] = latest_error_report_url
1560
+
1561
+ json_string = data.get("job_json_string")
1562
+
1563
+ # The job has been offloaded to GCS
1564
+ if include_json_string and json_string == "offloaded":
1565
+
1566
+ # Attempt to fetch JSON string from GCS
1567
+ response = self._send_server_request(
1568
+ uri="api/v0/remote-inference/pull",
1569
+ method="POST",
1570
+ payload={"job_uuid": job_uuid},
1571
+ )
1572
+ # Handle any errors in the response
1573
+ self._resolve_server_response(response)
1574
+ if "signed_url" not in response.json():
1575
+ from .exceptions import CoopResponseError
1576
+
1577
+ raise CoopResponseError("No signed url was provided.")
1578
+ signed_url = response.json().get("signed_url")
1579
+
1580
+ if signed_url == "": # The job is in legacy format
1581
+ job_json = json_string
1582
+
1583
+ try:
1584
+ response = requests.get(signed_url)
1585
+ self._resolve_gcs_response(response)
1586
+ job_json = json.dumps(response.json())
1587
+ except Exception:
1588
+ job_json = json_string
1589
+
1590
+ # If the job is in legacy format, we should already have the JSON string
1591
+ # from the first API call
1592
+ elif include_json_string and not json_string == "offloaded":
1593
+ job_json = json_string
1594
+
1595
+ # If include_json_string is False, we don't need the JSON string at all
1596
+ else:
1597
+ job_json = None
1598
+
1599
+ return RemoteInferenceResponse(
1600
+ **{
1601
+ "job_uuid": data.get("job_uuid"),
1602
+ "results_uuid": results_uuid,
1603
+ "results_url": results_url,
1604
+ "status": data.get("status"),
1605
+ "version": data.get("version"),
1606
+ "job_json_string": job_json,
1607
+ "latest_job_run_details": latest_job_run_details,
1608
+ }
1609
+ )
1610
+
1434
1611
  def _validate_remote_job_status_types(
1435
1612
  self, status: Union[RemoteJobStatus, List[RemoteJobStatus]]
1436
1613
  ) -> List[RemoteJobStatus]:
@@ -2470,7 +2647,7 @@ class Coop(CoopFunctionsMixin):
2470
2647
  if "signed_url" not in response.json():
2471
2648
  from .exceptions import CoopResponseError
2472
2649
 
2473
- raise CoopResponseError("No signed url was provided received")
2650
+ raise CoopResponseError("No signed url was provided.")
2474
2651
  signed_url = response.json().get("signed_url")
2475
2652
 
2476
2653
  if signed_url == "": # it is in old format
@@ -2872,6 +3049,53 @@ class Coop(CoopFunctionsMixin):
2872
3049
  self._resolve_server_response(response)
2873
3050
  return response.json()
2874
3051
 
3052
+ def pay_for_service(
3053
+ self,
3054
+ credits_transferred: int,
3055
+ recipient_username: str,
3056
+ service_name: str,
3057
+ ) -> dict:
3058
+ """
3059
+ Pay for a service.
3060
+
3061
+ This method transfers a specified number of credits from the authenticated user's
3062
+ account to another user's account on the Expected Parrot platform.
3063
+
3064
+ Parameters:
3065
+ credits_transferred (int): The number of credits to transfer to the recipient
3066
+ recipient_username (str): The username of the recipient
3067
+ service_name (str): The name of the service to pay for
3068
+
3069
+ Returns:
3070
+ dict: Information about the transfer transaction, including:
3071
+ - success: Whether the transaction was successful
3072
+ - transaction_id: A unique identifier for the transaction
3073
+ - remaining_credits: The number of credits remaining in the sender's account
3074
+
3075
+ Raises:
3076
+ CoopServerResponseError: If there's an error communicating with the server
3077
+ or if the transfer criteria aren't met (e.g., insufficient credits)
3078
+
3079
+ Example:
3080
+ >>> result = coop.pay_for_service(
3081
+ ... credits_transferred=100,
3082
+ ... service_name="service_name",
3083
+ ... recipient_username="friend_username",
3084
+ ... )
3085
+ >>> print(f"Transfer successful! You have {result['remaining_credits']} credits left.")
3086
+ """
3087
+ response = self._send_server_request(
3088
+ uri="api/v0/users/pay-for-service",
3089
+ method="POST",
3090
+ payload={
3091
+ "cost_credits": credits_transferred,
3092
+ "service_name": service_name,
3093
+ "recipient_username": recipient_username,
3094
+ },
3095
+ )
3096
+ self._resolve_server_response(response)
3097
+ return response.json()
3098
+
2875
3099
  def get_balance(self) -> dict:
2876
3100
  """
2877
3101
  Get the current credit balance for the authenticated user.
@@ -2897,6 +3121,29 @@ class Coop(CoopFunctionsMixin):
2897
3121
  self._resolve_server_response(response)
2898
3122
  return response.json()
2899
3123
 
3124
+ def get_profile(self) -> dict:
3125
+ """
3126
+ Get the current user's profile information.
3127
+
3128
+ This method retrieves the authenticated user's profile information from
3129
+ the Expected Parrot platform using their API key.
3130
+
3131
+ Returns:
3132
+ dict: User profile information including:
3133
+ - username: The user's username
3134
+ - email: The user's email address
3135
+
3136
+ Raises:
3137
+ CoopServerResponseError: If there's an error communicating with the server
3138
+
3139
+ Example:
3140
+ >>> profile = coop.get_profile()
3141
+ >>> print(f"Welcome, {profile['username']}!")
3142
+ """
3143
+ response = self._send_server_request(uri="api/v0/users/profile", method="GET")
3144
+ self._resolve_server_response(response)
3145
+ return response.json()
3146
+
2900
3147
  def login_gradio(self, timeout: int = 120, launch: bool = True, **launch_kwargs):
2901
3148
  """
2902
3149
  Start the EDSL auth token login flow inside a **Gradio** application.
@@ -3174,7 +3421,7 @@ def main():
3174
3421
  job = Jobs.example()
3175
3422
  coop.remote_inference_cost(job)
3176
3423
  job_coop_object = coop.remote_inference_create(job)
3177
- job_coop_results = coop.remote_inference_get(job_coop_object.get("uuid"))
3424
+ job_coop_results = coop.new_remote_inference_get(job_coop_object.get("uuid"))
3178
3425
  coop.get(job_coop_results.get("results_uuid"))
3179
3426
 
3180
3427
  import streamlit as st
@@ -26,7 +26,7 @@ class CoopJobsObjects(CoopObjects):
26
26
 
27
27
  c = Coop()
28
28
  job_details = [
29
- c.remote_inference_get(obj["uuid"], include_json_string=True)
29
+ c.new_remote_inference_get(obj["uuid"], include_json_string=True)
30
30
  for obj in self
31
31
  ]
32
32
 
@@ -53,7 +53,7 @@ class CoopJobsObjects(CoopObjects):
53
53
 
54
54
  for obj in self:
55
55
  if obj.get("results_uuid"):
56
- result = c.get(obj["results_uuid"])
56
+ result = c.pull(obj["results_uuid"], expected_object_type="results")
57
57
  results.append(result)
58
58
 
59
59
  return results
@@ -23,4 +23,6 @@ class CoopRegularObjects(CoopObjects):
23
23
  from ..coop import Coop
24
24
 
25
25
  c = Coop()
26
- return [c.get(obj["uuid"]) for obj in self]
26
+ return [
27
+ c.pull(obj["uuid"], expected_object_type=obj["object_type"]) for obj in self
28
+ ]
edsl/jobs/jobs.py CHANGED
@@ -1120,7 +1120,7 @@ class Jobs(Base):
1120
1120
  raise CoopValueError(
1121
1121
  "You must specify both a scenario list and a scenario list method to use scenarios with your survey."
1122
1122
  )
1123
- elif scenario_list_method is "loop":
1123
+ elif scenario_list_method == "loop":
1124
1124
  questions, long_scenario_list = self.survey.to_long_format(self.scenarios)
1125
1125
 
1126
1126
  # Replace the questions with new ones from the loop method
@@ -176,7 +176,7 @@ class JobsRemoteInferenceHandler:
176
176
  from ..coop import Coop
177
177
 
178
178
  coop = Coop()
179
- return coop.remote_inference_get(job_uuid)
179
+ return coop.new_remote_inference_get(job_uuid)
180
180
 
181
181
  def _construct_remote_job_fetcher(
182
182
  self, testing_simulated_response: Optional[Any] = None
@@ -445,9 +445,9 @@ class JobsRemoteInferenceHandler:
445
445
  model_cost_dict["input_cost_credits_with_cache"] = converter.usd_to_credits(
446
446
  input_cost_with_cache
447
447
  )
448
- model_cost_dict[
449
- "output_cost_credits_with_cache"
450
- ] = converter.usd_to_credits(output_cost_with_cache)
448
+ model_cost_dict["output_cost_credits_with_cache"] = (
449
+ converter.usd_to_credits(output_cost_with_cache)
450
+ )
451
451
  return list(expenses_by_model.values())
452
452
 
453
453
  def _fetch_results_and_log(
edsl/prompts/prompt.py CHANGED
@@ -305,8 +305,13 @@ class Prompt(PersistenceMixin, RepresentationMixin):
305
305
  Returns (rendered_text, captured_variables).
306
306
  """
307
307
  # Combine replacements.
308
- all_replacements = {**primary_replacement, **additional_replacements}
309
-
308
+ from ..scenarios import Scenario
309
+ # This fixed Issue 2027 - the scenario prefix was not being recoginized in the template
310
+ if isinstance(primary_replacement, Scenario):
311
+ additional = {'scenario': primary_replacement.to_dict()}
312
+ else:
313
+ additional = {}
314
+ all_replacements = {**primary_replacement, **additional_replacements, **additional}
310
315
  # If no replacements and no Jinja variables, just return the text.
311
316
  if not all_replacements and not _find_template_variables(text):
312
317
  return text, template_vars.get_all()
@@ -43,6 +43,7 @@ class Question(metaclass=Meta):
43
43
  subclass = get_question_classes.get(question_type, None)
44
44
  if subclass is None:
45
45
  from .exceptions import QuestionValueError
46
+
46
47
  raise QuestionValueError(
47
48
  f"No question registered with question_type {question_type}"
48
49
  )
@@ -65,7 +66,7 @@ class Question(metaclass=Meta):
65
66
  from ..coop import Coop
66
67
 
67
68
  coop = Coop()
68
- return coop.get(url_or_uuid, "question")
69
+ return coop.pull(url_or_uuid, "question")
69
70
 
70
71
  @classmethod
71
72
  def delete(cls, url_or_uuid: Union[str, UUID]):
@@ -146,6 +147,7 @@ def get_question_class(question_type):
146
147
  q2c = RegisterQuestionsMeta.question_types_to_classes()
147
148
  if question_type not in q2c:
148
149
  from .exceptions import QuestionValueError
150
+
149
151
  raise QuestionValueError(
150
152
  f"The question type, {question_type}, is not recognized. Recognied types are: {q2c.keys()}"
151
153
  )
@@ -171,4 +173,5 @@ question_purpose = {
171
173
 
172
174
  if __name__ == "__main__":
173
175
  import doctest
176
+
174
177
  doctest.testmod()
@@ -512,6 +512,75 @@ class FileStore(Scenario):
512
512
  )
513
513
  return info
514
514
 
515
+ def offload(self, inplace=False) -> "FileStore":
516
+ """
517
+ Offloads base64-encoded content from the FileStore by replacing 'base64_string'
518
+ with 'offloaded'. This reduces memory usage.
519
+
520
+ Args:
521
+ inplace (bool): If True, modify the current FileStore. If False, return a new one.
522
+
523
+ Returns:
524
+ FileStore: The modified FileStore (either self or a new instance).
525
+ """
526
+ if inplace:
527
+ if hasattr(self, "base64_string"):
528
+ self.base64_string = "offloaded"
529
+ return self
530
+ else:
531
+ # Create a copy and offload it
532
+ file_store_dict = self.to_dict()
533
+ if "base64_string" in file_store_dict:
534
+ file_store_dict["base64_string"] = "offloaded"
535
+ return self.__class__.from_dict(file_store_dict)
536
+
537
+ def save_to_gcs_bucket(self, signed_url: str) -> dict:
538
+ """
539
+ Saves the FileStore's file content to a Google Cloud Storage bucket using a signed URL.
540
+
541
+ Args:
542
+ signed_url (str): The signed URL for uploading to GCS bucket
543
+
544
+ Returns:
545
+ dict: Response from the GCS upload operation
546
+
547
+ Raises:
548
+ ValueError: If base64_string is offloaded or missing
549
+ requests.RequestException: If the upload fails
550
+ """
551
+ import requests
552
+ import base64
553
+
554
+ # Check if content is available
555
+ if not hasattr(self, "base64_string") or self.base64_string == "offloaded":
556
+ raise ValueError(
557
+ "File content is not available (offloaded or missing). Cannot upload to GCS."
558
+ )
559
+
560
+ # Decode base64 content to bytes
561
+ try:
562
+ file_content = base64.b64decode(self.base64_string)
563
+ except Exception as e:
564
+ raise ValueError(f"Failed to decode base64 content: {e}")
565
+
566
+ # Prepare headers with proper content type
567
+ headers = {
568
+ "Content-Type": self.mime_type or "application/octet-stream",
569
+ "Content-Length": str(len(file_content)),
570
+ }
571
+
572
+ # Upload to GCS using the signed URL
573
+ response = requests.put(signed_url, data=file_content, headers=headers)
574
+ response.raise_for_status()
575
+
576
+ return {
577
+ "status": "success",
578
+ "status_code": response.status_code,
579
+ "file_size": len(file_content),
580
+ "mime_type": self.mime_type,
581
+ "file_extension": self.suffix,
582
+ }
583
+
515
584
  @classmethod
516
585
  def pull(cls, url_or_uuid: Union[str, UUID]) -> "FileStore":
517
586
  """
@@ -280,6 +280,18 @@ class Scenario(Base, UserDict):
280
280
 
281
281
  target = self if inplace else Scenario()
282
282
 
283
+ # First check if this Scenario itself has a base64_string (e.g., from FileStore.to_dict())
284
+ if "base64_string" in self and isinstance(self.get("base64_string"), str):
285
+ # This is likely a Scenario created from FileStore.to_dict()
286
+ if inplace:
287
+ self["base64_string"] = "offloaded"
288
+ else:
289
+ # Copy all keys to target
290
+ for k, v in self.items():
291
+ target[k] = v
292
+ target["base64_string"] = "offloaded"
293
+ return target
294
+
283
295
  for key, value in self.items():
284
296
  if isinstance(value, FileStore):
285
297
  file_store_dict = value.to_dict()
@@ -297,6 +309,227 @@ class Scenario(Base, UserDict):
297
309
 
298
310
  return target
299
311
 
312
+ def save_to_gcs_bucket(self, signed_url_or_dict) -> dict:
313
+ """
314
+ Saves FileStore objects contained within this Scenario to a Google Cloud Storage bucket.
315
+
316
+ This method finds all FileStore objects in the Scenario and uploads them to GCS using
317
+ the provided signed URL(s). If the Scenario itself was created from a FileStore (has
318
+ base64_string as a top-level key), it uploads that content directly.
319
+
320
+ Args:
321
+ signed_url_or_dict: Either:
322
+ - str: Single signed URL (for single FileStore or Scenario from FileStore)
323
+ - dict: Mapping of scenario keys to signed URLs for multiple FileStore objects
324
+ e.g., {"video": "signed_url_1", "image": "signed_url_2"}
325
+
326
+ Returns:
327
+ dict: Summary of upload operations performed
328
+
329
+ Raises:
330
+ ValueError: If no uploadable content found or content is offloaded
331
+ requests.RequestException: If any upload fails
332
+ """
333
+ from edsl.scenarios import FileStore
334
+ import requests
335
+ import base64
336
+
337
+ upload_results = []
338
+
339
+ # Case 1: This Scenario was created from a FileStore (has direct base64_string)
340
+ if "base64_string" in self and isinstance(self.get("base64_string"), str):
341
+ if self["base64_string"] == "offloaded":
342
+ raise ValueError("File content is offloaded. Cannot upload to GCS.")
343
+
344
+ # For single FileStore scenario, expect string URL
345
+ if isinstance(signed_url_or_dict, dict):
346
+ raise ValueError(
347
+ "For Scenario created from FileStore, provide a single signed URL string, not a dictionary."
348
+ )
349
+
350
+ signed_url = signed_url_or_dict
351
+
352
+ # Get file info from Scenario keys
353
+ mime_type = self.get("mime_type", "application/octet-stream")
354
+ suffix = self.get("suffix", "")
355
+
356
+ # Decode and upload
357
+ try:
358
+ file_content = base64.b64decode(self["base64_string"])
359
+ except Exception as e:
360
+ raise ValueError(f"Failed to decode base64 content: {e}")
361
+
362
+ headers = {
363
+ "Content-Type": mime_type,
364
+ "Content-Length": str(len(file_content)),
365
+ }
366
+
367
+ response = requests.put(signed_url, data=file_content, headers=headers)
368
+ response.raise_for_status()
369
+
370
+ upload_results.append(
371
+ {
372
+ "type": "scenario_filestore_content",
373
+ "status": "success",
374
+ "status_code": response.status_code,
375
+ "file_size": len(file_content),
376
+ "mime_type": mime_type,
377
+ "file_extension": suffix,
378
+ }
379
+ )
380
+
381
+ # Case 2: Find FileStore objects in Scenario values
382
+ else:
383
+ # Collect all FileStore keys first
384
+ filestore_keys = [
385
+ key for key, value in self.items() if isinstance(value, FileStore)
386
+ ]
387
+
388
+ if not filestore_keys:
389
+ raise ValueError("No FileStore objects found in Scenario to upload.")
390
+
391
+ # Handle URL parameter
392
+ if isinstance(signed_url_or_dict, str):
393
+ # Single URL provided for multiple FileStore objects - this will cause overwrites
394
+ if len(filestore_keys) > 1:
395
+ raise ValueError(
396
+ f"Multiple FileStore objects found ({filestore_keys}) but only one signed URL provided. "
397
+ f"Provide a dictionary mapping keys to URLs to avoid overwrites: "
398
+ f"{{'{filestore_keys[0]}': 'url1', '{filestore_keys[1]}': 'url2', ...}}"
399
+ )
400
+
401
+ # Single FileStore object, single URL is fine
402
+ url_mapping = {filestore_keys[0]: signed_url_or_dict}
403
+
404
+ elif isinstance(signed_url_or_dict, dict):
405
+ # Dictionary of URLs provided
406
+ missing_keys = set(filestore_keys) - set(signed_url_or_dict.keys())
407
+ if missing_keys:
408
+ raise ValueError(
409
+ f"Missing signed URLs for FileStore keys: {list(missing_keys)}"
410
+ )
411
+
412
+ extra_keys = set(signed_url_or_dict.keys()) - set(filestore_keys)
413
+ if extra_keys:
414
+ raise ValueError(
415
+ f"Signed URLs provided for non-FileStore keys: {list(extra_keys)}"
416
+ )
417
+
418
+ url_mapping = signed_url_or_dict
419
+
420
+ else:
421
+ raise ValueError(
422
+ "signed_url_or_dict must be either a string or a dictionary"
423
+ )
424
+
425
+ # Upload each FileStore object
426
+ for key, value in self.items():
427
+ if isinstance(value, FileStore):
428
+ try:
429
+ result = value.save_to_gcs_bucket(url_mapping[key])
430
+ result["scenario_key"] = key
431
+ result["type"] = "filestore_object"
432
+ upload_results.append(result)
433
+ except Exception as e:
434
+ upload_results.append(
435
+ {
436
+ "scenario_key": key,
437
+ "type": "filestore_object",
438
+ "status": "error",
439
+ "error": str(e),
440
+ }
441
+ )
442
+
443
+ return {
444
+ "total_uploads": len(upload_results),
445
+ "successful_uploads": len(
446
+ [r for r in upload_results if r.get("status") == "success"]
447
+ ),
448
+ "failed_uploads": len(
449
+ [r for r in upload_results if r.get("status") == "error"]
450
+ ),
451
+ "upload_details": upload_results,
452
+ }
453
+
454
+ def get_filestore_info(self) -> dict:
455
+ """
456
+ Returns information about FileStore objects present in this Scenario.
457
+
458
+ This method is useful for determining how many signed URLs need to be generated
459
+ and what file extensions/types are present before calling save_to_gcs_bucket().
460
+
461
+ Returns:
462
+ dict: Information about FileStore objects containing:
463
+ - total_count: Total number of FileStore objects
464
+ - filestore_keys: List of scenario keys that contain FileStore objects
465
+ - file_extensions: Dictionary mapping keys to file extensions
466
+ - file_types: Dictionary mapping keys to MIME types
467
+ - is_filestore_scenario: Boolean indicating if this Scenario was created from a FileStore
468
+ - summary: Human-readable summary of files
469
+
470
+
471
+ """
472
+ from edsl.scenarios import FileStore
473
+
474
+ # Check if this Scenario was created from a FileStore
475
+ is_filestore_scenario = "base64_string" in self and isinstance(
476
+ self.get("base64_string"), str
477
+ )
478
+
479
+ if is_filestore_scenario:
480
+ # Single FileStore scenario
481
+ return {
482
+ "total_count": 1,
483
+ "filestore_keys": ["filestore_content"],
484
+ "file_extensions": {"filestore_content": self.get("suffix", "")},
485
+ "file_types": {
486
+ "filestore_content": self.get(
487
+ "mime_type", "application/octet-stream"
488
+ )
489
+ },
490
+ "is_filestore_scenario": True,
491
+ "summary": f"Single FileStore content with extension '{self.get('suffix', 'unknown')}'",
492
+ }
493
+
494
+ # Regular Scenario with FileStore objects as values
495
+ filestore_info = {}
496
+ file_extensions = {}
497
+ file_types = {}
498
+
499
+ for key, value in self.items():
500
+ if isinstance(value, FileStore):
501
+ filestore_info[key] = {
502
+ "extension": getattr(value, "suffix", ""),
503
+ "mime_type": getattr(
504
+ value, "mime_type", "application/octet-stream"
505
+ ),
506
+ "binary": getattr(value, "binary", True),
507
+ "path": getattr(value, "path", "unknown"),
508
+ }
509
+ file_extensions[key] = getattr(value, "suffix", "")
510
+ file_types[key] = getattr(
511
+ value, "mime_type", "application/octet-stream"
512
+ )
513
+
514
+ # Generate summary
515
+ if filestore_info:
516
+ ext_summary = [f"{key}({ext})" for key, ext in file_extensions.items()]
517
+ summary = (
518
+ f"{len(filestore_info)} FileStore objects: {', '.join(ext_summary)}"
519
+ )
520
+ else:
521
+ summary = "No FileStore objects found"
522
+
523
+ return {
524
+ "total_count": len(filestore_info),
525
+ "filestore_keys": list(filestore_info.keys()),
526
+ "file_extensions": file_extensions,
527
+ "file_types": file_types,
528
+ "is_filestore_scenario": False,
529
+ "detailed_info": filestore_info,
530
+ "summary": summary,
531
+ }
532
+
300
533
  def to_dict(
301
534
  self, add_edsl_version: bool = True, offload_base64: bool = False
302
535
  ) -> dict:
@@ -1334,7 +1334,6 @@ class DelimitedFileSource(Source):
1334
1334
  header_counts = defaultdict(lambda: 0)
1335
1335
  new_header = []
1336
1336
  for h in header:
1337
- print(header_counts)
1338
1337
  if header_counts[h] >= 1:
1339
1338
  new_header.append(f"{h}_{header_counts[h]}")
1340
1339
  warnings.warn(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: edsl
3
- Version: 0.1.61
3
+ Version: 0.1.62
4
4
  Summary: Create and analyze LLM-based surveys
5
5
  Home-page: https://www.expectedparrot.com/
6
6
  License: MIT
@@ -1,13 +1,13 @@
1
1
  edsl/__init__.py,sha256=EkpMsEKqKRbN9Qqcn_y8CjX8OjlWFyhxslLrt3SJY0Q,4827
2
2
  edsl/__init__original.py,sha256=PzMzANf98PrSleSThXT4anNkeVqZMdw0tfFonzsoiGk,4446
3
- edsl/__version__.py,sha256=CjOPJsj7rCFM2zpom_253GmmHGb2RQ8NuZwsEg0ZmF0,23
3
+ edsl/__version__.py,sha256=cwSKWX9cG1qs0I6C99TSkty5QpTa10uiqSeiXnsoOg0,23
4
4
  edsl/agents/__init__.py,sha256=AyhfXjygRHT1Pd9w16lcu5Bu0jnBmMPz86aKP1uRL3Y,93
5
5
  edsl/agents/agent.py,sha256=scTDLrvO5NAUQD9vGVzrPep9-wVcQoNbnQ0GoB5FgH4,58123
6
6
  edsl/agents/agent_list.py,sha256=EV-O1G0Atn1iwpNR78WXLJ-h_kAFpO74LyzCMe_5zLw,26916
7
7
  edsl/agents/descriptors.py,sha256=TfFQWJqhqTWyH89DkNmK6qtH3xV2fUyW9FbI5KnZXv0,4592
8
8
  edsl/agents/exceptions.py,sha256=7KMAtAHKqlkVkd_iVZC_mWXQnzDPV0V_n2iXaGAQgzc,5661
9
9
  edsl/base/__init__.py,sha256=h119NxrAJOV92jnX7ussXNjKFXqzySVGOjMG3G7Zkzc,992
10
- edsl/base/base_class.py,sha256=qR3-op-tu_Tgwhxc8HJpzIXDHnd2ZFaJOQltRvHN5Ls,50999
10
+ edsl/base/base_class.py,sha256=09icKXkd8J1mDpP1TLpFZvPVCp8YUiY4MOSW-eu3omU,51306
11
11
  edsl/base/base_exception.py,sha256=gwk4mNoS3TBe6446NiQeSrUrjUqjlB3_fcDFgV90Dms,7644
12
12
  edsl/base/data_transfer_models.py,sha256=JpEnlgdQ5_URixzZUr7MJuAY4U6obPo0rWfzDl39WNg,3934
13
13
  edsl/base/enums.py,sha256=46mqtWjeiL6NTsN8j-zGfY8QNOVXO4sVb1p1MjmD1N4,6613
@@ -40,12 +40,12 @@ edsl/conversation/exceptions.py,sha256=DoUCg-ymqGOjOl0cpGT8-sNRVsr3SEwdxGAKtdeZ2
40
40
  edsl/conversation/mug_negotiation.py,sha256=do3PTykM6A2cDGOcsohlevRgLpCICoPx8B0WIYe6hy8,2518
41
41
  edsl/conversation/next_speaker_utilities.py,sha256=bqr5JglCd6bdLc9IZ5zGOAsmN2F4ERiubSMYvZIG7qk,3629
42
42
  edsl/coop/__init__.py,sha256=DU2w1Nu8q6tMAa3xoPC722RrvGhmB_UgUUBJDUywsKY,1542
43
- edsl/coop/coop.py,sha256=fOBquOYwC3aY5fKSgcjXWp7r4UW6f8s8fv80cYcT6_8,124267
43
+ edsl/coop/coop.py,sha256=p6oBuot318sku3BqUXwQAYKwxHB3viarnloMdut5Nug,135375
44
44
  edsl/coop/coop_functions.py,sha256=d31kddfj9MVZaMhqwUvkSIBwrdCTQglIvFWVfUr4NuE,688
45
- edsl/coop/coop_jobs_objects.py,sha256=_OFPVLKswXY9mKl9b3Y7gxlUhaMZ7GULx5OqyANpecU,1701
45
+ edsl/coop/coop_jobs_objects.py,sha256=Xx6YF1Uk97d3RRhNDe7kN6BA9DXzYn_TWENd6DhZgms,1738
46
46
  edsl/coop/coop_objects.py,sha256=_cEspdAxh7BT672poxb0HsjU-QZ4Kthg-tKDvZ6I_v0,859
47
47
  edsl/coop/coop_prolific_filters.py,sha256=gSQI25tg2f-ouj3Gh_yMl4ppzMnGYt13kPRg-ICKZ64,6489
48
- edsl/coop/coop_regular_objects.py,sha256=indDQPeesQjHEX_CkICpJPI7o-R8KX66m9DOR9Z48w8,772
48
+ edsl/coop/coop_regular_objects.py,sha256=kKKHapF1xWx8jzgmFcY66HOr2cIqjYMcBI71Vq3nNN0,836
49
49
  edsl/coop/ep_key_handling.py,sha256=X0tskEaYKsRIbFUijaCL69uHYpLJcLbYFITzAu3PGJE,7872
50
50
  edsl/coop/exceptions.py,sha256=EY3eNTeJM15VzFnag93hgmiqn4pR3Y-6nS9ixKGIhM8,8874
51
51
  edsl/coop/price_fetcher.py,sha256=uvEPgKaSRsFq-ouRl5W9aksawUkJg9Lo7ucSePecwa4,4735
@@ -134,7 +134,7 @@ edsl/jobs/decorators.py,sha256=0Eot9pFPsWmQIJAafNd0f5hdb9RUAFp_hGMmSUTJ_C8,3272
134
134
  edsl/jobs/exceptions.py,sha256=5lktTya2VgiBR5Bd977tG2xHdrMjDqhPhQO17O6jIdc,7220
135
135
  edsl/jobs/fetch_invigilator.py,sha256=nzXAIulvOvuDpRDEN5TDNmEfikUEwrnS_XCtnYG2uPQ,2795
136
136
  edsl/jobs/html_table_job_logger.py,sha256=2ErAIi_Dgv_Y3l-AZ2bPUJO_X8hSrPfeFT9lEjt8X4g,34762
137
- edsl/jobs/jobs.py,sha256=y7krV7TwVFMho0x13Pwu_MrBUdMXFfyCqCImZRGRtZU,45393
137
+ edsl/jobs/jobs.py,sha256=lQf16XEeDYAoUjbM8AWqEvHT7-S5sKwqIBMcweeY5oI,45393
138
138
  edsl/jobs/jobs_checks.py,sha256=bfPJ3hQ4qvRBhyte4g-4J8zExJxJr3nlLHmtVmFPJcQ,5390
139
139
  edsl/jobs/jobs_component_constructor.py,sha256=9956UURv3eo-cURNPd4EV8wAQsY-AlEtQRmBu1nCOH8,6982
140
140
  edsl/jobs/jobs_interview_constructor.py,sha256=8nIhhwBQWH_aZ9ZWjvRgOL0y2y6juRTb3pVngQ9Cs8g,2017
@@ -143,7 +143,7 @@ edsl/jobs/jobs_remote_inference_logger.py,sha256=4I3DjIzxfWHjWBr7o_JPhj9f8M4LuuP
143
143
  edsl/jobs/jobs_runner_status.py,sha256=gW8EA-BAKpBvahqRipzomALEAQizd24aRW8G2y7faLQ,11905
144
144
  edsl/jobs/jobs_status_enums.py,sha256=8Kgtr-ffcGGniQ2x5gCOqwURb_HaBWmYcWbUB_KTCY0,214
145
145
  edsl/jobs/progress_bar_manager.py,sha256=d8wuZf7SHq3LCA36JIv1sfYymyHFOUsYRSRlRpR6K04,2832
146
- edsl/jobs/remote_inference.py,sha256=lBPVzctPDxHgwhlgucXnEmB5k4EHsNfzcjoBumcTPqk,24150
146
+ edsl/jobs/remote_inference.py,sha256=yRwFabzuBLrwDkKdEdy8PweHKi7-FnShF7t-5-2aTqI,24156
147
147
  edsl/jobs/results_exceptions_handler.py,sha256=VCtnd60xwdFznzGhtXPbxLmyVf3kIjR2419LUJdFjEQ,3053
148
148
  edsl/key_management/__init__.py,sha256=JiOJ71Ly9aw-tVYbWZu-qRjsW4QETYMQ9IJjsKgW1DQ,1274
149
149
  edsl/key_management/exceptions.py,sha256=dDtoDh1UL52BUBrAlCIc_McgtZCAQkUx6onoSz26qeM,2158
@@ -180,7 +180,7 @@ edsl/plugins/plugin_manager.py,sha256=ifuJLgcySmLvGOc8ka8tSj-3d6ju0NknEK22pLF1L8
180
180
  edsl/plugins/plugins_registry.py,sha256=stAaq6vkuurHc3ViHrLj5g2VomMpsLD9ufa-k-HHfgk,5165
181
181
  edsl/prompts/__init__.py,sha256=4UREcqKC6SIfYykwZbaCeXI5hEil0u2x5GQKasn_NLU,653
182
182
  edsl/prompts/exceptions.py,sha256=AcQCy8JGmS8ODCvRtu4aCH14OEI-oYxF0tX-ZAZ3Puk,4460
183
- edsl/prompts/prompt.py,sha256=mFCOAEHHKJ5RGMRtdkTlNMmRmsem-XligJjRVlO-PbY,14221
183
+ edsl/prompts/prompt.py,sha256=V_duf2qzvBd_fPZLD2bt1WelAfJQT8H3bJ8WoysqkXI,14537
184
184
  edsl/questions/ExceptionExplainer.py,sha256=BgM80FRPJjS_TrY6XaVmlT666MzY9DEagviGQj9-WEQ,2868
185
185
  edsl/questions/HTMLQuestion.py,sha256=lx3Sysm6fMZmFc9hifnkGslt7ZBpDEvziM9-IJFMJLU,3238
186
186
  edsl/questions/Quick.py,sha256=HRLT2Lmhd1Gj4ggkrpCMYhzeWsRwlQaigu2EzdiXb5Q,1717
@@ -218,7 +218,7 @@ edsl/questions/question_multiple_choice.py,sha256=uTWZ0FGE8czIxmiZ_6mvc8KR5efpat
218
218
  edsl/questions/question_multiple_choice_with_other.py,sha256=J0_3V5SfetQzqqVMgTIZ5TUwiY4X-bMCogSqquG0tzQ,23624
219
219
  edsl/questions/question_numerical.py,sha256=2b41BzrjcwnrVw98pDWjEQIw7Ge75chlgbbS2Jr25Wg,17549
220
220
  edsl/questions/question_rank.py,sha256=6oihp85lujt0EAoc7ZxZZf-3p8m-hqGBqSbJCRDUarM,23195
221
- edsl/questions/question_registry.py,sha256=oEG29JjO9gji8qZz_Eqr1Oi3XAOTPhWudtWWDIfPvl0,6324
221
+ edsl/questions/question_registry.py,sha256=YsuwHKwBpcYtt-XM8EfprF0lo_79QSJ36abUnAE6ZtE,6328
222
222
  edsl/questions/question_top_k.py,sha256=zAscGTBQisYX2jV9l3MCLz1PjziMqBlz81gybc-dot4,3233
223
223
  edsl/questions/question_yes_no.py,sha256=nF0RowcQucROyagcaCUi-yOveS8jfoiyOAlgjOfVnc8,2689
224
224
  edsl/questions/register_questions_meta.py,sha256=Ykf0zdVaOolI3YOVdEuVwCd3JByiggV008nBfgnBMfI,2697
@@ -291,7 +291,7 @@ edsl/scenarios/directory_scanner.py,sha256=xv-3HHRPsyGa8m6mHpqLjK-UBC-nhG9gz3VC5
291
291
  edsl/scenarios/document_chunker.py,sha256=EpB0V0oxLzpKntl00Qa3VZNPS7sg9aXdYyqKxhFFzTM,7680
292
292
  edsl/scenarios/exceptions.py,sha256=FeORBm90UthKHDp7cE8I7KJgyA3-pFKNpoivZRr8ifc,10636
293
293
  edsl/scenarios/file_methods.py,sha256=LkN7mZsadRaiNhvKPP_jY7OhUMEsfhEEFY-hpnwdplM,2794
294
- edsl/scenarios/file_store.py,sha256=YmcI9DcHwbTmsYk5RARpAHRuwjfM2RAL3kVjh5WnCn0,32910
294
+ edsl/scenarios/file_store.py,sha256=s4FpEHAjykaI1qd32cXOnv95jX2EIy01AHiELtwZCto,35376
295
295
  edsl/scenarios/handlers/__init__.py,sha256=_-A6vXzQPKga7fDyteDt1QPA6lDwmgERJKG8SrdhYxQ,965
296
296
  edsl/scenarios/handlers/csv_file_store.py,sha256=kXOms0ph5JJj6jSbpfQ-SZjuT4vvSRhq5AGpv1L4TPQ,1369
297
297
  edsl/scenarios/handlers/docx_file_store.py,sha256=KSKAAUIWF2K5xr92nx7UGQ9djgtDX4ke-Eyik8QAdlQ,2155
@@ -309,7 +309,7 @@ edsl/scenarios/handlers/sql_file_store.py,sha256=wa_Qw1-bk-tHhtQrp1IAxSAROygEQ5F
309
309
  edsl/scenarios/handlers/sqlite_file_store.py,sha256=rwsfxD5G_XNEa-aRCx6A83lW0i2OiS51EzYsJeTE7ps,4936
310
310
  edsl/scenarios/handlers/txt_file_store.py,sha256=oGMqm2X_dWTt0W2e2zDung2i_A_z2mMmm4rrQImnVtU,980
311
311
  edsl/scenarios/handlers/webm_file_store.py,sha256=UG3sPwsxbZAjM1H9rbpdkvXMrS3iRbaaN-4VNGh3JX8,3659
312
- edsl/scenarios/scenario.py,sha256=3LQhJ8QSVaatuV2DZOJwJDRgrwyx2zmOE0B-7AIVTtI,39184
312
+ edsl/scenarios/scenario.py,sha256=W1wWo-wh5hfUb42C8YHRq8Qu2spvgL2clp64bupIPYk,48840
313
313
  edsl/scenarios/scenario_join.py,sha256=1r_czZctN7JKbw38bQolKdz0kBaMqhWzo8IsxzHK1TY,5409
314
314
  edsl/scenarios/scenario_list.py,sha256=tPpFyNDTiMEkl59DA8VTBgGBDt67f1E8Od5qHMXULzM,88787
315
315
  edsl/scenarios/scenario_list_gc_test.py,sha256=VaZBg_GjfSaM92Gj3eiSt3aQ_rECDfD339ZCTqryfdc,4676
@@ -317,7 +317,7 @@ edsl/scenarios/scenario_list_memory_test.py,sha256=l_PeTJkh0MYQoRLIiFOI8hmzEyjf8
317
317
  edsl/scenarios/scenario_list_pdf_tools.py,sha256=sehQro5PzJ7Y4Ck9VJ8HTxKN8HSbk3aDipVYuxaJbdI,7686
318
318
  edsl/scenarios/scenario_list_source_refactor.md,sha256=LFbkxOgc7eWtsX1zQlfYwqWW4pLJNTTXPP7zsyB3iQY,1554
319
319
  edsl/scenarios/scenario_selector.py,sha256=7Hm4DitY7X6tKTAofhMMHIFkZf6qXFSminL2wBrUJ2w,6037
320
- edsl/scenarios/scenario_source.py,sha256=AIsSmpCg3cVDRp4rDYmqJ_ViH77BxbqOJbZYwfpgPTg,73244
320
+ edsl/scenarios/scenario_source.py,sha256=Uua3bOHCyuYjbnt5dj388iNHqNX99vCyCUhuqCA7Wmg,73211
321
321
  edsl/scenarios/tests/test_scenario_list_sources.py,sha256=2JBxBn-l_X22WPTKxMTlU10swkfN1DF6c_2-BONEy_E,2135
322
322
  edsl/surveys/__init__.py,sha256=04hdYqBBdb9jtZVkZffbFszr0yzXVPPFiI-XhgZRLu0,327
323
323
  edsl/surveys/base.py,sha256=XJHGEbbsH6hlYYkmI4isVLD8guLz8BdhR-eQRL78mc4,1115
@@ -384,8 +384,8 @@ edsl/utilities/restricted_python.py,sha256=248N2p5EWHDSpcK1G-q7DUoJeWy4sB6aO-RV0
384
384
  edsl/utilities/template_loader.py,sha256=SCAcnTnxNQ67MNSkmfz7F-S_u2peyGn2j1oRIqi1wfg,870
385
385
  edsl/utilities/utilities.py,sha256=irHheAGOnl_6RwI--Hi9StVzvsHcWCqB48PWsWJQYOw,12045
386
386
  edsl/utilities/wikipedia.py,sha256=I3Imbz3fzbaoA0ZLDsWUO2YpP_ovvaqtu-yd2Ye1BB0,6933
387
- edsl-0.1.61.dist-info/LICENSE,sha256=_qszBDs8KHShVYcYzdMz3HNMtH-fKN_p5zjoVAVumFc,1111
388
- edsl-0.1.61.dist-info/METADATA,sha256=dqV7wxOSMV-w2g7YfKnQV7GkvZB5WQ8ia7WFkV33UZ4,12076
389
- edsl-0.1.61.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
390
- edsl-0.1.61.dist-info/entry_points.txt,sha256=JnG7xqMtHaQu9BU-yPATxdyCeA48XJpuclnWCqMfIMU,38
391
- edsl-0.1.61.dist-info/RECORD,,
387
+ edsl-0.1.62.dist-info/LICENSE,sha256=_qszBDs8KHShVYcYzdMz3HNMtH-fKN_p5zjoVAVumFc,1111
388
+ edsl-0.1.62.dist-info/METADATA,sha256=4I8R2M4qeuE3YMXvGNls3yNSvjWnHmx6jkIxZdR8azo,12076
389
+ edsl-0.1.62.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
390
+ edsl-0.1.62.dist-info/entry_points.txt,sha256=JnG7xqMtHaQu9BU-yPATxdyCeA48XJpuclnWCqMfIMU,38
391
+ edsl-0.1.62.dist-info/RECORD,,
File without changes
File without changes