Maxenceleguery commited on
Commit
9c2aaab
·
1 Parent(s): 40e6934

:sparkles: Adding image modality

Browse files
Files changed (4) hide show
  1. .gitignore +3 -1
  2. app.py +175 -39
  3. pyproject.toml +1 -0
  4. uv.lock +23 -0
.gitignore CHANGED
@@ -1,2 +1,4 @@
1
  .venv
2
- .env
 
 
 
1
  .venv
2
+ .env
3
+
4
+ test*
app.py CHANGED
@@ -1,4 +1,6 @@
1
  import os
 
 
2
  import base64
3
  import gradio as gr
4
  import requests
@@ -17,6 +19,7 @@ load_dotenv()
17
  # --- Constants ---
18
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
19
 
 
20
  # --- Basic Agent Definition ---
21
  # ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
22
  class BasicAgent:
@@ -35,60 +38,166 @@ class BasicAgent:
35
  tools=[DuckDuckGoSearchTool()],
36
  model=OpenAIServerModel(model_id=OPENAI_MODEL_ID, api_key=OPENAI_API_KEY),
37
  additional_authorized_imports=["BeautifulSoup"],
38
- max_steps=20
39
  )
40
- def __call__(self, question: str) -> str:
 
41
  print(f"Agent received question (first 50 chars): {question[:50]}...")
42
  try:
43
- # Use the integrated CodeAgent to get the response
44
- result = self.agent.run(question)
 
 
 
 
 
 
 
 
45
  print(f"Agent returned answer: {result}")
46
  return result
47
  except Exception as e:
48
  print(f"Error during CodeAgent.run: {e}")
49
  return f"Error from CodeAgent: {e}"
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  def load_pil_image(image_path: str) -> Image.Image:
52
  """Loads an image from disk for processing by GPT-4O."""
53
  return Image.open(image_path)
54
 
 
55
  def load_image(image_path: str) -> str:
56
  """Loads image and encodes it as base64 string for GPT-4o."""
57
  with open(image_path, "rb") as f:
58
  encoded = base64.b64encode(f.read()).decode("utf-8")
59
  return f"data:image/jpeg;base64,{encoded}"
60
 
 
61
  def load_audio(audio_path: str) -> str:
62
  """Encodes audio as base64 for GPT-4o (if needed)."""
63
  with open(audio_path, "rb") as f:
64
  encoded = base64.b64encode(f.read()).decode("utf-8")
65
  return f"data:audio/wav;base64,{encoded}"
66
 
 
 
 
 
 
 
 
 
67
  def describe_image(image_path: str) -> str:
68
  """Sends image directly to GPT-4o to describe it."""
69
  image_base64 = load_image(image_path)
70
  messages = [
71
- {"role": "user", "content": [
72
- {"type": "text", "text": "Describe this image."},
73
- {"type": "image_url", "image_url": {"url": image_base64}},
74
- ]}
 
 
 
75
  ]
76
- response = openai.ChatCompletion.create(
77
- model="gpt-4o",
78
- messages=messages
79
- )
80
  return response.choices[0].message["content"]
81
 
82
- def run_and_submit_all( profile: gr.OAuthProfile | None):
 
83
  """
84
  Fetches all questions, runs the BasicAgent on them, submits all answers,
85
  and displays the results.
86
  """
87
  # --- Determine HF Space Runtime URL and Repo URL ---
88
- space_id = os.getenv("SPACE_ID") # Get the SPACE_ID for sending link to the code
89
 
90
  if profile:
91
- username= f"{profile.username}"
92
  print(f"User logged in: {username}")
93
  else:
94
  print("User not logged in.")
@@ -97,6 +206,7 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
97
  api_url = DEFAULT_API_URL
98
  questions_url = f"{api_url}/questions"
99
  submit_url = f"{api_url}/submit"
 
100
 
101
  # 1. Instantiate Agent ( modify this part to create your agent)
102
  try:
@@ -115,16 +225,16 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
115
  response.raise_for_status()
116
  questions_data = response.json()
117
  if not questions_data:
118
- print("Fetched questions list is empty.")
119
- return "Fetched questions list is empty or invalid format.", None
120
  print(f"Fetched {len(questions_data)} questions.")
121
  except requests.exceptions.RequestException as e:
122
  print(f"Error fetching questions: {e}")
123
  return f"Error fetching questions: {e}", None
124
  except requests.exceptions.JSONDecodeError as e:
125
- print(f"Error decoding JSON response from questions endpoint: {e}")
126
- print(f"Response text: {response.text[:500]}")
127
- return f"Error decoding server response for questions: {e}", None
128
  except Exception as e:
