edsl 0.1.41__py3-none-any.whl → 0.1.43__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 +1 -1
- edsl/agents/Invigilator.py +4 -3
- edsl/agents/InvigilatorBase.py +2 -1
- edsl/agents/PromptConstructor.py +92 -21
- edsl/agents/QuestionInstructionPromptBuilder.py +68 -9
- edsl/agents/QuestionTemplateReplacementsBuilder.py +7 -2
- edsl/agents/prompt_helpers.py +2 -2
- edsl/coop/coop.py +97 -19
- edsl/enums.py +3 -1
- edsl/exceptions/coop.py +4 -0
- edsl/exceptions/jobs.py +1 -9
- edsl/exceptions/language_models.py +8 -4
- edsl/exceptions/questions.py +8 -11
- edsl/inference_services/AvailableModelFetcher.py +4 -1
- edsl/inference_services/DeepSeekService.py +18 -0
- edsl/inference_services/registry.py +2 -0
- edsl/jobs/Jobs.py +60 -34
- edsl/jobs/JobsPrompts.py +64 -3
- edsl/jobs/JobsRemoteInferenceHandler.py +42 -25
- edsl/jobs/JobsRemoteInferenceLogger.py +1 -1
- edsl/jobs/buckets/BucketCollection.py +30 -0
- edsl/jobs/data_structures.py +1 -0
- edsl/jobs/interviews/Interview.py +1 -1
- edsl/jobs/loggers/HTMLTableJobLogger.py +6 -1
- edsl/jobs/results_exceptions_handler.py +2 -7
- edsl/jobs/tasks/TaskHistory.py +49 -17
- edsl/language_models/LanguageModel.py +7 -4
- edsl/language_models/ModelList.py +1 -1
- edsl/language_models/key_management/KeyLookupBuilder.py +47 -20
- edsl/language_models/key_management/models.py +10 -4
- edsl/language_models/model.py +49 -0
- edsl/prompts/Prompt.py +124 -61
- edsl/questions/descriptors.py +37 -23
- edsl/questions/question_base_gen_mixin.py +1 -0
- edsl/results/DatasetExportMixin.py +35 -6
- edsl/results/Result.py +9 -3
- edsl/results/Results.py +180 -2
- edsl/results/ResultsGGMixin.py +117 -60
- edsl/scenarios/PdfExtractor.py +3 -6
- edsl/scenarios/Scenario.py +35 -1
- edsl/scenarios/ScenarioList.py +22 -3
- edsl/scenarios/ScenarioListPdfMixin.py +9 -3
- edsl/surveys/Survey.py +1 -1
- edsl/templates/error_reporting/base.html +2 -4
- edsl/templates/error_reporting/exceptions_table.html +35 -0
- edsl/templates/error_reporting/interview_details.html +67 -53
- edsl/templates/error_reporting/interviews.html +4 -17
- edsl/templates/error_reporting/overview.html +31 -5
- edsl/templates/error_reporting/performance_plot.html +1 -1
- {edsl-0.1.41.dist-info → edsl-0.1.43.dist-info}/METADATA +2 -3
- {edsl-0.1.41.dist-info → edsl-0.1.43.dist-info}/RECORD +53 -51
- {edsl-0.1.41.dist-info → edsl-0.1.43.dist-info}/LICENSE +0 -0
- {edsl-0.1.41.dist-info → edsl-0.1.43.dist-info}/WHEEL +0 -0
edsl/scenarios/Scenario.py
CHANGED
@@ -358,7 +358,41 @@ class Scenario(Base, UserDict, ScenarioHtmlMixin):
|
|
358
358
|
def from_pdf(cls, pdf_path: str):
|
359
359
|
from edsl.scenarios.PdfExtractor import PdfExtractor
|
360
360
|
|
361
|
-
|
361
|
+
extractor = PdfExtractor(pdf_path)
|
362
|
+
return Scenario(extractor.get_pdf_dict())
|
363
|
+
|
364
|
+
@classmethod
|
365
|
+
def from_pdf_to_image(cls, pdf_path, image_format="jpeg"):
|
366
|
+
"""
|
367
|
+
Convert each page of a PDF into an image and create key/value for it.
|
368
|
+
|
369
|
+
:param pdf_path: Path to the PDF file.
|
370
|
+
:param image_format: Format of the output images (default is 'jpeg').
|
371
|
+
:return: ScenarioList instance containing the Scenario instances.
|
372
|
+
|
373
|
+
The scenario has a key "filepath" and one or more keys "page_{i}" for each page.
|
374
|
+
"""
|
375
|
+
import tempfile
|
376
|
+
from pdf2image import convert_from_path
|
377
|
+
from edsl.scenarios import Scenario
|
378
|
+
|
379
|
+
with tempfile.TemporaryDirectory() as output_folder:
|
380
|
+
# Convert PDF to images
|
381
|
+
images = convert_from_path(pdf_path)
|
382
|
+
|
383
|
+
scenario_dict = {"filepath":pdf_path}
|
384
|
+
|
385
|
+
# Save each page as an image and create Scenario instances
|
386
|
+
for i, image in enumerate(images):
|
387
|
+
image_path = os.path.join(output_folder, f"page_{i}.{image_format}")
|
388
|
+
image.save(image_path, image_format.upper())
|
389
|
+
|
390
|
+
from edsl import FileStore
|
391
|
+
scenario_dict[f"page_{i}"] = FileStore(image_path)
|
392
|
+
|
393
|
+
scenario = Scenario(scenario_dict)
|
394
|
+
|
395
|
+
return cls(scenario)
|
362
396
|
|
363
397
|
@classmethod
|
364
398
|
def from_docx(cls, docx_path: str) -> "Scenario":
|
edsl/scenarios/ScenarioList.py
CHANGED
@@ -1135,7 +1135,7 @@ class ScenarioList(Base, UserList, ScenarioListMixin):
|
|
1135
1135
|
return cls(observations)
|
1136
1136
|
|
1137
1137
|
@classmethod
|
1138
|
-
def from_google_sheet(cls, url: str, sheet_name: str = None) -> ScenarioList:
|
1138
|
+
def from_google_sheet(cls, url: str, sheet_name: str = None, column_names: Optional[List[str]]= None) -> ScenarioList:
|
1139
1139
|
"""Create a ScenarioList from a Google Sheet.
|
1140
1140
|
|
1141
1141
|
This method downloads the Google Sheet as an Excel file, saves it to a temporary file,
|
@@ -1145,6 +1145,8 @@ class ScenarioList(Base, UserList, ScenarioListMixin):
|
|
1145
1145
|
url (str): The URL to the Google Sheet.
|
1146
1146
|
sheet_name (str, optional): The name of the sheet to load. If None, the method will behave
|
1147
1147
|
the same as from_excel regarding multiple sheets.
|
1148
|
+
column_names (List[str], optional): If provided, use these names for the columns instead
|
1149
|
+
of the default column names from the sheet.
|
1148
1150
|
|
1149
1151
|
Returns:
|
1150
1152
|
ScenarioList: An instance of the ScenarioList class.
|
@@ -1172,8 +1174,25 @@ class ScenarioList(Base, UserList, ScenarioListMixin):
|
|
1172
1174
|
temp_file.write(response.content)
|
1173
1175
|
temp_filename = temp_file.name
|
1174
1176
|
|
1175
|
-
#
|
1176
|
-
|
1177
|
+
# First create the ScenarioList with default column names
|
1178
|
+
scenario_list = cls.from_excel(temp_filename, sheet_name=sheet_name)
|
1179
|
+
|
1180
|
+
# If column_names is provided, create a new ScenarioList with the specified names
|
1181
|
+
if column_names is not None:
|
1182
|
+
if len(column_names) != len(scenario_list[0].keys()):
|
1183
|
+
raise ValueError(
|
1184
|
+
f"Number of provided column names ({len(column_names)}) "
|
1185
|
+
f"does not match number of columns in sheet ({len(scenario_list[0].keys())})"
|
1186
|
+
)
|
1187
|
+
|
1188
|
+
# Create a codebook mapping original keys to new names
|
1189
|
+
original_keys = list(scenario_list[0].keys())
|
1190
|
+
codebook = dict(zip(original_keys, column_names))
|
1191
|
+
|
1192
|
+
# Return new ScenarioList with renamed columns
|
1193
|
+
return scenario_list.rename(codebook)
|
1194
|
+
else:
|
1195
|
+
return scenario_list
|
1177
1196
|
|
1178
1197
|
@classmethod
|
1179
1198
|
def from_delimited_file(
|
@@ -148,13 +148,15 @@ class ScenarioListPdfMixin:
|
|
148
148
|
return False
|
149
149
|
|
150
150
|
@classmethod
|
151
|
-
def
|
151
|
+
def from_pdf_to_image(cls, pdf_path, image_format="jpeg"):
|
152
152
|
"""
|
153
153
|
Convert each page of a PDF into an image and create Scenario instances.
|
154
154
|
|
155
155
|
:param pdf_path: Path to the PDF file.
|
156
156
|
:param image_format: Format of the output images (default is 'jpeg').
|
157
157
|
:return: ScenarioList instance containing the Scenario instances.
|
158
|
+
|
159
|
+
The scenario list has keys "filepath", "page", "content".
|
158
160
|
"""
|
159
161
|
import tempfile
|
160
162
|
from pdf2image import convert_from_path
|
@@ -171,10 +173,14 @@ class ScenarioListPdfMixin:
|
|
171
173
|
image_path = os.path.join(output_folder, f"page_{i+1}.{image_format}")
|
172
174
|
image.save(image_path, image_format.upper())
|
173
175
|
|
174
|
-
|
176
|
+
from edsl import FileStore
|
177
|
+
scenario = Scenario({
|
178
|
+
"filepath":image_path,
|
179
|
+
"page":i,
|
180
|
+
"content":FileStore(image_path)
|
181
|
+
})
|
175
182
|
scenarios.append(scenario)
|
176
183
|
|
177
|
-
# print(f"Saved {len(images)} pages as images in {output_folder}")
|
178
184
|
return cls(scenarios)
|
179
185
|
|
180
186
|
@staticmethod
|
edsl/surveys/Survey.py
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
<head>
|
4
4
|
<meta charset="UTF-8">
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
-
<title>
|
6
|
+
<title>Exceptions Report</title>
|
7
7
|
<style>
|
8
8
|
{{ css }}
|
9
9
|
</style>
|
@@ -15,9 +15,7 @@
|
|
15
15
|
</head>
|
16
16
|
<body>
|
17
17
|
{% include 'overview.html' %}
|
18
|
-
{% include '
|
19
|
-
{% include 'exceptions_by_model.html' %}
|
20
|
-
{% include 'exceptions_by_question_name.html' %}
|
18
|
+
{% include 'exceptions_table.html' %}
|
21
19
|
{% include 'interviews.html' %}
|
22
20
|
{% include 'performance_plot.html' %}
|
23
21
|
</body>
|
@@ -0,0 +1,35 @@
|
|
1
|
+
<style>
|
2
|
+
th, td {
|
3
|
+
padding: 0 10px; /* This applies the padding uniformly to all td elements */
|
4
|
+
}
|
5
|
+
</style>
|
6
|
+
|
7
|
+
<table border="1">
|
8
|
+
<thead>
|
9
|
+
<tr>
|
10
|
+
<th>Exception Type</th>
|
11
|
+
<th>Service</th>
|
12
|
+
<th>Model</th>
|
13
|
+
<th>Question Name</th>
|
14
|
+
<th>Total</th>
|
15
|
+
</tr>
|
16
|
+
</thead>
|
17
|
+
<tbody>
|
18
|
+
{% for (exception_type, service, model, question_name), count in exceptions_table.items() %}
|
19
|
+
<tr>
|
20
|
+
<td>{{ exception_type }}</td>
|
21
|
+
<td>{{ service }}</td>
|
22
|
+
<td>{{ model }}</td>
|
23
|
+
<td>{{ question_name }}</td>
|
24
|
+
<td>{{ count }}</td>
|
25
|
+
</tr>
|
26
|
+
{% endfor %}
|
27
|
+
</tbody>
|
28
|
+
</table>
|
29
|
+
<p>
|
30
|
+
<i>Note:</i> You may encounter repeated exceptions where retries were attempted.
|
31
|
+
You can modify the maximum number of attempts for failed API calls in `edsl/config.py`.
|
32
|
+
</p>
|
33
|
+
<p>
|
34
|
+
Click to expand the details below for information about each exception, including code for reproducing it.
|
35
|
+
</p>
|
@@ -1,43 +1,67 @@
|
|
1
|
-
<
|
2
|
-
|
1
|
+
<style>
|
2
|
+
td {
|
3
|
+
padding: 0 10px; /* This applies the padding uniformly to all td elements */
|
4
|
+
}
|
5
|
+
.toggle-btn {
|
6
|
+
background-color: #4CAF50;
|
7
|
+
color: white;
|
8
|
+
border: none;
|
9
|
+
padding: 10px 20px;
|
10
|
+
text-align: center;
|
11
|
+
text-decoration: none;
|
12
|
+
display: inline-block;
|
13
|
+
font-size: 16px;
|
14
|
+
margin: 4px 2px;
|
15
|
+
cursor: pointer;
|
16
|
+
border-radius: 8px;
|
17
|
+
white-space: nowrap;
|
18
|
+
}
|
19
|
+
.toggle-btn span.collapse {
|
20
|
+
display: none;
|
21
|
+
}
|
22
|
+
.exception-content {
|
23
|
+
max-width: 100%; /* Adjust this value based on your layout */
|
24
|
+
overflow-x: auto; /* Enables horizontal scrolling */
|
25
|
+
}
|
26
|
+
</style>
|
3
27
|
|
4
|
-
<
|
28
|
+
<div class="question">question_name: {{ question }}</div>
|
5
29
|
|
6
30
|
{% for exception_message in exceptions %}
|
7
31
|
<div class="exception-detail">
|
8
|
-
|
32
|
+
<div class="exception-header">
|
9
33
|
<span class="exception-exception">Exception: {{ exception_message.name }}</span>
|
10
|
-
<button class="toggle-btn"
|
11
|
-
|
12
|
-
|
34
|
+
<button id="toggleBtn" class="toggle-btn" onclick="toggleButton(this)" aria-expanded="false">
|
35
|
+
<span class="expand"> ▼ </span>
|
36
|
+
</button>
|
37
|
+
</div>
|
38
|
+
<div class="exception-content">
|
13
39
|
<table border="1">
|
14
|
-
<tr>
|
15
|
-
<th>Key</th>
|
16
|
-
<th>Value</th>
|
17
|
-
</tr>
|
18
40
|
<tr>
|
19
41
|
<td>Interview ID (index in results)</td>
|
20
42
|
<td>{{ index }}</td>
|
21
43
|
</tr>
|
22
44
|
<tr>
|
23
|
-
<td>Question name
|
45
|
+
<td>Question name</td>
|
24
46
|
<td>{{ question }}</td>
|
25
47
|
</tr>
|
26
|
-
|
27
48
|
<tr>
|
28
|
-
<td>Question type
|
49
|
+
<td>Question type</td>
|
29
50
|
<td>{{ exception_message.question_type }}</td>
|
30
51
|
</tr>
|
31
|
-
|
32
52
|
<tr>
|
33
53
|
<td>Human-readable question</td>
|
34
54
|
<td>{{ interview.survey._get_question_by_name(question).html(
|
35
55
|
scenario = interview.scenario,
|
36
56
|
agent = interview.agent,
|
37
|
-
answers = exception_message.answers
|
38
|
-
|
57
|
+
answers = exception_message.answers
|
58
|
+
)
|
39
59
|
}}</td>
|
40
60
|
</tr>
|
61
|
+
<tr>
|
62
|
+
<td>User Prompt</td>
|
63
|
+
<td><pre>{{ exception_message.rendered_prompts['user_prompt'] }}</pre></td>
|
64
|
+
</tr>
|
41
65
|
<tr>
|
42
66
|
<td>Scenario</td>
|
43
67
|
<td>{{ interview.scenario.__repr__() }}</td>
|
@@ -47,24 +71,20 @@
|
|
47
71
|
<td>{{ interview.agent.__repr__() }}</td>
|
48
72
|
</tr>
|
49
73
|
<tr>
|
50
|
-
<td>
|
51
|
-
<td>{{
|
74
|
+
<td>System Prompt</td>
|
75
|
+
<td><pre>{{ exception_message.rendered_prompts['system_prompt'] }}</pre></td>
|
52
76
|
</tr>
|
53
77
|
<tr>
|
54
78
|
<td>Inference service</td>
|
55
79
|
<td>{{ interview.model._inference_service_ }}</td>
|
56
80
|
</tr>
|
57
81
|
<tr>
|
58
|
-
<td>Model
|
59
|
-
<td>{{ interview.model.
|
60
|
-
</tr>
|
61
|
-
<tr>
|
62
|
-
<td>User Prompt</td>
|
63
|
-
<td><pre>{{ exception_message.rendered_prompts['user_prompt'] }}</pre></td>
|
82
|
+
<td>Model name</td>
|
83
|
+
<td>{{ interview.model.model }}</td>
|
64
84
|
</tr>
|
65
85
|
<tr>
|
66
|
-
<td>
|
67
|
-
<td
|
86
|
+
<td>Model parameters</td>
|
87
|
+
<td>{{ interview.model.__repr__() }}</td>
|
68
88
|
</tr>
|
69
89
|
<tr>
|
70
90
|
<td>Raw model response</td>
|
@@ -77,7 +97,7 @@
|
|
77
97
|
</td>
|
78
98
|
</tr>
|
79
99
|
<tr>
|
80
|
-
<td>Code to
|
100
|
+
<td>Code likely to reproduce the error</td>
|
81
101
|
<td>
|
82
102
|
<textarea id="codeToCopy" rows="10" cols="90">{{ exception_message.code_to_reproduce }}</textarea>
|
83
103
|
<button onclick="copyCode()">Copy</button>
|
@@ -85,32 +105,26 @@
|
|
85
105
|
</tr>
|
86
106
|
|
87
107
|
</table>
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
</
|
105
|
-
|
106
|
-
|
107
|
-
<div class="exception-time">Time: {{ exception_message.time }}</div>
|
108
|
-
<div class="exception-traceback">Traceback:
|
109
|
-
<text>
|
110
|
-
<pre>{{ exception_message.traceback }}</pre>
|
111
|
-
</text>
|
112
|
-
</div>
|
108
|
+
|
109
|
+
{% if exception_message.exception.__class__.__name__ == 'QuestionAnswerValidationError' %}
|
110
|
+
<h3>Answer validation details</h3>
|
111
|
+
<table border="1">
|
112
|
+
{% for field, (open_tag, close_tag, value) in exception_message.exception.to_html_dict().items() %}
|
113
|
+
<tr>
|
114
|
+
<td>{{ field }}</td>
|
115
|
+
<td><{{ open_tag }}> {{ value | escape }} <{{ close_tag }}></td>
|
116
|
+
</tr>
|
117
|
+
{% endfor %}
|
118
|
+
</table>
|
119
|
+
{% endif %}
|
120
|
+
<br><br>
|
121
|
+
<div class="exception-time">Time: {{ exception_message.time }}</div>
|
122
|
+
<div class="exception-traceback">Traceback:
|
123
|
+
<text>
|
124
|
+
<pre>{{ exception_message.traceback }}</pre>
|
125
|
+
</text>
|
113
126
|
</div>
|
114
127
|
</div>
|
128
|
+
</div>
|
115
129
|
|
116
130
|
{% endfor %}
|
@@ -1,19 +1,6 @@
|
|
1
|
-
|
2
|
-
{% if interviews|length > max_interviews %}
|
3
|
-
<h1>Only showing the first {{ max_interviews }} interviews with errors</h1>
|
4
|
-
{% else %}
|
5
|
-
<h1>Showing all interviews</h1>
|
6
|
-
{% endif %}
|
7
|
-
|
1
|
+
<h2>Exceptions Details</h2>
|
8
2
|
{% for index, interview in interviews.items() %}
|
9
|
-
{%
|
10
|
-
{%
|
11
|
-
|
12
|
-
Model: {{ interview.model.model }}
|
13
|
-
<h1>Failing questions</h1>
|
14
|
-
{% endif %}
|
15
|
-
{% for question, exceptions in interview.exceptions.items() %}
|
16
|
-
{% include 'interview_details.html' %}
|
17
|
-
{% endfor %}
|
18
|
-
{% endif %}
|
3
|
+
{% for question, exceptions in interview.exceptions.items() %}
|
4
|
+
{% include 'interview_details.html' %}
|
5
|
+
{% endfor %}
|
19
6
|
{% endfor %}
|
@@ -1,5 +1,31 @@
|
|
1
|
-
<
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
1
|
+
<style>
|
2
|
+
td {
|
3
|
+
padding: 0 10px; /* This applies the padding uniformly to all td elements */
|
4
|
+
}
|
5
|
+
</style>
|
6
|
+
|
7
|
+
<h1>Exceptions Report</h1>
|
8
|
+
<p>
|
9
|
+
This report summarizes exceptions encountered in the job that was run.
|
10
|
+
</p>
|
11
|
+
<p>
|
12
|
+
For advice on dealing with exceptions, please see the EDSL <a href="https://docs.expectedparrot.com/en/latest/exceptions.html">documentation</a> page. <br>
|
13
|
+
You can also post a question at the Expected Parrot <a href="https://discord.com/invite/mxAYkjfy9m">Discord channel</a>, open an issue on <a href="https://github.com/expectedparrot/edsl">GitHub</a>, or send an email to <a href="mailto:info@expectedparrot.com">info@expectedparrot.com</a>.
|
14
|
+
</p>
|
15
|
+
|
16
|
+
<h2>Overview</h2>
|
17
|
+
<table border="1">
|
18
|
+
<tbody>
|
19
|
+
<tr>
|
20
|
+
<td>Total interviews</td>
|
21
|
+
<td>{{ interviews|length }}</td>
|
22
|
+
</tr>
|
23
|
+
<tr>
|
24
|
+
<td>Interviews with exceptions</td>
|
25
|
+
<td>{{ num_exceptions }}</td>
|
26
|
+
</tr>
|
27
|
+
</tbody>
|
28
|
+
</table>
|
29
|
+
<p>
|
30
|
+
An "interview" is the result of one survey, taken by one agent, with one model and one scenario (if any).
|
31
|
+
</p>
|
@@ -1,2 +1,2 @@
|
|
1
|
-
<
|
1
|
+
<h2>Performance Plot</h2>
|
2
2
|
{{ performance_plot_html }}
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: edsl
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.43
|
4
4
|
Summary: Create and analyze LLM-based surveys
|
5
5
|
Home-page: https://www.expectedparrot.com/
|
6
6
|
License: MIT
|
@@ -17,7 +17,7 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
17
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
18
18
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
19
19
|
Requires-Dist: aiohttp (>=3.9.1,<4.0.0)
|
20
|
-
Requires-Dist: anthropic (>=0.
|
20
|
+
Requires-Dist: anthropic (>=0.45.0,<0.46.0)
|
21
21
|
Requires-Dist: azure-ai-inference (>=1.0.0b3,<2.0.0)
|
22
22
|
Requires-Dist: black[jupyter] (>=24.4.2,<25.0.0)
|
23
23
|
Requires-Dist: boto3 (>=1.34.161,<2.0.0)
|
@@ -37,7 +37,6 @@ Requires-Dist: pandas (>=2.1.4,<3.0.0)
|
|
37
37
|
Requires-Dist: platformdirs (>=4.3.6,<5.0.0)
|
38
38
|
Requires-Dist: pydot (>=2.0.0,<3.0.0)
|
39
39
|
Requires-Dist: pygments (>=2.17.2,<3.0.0)
|
40
|
-
Requires-Dist: pymupdf (>=1.24.4,<2.0.0)
|
41
40
|
Requires-Dist: pypdf2 (>=3.0.1,<4.0.0)
|
42
41
|
Requires-Dist: pyreadstat (>=1.2.7,<2.0.0)
|
43
42
|
Requires-Dist: python-docx (>=1.1.0,<2.0.0)
|