Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ readme = "README.md"
requires-python = ">=3.9" # Support 3.9+
dependencies = [
"python-pptx>=1.0.2",
"Pillow>=9.0.0",
]
license = "MIT"
license-files = ["LICEN[CS]E*"]
Expand All @@ -22,3 +23,6 @@ test = [

[project.urls]
Homepage = "https://github.com/npogeant/deckflow"

[tool.setuptools.package-data]
deckflow = ["py.typed"]
7 changes: 5 additions & 2 deletions src/deckflow/adders/image_adder.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import logging
from typing import Any
from PIL import Image

logger = logging.getLogger(__name__)

class ImageAdder:
"""Adder for adding images to slides."""

Expand Down Expand Up @@ -49,6 +52,6 @@ def add_image_from_text(slide: Any, text_element: Any, image_path: str, keep_hei
slide.shapes.add_picture(image_path, left=left, top=top, width=width, height=height)
return True

except Exception as e:
print(f"Error adding image: {e}")
except Exception:
logger.exception("Error adding image")
return False
7 changes: 5 additions & 2 deletions src/deckflow/content/chart_extractor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import logging
from typing import Any, Dict, List

logger = logging.getLogger(__name__)

def extract_chart_data(chart: Any) -> Dict[str, Any]:
""" Extract chart data safely from a pptx chart object."""
data: Dict[str, Any] = {'categories': [], 'series': {}}
Expand All @@ -19,6 +22,6 @@ def extract_chart_data(chart: Any) -> Dict[str, Any]:
if not isinstance(data.get('series'), dict):
data['series'] = {}
data['series'][name] = values
except Exception as e:
print(f"Error extracting chart data: {e}")
except Exception:
logger.exception("Error extracting chart data")
return data
9 changes: 6 additions & 3 deletions src/deckflow/content/registry.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import logging
from typing import Any, List, Dict, Optional

logger = logging.getLogger(__name__)


class ContentRegistry:
"""Registry for managing slide content items."""
Expand All @@ -14,12 +17,12 @@ def get_item_by_name(self, items: List[Dict[str, Any]], name: str, item_type: st
from .duplicate import DuplicateManager

if DuplicateManager.has_duplicates(items, name):
print(f"{name} has duplicates!")
logger.warning("%s has duplicates!", name)
return None

item = next((item for item in items if item['name'] == name), None)
if item is None:
print(f"No {item_type} found with this name")
logger.warning("No %s found with this name: %s", item_type, name)
return None

return item
Expand Down
7 changes: 5 additions & 2 deletions src/deckflow/content/table_extractor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import logging
from typing import Any, List

logger = logging.getLogger(__name__)

def extract_table_data(table: Any) -> List[List[str]]:
"""
Extract table data safely from a pptx table object.
Expand All @@ -14,7 +17,7 @@ def extract_table_data(table: Any) -> List[List[str]]:
for cell in row.cells:
row_data.append(cell.text)
data.append(row_data)
except Exception as e:
print(f"Error extracting table data: {e}")
except Exception:
logger.exception("Error extracting table data")

return data
7 changes: 5 additions & 2 deletions src/deckflow/content/text_extractor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import logging
from typing import Any

logger = logging.getLogger(__name__)

def extract_text(shape: Any) -> str:
""" Extract text data safely from a pptx shape."""
try:
Expand All @@ -9,6 +12,6 @@ def extract_text(shape: Any) -> str:
parts = [p.text for p in shape.text_frame.paragraphs]
return "\n".join(parts)
return ""
except Exception as e:
print(f"Error extracting text: {e}")
except Exception:
logger.exception("Error extracting text")
return ""
14 changes: 12 additions & 2 deletions src/deckflow/elements/chart.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, List
from typing import Any, Dict, List

from copy import deepcopy
from ..content.chart_extractor import extract_chart_data
Expand Down Expand Up @@ -33,6 +33,16 @@ def update_series(self, series_name: str, new_values: List[float]):
def update_categories(self, new_categories: List[str]):
self.data['categories'] = list(new_categories)
return self._updater.apply(self.data)


def update(self, new_data: Dict[str, Any]):
"""Merge categories/series from new_data and apply in a single write."""
if "categories" in new_data:
self.data['categories'] = list(new_data["categories"])
if "series" in new_data:
series = self.data.setdefault('series', {})
for series_name, values in new_data["series"].items():
series[series_name] = [v if v is not None else 0 for v in values]
return self._updater.apply(self.data)

def save_changes(self):
return self._updater.apply(self.data)
13 changes: 8 additions & 5 deletions src/deckflow/elements/table.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
from typing import Any, List, Optional, Union

from ..content.table_extractor import extract_table_data
Expand All @@ -6,6 +7,8 @@
from ..updaters.table_updater import TableUpdater
from ..formatters.table_printer import TablePrinter

logger = logging.getLogger(__name__)

class DeckTable:
"""Class to manage an individual PowerPoint table."""

Expand Down Expand Up @@ -33,21 +36,21 @@ def get_cell(self, row: int, col: int) -> Optional[str]:
if 0 <= row < self.rows and 0 <= col < self.cols:
return self.data[row][col]
else:
print(f"Invalid cell indices ({row}, {col}). Table size: {self.rows}x{self.cols}")
logger.warning("Invalid cell indices (%d, %d). Table size: %dx%d", row, col, self.rows, self.cols)
return None

def get_row(self, row_index: int) -> Optional[List[str]]:
if 0 <= row_index < self.rows:
return self.data[row_index][:]
else:
print(f"Invalid row index {row_index}. Table has {self.rows} rows")
logger.warning("Invalid row index %d. Table has %d rows", row_index, self.rows)
return None

def get_column(self, col_index: int) -> Optional[List[str]]:
if 0 <= col_index < self.cols:
return [row[col_index] for row in self.data]
else:
print(f"Invalid column index {col_index}. Table has {self.cols} columns")
logger.warning("Invalid column index %d. Table has %d columns", col_index, self.cols)
return None

def update_cell(self, row: int, col: int, value: str, color_by_value: bool = False):
Expand Down
13 changes: 8 additions & 5 deletions src/deckflow/elements/text.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import logging
from typing import Any

from ..content.text_extractor import extract_text
from ..formatters.color_analyzer import ColorAnalyzer
from ..formatters.font_properties import copy_font_properties
from ..updaters.text_updater import TextUpdater

logger = logging.getLogger(__name__)

class DeckText:
"""Class to manage an individual PowerPoint text element."""

Expand Down Expand Up @@ -34,7 +37,7 @@ def update(self, new_text: str, color_by_value: bool = False):
self.color_by_value = color_by_value

if self.current_content == self.original_content:
print(f"No changes to save for shape {self.name}")
logger.debug("No changes to save for shape %s", self.name)
return True

try:
Expand All @@ -43,10 +46,10 @@ def update(self, new_text: str, color_by_value: bool = False):
elif hasattr(self.shape, "text"):
self.shape.text = self.current_content
else:
print(f"Text {self.name} has no text support")
logger.warning("Text %s has no text support", self.name)
return False
print(f"Text updated for {self.name}")
logger.debug("Text updated for %s", self.name)
return True
except Exception as e:
print(f"Error updating text {self.name}: {e}")
except Exception:
logger.exception("Error updating text %s", self.name)
return False
7 changes: 5 additions & 2 deletions src/deckflow/formatters/font_properties.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import logging
from typing import Any

logger = logging.getLogger(__name__)

def copy_font_properties(source_font: Any, target_font: Any):
"""Copy font properties while tolerating missing attributes."""

Expand All @@ -22,5 +25,5 @@ def copy_font_properties(source_font: Any, target_font: Any):
target_font.color.brightness = source_font.color.brightness
except Exception:
pass
except Exception as e:
print(f"Warning: Could not copy some font properties: {e}")
except Exception:
logger.warning("Could not copy some font properties", exc_info=True)
Empty file added src/deckflow/py.typed
Empty file.
41 changes: 14 additions & 27 deletions src/deckflow/slide.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,7 @@ def update_chart(self, name: str, new_data: Dict[str, Any]) -> None:
if not chart:
raise ValueError(f"Chart '{name}' not found")

if "categories" in new_data:
chart.update_categories(new_data["categories"])
if "series" in new_data:
for series_name, values in new_data["series"].items():
chart.update_series(series_name, values)
chart.save_changes()
chart.update(new_data)

def update_table(self, name: str, new_data: List, by_rows: bool = True, by_columns: bool = False, color_by_value: bool = False) -> None:
"""Update a table with new data either by rows or by columns."""
Expand Down Expand Up @@ -107,32 +102,24 @@ def add_image_from_text(self, text_name: str, image_path: str, keep_height: bool
if not success:
raise RuntimeError(f"Failed to add image at text element '{text_name}'")

def remove_text(self, name: str) -> bool:
"""Remove a text element by its name."""
text_obj = self.get_text(name)
if not text_obj:
raise ValueError(f"Text element '{name}' not found")
removed = ElementRemover.remove_shape(text_obj.shape)
def _remove_element(self, name: str, getter, label: str) -> bool:
"""Look up an element by name via getter and remove its shape from the slide."""
element = getter(name)
if not element:
raise ValueError(f"{label} '{name}' not found")
removed = ElementRemover.remove_shape(element.shape)
if not removed:
raise RuntimeError(f"Failed to remove text element '{name}'")
raise RuntimeError(f"Failed to remove {label.lower()} '{name}'")
return True

def remove_text(self, name: str) -> bool:
"""Remove a text element by its name."""
return self._remove_element(name, self.get_text, "Text element")

def remove_chart(self, name: str) -> bool:
"""Remove a chart element by its name."""
chart_obj = self.get_chart(name)
if not chart_obj:
raise ValueError(f"Chart '{name}' not found")
removed = ElementRemover.remove_shape(chart_obj.shape)
if not removed:
raise RuntimeError(f"Failed to remove chart '{name}'")
return True
return self._remove_element(name, self.get_chart, "Chart")

def remove_table(self, name: str) -> bool:
"""Remove a table element by its name."""
table_obj = self.get_table(name)
if not table_obj:
raise ValueError(f"Table '{name}' not found")
removed = ElementRemover.remove_shape(table_obj.shape)
if not removed:
raise RuntimeError(f"Failed to remove table '{name}'")
return True
return self._remove_element(name, self.get_table, "Table")
7 changes: 5 additions & 2 deletions src/deckflow/updaters/chart_updater.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import logging
from typing import Any

logger = logging.getLogger(__name__)

class ChartUpdater:
"""Updater for chart elements, preserving formatting/colors."""

Expand All @@ -19,6 +22,6 @@ def apply(self, data: dict) -> bool:
chart_data.add_series(name, clean_values)
self.chart.replace_data(chart_data)
return True
except Exception as e:
print(f"Error applying chart data: {e}")
except Exception:
logger.exception("Error applying chart data")
return False
29 changes: 16 additions & 13 deletions src/deckflow/updaters/table_updater.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import logging
from typing import Any, List
from pptx.util import Pt
from pptx.dml.color import RGBColor

logger = logging.getLogger(__name__)

class TableUpdater:
"""Updater for table elements."""

Expand All @@ -18,34 +21,34 @@ def __init__(self, table: Any, color_analyzer: Any, formatter: Any):
def update_cell(self, row: int, col: int, value: Any) -> bool:
if 0 <= row < self.rows and 0 <= col < self.cols:
self.table.data[row][col] = "" if value is None else str(value)
print(f"Cell ({row},{col}) -> '{self.table.data[row][col]}'")
logger.debug("Cell (%d,%d) -> '%s'", row, col, self.table.data[row][col])
return True
print(f"Invalid cell indices ({row},{col}) for table {self.rows}x{self.cols}")
logger.warning("Invalid cell indices (%d,%d) for table %dx%d", row, col, self.rows, self.cols)
return False

def update_row(self, row_index: int, values: List[Any]) -> bool:
if not (0 <= row_index < self.rows):
print(f"Invalid row index {row_index}")
logger.warning("Invalid row index %d", row_index)
return False
for c in range(min(self.cols, len(values))):
self.table.data[row_index][c] = "" if values[c] is None else str(values[c])
print(f"Row {row_index} updated")
logger.debug("Row %d updated", row_index)
return True

def update_column(self, col_index: int, values: List[Any]) -> bool:
if not (0 <= col_index < self.cols):
print(f"Invalid column index {col_index}")
logger.warning("Invalid column index %d", col_index)
return False
for r in range(min(self.rows, len(values))):
self.table.data[r][col_index] = "" if values[r] is None else str(values[r])
print(f"Column {col_index} updated")
logger.debug("Column %d updated", col_index)
return True

def save_changes(self, color_by_value: bool = False):
try:
# Check if there are changes to save
if self.table.data == self.table.original_data:
print(f"No changes to save for table {self.table.name}")
logger.debug("No changes to save for table %s", self.table.name)
return True

# Apply changes to the actual PowerPoint table object
Expand Down Expand Up @@ -79,14 +82,14 @@ def save_changes(self, color_by_value: bool = False):

else:
cell.text = new_text
except Exception as e:
print(f"Warning: Could not update cell ({row_idx},{col_idx}): {e}")
except Exception:
logger.warning("Could not update cell (%d,%d)", row_idx, col_idx, exc_info=True)

# Update the original state
self.table.original_data = [row[:] for row in self.table.data]
print(f"Table {self.table.name} changes applied")
logger.debug("Table %s changes applied", self.table.name)
return True

except Exception as e:
print(f"Error saving table changes: {e}")
except Exception:
logger.exception("Error saving table changes")
return False
Loading