129
  print(f"An unexpected error occurred fetching questions: {e}")
130
  return f"An unexpected error occurred fetching questions: {e}", None
@@ -139,20 +249,43 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
139
  if not task_id or question_text is None:
140
  print(f"Skipping item with missing task_id or question: {item}")
141
  continue
 
 
 
 
 
142
  try:
143
- submitted_answer = agent(question_text)
144
- answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
145
- results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
 
 
 
 
 
 
 
 
146
  except Exception as e:
147
- print(f"Error running agent on task {task_id}: {e}")
148
- results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})
 
 
 
 
 
 
149
 
150
  if not answers_payload:
151
  print("Agent did not produce any answers to submit.")
152
  return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
153
 
154
- # 4. Prepare Submission
155
- submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
 
 
 
 
156
  status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
157
  print(status_update)
158
 
@@ -222,20 +355,19 @@ with gr.Blocks() as demo:
222
 
223
  run_button = gr.Button("Run Evaluation & Submit All Answers")
224
 
225
- status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
 
 
226
  # Removed max_rows=10 from DataFrame constructor
227
  results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
228
 
229
- run_button.click(
230
- fn=run_and_submit_all,
231
- outputs=[status_output, results_table]
232
- )
233
 
234
  if __name__ == "__main__":
235
- print("\n" + "-"*30 + " App Starting " + "-"*30)
236
  # Check for SPACE_HOST and SPACE_ID at startup for information
237
  space_host_startup = os.getenv("SPACE_HOST")
238
- space_id_startup = os.getenv("SPACE_ID") # Get SPACE_ID at startup
239
 
240
  if space_host_startup:
241
  print(f"✅ SPACE_HOST found: {space_host_startup}")
@@ -243,14 +375,18 @@ if __name__ == "__main__":
243
  else:
244
  print("ℹ️ SPACE_HOST environment variable not found (running locally?).")
245
 
246
- if space_id_startup: # Print repo URLs if SPACE_ID is found
247
  print(f"✅ SPACE_ID found: {space_id_startup}")
248
  print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}")
249
- print(f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main")
 
 
250
  else:
251
- print("ℹ️ SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.")
 
 
252
 
253
- print("-"*(60 + len(" App Starting ")) + "\n")
254
 
255
  print("Launching Gradio Interface for Basic Agent Evaluation...")
256
- demo.launch(debug=True, share=False)
 
1
  import os
2
+ import io
3
+ import json
4
  import base64
5
  import gradio as gr
6
  import requests
 
19
  # --- Constants ---
20
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
21
 
22
+
23
  # --- Basic Agent Definition ---
24
  # ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
25
  class BasicAgent:
 
38
  tools=[DuckDuckGoSearchTool()],
39
  model=OpenAIServerModel(model_id=OPENAI_MODEL_ID, api_key=OPENAI_API_KEY),
40
  additional_authorized_imports=["BeautifulSoup"],
41
+ max_steps=10,
42
  )
43
+
44
+ def __call__(self, question: str, file_data: dict = None) -> str:
45
  print(f"Agent received question (first 50 chars): {question[:50]}...")
46
  try:
47
+ images = None
48
+ if file_data is not None:
49
+ file_data = self.handle_file_data(file_data)
50
+ if file_data is not None:
51
+ images = file_data["images"]
52
+ if file_data["text"] is not None:
53
+ question += "\n\n" + file_data["text"]
54
+
55
+ result = self.agent.run(question, images=images)
56
+
57
  print(f"Agent returned answer: {result}")
58
  return result
59
  except Exception as e:
60
  print(f"Error during CodeAgent.run: {e}")
61
  return f"Error from CodeAgent: {e}"
62
 
