edsl 0.1.14__py3-none-any.whl → 0.1.40__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/Base.py +348 -38
- edsl/BaseDiff.py +260 -0
- edsl/TemplateLoader.py +24 -0
- edsl/__init__.py +46 -10
- edsl/__version__.py +1 -0
- edsl/agents/Agent.py +842 -144
- edsl/agents/AgentList.py +521 -25
- edsl/agents/Invigilator.py +250 -374
- edsl/agents/InvigilatorBase.py +257 -0
- edsl/agents/PromptConstructor.py +272 -0
- edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
- edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
- edsl/agents/descriptors.py +43 -13
- edsl/agents/prompt_helpers.py +129 -0
- edsl/agents/question_option_processor.py +172 -0
- edsl/auto/AutoStudy.py +130 -0
- edsl/auto/StageBase.py +243 -0
- edsl/auto/StageGenerateSurvey.py +178 -0
- edsl/auto/StageLabelQuestions.py +125 -0
- edsl/auto/StagePersona.py +61 -0
- edsl/auto/StagePersonaDimensionValueRanges.py +88 -0
- edsl/auto/StagePersonaDimensionValues.py +74 -0
- edsl/auto/StagePersonaDimensions.py +69 -0
- edsl/auto/StageQuestions.py +74 -0
- edsl/auto/SurveyCreatorPipeline.py +21 -0
- edsl/auto/utilities.py +218 -0
- edsl/base/Base.py +279 -0
- edsl/config.py +121 -104
- edsl/conversation/Conversation.py +290 -0
- edsl/conversation/car_buying.py +59 -0
- edsl/conversation/chips.py +95 -0
- edsl/conversation/mug_negotiation.py +81 -0
- edsl/conversation/next_speaker_utilities.py +93 -0
- edsl/coop/CoopFunctionsMixin.py +15 -0
- edsl/coop/ExpectedParrotKeyHandler.py +125 -0
- edsl/coop/PriceFetcher.py +54 -0
- edsl/coop/__init__.py +1 -0
- edsl/coop/coop.py +1029 -134
- edsl/coop/utils.py +131 -0
- edsl/data/Cache.py +560 -89
- edsl/data/CacheEntry.py +230 -0
- edsl/data/CacheHandler.py +168 -0
- edsl/data/RemoteCacheSync.py +186 -0
- edsl/data/SQLiteDict.py +292 -0
- edsl/data/__init__.py +5 -3
- edsl/data/orm.py +6 -33
- edsl/data_transfer_models.py +74 -27
- edsl/enums.py +165 -8
- edsl/exceptions/BaseException.py +21 -0
- edsl/exceptions/__init__.py +52 -46
- edsl/exceptions/agents.py +33 -15
- edsl/exceptions/cache.py +5 -0
- edsl/exceptions/coop.py +8 -0
- edsl/exceptions/general.py +34 -0
- edsl/exceptions/inference_services.py +5 -0
- edsl/exceptions/jobs.py +15 -0
- edsl/exceptions/language_models.py +46 -1
- edsl/exceptions/questions.py +80 -5
- edsl/exceptions/results.py +16 -5
- edsl/exceptions/scenarios.py +29 -0
- edsl/exceptions/surveys.py +13 -10
- edsl/inference_services/AnthropicService.py +106 -0
- edsl/inference_services/AvailableModelCacheHandler.py +184 -0
- edsl/inference_services/AvailableModelFetcher.py +215 -0
- edsl/inference_services/AwsBedrock.py +118 -0
- edsl/inference_services/AzureAI.py +215 -0
- edsl/inference_services/DeepInfraService.py +18 -0
- edsl/inference_services/GoogleService.py +143 -0
- edsl/inference_services/GroqService.py +20 -0
- edsl/inference_services/InferenceServiceABC.py +80 -0
- edsl/inference_services/InferenceServicesCollection.py +138 -0
- edsl/inference_services/MistralAIService.py +120 -0
- edsl/inference_services/OllamaService.py +18 -0
- edsl/inference_services/OpenAIService.py +236 -0
- edsl/inference_services/PerplexityService.py +160 -0
- edsl/inference_services/ServiceAvailability.py +135 -0
- edsl/inference_services/TestService.py +90 -0
- edsl/inference_services/TogetherAIService.py +172 -0
- edsl/inference_services/data_structures.py +134 -0
- edsl/inference_services/models_available_cache.py +118 -0
- edsl/inference_services/rate_limits_cache.py +25 -0
- edsl/inference_services/registry.py +41 -0
- edsl/inference_services/write_available.py +10 -0
- edsl/jobs/AnswerQuestionFunctionConstructor.py +223 -0
- edsl/jobs/Answers.py +21 -20
- edsl/jobs/FetchInvigilator.py +47 -0
- edsl/jobs/InterviewTaskManager.py +98 -0
- edsl/jobs/InterviewsConstructor.py +50 -0
- edsl/jobs/Jobs.py +684 -204
- edsl/jobs/JobsChecks.py +172 -0
- edsl/jobs/JobsComponentConstructor.py +189 -0
- edsl/jobs/JobsPrompts.py +270 -0
- edsl/jobs/JobsRemoteInferenceHandler.py +311 -0
- edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
- edsl/jobs/RequestTokenEstimator.py +30 -0
- edsl/jobs/async_interview_runner.py +138 -0
- edsl/jobs/buckets/BucketCollection.py +104 -0
- edsl/jobs/buckets/ModelBuckets.py +65 -0
- edsl/jobs/buckets/TokenBucket.py +283 -0
- edsl/jobs/buckets/TokenBucketAPI.py +211 -0
- edsl/jobs/buckets/TokenBucketClient.py +191 -0
- edsl/jobs/check_survey_scenario_compatibility.py +85 -0
- edsl/jobs/data_structures.py +120 -0
- edsl/jobs/decorators.py +35 -0
- edsl/jobs/interviews/Interview.py +392 -0
- edsl/jobs/interviews/InterviewExceptionCollection.py +99 -0
- edsl/jobs/interviews/InterviewExceptionEntry.py +186 -0
- edsl/jobs/interviews/InterviewStatistic.py +63 -0
- edsl/jobs/interviews/InterviewStatisticsCollection.py +25 -0
- edsl/jobs/interviews/InterviewStatusDictionary.py +78 -0
- edsl/jobs/interviews/InterviewStatusLog.py +92 -0
- edsl/jobs/interviews/ReportErrors.py +66 -0
- edsl/jobs/interviews/interview_status_enum.py +9 -0
- edsl/jobs/jobs_status_enums.py +9 -0
- edsl/jobs/loggers/HTMLTableJobLogger.py +304 -0
- edsl/jobs/results_exceptions_handler.py +98 -0
- edsl/jobs/runners/JobsRunnerAsyncio.py +151 -110
- edsl/jobs/runners/JobsRunnerStatus.py +298 -0
- edsl/jobs/tasks/QuestionTaskCreator.py +244 -0
- edsl/jobs/tasks/TaskCreators.py +64 -0
- edsl/jobs/tasks/TaskHistory.py +470 -0
- edsl/jobs/tasks/TaskStatusLog.py +23 -0
- edsl/jobs/tasks/task_status_enum.py +161 -0
- edsl/jobs/tokens/InterviewTokenUsage.py +27 -0
- edsl/jobs/tokens/TokenUsage.py +34 -0
- edsl/language_models/ComputeCost.py +63 -0
- edsl/language_models/LanguageModel.py +507 -386
- edsl/language_models/ModelList.py +164 -0
- edsl/language_models/PriceManager.py +127 -0
- edsl/language_models/RawResponseHandler.py +106 -0
- edsl/language_models/RegisterLanguageModelsMeta.py +184 -0
- edsl/language_models/__init__.py +1 -8
- edsl/language_models/fake_openai_call.py +15 -0
- edsl/language_models/fake_openai_service.py +61 -0
- edsl/language_models/key_management/KeyLookup.py +63 -0
- edsl/language_models/key_management/KeyLookupBuilder.py +273 -0
- edsl/language_models/key_management/KeyLookupCollection.py +38 -0
- edsl/language_models/key_management/__init__.py +0 -0
- edsl/language_models/key_management/models.py +131 -0
- edsl/language_models/model.py +256 -0
- edsl/language_models/repair.py +109 -41
- edsl/language_models/utilities.py +65 -0
- edsl/notebooks/Notebook.py +263 -0
- edsl/notebooks/NotebookToLaTeX.py +142 -0
- edsl/notebooks/__init__.py +1 -0
- edsl/prompts/Prompt.py +222 -93
- edsl/prompts/__init__.py +1 -1
- edsl/questions/ExceptionExplainer.py +77 -0
- edsl/questions/HTMLQuestion.py +103 -0
- edsl/questions/QuestionBase.py +518 -0
- edsl/questions/QuestionBasePromptsMixin.py +221 -0
- edsl/questions/QuestionBudget.py +164 -67
- edsl/questions/QuestionCheckBox.py +281 -62
- edsl/questions/QuestionDict.py +343 -0
- edsl/questions/QuestionExtract.py +136 -50
- edsl/questions/QuestionFreeText.py +79 -55
- edsl/questions/QuestionFunctional.py +138 -41
- edsl/questions/QuestionList.py +184 -57
- edsl/questions/QuestionMatrix.py +265 -0
- edsl/questions/QuestionMultipleChoice.py +293 -69
- edsl/questions/QuestionNumerical.py +109 -56
- edsl/questions/QuestionRank.py +244 -49
- edsl/questions/Quick.py +41 -0
- edsl/questions/SimpleAskMixin.py +74 -0
- edsl/questions/__init__.py +9 -6
- edsl/questions/{AnswerValidatorMixin.py → answer_validator_mixin.py} +153 -38
- edsl/questions/compose_questions.py +13 -7
- edsl/questions/data_structures.py +20 -0
- edsl/questions/decorators.py +21 -0
- edsl/questions/derived/QuestionLikertFive.py +28 -26
- edsl/questions/derived/QuestionLinearScale.py +41 -28
- edsl/questions/derived/QuestionTopK.py +34 -26
- edsl/questions/derived/QuestionYesNo.py +40 -27
- edsl/questions/descriptors.py +228 -74
- edsl/questions/loop_processor.py +149 -0
- edsl/questions/prompt_templates/question_budget.jinja +13 -0
- edsl/questions/prompt_templates/question_checkbox.jinja +32 -0
- edsl/questions/prompt_templates/question_extract.jinja +11 -0
- edsl/questions/prompt_templates/question_free_text.jinja +3 -0
- edsl/questions/prompt_templates/question_linear_scale.jinja +11 -0
- edsl/questions/prompt_templates/question_list.jinja +17 -0
- edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -0
- edsl/questions/prompt_templates/question_numerical.jinja +37 -0
- edsl/questions/question_base_gen_mixin.py +168 -0
- edsl/questions/question_registry.py +130 -46
- edsl/questions/register_questions_meta.py +71 -0
- edsl/questions/response_validator_abc.py +188 -0
- edsl/questions/response_validator_factory.py +34 -0
- edsl/questions/settings.py +5 -2
- edsl/questions/templates/__init__.py +0 -0
- edsl/questions/templates/budget/__init__.py +0 -0
- edsl/questions/templates/budget/answering_instructions.jinja +7 -0
- edsl/questions/templates/budget/question_presentation.jinja +7 -0
- edsl/questions/templates/checkbox/__init__.py +0 -0
- edsl/questions/templates/checkbox/answering_instructions.jinja +10 -0
- edsl/questions/templates/checkbox/question_presentation.jinja +22 -0
- edsl/questions/templates/dict/__init__.py +0 -0
- edsl/questions/templates/dict/answering_instructions.jinja +21 -0
- edsl/questions/templates/dict/question_presentation.jinja +1 -0
- edsl/questions/templates/extract/__init__.py +0 -0
- edsl/questions/templates/extract/answering_instructions.jinja +7 -0
- edsl/questions/templates/extract/question_presentation.jinja +1 -0
- edsl/questions/templates/free_text/__init__.py +0 -0
- edsl/questions/templates/free_text/answering_instructions.jinja +0 -0
- edsl/questions/templates/free_text/question_presentation.jinja +1 -0
- edsl/questions/templates/likert_five/__init__.py +0 -0
- edsl/questions/templates/likert_five/answering_instructions.jinja +10 -0
- edsl/questions/templates/likert_five/question_presentation.jinja +12 -0
- edsl/questions/templates/linear_scale/__init__.py +0 -0
- edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -0
- edsl/questions/templates/linear_scale/question_presentation.jinja +5 -0
- edsl/questions/templates/list/__init__.py +0 -0
- edsl/questions/templates/list/answering_instructions.jinja +4 -0
- edsl/questions/templates/list/question_presentation.jinja +5 -0
- edsl/questions/templates/matrix/__init__.py +1 -0
- edsl/questions/templates/matrix/answering_instructions.jinja +5 -0
- edsl/questions/templates/matrix/question_presentation.jinja +20 -0
- edsl/questions/templates/multiple_choice/__init__.py +0 -0
- edsl/questions/templates/multiple_choice/answering_instructions.jinja +9 -0
- edsl/questions/templates/multiple_choice/html.jinja +0 -0
- edsl/questions/templates/multiple_choice/question_presentation.jinja +12 -0
- edsl/questions/templates/numerical/__init__.py +0 -0
- edsl/questions/templates/numerical/answering_instructions.jinja +7 -0
- edsl/questions/templates/numerical/question_presentation.jinja +7 -0
- edsl/questions/templates/rank/__init__.py +0 -0
- edsl/questions/templates/rank/answering_instructions.jinja +11 -0
- edsl/questions/templates/rank/question_presentation.jinja +15 -0
- edsl/questions/templates/top_k/__init__.py +0 -0
- edsl/questions/templates/top_k/answering_instructions.jinja +8 -0
- edsl/questions/templates/top_k/question_presentation.jinja +22 -0
- edsl/questions/templates/yes_no/__init__.py +0 -0
- edsl/questions/templates/yes_no/answering_instructions.jinja +6 -0
- edsl/questions/templates/yes_no/question_presentation.jinja +12 -0
- edsl/results/CSSParameterizer.py +108 -0
- edsl/results/Dataset.py +550 -19
- edsl/results/DatasetExportMixin.py +594 -0
- edsl/results/DatasetTree.py +295 -0
- edsl/results/MarkdownToDocx.py +122 -0
- edsl/results/MarkdownToPDF.py +111 -0
- edsl/results/Result.py +477 -173
- edsl/results/Results.py +987 -269
- edsl/results/ResultsExportMixin.py +28 -125
- edsl/results/ResultsGGMixin.py +83 -15
- edsl/results/TableDisplay.py +125 -0
- edsl/results/TextEditor.py +50 -0
- edsl/results/__init__.py +1 -1
- edsl/results/file_exports.py +252 -0
- edsl/results/results_fetch_mixin.py +33 -0
- edsl/results/results_selector.py +145 -0
- edsl/results/results_tools_mixin.py +98 -0
- edsl/results/smart_objects.py +96 -0
- edsl/results/table_data_class.py +12 -0
- edsl/results/table_display.css +78 -0
- edsl/results/table_renderers.py +118 -0
- edsl/results/tree_explore.py +115 -0
- edsl/scenarios/ConstructDownloadLink.py +109 -0
- edsl/scenarios/DocumentChunker.py +102 -0
- edsl/scenarios/DocxScenario.py +16 -0
- edsl/scenarios/FileStore.py +543 -0
- edsl/scenarios/PdfExtractor.py +40 -0
- edsl/scenarios/Scenario.py +431 -62
- edsl/scenarios/ScenarioHtmlMixin.py +65 -0
- edsl/scenarios/ScenarioList.py +1415 -45
- edsl/scenarios/ScenarioListExportMixin.py +45 -0
- edsl/scenarios/ScenarioListPdfMixin.py +239 -0
- edsl/scenarios/__init__.py +2 -0
- edsl/scenarios/directory_scanner.py +96 -0
- edsl/scenarios/file_methods.py +85 -0
- edsl/scenarios/handlers/__init__.py +13 -0
- edsl/scenarios/handlers/csv.py +49 -0
- edsl/scenarios/handlers/docx.py +76 -0
- edsl/scenarios/handlers/html.py +37 -0
- edsl/scenarios/handlers/json.py +111 -0
- edsl/scenarios/handlers/latex.py +5 -0
- edsl/scenarios/handlers/md.py +51 -0
- edsl/scenarios/handlers/pdf.py +68 -0
- edsl/scenarios/handlers/png.py +39 -0
- edsl/scenarios/handlers/pptx.py +105 -0
- edsl/scenarios/handlers/py.py +294 -0
- edsl/scenarios/handlers/sql.py +313 -0
- edsl/scenarios/handlers/sqlite.py +149 -0
- edsl/scenarios/handlers/txt.py +33 -0
- edsl/scenarios/scenario_join.py +131 -0
- edsl/scenarios/scenario_selector.py +156 -0
- edsl/shared.py +1 -0
- edsl/study/ObjectEntry.py +173 -0
- edsl/study/ProofOfWork.py +113 -0
- edsl/study/SnapShot.py +80 -0
- edsl/study/Study.py +521 -0
- edsl/study/__init__.py +4 -0
- edsl/surveys/ConstructDAG.py +92 -0
- edsl/surveys/DAG.py +92 -11
- edsl/surveys/EditSurvey.py +221 -0
- edsl/surveys/InstructionHandler.py +100 -0
- edsl/surveys/Memory.py +9 -4
- edsl/surveys/MemoryManagement.py +72 -0
- edsl/surveys/MemoryPlan.py +156 -35
- edsl/surveys/Rule.py +221 -74
- edsl/surveys/RuleCollection.py +241 -61
- edsl/surveys/RuleManager.py +172 -0
- edsl/surveys/Simulator.py +75 -0
- edsl/surveys/Survey.py +1079 -339
- edsl/surveys/SurveyCSS.py +273 -0
- edsl/surveys/SurveyExportMixin.py +235 -40
- edsl/surveys/SurveyFlowVisualization.py +181 -0
- edsl/surveys/SurveyQualtricsImport.py +284 -0
- edsl/surveys/SurveyToApp.py +141 -0
- edsl/surveys/__init__.py +4 -2
- edsl/surveys/base.py +19 -3
- edsl/surveys/descriptors.py +17 -6
- edsl/surveys/instructions/ChangeInstruction.py +48 -0
- edsl/surveys/instructions/Instruction.py +56 -0
- edsl/surveys/instructions/InstructionCollection.py +82 -0
- edsl/surveys/instructions/__init__.py +0 -0
- edsl/templates/error_reporting/base.html +24 -0
- edsl/templates/error_reporting/exceptions_by_model.html +35 -0
- edsl/templates/error_reporting/exceptions_by_question_name.html +17 -0
- edsl/templates/error_reporting/exceptions_by_type.html +17 -0
- edsl/templates/error_reporting/interview_details.html +116 -0
- edsl/templates/error_reporting/interviews.html +19 -0
- edsl/templates/error_reporting/overview.html +5 -0
- edsl/templates/error_reporting/performance_plot.html +2 -0
- edsl/templates/error_reporting/report.css +74 -0
- edsl/templates/error_reporting/report.html +118 -0
- edsl/templates/error_reporting/report.js +25 -0
- edsl/tools/__init__.py +1 -0
- edsl/tools/clusters.py +192 -0
- edsl/tools/embeddings.py +27 -0
- edsl/tools/embeddings_plotting.py +118 -0
- edsl/tools/plotting.py +112 -0
- edsl/tools/summarize.py +18 -0
- edsl/utilities/PrettyList.py +56 -0
- edsl/utilities/SystemInfo.py +5 -0
- edsl/utilities/__init__.py +21 -20
- edsl/utilities/ast_utilities.py +3 -0
- edsl/utilities/data/Registry.py +2 -0
- edsl/utilities/decorators.py +41 -0
- edsl/utilities/gcp_bucket/__init__.py +0 -0
- edsl/utilities/gcp_bucket/cloud_storage.py +96 -0
- edsl/utilities/interface.py +310 -60
- edsl/utilities/is_notebook.py +18 -0
- edsl/utilities/is_valid_variable_name.py +11 -0
- edsl/utilities/naming_utilities.py +263 -0
- edsl/utilities/remove_edsl_version.py +24 -0
- edsl/utilities/repair_functions.py +28 -0
- edsl/utilities/restricted_python.py +70 -0
- edsl/utilities/utilities.py +203 -13
- edsl-0.1.40.dist-info/METADATA +111 -0
- edsl-0.1.40.dist-info/RECORD +362 -0
- {edsl-0.1.14.dist-info → edsl-0.1.40.dist-info}/WHEEL +1 -1
- edsl/agents/AgentListExportMixin.py +0 -24
- edsl/coop/old.py +0 -31
- edsl/data/Database.py +0 -141
- edsl/data/crud.py +0 -121
- edsl/jobs/Interview.py +0 -417
- edsl/jobs/JobsRunner.py +0 -63
- edsl/jobs/JobsRunnerStatusMixin.py +0 -115
- edsl/jobs/base.py +0 -47
- edsl/jobs/buckets.py +0 -166
- edsl/jobs/runners/JobsRunnerDryRun.py +0 -19
- edsl/jobs/runners/JobsRunnerStreaming.py +0 -54
- edsl/jobs/task_management.py +0 -218
- edsl/jobs/token_tracking.py +0 -78
- edsl/language_models/DeepInfra.py +0 -69
- edsl/language_models/OpenAI.py +0 -98
- edsl/language_models/model_interfaces/GeminiPro.py +0 -66
- edsl/language_models/model_interfaces/LanguageModelOpenAIFour.py +0 -8
- edsl/language_models/model_interfaces/LanguageModelOpenAIThreeFiveTurbo.py +0 -8
- edsl/language_models/model_interfaces/LlamaTwo13B.py +0 -21
- edsl/language_models/model_interfaces/LlamaTwo70B.py +0 -21
- edsl/language_models/model_interfaces/Mixtral8x7B.py +0 -24
- edsl/language_models/registry.py +0 -81
- edsl/language_models/schemas.py +0 -15
- edsl/language_models/unused/ReplicateBase.py +0 -83
- edsl/prompts/QuestionInstructionsBase.py +0 -6
- edsl/prompts/library/agent_instructions.py +0 -29
- edsl/prompts/library/agent_persona.py +0 -17
- edsl/prompts/library/question_budget.py +0 -26
- edsl/prompts/library/question_checkbox.py +0 -32
- edsl/prompts/library/question_extract.py +0 -19
- edsl/prompts/library/question_freetext.py +0 -14
- edsl/prompts/library/question_linear_scale.py +0 -20
- edsl/prompts/library/question_list.py +0 -22
- edsl/prompts/library/question_multiple_choice.py +0 -44
- edsl/prompts/library/question_numerical.py +0 -31
- edsl/prompts/library/question_rank.py +0 -21
- edsl/prompts/prompt_config.py +0 -33
- edsl/prompts/registry.py +0 -185
- edsl/questions/Question.py +0 -240
- edsl/report/InputOutputDataTypes.py +0 -134
- edsl/report/RegressionMixin.py +0 -28
- edsl/report/ReportOutputs.py +0 -1228
- edsl/report/ResultsFetchMixin.py +0 -106
- edsl/report/ResultsOutputMixin.py +0 -14
- edsl/report/demo.ipynb +0 -645
- edsl/results/ResultsDBMixin.py +0 -184
- edsl/surveys/SurveyFlowVisualizationMixin.py +0 -92
- edsl/trackers/Tracker.py +0 -91
- edsl/trackers/TrackerAPI.py +0 -196
- edsl/trackers/TrackerTasks.py +0 -70
- edsl/utilities/pastebin.py +0 -141
- edsl-0.1.14.dist-info/METADATA +0 -69
- edsl-0.1.14.dist-info/RECORD +0 -141
- /edsl/{language_models/model_interfaces → inference_services}/__init__.py +0 -0
- /edsl/{report/__init__.py → jobs/runners/JobsRunnerStatusData.py} +0 -0
- /edsl/{trackers/__init__.py → language_models/ServiceDataSources.py} +0 -0
- {edsl-0.1.14.dist-info → edsl-0.1.40.dist-info}/LICENSE +0 -0
@@ -0,0 +1,543 @@
|
|
1
|
+
import base64
|
2
|
+
import io
|
3
|
+
import tempfile
|
4
|
+
import mimetypes
|
5
|
+
import os
|
6
|
+
from typing import Dict, Any, IO, Optional
|
7
|
+
|
8
|
+
from edsl.scenarios.Scenario import Scenario
|
9
|
+
from edsl.utilities.remove_edsl_version import remove_edsl_version
|
10
|
+
|
11
|
+
from edsl.scenarios.file_methods import FileMethods
|
12
|
+
|
13
|
+
|
14
|
+
class FileStore(Scenario):
|
15
|
+
__documentation__ = "https://docs.expectedparrot.com/en/latest/filestore.html"
|
16
|
+
|
17
|
+
def __init__(
|
18
|
+
self,
|
19
|
+
path: Optional[str] = None,
|
20
|
+
mime_type: Optional[str] = None,
|
21
|
+
binary: Optional[bool] = None,
|
22
|
+
suffix: Optional[str] = None,
|
23
|
+
base64_string: Optional[str] = None,
|
24
|
+
external_locations: Optional[Dict[str, str]] = None,
|
25
|
+
extracted_text: Optional[str] = None,
|
26
|
+
**kwargs,
|
27
|
+
):
|
28
|
+
if path is None and "filename" in kwargs:
|
29
|
+
path = kwargs["filename"]
|
30
|
+
|
31
|
+
self._path = path # Store the original path privately
|
32
|
+
self._temp_path = None # Track any generated temporary file
|
33
|
+
|
34
|
+
self.suffix = suffix or path.split(".")[-1]
|
35
|
+
self.binary = binary or False
|
36
|
+
self.mime_type = (
|
37
|
+
mime_type or mimetypes.guess_type(path)[0] or "application/octet-stream"
|
38
|
+
)
|
39
|
+
self.base64_string = base64_string or self.encode_file_to_base64_string(path)
|
40
|
+
self.external_locations = external_locations or {}
|
41
|
+
|
42
|
+
self.extracted_text = (
|
43
|
+
self.extract_text() if extracted_text is None else extracted_text
|
44
|
+
)
|
45
|
+
|
46
|
+
super().__init__(
|
47
|
+
{
|
48
|
+
"path": path,
|
49
|
+
"base64_string": self.base64_string,
|
50
|
+
"binary": self.binary,
|
51
|
+
"suffix": self.suffix,
|
52
|
+
"mime_type": self.mime_type,
|
53
|
+
"external_locations": self.external_locations,
|
54
|
+
"extracted_text": self.extracted_text,
|
55
|
+
}
|
56
|
+
)
|
57
|
+
|
58
|
+
@property
|
59
|
+
def path(self) -> str:
|
60
|
+
"""
|
61
|
+
Property that returns a valid path to the file content.
|
62
|
+
If the original path doesn't exist, generates a temporary file from the base64 content.
|
63
|
+
"""
|
64
|
+
# Check if original path exists and is accessible
|
65
|
+
if self._path and os.path.isfile(self._path):
|
66
|
+
return self._path
|
67
|
+
|
68
|
+
# If we already have a valid temporary file, use it
|
69
|
+
if self._temp_path and os.path.isfile(self._temp_path):
|
70
|
+
return self._temp_path
|
71
|
+
|
72
|
+
# Generate a new temporary file from base64 content
|
73
|
+
self._temp_path = self.to_tempfile(self.suffix)
|
74
|
+
return self._temp_path
|
75
|
+
|
76
|
+
def __str__(self):
|
77
|
+
return "FileStore: self.path"
|
78
|
+
|
79
|
+
@classmethod
|
80
|
+
def example(cls, example_type="txt"):
|
81
|
+
file_methods_class = FileMethods.get_handler(example_type)
|
82
|
+
if file_methods_class:
|
83
|
+
return cls(file_methods_class().example())
|
84
|
+
else:
|
85
|
+
print(f"Example for {example_type} is not supported.")
|
86
|
+
|
87
|
+
@property
|
88
|
+
def size(self) -> int:
|
89
|
+
if self.base64_string != None:
|
90
|
+
return (len(self.base64_string) / 4.0) * 3 # from base64 to char size
|
91
|
+
return os.path.getsize(self.path)
|
92
|
+
|
93
|
+
def upload_google(self, refresh: bool = False) -> None:
|
94
|
+
import google.generativeai as genai
|
95
|
+
|
96
|
+
genai.configure(api_key=os.getenv("GOOGLE_API_KEY"))
|
97
|
+
google_info = genai.upload_file(self.path, mime_type=self.mime_type)
|
98
|
+
self.external_locations["google"] = google_info.to_dict()
|
99
|
+
|
100
|
+
@classmethod
|
101
|
+
@remove_edsl_version
|
102
|
+
def from_dict(cls, d):
|
103
|
+
# return cls(d["filename"], d["binary"], d["suffix"], d["base64_string"])
|
104
|
+
return cls(**d)
|
105
|
+
|
106
|
+
def __repr__(self):
|
107
|
+
import reprlib
|
108
|
+
|
109
|
+
r = reprlib.Repr()
|
110
|
+
r.maxstring = 20 # Limit strings to 20 chars
|
111
|
+
r.maxother = 30 # Limit other types to 30 chars
|
112
|
+
|
113
|
+
params = ", ".join(f"{key}={r.repr(value)}" for key, value in self.data.items())
|
114
|
+
return f"{self.__class__.__name__}({params})"
|
115
|
+
|
116
|
+
def _repr_html_(self):
|
117
|
+
parent_html = super()._repr_html_()
|
118
|
+
from edsl.scenarios.ConstructDownloadLink import ConstructDownloadLink
|
119
|
+
|
120
|
+
link = ConstructDownloadLink(self).html_create_link(self.path, style=None)
|
121
|
+
return f"{parent_html}<br>{link}"
|
122
|
+
|
123
|
+
def encode_file_to_base64_string(self, file_path: str):
|
124
|
+
try:
|
125
|
+
# Attempt to open the file in text mode
|
126
|
+
with open(file_path, "r") as text_file:
|
127
|
+
# Read the text data
|
128
|
+
text_data = text_file.read()
|
129
|
+
# Encode the text data to a base64 string
|
130
|
+
base64_encoded_data = base64.b64encode(text_data.encode("utf-8"))
|
131
|
+
except UnicodeDecodeError:
|
132
|
+
# If reading as text fails, open the file in binary mode
|
133
|
+
with open(file_path, "rb") as binary_file:
|
134
|
+
# Read the binary data
|
135
|
+
binary_data = binary_file.read()
|
136
|
+
# Encode the binary data to a base64 string
|
137
|
+
base64_encoded_data = base64.b64encode(binary_data)
|
138
|
+
self.binary = True
|
139
|
+
# Convert the base64 bytes to a string
|
140
|
+
base64_string = base64_encoded_data.decode("utf-8")
|
141
|
+
|
142
|
+
return base64_string
|
143
|
+
|
144
|
+
def open(self) -> "IO":
|
145
|
+
if self.binary:
|
146
|
+
return self.base64_to_file(self.base64_string, is_binary=True)
|
147
|
+
else:
|
148
|
+
return self.base64_to_text_file(self.base64_string)
|
149
|
+
|
150
|
+
def write(self, filename: Optional[str] = None) -> str:
|
151
|
+
"""
|
152
|
+
Write the file content to disk, either to a specified filename or a temporary file.
|
153
|
+
|
154
|
+
Args:
|
155
|
+
filename (Optional[str]): The destination filename. If None, creates a temporary file.
|
156
|
+
|
157
|
+
Returns:
|
158
|
+
str: The path to the written file.
|
159
|
+
"""
|
160
|
+
# Determine the mode based on binary flag
|
161
|
+
mode = "wb" if self.binary else "w"
|
162
|
+
|
163
|
+
# If no filename provided, create a temporary file
|
164
|
+
if filename is None:
|
165
|
+
from tempfile import NamedTemporaryFile
|
166
|
+
|
167
|
+
with NamedTemporaryFile(delete=False, suffix="." + self.suffix) as f:
|
168
|
+
filename = f.name
|
169
|
+
|
170
|
+
# Write the content using the appropriate mode
|
171
|
+
try:
|
172
|
+
with open(filename, mode) as f:
|
173
|
+
content = self.open().read()
|
174
|
+
# For text mode, ensure we're writing a string
|
175
|
+
if not self.binary and isinstance(content, bytes):
|
176
|
+
content = content.decode("utf-8")
|
177
|
+
f.write(content)
|
178
|
+
print(f"File written to {filename}")
|
179
|
+
except Exception as e:
|
180
|
+
print(f"Error writing file: {e}")
|
181
|
+
raise
|
182
|
+
|
183
|
+
# return filename
|
184
|
+
|
185
|
+
@staticmethod
|
186
|
+
def base64_to_text_file(base64_string) -> "IO":
|
187
|
+
# Decode the base64 string to bytes
|
188
|
+
text_data_bytes = base64.b64decode(base64_string)
|
189
|
+
|
190
|
+
# Convert bytes to string
|
191
|
+
text_data = text_data_bytes.decode("utf-8")
|
192
|
+
|
193
|
+
# Create a StringIO object from the text data
|
194
|
+
text_file = io.StringIO(text_data)
|
195
|
+
|
196
|
+
return text_file
|
197
|
+
|
198
|
+
@staticmethod
|
199
|
+
def base64_to_file(base64_string, is_binary=True):
|
200
|
+
# Decode the base64 string to bytes
|
201
|
+
file_data = base64.b64decode(base64_string)
|
202
|
+
|
203
|
+
if is_binary:
|
204
|
+
# Create a BytesIO object for binary data
|
205
|
+
return io.BytesIO(file_data)
|
206
|
+
else:
|
207
|
+
# Convert bytes to string for text data
|
208
|
+
text_data = file_data.decode("utf-8")
|
209
|
+
# Create a StringIO object for text data
|
210
|
+
return io.StringIO(text_data)
|
211
|
+
|
212
|
+
@property
|
213
|
+
def text(self):
|
214
|
+
if self.binary:
|
215
|
+
import warnings
|
216
|
+
|
217
|
+
warnings.warn("This is a binary file.")
|
218
|
+
else:
|
219
|
+
return self.base64_to_text_file(self.base64_string).read()
|
220
|
+
|
221
|
+
def to_tempfile(self, suffix=None):
|
222
|
+
if suffix is None:
|
223
|
+
suffix = self.suffix
|
224
|
+
if self.binary:
|
225
|
+
file_like_object = self.base64_to_file(
|
226
|
+
self["base64_string"], is_binary=True
|
227
|
+
)
|
228
|
+
else:
|
229
|
+
file_like_object = self.base64_to_text_file(self.base64_string)
|
230
|
+
|
231
|
+
# Create a named temporary file
|
232
|
+
mode = "wb" if self.binary else "w"
|
233
|
+
temp_file = tempfile.NamedTemporaryFile(
|
234
|
+
delete=False, suffix="." + suffix, mode=mode
|
235
|
+
)
|
236
|
+
|
237
|
+
if self.binary:
|
238
|
+
temp_file.write(file_like_object.read())
|
239
|
+
else:
|
240
|
+
temp_file.write(file_like_object.read())
|
241
|
+
|
242
|
+
temp_file.close()
|
243
|
+
|
244
|
+
return temp_file.name
|
245
|
+
|
246
|
+
def view(self) -> None:
|
247
|
+
handler = FileMethods.get_handler(self.suffix)
|
248
|
+
if handler:
|
249
|
+
handler(self.path).view()
|
250
|
+
else:
|
251
|
+
print(f"Viewing of {self.suffix} files is not supported.")
|
252
|
+
|
253
|
+
def extract_text(self) -> str:
|
254
|
+
handler = FileMethods.get_handler(self.suffix)
|
255
|
+
if handler and hasattr(handler, "extract_text"):
|
256
|
+
return handler(self.path).extract_text()
|
257
|
+
|
258
|
+
if not self.binary:
|
259
|
+
return self.text
|
260
|
+
|
261
|
+
return None
|
262
|
+
# raise TypeError("No text method found for this file type.")
|
263
|
+
|
264
|
+
def push(
|
265
|
+
self, description: Optional[str] = None, visibility: str = "unlisted"
|
266
|
+
) -> dict:
|
267
|
+
"""
|
268
|
+
Push the object to Coop.
|
269
|
+
:param description: The description of the object to push.
|
270
|
+
:param visibility: The visibility of the object to push.
|
271
|
+
"""
|
272
|
+
scenario_version = Scenario.from_dict(self.to_dict())
|
273
|
+
if description is None:
|
274
|
+
description = "File: " + self.path
|
275
|
+
info = scenario_version.push(description=description, visibility=visibility)
|
276
|
+
return info
|
277
|
+
|
278
|
+
@classmethod
|
279
|
+
def pull(cls, uuid: str, expected_parrot_url: Optional[str] = None) -> "FileStore":
|
280
|
+
"""
|
281
|
+
:param uuid: The UUID of the object to pull.
|
282
|
+
:param expected_parrot_url: The URL of the Parrot server to use.
|
283
|
+
:return: The object pulled from the Parrot server.
|
284
|
+
"""
|
285
|
+
scenario_version = Scenario.pull(uuid, expected_parrot_url=expected_parrot_url)
|
286
|
+
return cls.from_dict(scenario_version.to_dict())
|
287
|
+
|
288
|
+
@classmethod
|
289
|
+
def from_url(
|
290
|
+
cls,
|
291
|
+
url: str,
|
292
|
+
download_path: Optional[str] = None,
|
293
|
+
mime_type: Optional[str] = None,
|
294
|
+
) -> "FileStore":
|
295
|
+
"""
|
296
|
+
:param url: The URL of the file to download.
|
297
|
+
:param download_path: The path to save the downloaded file.
|
298
|
+
:param mime_type: The MIME type of the file. If None, it will be guessed from the file extension.
|
299
|
+
"""
|
300
|
+
import requests
|
301
|
+
from urllib.parse import urlparse
|
302
|
+
|
303
|
+
response = requests.get(url, stream=True)
|
304
|
+
response.raise_for_status() # Raises an HTTPError for bad responses
|
305
|
+
|
306
|
+
# Get the filename from the URL if download_path is not provided
|
307
|
+
if download_path is None:
|
308
|
+
filename = os.path.basename(urlparse(url).path)
|
309
|
+
if not filename:
|
310
|
+
filename = "downloaded_file"
|
311
|
+
# download_path = filename
|
312
|
+
download_path = os.path.join(os.getcwd(), filename)
|
313
|
+
|
314
|
+
# Ensure the directory exists
|
315
|
+
os.makedirs(os.path.dirname(download_path), exist_ok=True)
|
316
|
+
|
317
|
+
# Write the file
|
318
|
+
with open(download_path, "wb") as file:
|
319
|
+
for chunk in response.iter_content(chunk_size=8192):
|
320
|
+
file.write(chunk)
|
321
|
+
|
322
|
+
# Create and return a new File instance
|
323
|
+
return cls(download_path, mime_type=mime_type)
|
324
|
+
|
325
|
+
def create_link(self, custom_filename=None, style=None):
|
326
|
+
from edsl.scenarios.ConstructDownloadLink import ConstructDownloadLink
|
327
|
+
|
328
|
+
return ConstructDownloadLink(self).create_link(custom_filename, style)
|
329
|
+
|
330
|
+
def to_pandas(self):
|
331
|
+
"""
|
332
|
+
Convert the file content to a pandas DataFrame if supported by the file handler.
|
333
|
+
|
334
|
+
Returns:
|
335
|
+
pandas.DataFrame: The data from the file as a DataFrame
|
336
|
+
|
337
|
+
Raises:
|
338
|
+
AttributeError: If the file type's handler doesn't support pandas conversion
|
339
|
+
"""
|
340
|
+
handler = FileMethods.get_handler(self.suffix)
|
341
|
+
if handler and hasattr(handler, "to_pandas"):
|
342
|
+
return handler(self.path).to_pandas()
|
343
|
+
raise AttributeError(
|
344
|
+
f"Converting {self.suffix} files to pandas DataFrame is not supported"
|
345
|
+
)
|
346
|
+
|
347
|
+
def __getattr__(self, name):
|
348
|
+
"""
|
349
|
+
Delegate pandas DataFrame methods to the underlying DataFrame if this is a CSV file
|
350
|
+
"""
|
351
|
+
if self.suffix == "csv":
|
352
|
+
# Get the pandas DataFrame
|
353
|
+
df = self.to_pandas()
|
354
|
+
# Check if the requested attribute exists in the DataFrame
|
355
|
+
if hasattr(df, name):
|
356
|
+
return getattr(df, name)
|
357
|
+
# If not a CSV or attribute doesn't exist in DataFrame, raise AttributeError
|
358
|
+
raise AttributeError(
|
359
|
+
f"'{self.__class__.__name__}' object has no attribute '{name}'"
|
360
|
+
)
|
361
|
+
|
362
|
+
|
363
|
+
class CSVFileStore(FileStore):
|
364
|
+
@classmethod
|
365
|
+
def example(cls):
|
366
|
+
from edsl.results.Results import Results
|
367
|
+
|
368
|
+
r = Results.example()
|
369
|
+
import tempfile
|
370
|
+
|
371
|
+
with tempfile.NamedTemporaryFile(suffix=".csv", delete=False) as f:
|
372
|
+
r.to_csv(filename=f.name)
|
373
|
+
|
374
|
+
return cls(f.name)
|
375
|
+
|
376
|
+
def view(self):
|
377
|
+
import pandas as pd
|
378
|
+
|
379
|
+
return pd.read_csv(self.to_tempfile())
|
380
|
+
|
381
|
+
|
382
|
+
class PDFFileStore(FileStore):
|
383
|
+
def view(self):
|
384
|
+
pdf_path = self.to_tempfile()
|
385
|
+
print(f"PDF path: {pdf_path}") # Print the path to ensure it exists
|
386
|
+
import os
|
387
|
+
import subprocess
|
388
|
+
|
389
|
+
if os.path.exists(pdf_path):
|
390
|
+
try:
|
391
|
+
if os.name == "posix":
|
392
|
+
# for cool kids
|
393
|
+
subprocess.run(["open", pdf_path], check=True) # macOS
|
394
|
+
elif os.name == "nt":
|
395
|
+
os.startfile(pdf_path) # Windows
|
396
|
+
else:
|
397
|
+
subprocess.run(["xdg-open", pdf_path], check=True) # Linux
|
398
|
+
except Exception as e:
|
399
|
+
print(f"Error opening PDF: {e}")
|
400
|
+
else:
|
401
|
+
print("PDF file was not created successfully.")
|
402
|
+
|
403
|
+
@classmethod
|
404
|
+
def example(cls):
|
405
|
+
import textwrap
|
406
|
+
|
407
|
+
pdf_string = textwrap.dedent(
|
408
|
+
"""\
|
409
|
+
%PDF-1.4
|
410
|
+
1 0 obj
|
411
|
+
<< /Type /Catalog /Pages 2 0 R >>
|
412
|
+
endobj
|
413
|
+
2 0 obj
|
414
|
+
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
415
|
+
endobj
|
416
|
+
3 0 obj
|
417
|
+
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >>
|
418
|
+
endobj
|
419
|
+
4 0 obj
|
420
|
+
<< /Length 44 >>
|
421
|
+
stream
|
422
|
+
BT
|
423
|
+
/F1 24 Tf
|
424
|
+
100 700 Td
|
425
|
+
(Hello, World!) Tj
|
426
|
+
ET
|
427
|
+
endstream
|
428
|
+
endobj
|
429
|
+
5 0 obj
|
430
|
+
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
|
431
|
+
endobj
|
432
|
+
6 0 obj
|
433
|
+
<< /ProcSet [/PDF /Text] /Font << /F1 5 0 R >> >>
|
434
|
+
endobj
|
435
|
+
xref
|
436
|
+
0 7
|
437
|
+
0000000000 65535 f
|
438
|
+
0000000010 00000 n
|
439
|
+
0000000053 00000 n
|
440
|
+
0000000100 00000 n
|
441
|
+
0000000173 00000 n
|
442
|
+
0000000232 00000 n
|
443
|
+
0000000272 00000 n
|
444
|
+
trailer
|
445
|
+
<< /Size 7 /Root 1 0 R >>
|
446
|
+
startxref
|
447
|
+
318
|
448
|
+
%%EOF"""
|
449
|
+
)
|
450
|
+
import tempfile
|
451
|
+
|
452
|
+
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as f:
|
453
|
+
f.write(pdf_string.encode())
|
454
|
+
|
455
|
+
return cls(f.name)
|
456
|
+
|
457
|
+
|
458
|
+
class PNGFileStore(FileStore):
|
459
|
+
@classmethod
|
460
|
+
def example(cls):
|
461
|
+
import textwrap
|
462
|
+
|
463
|
+
png_string = textwrap.dedent(
|
464
|
+
"""\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x01\x00\x00\x00\x01\x00\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\x0cIDAT\x08\xd7c\x00\x01"""
|
465
|
+
)
|
466
|
+
import tempfile
|
467
|
+
|
468
|
+
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
|
469
|
+
f.write(png_string.encode())
|
470
|
+
|
471
|
+
return cls(f.name)
|
472
|
+
|
473
|
+
def view(self):
|
474
|
+
import matplotlib.pyplot as plt
|
475
|
+
import matplotlib.image as mpimg
|
476
|
+
|
477
|
+
img = mpimg.imread(self.to_tempfile())
|
478
|
+
plt.imshow(img)
|
479
|
+
plt.show()
|
480
|
+
|
481
|
+
|
482
|
+
class SQLiteFileStore(FileStore):
|
483
|
+
@classmethod
|
484
|
+
def example(cls):
|
485
|
+
import sqlite3
|
486
|
+
import tempfile
|
487
|
+
|
488
|
+
with tempfile.NamedTemporaryFile(suffix=".sqlite", delete=False) as f:
|
489
|
+
conn = sqlite3.connect(f.name)
|
490
|
+
c = conn.cursor()
|
491
|
+
c.execute("""CREATE TABLE stocks (date text)""")
|
492
|
+
conn.commit()
|
493
|
+
|
494
|
+
return cls(f.name)
|
495
|
+
|
496
|
+
def view(self):
|
497
|
+
import subprocess
|
498
|
+
import os
|
499
|
+
|
500
|
+
sqlite_path = self.to_tempfile()
|
501
|
+
os.system(f"sqlite3 {sqlite_path}")
|
502
|
+
|
503
|
+
|
504
|
+
class HTMLFileStore(FileStore):
|
505
|
+
@classmethod
|
506
|
+
def example(cls):
|
507
|
+
import tempfile
|
508
|
+
|
509
|
+
with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as f:
|
510
|
+
f.write("<html><body><h1>Test</h1></body></html>".encode())
|
511
|
+
|
512
|
+
return cls(f.name)
|
513
|
+
|
514
|
+
def view(self):
|
515
|
+
import webbrowser
|
516
|
+
|
517
|
+
html_path = self.to_tempfile()
|
518
|
+
webbrowser.open("file://" + html_path)
|
519
|
+
|
520
|
+
|
521
|
+
if __name__ == "__main__":
|
522
|
+
import doctest
|
523
|
+
|
524
|
+
doctest.testmod()
|
525
|
+
|
526
|
+
# fs = FileStore.example("pdf")
|
527
|
+
# fs.view()
|
528
|
+
|
529
|
+
formats = FileMethods.supported_file_types()
|
530
|
+
for file_type in formats:
|
531
|
+
print("Now testinging", file_type)
|
532
|
+
fs = FileStore.example(file_type)
|
533
|
+
fs.view()
|
534
|
+
input("Press Enter to continue...")
|
535
|
+
|
536
|
+
# pdf_example.view()
|
537
|
+
# FileStore(pdf_example).view()
|
538
|
+
|
539
|
+
# pdf_methods = methods.get("pdf")
|
540
|
+
# file = pdf_methods().example()
|
541
|
+
# pdf_methods(file).view()
|
542
|
+
|
543
|
+
# print(FileMethods._handlers)
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
|
4
|
+
class PdfExtractor:
|
5
|
+
def __init__(self, pdf_path: str, parent_object: object):
|
6
|
+
self.pdf_path = pdf_path
|
7
|
+
self.constructor = parent_object.__class__
|
8
|
+
|
9
|
+
def get_object(self) -> object:
|
10
|
+
return self.constructor(self._get_pdf_dict())
|
11
|
+
|
12
|
+
def _get_pdf_dict(self) -> dict:
|
13
|
+
# Ensure the file exists
|
14
|
+
import fitz
|
15
|
+
|
16
|
+
if not os.path.exists(self.pdf_path):
|
17
|
+
raise FileNotFoundError(f"The file {self.pdf_path} does not exist.")
|
18
|
+
|
19
|
+
# Open the PDF file
|
20
|
+
document = fitz.open(self.pdf_path)
|
21
|
+
|
22
|
+
# Get the filename from the path
|
23
|
+
filename = os.path.basename(self.pdf_path)
|
24
|
+
|
25
|
+
# Iterate through each page and extract text
|
26
|
+
text = ""
|
27
|
+
for page_num in range(len(document)):
|
28
|
+
page = document.load_page(page_num)
|
29
|
+
blocks = page.get_text("blocks") # Extract text blocks
|
30
|
+
|
31
|
+
# Sort blocks by their vertical position (y0) to maintain reading order
|
32
|
+
blocks.sort(key=lambda b: (b[1], b[0])) # Sort by y0 first, then x0
|
33
|
+
|
34
|
+
# Combine the text blocks in order
|
35
|
+
for block in blocks:
|
36
|
+
text += block[4] + "\n"
|
37
|
+
|
38
|
+
# Create a dictionary for the combined text
|
39
|
+
page_info = {"filename": filename, "text": text}
|
40
|
+
return page_info
|