edsl 0.1.47__py3-none-any.whl → 0.1.48__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/__init__.py +44 -39
- edsl/__version__.py +1 -1
- edsl/agents/__init__.py +4 -2
- edsl/agents/{Agent.py → agent.py} +442 -152
- edsl/agents/{AgentList.py → agent_list.py} +220 -162
- edsl/agents/descriptors.py +46 -7
- edsl/{exceptions/agents.py → agents/exceptions.py} +3 -12
- edsl/base/__init__.py +75 -0
- edsl/base/base_class.py +1303 -0
- edsl/base/data_transfer_models.py +114 -0
- edsl/base/enums.py +215 -0
- edsl/base.py +8 -0
- edsl/buckets/__init__.py +25 -0
- edsl/buckets/bucket_collection.py +324 -0
- edsl/buckets/model_buckets.py +206 -0
- edsl/buckets/token_bucket.py +502 -0
- edsl/{jobs/buckets/TokenBucketAPI.py → buckets/token_bucket_api.py} +1 -1
- edsl/buckets/token_bucket_client.py +509 -0
- edsl/caching/__init__.py +20 -0
- edsl/caching/cache.py +814 -0
- edsl/caching/cache_entry.py +427 -0
- edsl/{data/CacheHandler.py → caching/cache_handler.py} +14 -15
- edsl/caching/exceptions.py +24 -0
- edsl/caching/orm.py +30 -0
- edsl/{data/RemoteCacheSync.py → caching/remote_cache_sync.py} +3 -3
- edsl/caching/sql_dict.py +441 -0
- edsl/config/__init__.py +8 -0
- edsl/config/config_class.py +177 -0
- edsl/config.py +4 -176
- edsl/conversation/Conversation.py +7 -7
- edsl/conversation/car_buying.py +4 -4
- edsl/conversation/chips.py +6 -6
- edsl/coop/__init__.py +25 -2
- edsl/coop/coop.py +303 -67
- edsl/coop/{ExpectedParrotKeyHandler.py → ep_key_handling.py} +86 -10
- edsl/coop/exceptions.py +62 -0
- edsl/coop/price_fetcher.py +126 -0
- edsl/coop/utils.py +89 -24
- edsl/data_transfer_models.py +5 -72
- edsl/dataset/__init__.py +10 -0
- edsl/{results/Dataset.py → dataset/dataset.py} +116 -36
- edsl/{results/DatasetExportMixin.py → dataset/dataset_operations_mixin.py} +606 -122
- edsl/{results/DatasetTree.py → dataset/dataset_tree.py} +156 -75
- edsl/{results/TableDisplay.py → dataset/display/table_display.py} +18 -7
- edsl/{results → dataset/display}/table_renderers.py +58 -2
- edsl/{results → dataset}/file_exports.py +4 -5
- edsl/{results → dataset}/smart_objects.py +2 -2
- edsl/enums.py +5 -205
- edsl/inference_services/__init__.py +5 -0
- edsl/inference_services/{AvailableModelCacheHandler.py → available_model_cache_handler.py} +2 -3
- edsl/inference_services/{AvailableModelFetcher.py → available_model_fetcher.py} +8 -14
- edsl/inference_services/data_structures.py +3 -2
- edsl/{exceptions/inference_services.py → inference_services/exceptions.py} +1 -1
- edsl/inference_services/{InferenceServiceABC.py → inference_service_abc.py} +1 -1
- edsl/inference_services/{InferenceServicesCollection.py → inference_services_collection.py} +8 -7
- edsl/inference_services/registry.py +4 -41
- edsl/inference_services/{ServiceAvailability.py → service_availability.py} +5 -25
- edsl/inference_services/services/__init__.py +31 -0
- edsl/inference_services/{AnthropicService.py → services/anthropic_service.py} +3 -3
- edsl/inference_services/{AwsBedrock.py → services/aws_bedrock.py} +2 -2
- edsl/inference_services/{AzureAI.py → services/azure_ai.py} +2 -2
- edsl/inference_services/{DeepInfraService.py → services/deep_infra_service.py} +1 -3
- edsl/inference_services/{DeepSeekService.py → services/deep_seek_service.py} +2 -4
- edsl/inference_services/{GoogleService.py → services/google_service.py} +5 -4
- edsl/inference_services/{GroqService.py → services/groq_service.py} +1 -1
- edsl/inference_services/{MistralAIService.py → services/mistral_ai_service.py} +3 -3
- edsl/inference_services/{OllamaService.py → services/ollama_service.py} +1 -7
- edsl/inference_services/{OpenAIService.py → services/open_ai_service.py} +5 -6
- edsl/inference_services/{PerplexityService.py → services/perplexity_service.py} +3 -7
- edsl/inference_services/{TestService.py → services/test_service.py} +7 -6
- edsl/inference_services/{TogetherAIService.py → services/together_ai_service.py} +2 -6
- edsl/inference_services/{XAIService.py → services/xai_service.py} +1 -1
- edsl/inference_services/write_available.py +1 -2
- edsl/instructions/__init__.py +6 -0
- edsl/{surveys/instructions/Instruction.py → instructions/instruction.py} +11 -6
- edsl/{surveys/instructions/InstructionCollection.py → instructions/instruction_collection.py} +10 -5
- edsl/{surveys/InstructionHandler.py → instructions/instruction_handler.py} +3 -3
- edsl/{jobs/interviews → interviews}/ReportErrors.py +2 -2
- edsl/interviews/__init__.py +4 -0
- edsl/{jobs/AnswerQuestionFunctionConstructor.py → interviews/answering_function.py} +45 -18
- edsl/{jobs/interviews/InterviewExceptionEntry.py → interviews/exception_tracking.py} +107 -22
- edsl/interviews/interview.py +638 -0
- edsl/{jobs/interviews/InterviewStatusDictionary.py → interviews/interview_status_dictionary.py} +21 -12
- edsl/{jobs/interviews/InterviewStatusLog.py → interviews/interview_status_log.py} +16 -7
- edsl/{jobs/InterviewTaskManager.py → interviews/interview_task_manager.py} +12 -7
- edsl/{jobs/RequestTokenEstimator.py → interviews/request_token_estimator.py} +8 -3
- edsl/{jobs/interviews/InterviewStatistic.py → interviews/statistics.py} +36 -10
- edsl/invigilators/__init__.py +38 -0
- edsl/invigilators/invigilator_base.py +477 -0
- edsl/{agents/Invigilator.py → invigilators/invigilators.py} +263 -10
- edsl/invigilators/prompt_constructor.py +476 -0
- edsl/{agents → invigilators}/prompt_helpers.py +2 -1
- edsl/{agents/QuestionInstructionPromptBuilder.py → invigilators/question_instructions_prompt_builder.py} +18 -13
- edsl/{agents → invigilators}/question_option_processor.py +96 -21
- edsl/{agents/QuestionTemplateReplacementsBuilder.py → invigilators/question_template_replacements_builder.py} +64 -12
- edsl/jobs/__init__.py +7 -1
- edsl/jobs/async_interview_runner.py +99 -35
- edsl/jobs/check_survey_scenario_compatibility.py +7 -5
- edsl/jobs/data_structures.py +153 -22
- edsl/{exceptions/jobs.py → jobs/exceptions.py} +2 -1
- edsl/jobs/{FetchInvigilator.py → fetch_invigilator.py} +4 -4
- edsl/jobs/{loggers/HTMLTableJobLogger.py → html_table_job_logger.py} +6 -2
- edsl/jobs/{Jobs.py → jobs.py} +313 -167
- edsl/jobs/{JobsChecks.py → jobs_checks.py} +15 -7
- edsl/jobs/{JobsComponentConstructor.py → jobs_component_constructor.py} +19 -17
- edsl/jobs/{InterviewsConstructor.py → jobs_interview_constructor.py} +10 -5
- edsl/jobs/jobs_pricing_estimation.py +347 -0
- edsl/jobs/{JobsRemoteInferenceLogger.py → jobs_remote_inference_logger.py} +4 -3
- edsl/jobs/jobs_runner_asyncio.py +282 -0
- edsl/jobs/{JobsRemoteInferenceHandler.py → remote_inference.py} +19 -22
- edsl/jobs/results_exceptions_handler.py +2 -2
- edsl/key_management/__init__.py +28 -0
- edsl/key_management/key_lookup.py +161 -0
- edsl/{language_models/key_management/KeyLookupBuilder.py → key_management/key_lookup_builder.py} +118 -47
- edsl/key_management/key_lookup_collection.py +82 -0
- edsl/key_management/models.py +218 -0
- edsl/language_models/__init__.py +7 -2
- edsl/language_models/{ComputeCost.py → compute_cost.py} +18 -3
- edsl/{exceptions/language_models.py → language_models/exceptions.py} +2 -1
- edsl/language_models/language_model.py +1080 -0
- edsl/language_models/model.py +10 -25
- edsl/language_models/{ModelList.py → model_list.py} +9 -14
- edsl/language_models/{RawResponseHandler.py → raw_response_handler.py} +1 -1
- edsl/language_models/{RegisterLanguageModelsMeta.py → registry.py} +1 -1
- edsl/language_models/repair.py +4 -4
- edsl/language_models/utilities.py +4 -4
- edsl/notebooks/__init__.py +3 -1
- edsl/notebooks/{Notebook.py → notebook.py} +7 -8
- edsl/prompts/__init__.py +1 -1
- edsl/{exceptions/prompts.py → prompts/exceptions.py} +3 -1
- edsl/prompts/{Prompt.py → prompt.py} +101 -95
- edsl/questions/HTMLQuestion.py +1 -1
- edsl/questions/__init__.py +154 -25
- edsl/questions/answer_validator_mixin.py +1 -1
- edsl/questions/compose_questions.py +4 -3
- edsl/questions/derived/question_likert_five.py +166 -0
- edsl/questions/derived/{QuestionLinearScale.py → question_linear_scale.py} +4 -4
- edsl/questions/derived/{QuestionTopK.py → question_top_k.py} +4 -4
- edsl/questions/derived/{QuestionYesNo.py → question_yes_no.py} +4 -5
- edsl/questions/descriptors.py +24 -30
- edsl/questions/loop_processor.py +65 -19
- edsl/questions/question_base.py +881 -0
- edsl/questions/question_base_gen_mixin.py +15 -16
- edsl/questions/{QuestionBasePromptsMixin.py → question_base_prompts_mixin.py} +2 -2
- edsl/questions/{QuestionBudget.py → question_budget.py} +3 -4
- edsl/questions/{QuestionCheckBox.py → question_check_box.py} +16 -16
- edsl/questions/{QuestionDict.py → question_dict.py} +39 -5
- edsl/questions/{QuestionExtract.py → question_extract.py} +9 -9
- edsl/questions/question_free_text.py +282 -0
- edsl/questions/{QuestionFunctional.py → question_functional.py} +6 -5
- edsl/questions/{QuestionList.py → question_list.py} +6 -7
- edsl/questions/{QuestionMatrix.py → question_matrix.py} +6 -5
- edsl/questions/{QuestionMultipleChoice.py → question_multiple_choice.py} +126 -21
- edsl/questions/{QuestionNumerical.py → question_numerical.py} +5 -5
- edsl/questions/{QuestionRank.py → question_rank.py} +6 -6
- edsl/questions/question_registry.py +4 -9
- edsl/questions/register_questions_meta.py +8 -4
- edsl/questions/response_validator_abc.py +17 -16
- edsl/results/__init__.py +4 -1
- edsl/{exceptions/results.py → results/exceptions.py} +1 -1
- edsl/results/report.py +197 -0
- edsl/results/{Result.py → result.py} +131 -45
- edsl/results/{Results.py → results.py} +365 -220
- edsl/results/results_selector.py +344 -25
- edsl/scenarios/__init__.py +30 -3
- edsl/scenarios/{ConstructDownloadLink.py → construct_download_link.py} +7 -0
- edsl/scenarios/directory_scanner.py +156 -13
- edsl/scenarios/document_chunker.py +186 -0
- edsl/scenarios/exceptions.py +101 -0
- edsl/scenarios/file_methods.py +2 -3
- edsl/scenarios/{FileStore.py → file_store.py} +275 -189
- edsl/scenarios/handlers/__init__.py +14 -14
- edsl/scenarios/handlers/{csv.py → csv_file_store.py} +1 -2
- edsl/scenarios/handlers/{docx.py → docx_file_store.py} +8 -7
- edsl/scenarios/handlers/{html.py → html_file_store.py} +1 -2
- edsl/scenarios/handlers/{jpeg.py → jpeg_file_store.py} +1 -1
- edsl/scenarios/handlers/{json.py → json_file_store.py} +1 -1
- edsl/scenarios/handlers/latex_file_store.py +5 -0
- edsl/scenarios/handlers/{md.py → md_file_store.py} +1 -1
- edsl/scenarios/handlers/{pdf.py → pdf_file_store.py} +2 -2
- edsl/scenarios/handlers/{png.py → png_file_store.py} +1 -1
- edsl/scenarios/handlers/{pptx.py → pptx_file_store.py} +8 -7
- edsl/scenarios/handlers/{py.py → py_file_store.py} +1 -3
- edsl/scenarios/handlers/{sql.py → sql_file_store.py} +2 -1
- edsl/scenarios/handlers/{sqlite.py → sqlite_file_store.py} +2 -3
- edsl/scenarios/handlers/{txt.py → txt_file_store.py} +1 -1
- edsl/scenarios/scenario.py +928 -0
- edsl/scenarios/scenario_join.py +18 -5
- edsl/scenarios/{ScenarioList.py → scenario_list.py} +294 -106
- edsl/scenarios/{ScenarioListPdfMixin.py → scenario_list_pdf_tools.py} +16 -15
- edsl/scenarios/scenario_selector.py +5 -1
- edsl/study/ObjectEntry.py +2 -2
- edsl/study/SnapShot.py +5 -5
- edsl/study/Study.py +18 -19
- edsl/study/__init__.py +6 -4
- edsl/surveys/__init__.py +7 -4
- edsl/surveys/dag/__init__.py +2 -0
- edsl/surveys/{ConstructDAG.py → dag/construct_dag.py} +3 -3
- edsl/surveys/{DAG.py → dag/dag.py} +13 -10
- edsl/surveys/descriptors.py +1 -1
- edsl/surveys/{EditSurvey.py → edit_survey.py} +9 -9
- edsl/{exceptions/surveys.py → surveys/exceptions.py} +1 -2
- edsl/surveys/memory/__init__.py +3 -0
- edsl/surveys/{MemoryPlan.py → memory/memory_plan.py} +10 -9
- edsl/surveys/rules/__init__.py +3 -0
- edsl/surveys/{Rule.py → rules/rule.py} +103 -43
- edsl/surveys/{RuleCollection.py → rules/rule_collection.py} +21 -30
- edsl/surveys/{RuleManager.py → rules/rule_manager.py} +19 -13
- edsl/surveys/survey.py +1743 -0
- edsl/surveys/{SurveyExportMixin.py → survey_export.py} +22 -27
- edsl/surveys/{SurveyFlowVisualization.py → survey_flow_visualization.py} +11 -2
- edsl/surveys/{Simulator.py → survey_simulator.py} +10 -3
- edsl/tasks/__init__.py +32 -0
- edsl/{jobs/tasks/QuestionTaskCreator.py → tasks/question_task_creator.py} +115 -57
- edsl/tasks/task_creators.py +135 -0
- edsl/{jobs/tasks/TaskHistory.py → tasks/task_history.py} +86 -47
- edsl/{jobs/tasks → tasks}/task_status_enum.py +91 -7
- edsl/tasks/task_status_log.py +85 -0
- edsl/tokens/__init__.py +2 -0
- edsl/tokens/interview_token_usage.py +53 -0
- edsl/utilities/PrettyList.py +1 -1
- edsl/utilities/SystemInfo.py +25 -22
- edsl/utilities/__init__.py +29 -21
- edsl/utilities/gcp_bucket/__init__.py +2 -0
- edsl/utilities/gcp_bucket/cloud_storage.py +99 -96
- edsl/utilities/interface.py +44 -536
- edsl/{results/MarkdownToPDF.py → utilities/markdown_to_pdf.py} +13 -5
- edsl/utilities/repair_functions.py +1 -1
- {edsl-0.1.47.dist-info → edsl-0.1.48.dist-info}/METADATA +1 -1
- edsl-0.1.48.dist-info/RECORD +347 -0
- edsl/Base.py +0 -493
- edsl/BaseDiff.py +0 -260
- edsl/agents/InvigilatorBase.py +0 -260
- edsl/agents/PromptConstructor.py +0 -318
- edsl/coop/PriceFetcher.py +0 -54
- edsl/data/Cache.py +0 -582
- edsl/data/CacheEntry.py +0 -238
- edsl/data/SQLiteDict.py +0 -292
- edsl/data/__init__.py +0 -5
- edsl/data/orm.py +0 -10
- edsl/exceptions/cache.py +0 -5
- edsl/exceptions/coop.py +0 -14
- edsl/exceptions/data.py +0 -14
- edsl/exceptions/scenarios.py +0 -29
- edsl/jobs/Answers.py +0 -43
- edsl/jobs/JobsPrompts.py +0 -354
- edsl/jobs/buckets/BucketCollection.py +0 -134
- edsl/jobs/buckets/ModelBuckets.py +0 -65
- edsl/jobs/buckets/TokenBucket.py +0 -283
- edsl/jobs/buckets/TokenBucketClient.py +0 -191
- edsl/jobs/interviews/Interview.py +0 -395
- edsl/jobs/interviews/InterviewExceptionCollection.py +0 -99
- edsl/jobs/interviews/InterviewStatisticsCollection.py +0 -25
- edsl/jobs/runners/JobsRunnerAsyncio.py +0 -163
- edsl/jobs/runners/JobsRunnerStatusData.py +0 -0
- edsl/jobs/tasks/TaskCreators.py +0 -64
- edsl/jobs/tasks/TaskStatusLog.py +0 -23
- edsl/jobs/tokens/InterviewTokenUsage.py +0 -27
- edsl/language_models/LanguageModel.py +0 -635
- edsl/language_models/ServiceDataSources.py +0 -0
- edsl/language_models/key_management/KeyLookup.py +0 -63
- edsl/language_models/key_management/KeyLookupCollection.py +0 -38
- edsl/language_models/key_management/models.py +0 -137
- edsl/questions/QuestionBase.py +0 -544
- edsl/questions/QuestionFreeText.py +0 -130
- edsl/questions/derived/QuestionLikertFive.py +0 -76
- edsl/results/ResultsExportMixin.py +0 -45
- edsl/results/TextEditor.py +0 -50
- edsl/results/results_fetch_mixin.py +0 -33
- edsl/results/results_tools_mixin.py +0 -98
- edsl/scenarios/DocumentChunker.py +0 -104
- edsl/scenarios/Scenario.py +0 -548
- edsl/scenarios/ScenarioHtmlMixin.py +0 -65
- edsl/scenarios/ScenarioListExportMixin.py +0 -45
- edsl/scenarios/handlers/latex.py +0 -5
- edsl/shared.py +0 -1
- edsl/surveys/Survey.py +0 -1301
- edsl/surveys/SurveyQualtricsImport.py +0 -284
- edsl/surveys/SurveyToApp.py +0 -141
- edsl/surveys/instructions/__init__.py +0 -0
- edsl/tools/__init__.py +0 -1
- edsl/tools/clusters.py +0 -192
- edsl/tools/embeddings.py +0 -27
- edsl/tools/embeddings_plotting.py +0 -118
- edsl/tools/plotting.py +0 -112
- edsl/tools/summarize.py +0 -18
- edsl/utilities/data/Registry.py +0 -6
- edsl/utilities/data/__init__.py +0 -1
- edsl/utilities/data/scooter_results.json +0 -1
- edsl-0.1.47.dist-info/RECORD +0 -354
- /edsl/coop/{CoopFunctionsMixin.py → coop_functions.py} +0 -0
- /edsl/{results → dataset/display}/CSSParameterizer.py +0 -0
- /edsl/{language_models/key_management → dataset/display}/__init__.py +0 -0
- /edsl/{results → dataset/display}/table_data_class.py +0 -0
- /edsl/{results → dataset/display}/table_display.css +0 -0
- /edsl/{results/ResultsGGMixin.py → dataset/r/ggplot.py} +0 -0
- /edsl/{results → dataset}/tree_explore.py +0 -0
- /edsl/{surveys/instructions/ChangeInstruction.py → instructions/change_instruction.py} +0 -0
- /edsl/{jobs/interviews → interviews}/interview_status_enum.py +0 -0
- /edsl/jobs/{runners/JobsRunnerStatus.py → jobs_runner_status.py} +0 -0
- /edsl/language_models/{PriceManager.py → price_manager.py} +0 -0
- /edsl/language_models/{fake_openai_call.py → unused/fake_openai_call.py} +0 -0
- /edsl/language_models/{fake_openai_service.py → unused/fake_openai_service.py} +0 -0
- /edsl/notebooks/{NotebookToLaTeX.py → notebook_to_latex.py} +0 -0
- /edsl/{exceptions/questions.py → questions/exceptions.py} +0 -0
- /edsl/questions/{SimpleAskMixin.py → simple_ask_mixin.py} +0 -0
- /edsl/surveys/{Memory.py → memory/memory.py} +0 -0
- /edsl/surveys/{MemoryManagement.py → memory/memory_management.py} +0 -0
- /edsl/surveys/{SurveyCSS.py → survey_css.py} +0 -0
- /edsl/{jobs/tokens/TokenUsage.py → tokens/token_usage.py} +0 -0
- /edsl/{results/MarkdownToDocx.py → utilities/markdown_to_docx.py} +0 -0
- /edsl/{TemplateLoader.py → utilities/template_loader.py} +0 -0
- {edsl-0.1.47.dist-info → edsl-0.1.48.dist-info}/LICENSE +0 -0
- {edsl-0.1.47.dist-info → edsl-0.1.48.dist-info}/WHEEL +0 -0
@@ -0,0 +1,427 @@
|
|
1
|
+
"""
|
2
|
+
CacheEntry implementation for the EDSL data caching system.
|
3
|
+
|
4
|
+
This module provides the CacheEntry class, which represents a single cached
|
5
|
+
language model response along with its associated metadata. Cache entries are
|
6
|
+
uniquely identified by a hash of their key fields, making it efficient to
|
7
|
+
store and retrieve responses for identical prompts.
|
8
|
+
"""
|
9
|
+
|
10
|
+
from __future__ import annotations
|
11
|
+
import json
|
12
|
+
import datetime
|
13
|
+
import hashlib
|
14
|
+
from typing import Optional, Dict, List, Any, Union
|
15
|
+
from uuid import uuid4
|
16
|
+
|
17
|
+
from ..base import RepresentationMixin
|
18
|
+
|
19
|
+
|
20
|
+
class CacheEntry(RepresentationMixin):
|
21
|
+
"""
|
22
|
+
Represents a single cached language model response with associated metadata.
|
23
|
+
|
24
|
+
CacheEntry objects store language model responses along with the prompts and
|
25
|
+
parameters that generated them. Each entry is uniquely identified by a hash
|
26
|
+
of its key fields (model, parameters, prompts, and iteration), making it
|
27
|
+
possible to efficiently retrieve cached responses for identical inputs.
|
28
|
+
|
29
|
+
Attributes:
|
30
|
+
model (str): The language model identifier (e.g., "gpt-3.5-turbo")
|
31
|
+
parameters (dict): Model parameters used for generation (e.g., temperature)
|
32
|
+
system_prompt (str): The system prompt provided to the model
|
33
|
+
user_prompt (str): The user prompt provided to the model
|
34
|
+
output (str): The generated response from the language model
|
35
|
+
iteration (int): Iteration number, for when multiple outputs are generated
|
36
|
+
with the same prompts (defaults to 0)
|
37
|
+
timestamp (int): Unix timestamp when the entry was created
|
38
|
+
service (str, optional): The service provider for the model (e.g., "openai")
|
39
|
+
|
40
|
+
Class Attributes:
|
41
|
+
key_fields (List[str]): Fields used to generate the unique hash key
|
42
|
+
all_fields (List[str]): All fields stored in the cache entry
|
43
|
+
"""
|
44
|
+
|
45
|
+
key_fields = ["model", "parameters", "system_prompt", "user_prompt", "iteration"]
|
46
|
+
all_fields = key_fields + ["timestamp", "output", "service"]
|
47
|
+
|
48
|
+
def __init__(
|
49
|
+
self,
|
50
|
+
*,
|
51
|
+
model: str,
|
52
|
+
parameters: dict,
|
53
|
+
system_prompt: str,
|
54
|
+
user_prompt: str,
|
55
|
+
iteration: Optional[int] = None,
|
56
|
+
output: str,
|
57
|
+
timestamp: Optional[int] = None,
|
58
|
+
service: Optional[str] = None,
|
59
|
+
):
|
60
|
+
self.model = model
|
61
|
+
self.parameters = parameters
|
62
|
+
self.system_prompt = system_prompt
|
63
|
+
self.user_prompt = user_prompt
|
64
|
+
self.output = output
|
65
|
+
self.iteration = iteration or 0
|
66
|
+
self.timestamp = timestamp or int(
|
67
|
+
datetime.datetime.now(datetime.timezone.utc).timestamp()
|
68
|
+
)
|
69
|
+
self.service = service
|
70
|
+
self._check_types()
|
71
|
+
|
72
|
+
def _check_types(self) -> None:
|
73
|
+
"""
|
74
|
+
Validates that all attributes have the correct types.
|
75
|
+
|
76
|
+
This method is called during initialization to ensure that all
|
77
|
+
attributes have the expected types, raising TypeError exceptions
|
78
|
+
with descriptive messages when validation fails.
|
79
|
+
|
80
|
+
Raises:
|
81
|
+
TypeError: If any attribute has an incorrect type
|
82
|
+
"""
|
83
|
+
if not isinstance(self.model, str):
|
84
|
+
raise TypeError("`model` should be a string.")
|
85
|
+
if not isinstance(self.parameters, dict):
|
86
|
+
raise TypeError("`parameters` should be a dictionary.")
|
87
|
+
if not isinstance(self.system_prompt, str):
|
88
|
+
raise TypeError("`system_prompt` should be a string.")
|
89
|
+
if not isinstance(self.user_prompt, str):
|
90
|
+
raise TypeError("`user_prompt` should be a string")
|
91
|
+
if not isinstance(self.output, str):
|
92
|
+
raise TypeError("`output` should be a string")
|
93
|
+
if not isinstance(self.iteration, int):
|
94
|
+
raise TypeError("`iteration` should be an integer")
|
95
|
+
# Note: timestamp is stored as int for compatibility, but could be float in future
|
96
|
+
if not isinstance(self.timestamp, int):
|
97
|
+
raise TypeError(f"`timestamp` should be an integer")
|
98
|
+
if self.service is not None and not isinstance(self.service, str):
|
99
|
+
raise TypeError("`service` should be either a string or None")
|
100
|
+
|
101
|
+
@classmethod
|
102
|
+
def gen_key(
|
103
|
+
cls, *, model: str, parameters: Dict[str, Any],
|
104
|
+
system_prompt: str, user_prompt: str, iteration: int
|
105
|
+
) -> str:
|
106
|
+
"""
|
107
|
+
Generates a unique key hash for the cache entry based on input parameters.
|
108
|
+
|
109
|
+
This method creates a deterministic hash key by concatenating the model name,
|
110
|
+
parameters (sorted to ensure consistency), system prompt, user prompt, and
|
111
|
+
iteration number. The hash enables efficient lookup of cache entries with
|
112
|
+
identical inputs.
|
113
|
+
|
114
|
+
Args:
|
115
|
+
model: The language model identifier
|
116
|
+
parameters: Dictionary of model parameters (will be sorted for consistency)
|
117
|
+
system_prompt: The system prompt provided to the model
|
118
|
+
user_prompt: The user prompt provided to the model
|
119
|
+
iteration: Iteration number for this combination of inputs
|
120
|
+
|
121
|
+
Returns:
|
122
|
+
A hex-encoded MD5 hash string that uniquely identifies this combination
|
123
|
+
of inputs
|
124
|
+
|
125
|
+
Note:
|
126
|
+
- The hash treats single and double quotes as equivalent
|
127
|
+
- Parameters are sorted to ensure consistent hashing regardless of order
|
128
|
+
"""
|
129
|
+
long_key = f"{model}{json.dumps(parameters, sort_keys=True)}{system_prompt}{user_prompt}{iteration}"
|
130
|
+
return hashlib.md5(long_key.encode()).hexdigest()
|
131
|
+
|
132
|
+
@property
|
133
|
+
def key(self) -> str:
|
134
|
+
"""
|
135
|
+
Returns the unique hash key for this cache entry.
|
136
|
+
|
137
|
+
This property extracts the key fields from the instance and generates
|
138
|
+
a hash key using the gen_key classmethod. The key uniquely identifies
|
139
|
+
this combination of model, parameters, prompts, and iteration.
|
140
|
+
|
141
|
+
Returns:
|
142
|
+
A hex-encoded MD5 hash string that uniquely identifies this cache entry
|
143
|
+
"""
|
144
|
+
d = {k: value for k, value in self.__dict__.items() if k in self.key_fields}
|
145
|
+
return self.gen_key(**d)
|
146
|
+
|
147
|
+
def to_dict(self, add_edsl_version: bool = True) -> Dict[str, Any]:
|
148
|
+
"""
|
149
|
+
Converts the cache entry to a dictionary representation.
|
150
|
+
|
151
|
+
This method creates a dictionary containing all fields of the cache entry,
|
152
|
+
making it suitable for serialization or storage.
|
153
|
+
|
154
|
+
Args:
|
155
|
+
add_edsl_version: If True, adds EDSL version information to the dict
|
156
|
+
(Currently disabled pending implementation)
|
157
|
+
|
158
|
+
Returns:
|
159
|
+
A dictionary representation of the cache entry with all fields
|
160
|
+
|
161
|
+
Note:
|
162
|
+
The edsl_version feature is currently disabled in the implementation
|
163
|
+
"""
|
164
|
+
d = {
|
165
|
+
"model": self.model,
|
166
|
+
"parameters": self.parameters,
|
167
|
+
"system_prompt": self.system_prompt,
|
168
|
+
"user_prompt": self.user_prompt,
|
169
|
+
"output": self.output,
|
170
|
+
"iteration": self.iteration,
|
171
|
+
"timestamp": self.timestamp,
|
172
|
+
"service": self.service,
|
173
|
+
}
|
174
|
+
# Feature for adding version information (currently disabled)
|
175
|
+
# if add_edsl_version:
|
176
|
+
# from edsl import __version__
|
177
|
+
# d["edsl_version"] = __version__
|
178
|
+
# d["edsl_class_name"] = self.__class__.__name__
|
179
|
+
return d
|
180
|
+
|
181
|
+
def keys(self) -> List[str]:
|
182
|
+
"""
|
183
|
+
Returns a list of field names in this cache entry.
|
184
|
+
|
185
|
+
This method enables dict-like access to cache entry field names.
|
186
|
+
|
187
|
+
Returns:
|
188
|
+
List of field names from the dictionary representation
|
189
|
+
"""
|
190
|
+
return list(self.to_dict().keys())
|
191
|
+
|
192
|
+
def values(self) -> List[Any]:
|
193
|
+
"""
|
194
|
+
Returns a list of values for all fields in this cache entry.
|
195
|
+
|
196
|
+
This method enables dict-like access to cache entry values.
|
197
|
+
|
198
|
+
Returns:
|
199
|
+
List of values from the dictionary representation
|
200
|
+
"""
|
201
|
+
return list(self.to_dict().values())
|
202
|
+
|
203
|
+
def __getitem__(self, key: str) -> Any:
|
204
|
+
"""
|
205
|
+
Enables dictionary-style access to cache entry attributes.
|
206
|
+
|
207
|
+
This method allows accessing cache entry attributes using dictionary
|
208
|
+
syntax (e.g., entry["model"] instead of entry.model).
|
209
|
+
|
210
|
+
Args:
|
211
|
+
key: The name of the attribute to access
|
212
|
+
|
213
|
+
Returns:
|
214
|
+
The value of the specified attribute
|
215
|
+
|
216
|
+
Raises:
|
217
|
+
AttributeError: If the specified attribute doesn't exist
|
218
|
+
"""
|
219
|
+
return getattr(self, key)
|
220
|
+
|
221
|
+
@classmethod
|
222
|
+
def from_dict(cls, data: Dict[str, Any]) -> CacheEntry:
|
223
|
+
"""
|
224
|
+
Creates a CacheEntry object from a dictionary representation.
|
225
|
+
|
226
|
+
This factory method enables reconstruction of CacheEntry objects
|
227
|
+
from serialized dictionary representations, such as those produced
|
228
|
+
by the to_dict method.
|
229
|
+
|
230
|
+
Args:
|
231
|
+
data: Dictionary containing required CacheEntry fields
|
232
|
+
|
233
|
+
Returns:
|
234
|
+
A new CacheEntry instance with fields populated from the dictionary
|
235
|
+
|
236
|
+
Raises:
|
237
|
+
TypeError: If data contains fields with incorrect types
|
238
|
+
KeyError: If required fields are missing from data
|
239
|
+
"""
|
240
|
+
return cls(**data)
|
241
|
+
|
242
|
+
def __eq__(self, other: Any) -> bool:
|
243
|
+
"""
|
244
|
+
Compares this cache entry with another for equality.
|
245
|
+
|
246
|
+
This method checks if all fields except timestamp are equal between
|
247
|
+
this cache entry and another. The timestamp is excluded from the
|
248
|
+
comparison because it's typically not relevant for determining if
|
249
|
+
two entries represent the same cached response.
|
250
|
+
|
251
|
+
Args:
|
252
|
+
other: Another object to compare with this cache entry
|
253
|
+
|
254
|
+
Returns:
|
255
|
+
True if all fields except timestamp are equal, False otherwise
|
256
|
+
|
257
|
+
Note:
|
258
|
+
Returns False if other is not a CacheEntry instance
|
259
|
+
"""
|
260
|
+
if not isinstance(other, CacheEntry):
|
261
|
+
return False
|
262
|
+
for field in self.all_fields:
|
263
|
+
if getattr(self, field) != getattr(other, field) and field != "timestamp":
|
264
|
+
return False
|
265
|
+
return True
|
266
|
+
|
267
|
+
def __repr__(self) -> str:
|
268
|
+
"""
|
269
|
+
Returns a string representation of this cache entry.
|
270
|
+
|
271
|
+
This method creates a string representation that displays all fields
|
272
|
+
of the cache entry in a format that can be evaluated to recreate
|
273
|
+
the object.
|
274
|
+
|
275
|
+
Returns:
|
276
|
+
A string representation that can be passed to eval() to recreate
|
277
|
+
this cache entry
|
278
|
+
"""
|
279
|
+
return (
|
280
|
+
f"CacheEntry(model={repr(self.model)}, "
|
281
|
+
f"parameters={self.parameters}, "
|
282
|
+
f"system_prompt={repr(self.system_prompt)}, "
|
283
|
+
f"user_prompt={repr(self.user_prompt)}, "
|
284
|
+
f"output={repr(self.output)}, "
|
285
|
+
f"iteration={self.iteration}, "
|
286
|
+
f"timestamp={self.timestamp}, "
|
287
|
+
f"service={repr(self.service)})"
|
288
|
+
)
|
289
|
+
|
290
|
+
@classmethod
|
291
|
+
def example(cls, randomize: bool = False) -> CacheEntry:
|
292
|
+
"""
|
293
|
+
Creates an example CacheEntry instance for testing and demonstration.
|
294
|
+
|
295
|
+
This factory method generates a pre-populated CacheEntry with example
|
296
|
+
values, useful for testing, documentation, and examples.
|
297
|
+
|
298
|
+
Args:
|
299
|
+
randomize: If True, adds a random UUID to the system prompt to make
|
300
|
+
the entry unique and generate a different hash key
|
301
|
+
|
302
|
+
Returns:
|
303
|
+
A fully populated example CacheEntry instance
|
304
|
+
|
305
|
+
Example:
|
306
|
+
>>> entry = CacheEntry.example()
|
307
|
+
>>> isinstance(entry, CacheEntry)
|
308
|
+
True
|
309
|
+
>>> entry.model
|
310
|
+
'gpt-3.5-turbo'
|
311
|
+
"""
|
312
|
+
addition = "" if not randomize else str(uuid4())
|
313
|
+
return CacheEntry(
|
314
|
+
model="gpt-3.5-turbo",
|
315
|
+
parameters={"temperature": 0.5},
|
316
|
+
system_prompt=f"The quick brown fox jumps over the lazy dog.{addition}",
|
317
|
+
user_prompt="What does the fox say?",
|
318
|
+
output="The fox says 'hello'",
|
319
|
+
iteration=1,
|
320
|
+
timestamp=int(datetime.datetime.now(datetime.timezone.utc).timestamp()),
|
321
|
+
service="openai",
|
322
|
+
)
|
323
|
+
|
324
|
+
@classmethod
|
325
|
+
def example_dict(cls) -> Dict[str, CacheEntry]:
|
326
|
+
"""
|
327
|
+
Creates an example dictionary mapping a key to a CacheEntry.
|
328
|
+
|
329
|
+
This method demonstrates how CacheEntry objects are typically stored
|
330
|
+
in a cache, with their hash keys as dictionary keys.
|
331
|
+
|
332
|
+
Returns:
|
333
|
+
A dictionary with a single entry mapping the example entry's key
|
334
|
+
to the example entry
|
335
|
+
|
336
|
+
Note:
|
337
|
+
This is particularly useful for testing and demonstrating the
|
338
|
+
Cache class functionality
|
339
|
+
"""
|
340
|
+
cache_entry = cls.example()
|
341
|
+
return {cache_entry.key: cache_entry}
|
342
|
+
|
343
|
+
@classmethod
|
344
|
+
def fetch_input_example(cls) -> Dict[str, Any]:
|
345
|
+
"""
|
346
|
+
Creates an example input dictionary for a 'fetch' operation.
|
347
|
+
|
348
|
+
This method generates a dictionary containing the fields needed to
|
349
|
+
look up a cache entry (everything except the response/output fields).
|
350
|
+
|
351
|
+
Returns:
|
352
|
+
A dictionary with fields needed to generate a cache key for lookup
|
353
|
+
|
354
|
+
Note:
|
355
|
+
This is used by the Cache class to demonstrate fetch operations
|
356
|
+
"""
|
357
|
+
input = cls.example().to_dict()
|
358
|
+
_ = input.pop("timestamp")
|
359
|
+
_ = input.pop("output")
|
360
|
+
_ = input.pop("service")
|
361
|
+
return input
|
362
|
+
|
363
|
+
@classmethod
|
364
|
+
def store_input_example(cls) -> Dict[str, Any]:
|
365
|
+
"""
|
366
|
+
Creates an example input dictionary for a 'store' operation.
|
367
|
+
|
368
|
+
This method generates a dictionary containing the fields needed to
|
369
|
+
store a new cache entry, with 'output' renamed to 'response' to match
|
370
|
+
the API of the Cache.store method.
|
371
|
+
|
372
|
+
Returns:
|
373
|
+
A dictionary with fields needed to store a new cache entry
|
374
|
+
|
375
|
+
Note:
|
376
|
+
This is used by the Cache class to demonstrate store operations
|
377
|
+
"""
|
378
|
+
input = cls.example().to_dict()
|
379
|
+
_ = input.pop("timestamp")
|
380
|
+
input["response"] = input.pop("output")
|
381
|
+
return input
|
382
|
+
|
383
|
+
|
384
|
+
def main() -> None:
|
385
|
+
"""
|
386
|
+
Demonstration of CacheEntry functionality for interactive testing.
|
387
|
+
|
388
|
+
This function demonstrates the key features of the CacheEntry class,
|
389
|
+
including creating entries, calculating hash keys, converting to/from
|
390
|
+
dictionaries, and comparing entries.
|
391
|
+
|
392
|
+
Note:
|
393
|
+
This function is intended to be run in an interactive Python session
|
394
|
+
for exploration and testing, not as part of normal code execution.
|
395
|
+
"""
|
396
|
+
from .cache_entry import CacheEntry
|
397
|
+
|
398
|
+
# Create an example cache entry
|
399
|
+
cache_entry = CacheEntry.example()
|
400
|
+
print(f"Example cache entry: {cache_entry}")
|
401
|
+
|
402
|
+
# Demonstrate key generation
|
403
|
+
print(f"Cache key: {cache_entry.key}")
|
404
|
+
|
405
|
+
# Demonstrate serialization and deserialization
|
406
|
+
entry_dict = cache_entry.to_dict()
|
407
|
+
print(f"Dictionary representation: {entry_dict}")
|
408
|
+
reconstructed = CacheEntry.from_dict(entry_dict)
|
409
|
+
print(f"Reconstructed from dict: {reconstructed}")
|
410
|
+
|
411
|
+
# Demonstrate equality comparisons
|
412
|
+
print(f"Same content equals: {cache_entry == CacheEntry.example()}")
|
413
|
+
print(f"Same key equals: {cache_entry.key == CacheEntry.example().key}")
|
414
|
+
|
415
|
+
# Demonstrate repr evaluation
|
416
|
+
print(f"Repr can be evaluated: {eval(repr(cache_entry)) == cache_entry}")
|
417
|
+
|
418
|
+
# Demonstrate utility methods
|
419
|
+
print(f"Example dict: {CacheEntry.example_dict()}")
|
420
|
+
print(f"Fetch input example: {CacheEntry.fetch_input_example()}")
|
421
|
+
print(f"Store input example: {CacheEntry.store_input_example()}")
|
422
|
+
|
423
|
+
|
424
|
+
if __name__ == "__main__":
|
425
|
+
import doctest
|
426
|
+
|
427
|
+
doctest.testmod()
|
@@ -6,15 +6,14 @@ import shutil
|
|
6
6
|
from typing import TYPE_CHECKING
|
7
7
|
|
8
8
|
if TYPE_CHECKING:
|
9
|
-
from
|
10
|
-
from
|
11
|
-
|
9
|
+
from .cache import Cache
|
10
|
+
from .cache_entry import CacheEntry
|
12
11
|
|
13
12
|
def set_session_cache(cache: "Cache") -> None:
|
14
13
|
"""
|
15
14
|
Set the session cache.
|
16
15
|
"""
|
17
|
-
from
|
16
|
+
from ..config import CONFIG
|
18
17
|
|
19
18
|
CONFIG.EDSL_SESSION_CACHE = cache
|
20
19
|
|
@@ -23,7 +22,7 @@ def unset_session_cache() -> None:
|
|
23
22
|
"""
|
24
23
|
Unset the session cache.
|
25
24
|
"""
|
26
|
-
from
|
25
|
+
from ..config import CONFIG
|
27
26
|
|
28
27
|
if hasattr(CONFIG, "EDSL_SESSION_CACHE"):
|
29
28
|
del CONFIG.EDSL_SESSION_CACHE
|
@@ -32,11 +31,14 @@ def unset_session_cache() -> None:
|
|
32
31
|
class CacheHandler:
|
33
32
|
"""
|
34
33
|
This CacheHandler figures out what caches are available and does migrations, as needed.
|
34
|
+
|
35
|
+
>>> cache_handler = CacheHandler()
|
36
|
+
>>> c = cache_handler.get_cache()
|
35
37
|
"""
|
36
38
|
|
37
39
|
@property
|
38
40
|
def CACHE_PATH(self):
|
39
|
-
from
|
41
|
+
from ..config import CONFIG
|
40
42
|
|
41
43
|
return CONFIG.get("EDSL_DATABASE_PATH")
|
42
44
|
|
@@ -62,17 +64,17 @@ class CacheHandler:
|
|
62
64
|
"""
|
63
65
|
Generate a Cache object.
|
64
66
|
"""
|
65
|
-
from
|
67
|
+
from .cache import Cache
|
66
68
|
|
67
69
|
if self.test:
|
68
70
|
return Cache(data={})
|
69
71
|
|
70
|
-
from
|
72
|
+
from ..config import CONFIG
|
71
73
|
|
72
74
|
if hasattr(CONFIG, "EDSL_SESSION_CACHE"):
|
73
75
|
return CONFIG.EDSL_SESSION_CACHE
|
74
76
|
|
75
|
-
from
|
77
|
+
from .sql_dict import SQLiteDict
|
76
78
|
|
77
79
|
cache = Cache(data=SQLiteDict(self.CACHE_PATH))
|
78
80
|
return cache
|
@@ -114,15 +116,13 @@ class CacheHandler:
|
|
114
116
|
return old_data
|
115
117
|
|
116
118
|
def _parse_old_cache_entry(self, row: tuple, schema) -> CacheEntry:
|
117
|
-
"""
|
118
|
-
Parse an old cache entry.
|
119
|
-
"""
|
119
|
+
"""Parse an old cache entry."""
|
120
120
|
entry_dict = {k: row[i] for i, k in enumerate(schema.keys())}
|
121
121
|
_ = entry_dict.pop("id")
|
122
122
|
entry_dict["user_prompt"] = entry_dict.pop("prompt")
|
123
123
|
parameters = entry_dict["parameters"]
|
124
124
|
entry_dict["parameters"] = ast.literal_eval(parameters)
|
125
|
-
from
|
125
|
+
from .cache_entry import CacheEntry
|
126
126
|
|
127
127
|
entry = CacheEntry(**entry_dict)
|
128
128
|
return entry
|
@@ -137,6 +137,7 @@ class CacheHandler:
|
|
137
137
|
"""
|
138
138
|
Read in a new-style sqlite cache and return a dictionary of dictionaries.
|
139
139
|
"""
|
140
|
+
import sqlite3
|
140
141
|
conn = sqlite3.connect(uri)
|
141
142
|
with conn:
|
142
143
|
cur = conn.cursor()
|
@@ -162,7 +163,5 @@ class CacheHandler:
|
|
162
163
|
|
163
164
|
|
164
165
|
if __name__ == "__main__":
|
165
|
-
# ch = CacheHandler()
|
166
166
|
import doctest
|
167
|
-
|
168
167
|
doctest.testmod()
|
@@ -0,0 +1,24 @@
|
|
1
|
+
"""
|
2
|
+
Custom exceptions for the data module in the EDSL framework.
|
3
|
+
|
4
|
+
This module defines exceptions that are raised during cache operations,
|
5
|
+
such as when entries cannot be stored, retrieved, or synchronized.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from ..base import BaseException
|
9
|
+
|
10
|
+
|
11
|
+
class CacheError(BaseException):
|
12
|
+
"""
|
13
|
+
Exception raised for errors related to cache operations.
|
14
|
+
|
15
|
+
This exception is raised when cache operations fail, such as when:
|
16
|
+
- A cache key cannot be generated
|
17
|
+
- A cache entry cannot be stored or retrieved
|
18
|
+
- A cache synchronization operation fails
|
19
|
+
- Cache migration encounters an error
|
20
|
+
|
21
|
+
Attributes:
|
22
|
+
message (str): Explanation of the error
|
23
|
+
"""
|
24
|
+
relevant_doc = "https://docs.expectedparrot.com/en/latest/data.html#cache"
|
edsl/caching/orm.py
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
"""
|
2
|
+
SQLAlchemy ORM definitions for EDSL data persistence.
|
3
|
+
|
4
|
+
This module defines the SQLAlchemy ORM models used for storing cache data
|
5
|
+
in a SQL database. It provides a simple key-value schema that allows
|
6
|
+
for efficient storage and retrieval of cached data.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from sqlalchemy import Column, String
|
10
|
+
from sqlalchemy.ext.declarative import declarative_base
|
11
|
+
|
12
|
+
Base = declarative_base()
|
13
|
+
|
14
|
+
|
15
|
+
class Data(Base):
|
16
|
+
"""
|
17
|
+
SQLAlchemy ORM model for key-value data storage.
|
18
|
+
|
19
|
+
This class represents a table in the SQL database with a simple
|
20
|
+
key-value schema. It is used by the Cache and SQLiteDict classes
|
21
|
+
to store cached data persistently.
|
22
|
+
|
23
|
+
Attributes:
|
24
|
+
__tablename__ (str): Name of the database table ("data")
|
25
|
+
key (Column): Primary key column for storing lookup keys
|
26
|
+
value (Column): Column for storing serialized data values
|
27
|
+
"""
|
28
|
+
__tablename__ = "data"
|
29
|
+
key = Column(String, primary_key=True)
|
30
|
+
value = Column(String)
|
@@ -4,9 +4,9 @@ from contextlib import AbstractContextManager
|
|
4
4
|
from collections import UserList
|
5
5
|
|
6
6
|
if TYPE_CHECKING:
|
7
|
-
from .
|
7
|
+
from .cache import Cache
|
8
8
|
from edsl.coop.coop import Coop
|
9
|
-
from .
|
9
|
+
from .cache_entry import CacheEntry
|
10
10
|
|
11
11
|
from logging import Logger
|
12
12
|
|
@@ -35,7 +35,7 @@ class CacheEntriesList(UserList):
|
|
35
35
|
return f"CacheEntries({entries_repr})"
|
36
36
|
|
37
37
|
def to_cache(self) -> "Cache":
|
38
|
-
from edsl.
|
38
|
+
from edsl.caching.cache import Cache
|
39
39
|
|
40
40
|
return Cache({entry.key: entry for entry in self.data})
|
41
41
|
|