63
+ def handle_file_data(self, file_data: dict) -> dict:
64
+ if file_data["type"] in ["excel", "csv"]:
65
+ return {"images": None, "text": file_data["data"]}
66
+ elif file_data["type"] == "image":
67
+ return {"images": [file_data["data"]], "text": None}
68
+ elif file_data["type"] == "text":
69
+ return {"images": None, "text": file_data["data"]}
70
+ elif file_data["type"] == "audio":
71
+ return None
72
+ else:
73
+ return None
74
+
75
+
76
+ def load_file_from_response(response):
77
+ """
78
+ Loads and identifies file content from an HTTP response based on its content-type.
79
+ Returns a dictionary with 'type' and 'data' keys.
80
+ """
81
+ content_type = response.headers.get("content-type", "").lower()
82
+ content_bytes = response.content
83
+
84
+ try:
85
+ if "application/json" in content_type:
86
+ if "No file path" in response.json()["detail"]:
87
+ return None
88
+ return {"type": "json", "data": response.json()}
89
+
90
+ elif "text/csv" in content_type:
91
+ return {"type": "csv", "data": pd.read_csv(io.StringIO(response.text))}
92
+
93
+ elif "text/plain" in content_type or "text/x-python" in content_type:
94
+ return {"type": "text", "data": response.text}
95
+
96
+ elif "image/" in content_type:
97
+ return {"type": "image", "data": Image.open(io.BytesIO(content_bytes))}
98
+
99
+ elif "audio/" in content_type:
100
+ audio_data, sample_rate = sf.read(io.BytesIO(content_bytes))
101
+ return {
102
+ "type": "audio",
103
+ "data": {"array": audio_data, "sample_rate": sample_rate},
104
+ }
105
+
106
+ elif "application/octet-stream" in content_type:
107
+ # Try Excel
108
+ try:
109
+ excel_data = pd.read_excel(io.BytesIO(content_bytes))
110
+ return {"type": "excel", "data": excel_data}
111
+ except Exception:
112
+ pass
113
+
114
+ # Try image
115
+ try:
116
+ img = Image.open(io.BytesIO(content_bytes))
117
+ return {"type": "image", "data": img}
118
+ except UnidentifiedImageError:
119
+ pass
120
+
121
+ # Try audio
122
+ try:
123
+ audio_data, sample_rate = sf.read(io.BytesIO(content_bytes))
124
+ return {
125
+ "type": "audio",
126
+ "data": {"array": audio_data, "sample_rate": sample_rate},
127
+ }
128
+ except RuntimeError:
129
+ pass
130
+
131
+ # Try UTF-8 text
132
+ try:
133
+ text = content_bytes.decode("utf-8")
134
+ return {"type": "text", "data": text}
135
+ except UnicodeDecodeError:
136
+ pass
137
+
138
+ return {"type": "binary", "data": content_bytes}
139
+
140
+ else:
141
+ print(f"⚠️ Unhandled content type: {content_type}")
142
+ return {"type": "unknown", "data": content_bytes}
143
+
144
+ except Exception as e:
145
+ print(f"❌ Failed to process content: {e}")
146
+ return {"type": "error", "data": str(e)}
147
+
148
+
149
  def load_pil_image(image_path: str) -> Image.Image:
150
  """Loads an image from disk for processing by GPT-4O."""
151
  return Image.open(image_path)
152
 
153
+
154
  def load_image(image_path: str) -> str:
155
  """Loads image and encodes it as base64 string for GPT-4o."""
156
  with open(image_path, "rb") as f:
157
  encoded = base64.b64encode(f.read()).decode("utf-8")
158
  return f"data:image/jpeg;base64,{encoded}"
159
 
160
+
161
  def load_audio(audio_path: str) -> str:
162
  """Encodes audio as base64 for GPT-4o (if needed)."""
163
  with open(audio_path, "rb") as f:
164
  encoded = base64.b64encode(f.read()).decode("utf-8")
165
  return f"data:audio/wav;base64,{encoded}"
166
 
167
+
168
+ def transcribe_audio(audio_path: str) -> str:
169
+ """Transcribes audio file using OpenAI Whisper model (whisper-1)."""
170
+ with open(audio_path, "rb") as f:
171
+ transcript = openai.Audio.transcribe("whisper-1", f)
172
+ return transcript.get("text", "")
173
+
174
+
175
  def describe_image(image_path: str) -> str:
176
  """Sends image directly to GPT-4o to describe it."""
177
  image_base64 = load_image(image_path)
178
  messages = [
179
+ {
180
+ "role": "user",
181
+ "content": [
182
+ {"type": "text", "text": "Describe this image."},
183
+ {"type": "image_url", "image_url": {"url": image_base64}},
184
+ ],
185
+ }
186
  ]
187
+ response = openai.ChatCompletion.create(model="gpt-4o", messages=messages)
 
 
 
188
  return response.choices[0].message["content"]
189
 
190
+
191
+ def run_and_submit_all(profile: gr.OAuthProfile | None):
192
  """
193
  Fetches all questions, runs the BasicAgent on them, submits all answers,
194
  and displays the results.
195
  """
196
  # --- Determine HF Space Runtime URL and Repo URL ---
197
+ space_id = os.getenv("SPACE_ID") # Get the SPACE_ID for sending link to the code
198
 
199
  if profile:
200
+ username = f"{profile.username}"
201
  print(f"User logged in: {username}")
202
  else:
203
  print("User not logged in.")
 
206
  api_url = DEFAULT_API_URL
