Compare commits
2 Commits
bc12df28a5
...
5f9e67a312
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f9e67a312 | |||
| 3ee25f9e33 |
@@ -29,18 +29,6 @@ def _():
|
|||||||
|
|
||||||
|
|
||||||
@app.cell(hide_code=True)
|
@app.cell(hide_code=True)
|
||||||
def _(mo):
|
|
||||||
mo.outline(label="Table of Contents")
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
@app.cell
|
|
||||||
def _():
|
|
||||||
# Select Dataset
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
@app.cell
|
|
||||||
def _(mo):
|
def _(mo):
|
||||||
file_browser = mo.ui.file_browser(
|
file_browser = mo.ui.file_browser(
|
||||||
initial_path="./data/exports", multiple=False, restrict_navigation=True, filetypes=[".csv"], label="Select 'Labels' File"
|
initial_path="./data/exports", multiple=False, restrict_navigation=True, filetypes=[".csv"], label="Select 'Labels' File"
|
||||||
@@ -59,12 +47,6 @@ def _(Path, file_browser, mo):
|
|||||||
return QSF_FILE, RESULTS_FILE
|
return QSF_FILE, RESULTS_FILE
|
||||||
|
|
||||||
|
|
||||||
@app.cell
|
|
||||||
def _(RESULTS_FILE, mo):
|
|
||||||
mo.stop(not RESULTS_FILE.name.lower().endswith('labels.csv'), mo.md("**⚠️ Make sure you select a `_Labels.csv` file above**"))
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
@app.cell
|
@app.cell
|
||||||
def _(JPMCSurvey, QSF_FILE, RESULTS_FILE, mo):
|
def _(JPMCSurvey, QSF_FILE, RESULTS_FILE, mo):
|
||||||
S = JPMCSurvey(RESULTS_FILE, QSF_FILE)
|
S = JPMCSurvey(RESULTS_FILE, QSF_FILE)
|
||||||
@@ -76,12 +58,6 @@ def _(JPMCSurvey, QSF_FILE, RESULTS_FILE, mo):
|
|||||||
|
|
||||||
|
|
||||||
@app.cell
|
@app.cell
|
||||||
def _():
|
|
||||||
# check_straight_liners(S.get_ss_green_blue(data_all)[0])
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
@app.cell(hide_code=True)
|
|
||||||
def _(Path, RESULTS_FILE, mo):
|
def _(Path, RESULTS_FILE, mo):
|
||||||
mo.md(f"""
|
mo.md(f"""
|
||||||
|
|
||||||
@@ -112,6 +88,26 @@ def _(check_progress, data_all, duration_validation, mo):
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _(mo):
|
||||||
|
mo.md(r"""
|
||||||
|
### ⚠️ ToDo: "straight-liner" detection and removal
|
||||||
|
""")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _(mo):
|
||||||
|
mo.md(r"""
|
||||||
|
---
|
||||||
|
|
||||||
|
# Data Filter
|
||||||
|
|
||||||
|
Use to select a subset of the data for the following analysis
|
||||||
|
""")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
@app.cell(hide_code=True)
|
@app.cell(hide_code=True)
|
||||||
def _(S, mo):
|
def _(S, mo):
|
||||||
filter_form = mo.md('''
|
filter_form = mo.md('''
|
||||||
@@ -147,7 +143,7 @@ def _(S, mo):
|
|||||||
return (filter_form,)
|
return (filter_form,)
|
||||||
|
|
||||||
|
|
||||||
@app.cell(hide_code=True)
|
@app.cell
|
||||||
def _(S, data_all, filter_form, mo):
|
def _(S, data_all, filter_form, mo):
|
||||||
mo.stop(filter_form.value is None, mo.md("**Please submit filter above to proceed**"))
|
mo.stop(filter_form.value is None, mo.md("**Please submit filter above to proceed**"))
|
||||||
_d = S.filter_data(data_all, age=filter_form.value['age'], gender=filter_form.value['gender'], income=filter_form.value['income'], ethnicity=filter_form.value['ethnicity'], consumer=filter_form.value['consumer'])
|
_d = S.filter_data(data_all, age=filter_form.value['age'], gender=filter_form.value['gender'], income=filter_form.value['income'], ethnicity=filter_form.value['ethnicity'], consumer=filter_form.value['consumer'])
|
||||||
@@ -173,6 +169,20 @@ def _(S, data, mo):
|
|||||||
return (char_rank,)
|
return (char_rank,)
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell(hide_code=True)
|
||||||
|
def _(mo):
|
||||||
|
mo.md(r"""
|
||||||
|
|
||||||
|
""")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell(hide_code=True)
|
||||||
|
def _():
|
||||||
|
# char_rank = S.get_character_ranking(data)[0]
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
@app.cell
|
@app.cell
|
||||||
def _(S, char_rank, mo):
|
def _(S, char_rank, mo):
|
||||||
mo.md(f"""
|
mo.md(f"""
|
||||||
@@ -218,6 +228,13 @@ def _(S, data, mo):
|
|||||||
return (v_18_8_3,)
|
return (v_18_8_3,)
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell(hide_code=True)
|
||||||
|
def _():
|
||||||
|
|
||||||
|
# print(v_18_8_3.head())
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
@app.cell(hide_code=True)
|
@app.cell(hide_code=True)
|
||||||
def _(S, mo, v_18_8_3):
|
def _(S, mo, v_18_8_3):
|
||||||
mo.md(f"""
|
mo.md(f"""
|
||||||
@@ -240,7 +257,7 @@ def _(S, mo, v_18_8_3):
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
@app.cell(hide_code=True)
|
@app.cell
|
||||||
def _(S, calculate_weighted_ranking_scores, data):
|
def _(S, calculate_weighted_ranking_scores, data):
|
||||||
top3_voices = S.get_top_3_voices(data)[0]
|
top3_voices = S.get_top_3_voices(data)[0]
|
||||||
top3_voices_weighted = calculate_weighted_ranking_scores(top3_voices)
|
top3_voices_weighted = calculate_weighted_ranking_scores(top3_voices)
|
||||||
@@ -284,6 +301,11 @@ def _(S, mo, top3_voices):
|
|||||||
|
|
||||||
|
|
||||||
@app.cell
|
@app.cell
|
||||||
|
def _():
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell(hide_code=True)
|
||||||
def _(S, data, mo, utils):
|
def _(S, data, mo, utils):
|
||||||
ss_or, choice_map_or = S.get_ss_orange_red(data)
|
ss_or, choice_map_or = S.get_ss_orange_red(data)
|
||||||
ss_gb, choice_map_gb = S.get_ss_green_blue(data)
|
ss_gb, choice_map_gb = S.get_ss_green_blue(data)
|
||||||
@@ -322,12 +344,12 @@ def _(S, mo, pl, ss_long):
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
@app.cell(hide_code=True)
|
@app.cell
|
||||||
def _():
|
def _():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
@app.cell
|
@app.cell(hide_code=True)
|
||||||
def _(S, data, mo):
|
def _(S, data, mo):
|
||||||
vscales = S.get_voice_scale_1_10(data)[0]
|
vscales = S.get_voice_scale_1_10(data)[0]
|
||||||
# plot_average_scores_with_counts(vscales, x_label='Voice', width=1000)
|
# plot_average_scores_with_counts(vscales, x_label='Voice', width=1000)
|
||||||
@@ -337,7 +359,7 @@ def _(S, data, mo):
|
|||||||
return (vscales,)
|
return (vscales,)
|
||||||
|
|
||||||
|
|
||||||
@app.cell
|
@app.cell(hide_code=True)
|
||||||
def _(S, mo, vscales):
|
def _(S, mo, vscales):
|
||||||
mo.md(f"""
|
mo.md(f"""
|
||||||
### How does each voice score on a scale from 1-10?
|
### How does each voice score on a scale from 1-10?
|
||||||
@@ -352,7 +374,7 @@ def _():
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
@app.cell(hide_code=True)
|
@app.cell
|
||||||
def _(mo):
|
def _(mo):
|
||||||
mo.md(r"""
|
mo.md(r"""
|
||||||
|
|
||||||
@@ -360,7 +382,7 @@ def _(mo):
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
@app.cell(hide_code=True)
|
@app.cell
|
||||||
def _(mo):
|
def _(mo):
|
||||||
mo.md(r"""
|
mo.md(r"""
|
||||||
|
|
||||||
@@ -400,7 +422,7 @@ def _(choice_map, mo, ss_all, utils, vscales):
|
|||||||
return df_style, joined_df
|
return df_style, joined_df
|
||||||
|
|
||||||
|
|
||||||
@app.cell
|
@app.cell(hide_code=True)
|
||||||
def _(S, SPEAKING_STYLES, joined_df, mo):
|
def _(S, SPEAKING_STYLES, joined_df, mo):
|
||||||
_content = """### Total Results
|
_content = """### Total Results
|
||||||
|
|
||||||
@@ -431,34 +453,18 @@ def _(mo):
|
|||||||
|
|
||||||
- [ ] 4 correlation diagrams considering each speaking style (4) and all female voice results.
|
- [ ] 4 correlation diagrams considering each speaking style (4) and all female voice results.
|
||||||
- [ ] 4 correlation diagrams considering each speaking style (4) and all male voice results.
|
- [ ] 4 correlation diagrams considering each speaking style (4) and all male voice results.
|
||||||
""")
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
@app.cell(hide_code=True)
|
|
||||||
def _(mo):
|
|
||||||
mo.md(r"""
|
|
||||||
## Correlations Voice Speaking Styles <-> Voice Ranking Points
|
## Correlations Voice Speaking Styles <-> Voice Ranking Points
|
||||||
""")
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
@app.cell(hide_code=True)
|
|
||||||
def _(mo):
|
|
||||||
mo.md(r"""
|
|
||||||
Let’s show how scoring better on these speaking styles correlates (or not) with better Vocie Ranking results. For each speaking style we show how the traits in these speaking styles correlate with voice ranking points. This gives us a total of 4 correlation diagrams.
|
Let’s show how scoring better on these speaking styles correlates (or not) with better Vocie Ranking results. For each speaking style we show how the traits in these speaking styles correlate with voice ranking points. This gives us a total of 4 correlation diagrams.
|
||||||
|
|
||||||
Example for speaking style green:
|
Example for speaking style green:
|
||||||
- Trait 1: Friendly | Conversational | Down-to-earth
|
- Trait 1: Friendly | Conversational | Down-to-earth
|
||||||
- Trait 2: Approachable | Familiar | Warm
|
- Trait 2: Approachable | Familiar | Warm
|
||||||
- Trait 3: Optimistic | Benevolent | Positive | Appreciative
|
- Trait 3: Optimistic | Benevolent | Positive | Appreciative
|
||||||
""")
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
@app.cell(hide_code=True)
|
|
||||||
def _(mo):
|
|
||||||
mo.md(r"""
|
|
||||||
### Total Results
|
### Total Results
|
||||||
|
|
||||||
- [ ] 4 correlation diagrams
|
- [ ] 4 correlation diagrams
|
||||||
@@ -466,7 +472,31 @@ def _(mo):
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
@app.cell
|
@app.cell(hide_code=True)
|
||||||
|
def _(mo):
|
||||||
|
mo.md(r"""
|
||||||
|
|
||||||
|
""")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell(hide_code=True)
|
||||||
|
def _(mo):
|
||||||
|
mo.md(r"""
|
||||||
|
|
||||||
|
""")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell(hide_code=True)
|
||||||
|
def _(mo):
|
||||||
|
mo.md(r"""
|
||||||
|
|
||||||
|
""")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell(hide_code=True)
|
||||||
def _(S, SPEAKING_STYLES, df_style, mo, top3_voices, utils):
|
def _(S, SPEAKING_STYLES, df_style, mo, top3_voices, utils):
|
||||||
df_ranking = utils.process_voice_ranking_data(top3_voices)
|
df_ranking = utils.process_voice_ranking_data(top3_voices)
|
||||||
joined = df_style.join(df_ranking, on=['_recordId', 'Voice'], how='inner')
|
joined = df_style.join(df_ranking, on=['_recordId', 'Voice'], how='inner')
|
||||||
@@ -490,7 +520,7 @@ def _(S, SPEAKING_STYLES, df_style, mo, top3_voices, utils):
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
@app.cell(hide_code=True)
|
@app.cell
|
||||||
def _(mo):
|
def _(mo):
|
||||||
mo.md(r"""
|
mo.md(r"""
|
||||||
### Female / Male Voices considered seperately
|
### Female / Male Voices considered seperately
|
||||||
@@ -501,49 +531,5 @@ def _(mo):
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
@app.cell(hide_code=True)
|
|
||||||
def _(mo):
|
|
||||||
mo.md(r"""
|
|
||||||
## Correlation Heatmap all evaluations <-> voice acoustic data
|
|
||||||
|
|
||||||
- [ ] Heatmap for male voices
|
|
||||||
- [ ] Heatmap for female voices
|
|
||||||
""")
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
@app.cell(hide_code=True)
|
|
||||||
def _(mo):
|
|
||||||
mo.md(r"""
|
|
||||||
## Most Prominent Character Personality Traits
|
|
||||||
""")
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
@app.cell(hide_code=True)
|
|
||||||
def _(mo):
|
|
||||||
mo.md(r"""
|
|
||||||
The last question of the survey is about traits for the described character's personality. For each Character personality, we want to display the 8 most chosen character personality traits. This will give us a total of 4 diagrams, one for each character personality included in the test.
|
|
||||||
|
|
||||||
- [ ] Bank Teller
|
|
||||||
- [ ] Familiar Friend
|
|
||||||
- [ ] The Coach
|
|
||||||
- [ ] Personal Assistant
|
|
||||||
""")
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
@app.cell
|
|
||||||
def _(mo):
|
|
||||||
mo.md(r"""
|
|
||||||
---
|
|
||||||
|
|
||||||
# Results per subgroup
|
|
||||||
|
|
||||||
Use the dropdown selector at the top to filter the data and generate all the plots again
|
|
||||||
""")
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run()
|
app.run()
|
||||||
|
|||||||
73
99_example_ppt_replace_images.py
Normal file
73
99_example_ppt_replace_images.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import marimo
|
||||||
|
|
||||||
|
__generated_with = "0.19.2"
|
||||||
|
app = marimo.App(width="medium")
|
||||||
|
|
||||||
|
with app.setup:
|
||||||
|
import marimo as mo
|
||||||
|
from pathlib import Path
|
||||||
|
import utils
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _():
|
||||||
|
mo.md(r"""
|
||||||
|
# Tag existing images with Alt-Text
|
||||||
|
|
||||||
|
Based on image content
|
||||||
|
""")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _():
|
||||||
|
TAG_SOURCE = Path('data/test_tag_source.pptx')
|
||||||
|
TAG_TARGET = Path('data/test_tag_target.pptx')
|
||||||
|
TAG_IMAGE_DIR = Path('figures/OneDrive_2026-01-28/')
|
||||||
|
return TAG_IMAGE_DIR, TAG_SOURCE, TAG_TARGET
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _(TAG_IMAGE_DIR, TAG_SOURCE, TAG_TARGET):
|
||||||
|
utils.update_ppt_alt_text(ppt_path=TAG_SOURCE, image_source_dir=TAG_IMAGE_DIR, output_path=TAG_TARGET)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _():
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _():
|
||||||
|
mo.md(r"""
|
||||||
|
# Replace Images using Alt-Text
|
||||||
|
""")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _():
|
||||||
|
REPLACE_SOURCE = Path('data/test_replace_source.pptx')
|
||||||
|
REPLACE_TARGET = Path('data/test_replace_target.pptx')
|
||||||
|
return REPLACE_SOURCE, REPLACE_TARGET
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _():
|
||||||
|
IMAGE_FILE = Path('figures/OneDrive_2026-01-28/Cons-Early_Professional/cold_distant_approachable_familiar_warm.png')
|
||||||
|
return (IMAGE_FILE,)
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _(IMAGE_FILE, REPLACE_SOURCE, REPLACE_TARGET):
|
||||||
|
utils.pptx_replace_named_image(
|
||||||
|
presentation_path=REPLACE_SOURCE,
|
||||||
|
target_tag=utils.image_alt_text_generator(IMAGE_FILE),
|
||||||
|
new_image_path=IMAGE_FILE,
|
||||||
|
save_path=REPLACE_TARGET)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run()
|
||||||
2
plots.py
2
plots.py
@@ -496,7 +496,7 @@ class JPMCPlotsMixin:
|
|||||||
|
|
||||||
# Bar chart
|
# Bar chart
|
||||||
bars = alt.Chart(weighted_df).mark_bar(color=color).encode(
|
bars = alt.Chart(weighted_df).mark_bar(color=color).encode(
|
||||||
x=alt.X('Character:N', title=x_label),
|
x=alt.X('Character:N', title=x_label, sort='-y'),
|
||||||
y=alt.Y('Weighted Score:Q', title=y_label),
|
y=alt.Y('Weighted Score:Q', title=y_label),
|
||||||
tooltip=[
|
tooltip=[
|
||||||
alt.Tooltip('Character:N'),
|
alt.Tooltip('Character:N'),
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ dependencies = [
|
|||||||
"polars>=1.37.1",
|
"polars>=1.37.1",
|
||||||
"pyarrow>=23.0.0",
|
"pyarrow>=23.0.0",
|
||||||
"pysqlite3>=0.6.0",
|
"pysqlite3>=0.6.0",
|
||||||
|
"python-pptx>=1.0.2",
|
||||||
"pyzmq>=27.1.0",
|
"pyzmq>=27.1.0",
|
||||||
"requests>=2.32.5",
|
"requests>=2.32.5",
|
||||||
"taguette>=1.5.1",
|
"taguette>=1.5.1",
|
||||||
|
|||||||
252
utils.py
252
utils.py
@@ -4,9 +4,259 @@ import pandas as pd
|
|||||||
from typing import Union
|
from typing import Union
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
from plots import JPMCPlotsMixin
|
from plots import JPMCPlotsMixin
|
||||||
|
|
||||||
import marimo as mo
|
|
||||||
|
from pptx import Presentation
|
||||||
|
from pptx.enum.shapes import MSO_SHAPE_TYPE
|
||||||
|
|
||||||
|
|
||||||
|
def image_alt_text_generator(fpath):
|
||||||
|
"""convert image file path to alt text
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not isinstance(fpath, Path):
|
||||||
|
fpath = Path(fpath)
|
||||||
|
|
||||||
|
fparts = fpath.parts
|
||||||
|
assert fparts[0] == 'figures', "Image file path must start with 'figures'"
|
||||||
|
|
||||||
|
return Path('/'.join(fparts[2:])).as_posix()
|
||||||
|
|
||||||
|
def pptx_replace_named_image(presentation_path, target_tag, new_image_path, save_path):
|
||||||
|
"""
|
||||||
|
Finds and replaces specific images in a PowerPoint presentation while
|
||||||
|
preserving their original position, size, and aspect ratio.
|
||||||
|
|
||||||
|
This function performs a 'surgical' replacement: it records the coordinates
|
||||||
|
of the existing image, removes it from the slide's XML, and inserts a
|
||||||
|
new image into the exact same bounding box. It identifies the target
|
||||||
|
image by searching for a specific string within the Shape Name
|
||||||
|
(Selection Pane) or Alt Text.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
presentation_path (str): The file path to the source .pptx file.
|
||||||
|
target_tag (str): The unique identifier to look for (e.g., 'HERO_IMAGE').
|
||||||
|
This is case-sensitive and checks both the shape name and alt text.
|
||||||
|
new_image_path (str): The file path to the new image (PNG, JPG, etc.).
|
||||||
|
save_path (str): The file path where the modified presentation will be saved.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None: Saves the file directly to the provided save_path.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: If the source presentation or new image is not found.
|
||||||
|
PermissionError: If the save_path is currently open or locked.
|
||||||
|
"""
|
||||||
|
prs = Presentation(presentation_path)
|
||||||
|
|
||||||
|
for i, slide in enumerate(prs.slides):
|
||||||
|
# Iterate over a list copy of shapes to safely modify the slide during iteration
|
||||||
|
print(f"Processing Slide {i + 1}...")
|
||||||
|
print(f"Total Shapes: {len(slide.shapes)} shapes")
|
||||||
|
|
||||||
|
for shape in list(slide.shapes):
|
||||||
|
print(f"Checking shape: {shape.name} of type {shape.shape_type}...")
|
||||||
|
|
||||||
|
shape_name = shape.name or ""
|
||||||
|
alt_text = ""
|
||||||
|
|
||||||
|
# More robust strategy: Check for alt text in ANY valid element property
|
||||||
|
# This allows replacing Pictures, Placeholders, GraphicFrames, etc.
|
||||||
|
try:
|
||||||
|
# Check for common property names used by python-pptx elements to store non-visual props
|
||||||
|
# nvPicPr (Picture), nvSpPr (Shape/Placeholder), nvGrpSpPr (Group),
|
||||||
|
# nvGraphicFramePr (GraphicFrame), nvCxnSpPr (Connector)
|
||||||
|
nvPr = None
|
||||||
|
for attr in ['nvPicPr', 'nvSpPr', 'nvGrpSpPr', 'nvGraphicFramePr', 'nvCxnSpPr']:
|
||||||
|
if hasattr(shape._element, attr):
|
||||||
|
nvPr = getattr(shape._element, attr)
|
||||||
|
break
|
||||||
|
|
||||||
|
if nvPr is not None and hasattr(nvPr, 'cNvPr'):
|
||||||
|
alt_text = nvPr.cNvPr.get("descr", "")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print(f"Alt Text for shape '{shape_name}': {alt_text}")
|
||||||
|
|
||||||
|
if target_tag in shape_name or target_tag in alt_text:
|
||||||
|
print(f"Found it! Replacing {shape_name}...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Record coordinates
|
||||||
|
left, top, width, height = shape.left, shape.top, shape.width, shape.height
|
||||||
|
|
||||||
|
# Remove old shape
|
||||||
|
old_element = shape._element
|
||||||
|
old_element.getparent().remove(old_element)
|
||||||
|
|
||||||
|
# Add new image at the same spot
|
||||||
|
slide.shapes.add_picture(str(new_image_path), left, top, width, height)
|
||||||
|
except AttributeError:
|
||||||
|
print(f"Could not replace {shape_name} - might be missing dimensions.")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"Skipping shape '{shape_name}' with alt text '{alt_text}'")
|
||||||
|
|
||||||
|
prs.save(save_path)
|
||||||
|
print(f"Successfully saved to {save_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def _calculate_file_sha1(file_path: Union[str, Path]) -> str:
|
||||||
|
"""Calculate SHA1 hash of a file."""
|
||||||
|
sha1 = hashlib.sha1()
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
while True:
|
||||||
|
data = f.read(65536)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
sha1.update(data)
|
||||||
|
return sha1.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _build_image_hash_map(root_dir: Union[str, Path]) -> dict:
|
||||||
|
"""
|
||||||
|
Recursively walk the directory and build a map of SHA1 hashes to file paths.
|
||||||
|
Only includes common image extensions.
|
||||||
|
"""
|
||||||
|
hash_map = {}
|
||||||
|
valid_extensions = {'.png', '.jpg', '.jpeg', '.tiff', '.bmp', '.gif'}
|
||||||
|
|
||||||
|
root = Path(root_dir)
|
||||||
|
print(f"Building image hash map from {root}...")
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for root_path, dirs, files in os.walk(root):
|
||||||
|
for file in files:
|
||||||
|
file_path = Path(root_path) / file
|
||||||
|
if file_path.suffix.lower() in valid_extensions:
|
||||||
|
try:
|
||||||
|
file_sha1 = _calculate_file_sha1(file_path)
|
||||||
|
# We store the absolute path for reference, but we might just need the path relative to project for alt text
|
||||||
|
hash_map[file_sha1] = file_path
|
||||||
|
count += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error hashing {file_path}: {e}")
|
||||||
|
|
||||||
|
print(f"Indexed {count} images.")
|
||||||
|
return hash_map
|
||||||
|
|
||||||
|
|
||||||
|
def _iter_picture_shapes(shapes):
|
||||||
|
"""
|
||||||
|
Recursively iterate over shapes and yield those that are pictures
|
||||||
|
(have an 'image' property), diving into groups.
|
||||||
|
"""
|
||||||
|
for shape in shapes:
|
||||||
|
# Check groups recursively
|
||||||
|
if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
|
||||||
|
yield from _iter_picture_shapes(shape.shapes)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if shape has image property (Pictures, Placeholders with images)
|
||||||
|
if hasattr(shape, 'image'):
|
||||||
|
yield shape
|
||||||
|
|
||||||
|
|
||||||
|
def update_ppt_alt_text(ppt_path: Union[str, Path], image_source_dir: Union[str, Path], output_path: Union[str, Path] = None):
|
||||||
|
"""
|
||||||
|
Updates the alt text of images in a PowerPoint presentation by matching
|
||||||
|
their content (SHA1 hash) with images in a source directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ppt_path (str/Path): Path to the PowerPoint file.
|
||||||
|
image_source_dir (str/Path): Directory containing source images to match against.
|
||||||
|
output_path (str/Path, optional): Path to save the updated presentation.
|
||||||
|
If None, overwrites the input file.
|
||||||
|
"""
|
||||||
|
if output_path is None:
|
||||||
|
output_path = ppt_path
|
||||||
|
|
||||||
|
# 1. Build lookup map of {sha1: file_path} from the source directory
|
||||||
|
image_hash_map = _build_image_hash_map(image_source_dir)
|
||||||
|
|
||||||
|
# 2. Open Presentation
|
||||||
|
try:
|
||||||
|
prs = Presentation(ppt_path)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error opening presentation {ppt_path}: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
updates_count = 0
|
||||||
|
slides = list(prs.slides)
|
||||||
|
total_slides = len(slides)
|
||||||
|
|
||||||
|
print(f"Processing {total_slides} slides...")
|
||||||
|
|
||||||
|
for i, slide in enumerate(slides):
|
||||||
|
# Use recursive iterator to find all pictures including those in groups/placeholders
|
||||||
|
picture_shapes = list(_iter_picture_shapes(slide.shapes))
|
||||||
|
|
||||||
|
for shape in picture_shapes:
|
||||||
|
try:
|
||||||
|
# shape.image.sha1 returns the SHA1 hash of the image blob
|
||||||
|
current_sha1 = shape.image.sha1
|
||||||
|
|
||||||
|
if current_sha1 in image_hash_map:
|
||||||
|
original_path = image_hash_map[current_sha1]
|
||||||
|
|
||||||
|
# Generate Alt Text
|
||||||
|
try:
|
||||||
|
# Prepare path for generator.
|
||||||
|
# Try to relativize to CWD if capable
|
||||||
|
pass_path = original_path
|
||||||
|
try:
|
||||||
|
pass_path = original_path.relative_to(Path.cwd())
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
new_alt_text = image_alt_text_generator(pass_path)
|
||||||
|
|
||||||
|
# Check existing alt text to avoid redundant updates/log them
|
||||||
|
# Accessing alt text via cNvPr
|
||||||
|
# Note: Different shape types might store non-visual props differently
|
||||||
|
# Picture: nvPicPr.cNvPr
|
||||||
|
# GraphicFrame: nvGraphicFramePr.cNvPr
|
||||||
|
# Group: nvGrpSpPr.cNvPr
|
||||||
|
# Shape/Placeholder: nvSpPr.cNvPr
|
||||||
|
|
||||||
|
nvPr = None
|
||||||
|
for attr in ['nvPicPr', 'nvSpPr', 'nvGrpSpPr', 'nvGraphicFramePr', 'nvCxnSpPr']:
|
||||||
|
if hasattr(shape._element, attr):
|
||||||
|
nvPr = getattr(shape._element, attr)
|
||||||
|
break
|
||||||
|
|
||||||
|
if nvPr and hasattr(nvPr, 'cNvPr'):
|
||||||
|
cNvPr = nvPr.cNvPr
|
||||||
|
existing_alt_text = cNvPr.get("descr", "")
|
||||||
|
|
||||||
|
if existing_alt_text != new_alt_text:
|
||||||
|
print(f"Slide {i+1}: Updating alt text for image matches '{pass_path}'")
|
||||||
|
print(f" Old: '{existing_alt_text}' -> New: '{new_alt_text}'")
|
||||||
|
cNvPr.set("descr", new_alt_text)
|
||||||
|
updates_count += 1
|
||||||
|
else:
|
||||||
|
print(f"Could not find cNvPr for shape on slide {i+1}")
|
||||||
|
|
||||||
|
except AssertionError as e:
|
||||||
|
print(f"Skipping match for {original_path} due to generator error: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating alt text for {original_path}: {e}")
|
||||||
|
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing shape on slide {i+1}: {e}")
|
||||||
|
|
||||||
|
if updates_count > 0:
|
||||||
|
prs.save(output_path)
|
||||||
|
print(f"Saved updated presentation to {output_path} with {updates_count} updates.")
|
||||||
|
else:
|
||||||
|
print("No images matched or required updates.")
|
||||||
|
|
||||||
|
|
||||||
def extract_voice_label(html_str: str) -> str:
|
def extract_voice_label(html_str: str) -> str:
|
||||||
"""
|
"""
|
||||||
|
|||||||
97
uv.lock
generated
97
uv.lock
generated
@@ -840,6 +840,86 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/f3/f5/c7dd8cdbd57454b23d89799c22cd42b6d2dda283cd87d7b198dc424a462c/loro-1.10.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42d6a5ce5bc518eaa682413e82d597299650eeb03e8bc39341752d6e0d22503e", size = 3541282, upload-time = "2025-12-09T10:13:42.189Z" },
|
{ url = "https://files.pythonhosted.org/packages/f3/f5/c7dd8cdbd57454b23d89799c22cd42b6d2dda283cd87d7b198dc424a462c/loro-1.10.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42d6a5ce5bc518eaa682413e82d597299650eeb03e8bc39341752d6e0d22503e", size = 3541282, upload-time = "2025-12-09T10:13:42.189Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lxml"
|
||||||
|
version = "6.0.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mako"
|
name = "mako"
|
||||||
version = "1.3.10"
|
version = "1.3.10"
|
||||||
@@ -1335,6 +1415,7 @@ dependencies = [
|
|||||||
{ name = "polars" },
|
{ name = "polars" },
|
||||||
{ name = "pyarrow" },
|
{ name = "pyarrow" },
|
||||||
{ name = "pysqlite3" },
|
{ name = "pysqlite3" },
|
||||||
|
{ name = "python-pptx" },
|
||||||
{ name = "pyzmq" },
|
{ name = "pyzmq" },
|
||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
{ name = "taguette" },
|
{ name = "taguette" },
|
||||||
@@ -1356,6 +1437,7 @@ requires-dist = [
|
|||||||
{ name = "polars", specifier = ">=1.37.1" },
|
{ name = "polars", specifier = ">=1.37.1" },
|
||||||
{ name = "pyarrow", specifier = ">=23.0.0" },
|
{ name = "pyarrow", specifier = ">=23.0.0" },
|
||||||
{ name = "pysqlite3", specifier = ">=0.6.0" },
|
{ name = "pysqlite3", specifier = ">=0.6.0" },
|
||||||
|
{ name = "python-pptx", specifier = ">=1.0.2" },
|
||||||
{ name = "pyzmq", specifier = ">=27.1.0" },
|
{ name = "pyzmq", specifier = ">=27.1.0" },
|
||||||
{ name = "requests", specifier = ">=2.32.5" },
|
{ name = "requests", specifier = ">=2.32.5" },
|
||||||
{ name = "taguette", specifier = ">=1.5.1" },
|
{ name = "taguette", specifier = ">=1.5.1" },
|
||||||
@@ -1714,6 +1796,21 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-pptx"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "lxml" },
|
||||||
|
{ name = "pillow" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "xlsxwriter" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/52/a9/0c0db8d37b2b8a645666f7fd8accea4c6224e013c42b1d5c17c93590cd06/python_pptx-1.0.2.tar.gz", hash = "sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095", size = 10109297, upload-time = "2024-08-07T17:33:37.772Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/4f/00be2196329ebbff56ce564aa94efb0fbc828d00de250b1980de1a34ab49/python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba", size = 472788, upload-time = "2024-08-07T17:33:28.192Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytz"
|
name = "pytz"
|
||||||
version = "2025.2"
|
version = "2025.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user