207
  questions_url = f"{api_url}/questions"
208
  submit_url = f"{api_url}/submit"
209
+ files_url = f"{api_url}/files" # /files/{task_id}
210
 
211
  # 1. Instantiate Agent ( modify this part to create your agent)
212
  try:
 
225
  response.raise_for_status()
226
  questions_data = response.json()
227
  if not questions_data:
228
+ print("Fetched questions list is empty.")
229
+ return "Fetched questions list is empty or invalid format.", None
230
  print(f"Fetched {len(questions_data)} questions.")
231
  except requests.exceptions.RequestException as e:
232
  print(f"Error fetching questions: {e}")
233
  return f"Error fetching questions: {e}", None
234
  except requests.exceptions.JSONDecodeError as e:
235
+ print(f"Error decoding JSON response from questions endpoint: {e}")
236
+ print(f"Response text: {response.text[:500]}")
237
+ return f"Error decoding server response for questions: {e}", None
238
  except Exception as e:
239
  print(f"An unexpected error occurred fetching questions: {e}")
240
  return f"An unexpected error occurred fetching questions: {e}", None
 
249
  if not task_id or question_text is None:
250
  print(f"Skipping item with missing task_id or question: {item}")
251
  continue
252
+
253
+ file_data = load_file_from_response(
254
+ requests.get(f"{files_url}/{task_id}", timeout=15)
255
+ )
256
+
257
  try:
258
+ submitted_answer = agent(question_text, file_data)
259
+ answers_payload.append(
260
+ {"task_id": task_id, "submitted_answer": submitted_answer}
261
+ )
262
+ results_log.append(
263
+ {
264
+ "Task ID": task_id,
265
+ "Question": question_text,
266
+ "Submitted Answer": submitted_answer,
267
+ }
268
+ )
269
  except Exception as e:
270
+ print(f"Error running agent on task {task_id}: {e}")
271
+ results_log.append(
272
+ {
273
+ "Task ID": task_id,
274
+ "Question": question_text,
275
+ "Submitted Answer": f"AGENT ERROR: {e}",
276
+ }
277
+ )
278
 
279
  if not answers_payload:
280
  print("Agent did not produce any answers to submit.")
281
  return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
282
 
283
+ # 4. Prepare Submission
284
+ submission_data = {
285
+ "username": username.strip(),
286
+ "agent_code": agent_code,
287
+ "answers": answers_payload,
288
+ }
289
  status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
290
  print(status_update)
291
 
 
355
 
356
  run_button = gr.Button("Run Evaluation & Submit All Answers")
357
 
358
+ status_output = gr.Textbox(
359
+ label="Run Status / Submission Result", lines=5, interactive=False
360
+ )
361
  # Removed max_rows=10 from DataFrame constructor
362
  results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
363
 
364
+ run_button.click(fn=run_and_submit_all, outputs=[status_output, results_table])
 
 
 
365
 
366
  if __name__ == "__main__":
367
+ print("\n" + "-" * 30 + " App Starting " + "-" * 30)
368
  # Check for SPACE_HOST and SPACE_ID at startup for information
369
  space_host_startup = os.getenv("SPACE_HOST")
370
+ space_id_startup = os.getenv("SPACE_ID") # Get SPACE_ID at startup
371
 
372
  if space_host_startup:
373
  print(f"✅ SPACE_HOST found: {space_host_startup}")
 
375
  else:
376
  print("ℹ️ SPACE_HOST environment variable not found (running locally?).")
377
 
378
+ if space_id_startup: # Print repo URLs if SPACE_ID is found
379
  print(f"✅ SPACE_ID found: {space_id_startup}")
380
  print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}")
381
+ print(
382
+ f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main"
383
+ )
384
  else:
385
+ print(
386
+ "ℹ️ SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined."
387
+ )
388
 
389
+ print("-" * (60 + len(" App Starting ")) + "\n")
390
 
391
  print("Launching Gradio Interface for Basic Agent Evaluation...")
392
+ demo.launch(debug=True, share=False)
pyproject.toml CHANGED
@@ -8,6 +8,7 @@ dependencies = [
8
  "beautifulsoup4>=4.13.4",
9
  "dotenv>=0.9.9",
10
  "gradio[oauth]>=5.26.0",
 
11
  "requests>=2.32.3",
12
  "smolagents[openai]>=1.14.0",
13
  "soundfile>=0.13.1",
 
8
  "beautifulsoup4>=4.13.4",
9
  "dotenv>=0.9.9",
10
  "gradio[oauth]>=5.26.0",
11
+ "openpyxl>=3.1.5",
12
  "requests>=2.32.3",
13
  "smolagents[openai]>=1.14.0",
14
  "soundfile>=0.13.1",
uv.lock CHANGED
@@ -14,6 +14,7 @@ dependencies = [
14
  { name = "beautifulsoup4" },
15
  { name = "dotenv" },
16
  { name = "gradio", extra = ["oauth"] },
 
17
  { name = "requests" },
18
  { name = "smolagents", extra = ["openai"] },
19
  { name = "soundfile" },
@@ -24,6 +25,7 @@ requires-dist = [
24
  { name = "beautifulsoup4", specifier = ">=4.13.4" },
25
  { name = "dotenv", specifier = ">=0.9.9" },
26
  { name = "gradio", extras = ["oauth"], specifier = ">=5.26.0" },
 
27
  { name = "requests", specifier = ">=2.32.3" },
28
  { name = "smolagents", extras = ["openai"], specifier = ">=1.14.0" },
29
  { name = "soundfile", specifier = ">=0.13.1" },
@@ -293,6 +295,15 @@ wheels = [
293
  { url = "https://files.pythonhosted.org/packages/83/a2/66adca41164860dee6d2d47b506fef3262c8879aab727b687c798d67313f/duckduckgo_search-8.0.1-py3-none-any.whl", hash = "sha256:87ea18d9abb1cd5dc8f63fc70ac867996acce2cb5e0129d191b9491c202420be", size = 18125 },
294
  ]
295
 
 
 
 
 
 
 
 
 
 
296
  [[package]]
297
  name = "fastapi"
298
  version = "0.115.12"
@@ -697,6 +708,18 @@ wheels = [
697
  { url = "https://files.pythonhosted.org/packages/59/aa/84e02ab500ca871eb8f62784426963a1c7c17a72fea3c7f268af4bbaafa5/openai-1.76.0-py3-none-any.whl", hash = "sha256:a712b50e78cf78e6d7b2a8f69c4978243517c2c36999756673e07a14ce37dc0a", size = 661201 },
698
  ]
699
 
 
 
 
 
 
 
 
 
 
 
 
 
700
  [[package]]
701
  name = "orjson"
702
  version = "3.10.16"
 
14
  { name = "beautifulsoup4" },
15
  { name = "dotenv" },
16
  { name = "gradio", extra = ["oauth"] },
17
+ { name = "openpyxl" },
18
  { name = "requests" },
19
  { name = "smolagents", extra = ["openai"] },
20
  { name = "soundfile" },
 
25
  { name = "beautifulsoup4", specifier = ">=4.13.4" },
26
  { name = "dotenv", specifier = ">=0.9.9" },
27
  { name = "gradio", extras = ["oauth"], specifier = ">=5.26.0" },
28
+ { name = "openpyxl", specifier = ">=3.1.5" },
29
  { name = "requests", specifier = ">=2.32.3" },
30
  { name = "smolagents", extras = ["openai"], specifier = ">=1.14.0" },
31
  { name = "soundfile", specifier = ">=0.13.1" },
 
295
  { url = "https://files.pythonhosted.org/packages/83/a2/66adca41164860dee6d2d47b506fef3262c8879aab727b687c798d67313f/duckduckgo_search-8.0.1-py3-none-any.whl", hash = "sha256:87ea18d9abb1cd5dc8f63fc70ac867996acce2cb5e0129d191b9491c202420be", size = 18125 },
296
  ]
297
 
298
+ [[package]]
299
+ name = "et-xmlfile"
300
+ version = "2.0.0"
301
+ source = { registry = "https://pypi.org/simple" }
302
+ sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234 }
303
+ wheels = [
304
+ { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059 },
305
+ ]
306
+
307
  [[package]]
308
  name = "fastapi"
309
  version = "0.115.12"
 
708
  { url = "https://files.pythonhosted.org/packages/59/aa/84e02ab500ca871eb8f62784426963a1c7c17a72fea3c7f268af4bbaafa5/openai-1.76.0-py3-none-any.whl", hash = "sha256:a712b50e78cf78e6d7b2a8f69c4978243517c2c36999756673e07a14ce37dc0a", size = 661201 },
709
  ]
710
 
711
+ [[package]]
712
+ name = "openpyxl"
713
+ version = "3.1.5"
714
+ source = { registry = "https://pypi.org/simple" }
715
+ dependencies = [
716
+ { name = "et-xmlfile" },
717
+ ]
718
+ sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464 }
719
+ wheels = [
720
+ { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910 },
721
+ ]
722
+
723
  [[package]]
724
  name = "orjson"
725
  version = "3.10.16"