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
67 changes: 67 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: release

on:
push:
tags:
- 'v*'

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Validate the tag
run: |
tag="${GITHUB_REF_NAME}"
echo "Releasing tag: $tag"
if ! echo "$tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$'; then
echo "::error::Tag '$tag' is not vX.Y.Z (optionally -prerelease). Not releasing."
exit 1
fi
if ! git branch -r --contains "$tag" | grep -q 'origin/master'; then
echo "::error::Tag '$tag' is not on master. Release only from master."
exit 1
fi
prev=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | grep -v "^${tag}$" | head -1)
if [ -n "$prev" ]; then
newest=$(printf '%s\n%s\n' "$prev" "$tag" | sort -V | tail -1)
if [ "$newest" != "$tag" ]; then
echo "::error::Tag '$tag' is not newer than the last release '$prev'. Tags must go in order."
exit 1
fi
echo "Previous release: $prev"
fi

- uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install
run: |
python -m pip install --upgrade pip build twine
pip install -e ".[dev]"

- name: Test
run: python -m pytest tests/test_request_options_unit.py tests/test_files_api_unit.py tests/test_webhooks.py

- name: Build
run: |
rm -rf dist build ./*.egg-info
python -m build
python -m twine check dist/*

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}

- name: Create GitHub release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release create "$GITHUB_REF_NAME" dist/* --title "$GITHUB_REF_NAME" --generate-notes
30 changes: 30 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: test

on:
pull_request:
push:
branches: [master]

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ['3.8', '3.12']
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"

- name: Test
run: python -m pytest tests/test_request_options_unit.py tests/test_files_api_unit.py tests/test_webhooks.py
7 changes: 7 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# mpxpy changelog

## September 22, 2026

- Automate PyPI releases. The package version is now derived from the git tag via setuptools-scm, so there is no version to bump by hand. Pushing a `vX.Y.Z` tag from master runs `.github/workflows/release.yml`, which builds the sdist and wheel, publishes to PyPI, and creates a published (non-draft) GitHub release. The tag must be valid semver, sit on master, and be newer than the last release, or the run fails before anything is published.
- Add `convert_to_xlsx` to `pdf_new` and `conversion_new`, plus `to_xlsx_file`/`to_xlsx_bytes` on `Pdf` and `Conversion`, giving xlsx the same convenience flag as the other formats
- Add `include_hyperlinks` to `pdf_new` to extract PDF link annotations into per-line `links` data
- Add `convert_to_mmd_overlay_pdf` to `pdf_new` for the new `mmd.overlay.pdf` format (your submitted PDF returned with an invisible, screen-reader-ready text layer, PDF/UA tagged by default; PDF input only), plus `to_mmd_overlay_pdf_file`/`to_mmd_overlay_pdf_bytes` on `Pdf` and `File`. Format options go through `conversion_options["mmd.overlay.pdf"]`

## August 18, 2026

- Add webhooks support
Expand Down
31 changes: 30 additions & 1 deletion mpxpy/conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class Conversion:
convert_to_mmd_zip: Optional boolean to automatically convert your result to mmd.zip
convert_to_pptx: Optional boolean to automatically convert your result to pptx
convert_to_html_zip: Optional boolean to automatically convert your result to html.zip
convert_to_xlsx: Optional boolean to automatically convert your result to xlsx
"""
def __init__(
self,
Expand All @@ -42,6 +43,7 @@ def __init__(
convert_to_mmd_zip: Optional[bool] = False,
convert_to_pptx: Optional[bool] = False,
convert_to_html_zip: Optional[bool] = False,
convert_to_xlsx: Optional[bool] = False,
request_options: Optional[Dict[str, Any]] = None,
):
"""Initialize a Conversion instance.
Expand All @@ -59,6 +61,7 @@ def __init__(
convert_to_mmd_zip: Optional boolean to automatically convert your result to mmd.zip
convert_to_pptx: Optional boolean to automatically convert your result to pptx
convert_to_html_zip: Optional boolean to automatically convert your result to html.zip
convert_to_xlsx: Optional boolean to automatically convert your result to xlsx

Raises:
ValidationError: If auth is not provided or conversion_id is empty.
Expand All @@ -81,6 +84,7 @@ def __init__(
self.convert_to_mmd_zip = convert_to_mmd_zip
self.convert_to_pptx = convert_to_pptx
self.convert_to_html_zip = convert_to_html_zip
self.convert_to_xlsx = convert_to_xlsx
self.request_options = request_options or {}

def wait_until_complete(self, timeout: int=60):
Expand Down Expand Up @@ -476,4 +480,29 @@ def to_html_zip_bytes(self) -> bytes:
Raises:
ConversionIncompleteError: If the conversion is not complete
"""
return self.bytes_result(conversion_format='html.zip')
return self.bytes_result(conversion_format='html.zip')

def to_xlsx_file(self, path: str) -> str:
"""Save the processed conversion result to an XLSX file.

Args:
path: The local file path where the XLSX output will be saved

Returns:
output_path: The path of the saved XLSX file

Raises:
ConversionIncompleteError: If the conversion is not complete
"""
return self.save_file(path=path, conversion_format='xlsx')

def to_xlsx_bytes(self) -> bytes:
"""Get the processed conversion result in XLSX format as bytes.

Returns:
bytes: The binary content of the XLSX result

Raises:
ConversionIncompleteError: If the conversion is not complete
"""
return self.bytes_result(conversion_format='xlsx')
8 changes: 8 additions & 0 deletions mpxpy/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,10 @@ def to_html_zip_bytes(self) -> bytes:
"""Get the processed file result as html.zip bytes."""
return self.bytes_result(conversion_format='html.zip')

def to_mmd_overlay_pdf_bytes(self) -> bytes:
"""Get the accessible overlay PDF (your submitted PDF with an invisible text layer) as bytes."""
return self.bytes_result(conversion_format='mmd.overlay.pdf')

def to_jpg_bytes(self) -> bytes:
"""Get the processed file result as JPG bytes."""
return self.bytes_result(conversion_format='jpg')
Expand Down Expand Up @@ -444,6 +448,10 @@ def to_xlsx_file(self, path: str) -> str:
"""Save the processed file result to an XLSX file at a local path."""
return self.save_file(path=path, conversion_format='xlsx')

def to_mmd_overlay_pdf_file(self, path: str) -> str:
"""Save the accessible overlay PDF (your submitted PDF with an invisible text layer) to a local path."""
return self.save_file(path=path, conversion_format='mmd.overlay.pdf')

def to_pptx_file(self, path: str) -> str:
"""Save the processed file result to a PPTX file at a local path."""
return self.save_file(path=path, conversion_format='pptx')
Expand Down
24 changes: 23 additions & 1 deletion mpxpy/mathpix_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@ def _reject_reserved_extra_options(
'remove_section_numbering', 'preserve_section_numbering', 'enable_tables_fallback',
'fullwidth_punctuation', 'conversion_formats', 'file_batch_id', 'callback_url',
'callback_headers', 'callback_events', 'disable_itemize',
'disable_lstlisting', 'include_page_info', 'include_page_breaks', 'conversion_options',
'disable_lstlisting', 'include_page_info', 'include_page_breaks', 'include_hyperlinks',
'conversion_options',
}

_CONVERSION_REQUEST_OPTION_KEYS: Set[str] = {'mmd', 'formats', 'conversion_options'}
Expand Down Expand Up @@ -450,6 +451,8 @@ def pdf_new(
convert_to_mmd_zip: Optional[bool] = False,
convert_to_pptx: Optional[bool] = False,
convert_to_html_zip: Optional[bool] = False,
convert_to_xlsx: Optional[bool] = False,
convert_to_mmd_overlay_pdf: Optional[bool] = False,
improve_mathpix: Optional[bool] = True,
file_batch_id: Optional[str] = None,
callback_url: Optional[str] = None,
Expand All @@ -459,6 +462,7 @@ def pdf_new(
disable_lstlisting: Optional[bool] = None,
include_page_info: Optional[bool] = None,
include_page_breaks: Optional[bool] = None,
include_hyperlinks: Optional[bool] = None,
conversion_options: Optional[Dict[str, Any]] = None,
extra_options: Optional[Dict[str, Any]] = None,
) -> Pdf:
Expand Down Expand Up @@ -496,6 +500,8 @@ def pdf_new(
convert_to_mmd_zip: Optional boolean to automatically convert your result to mmd.zip
convert_to_pptx: Optional boolean to automatically convert your result to pptx
convert_to_html_zip: Optional boolean to automatically convert your result to html.zip
convert_to_xlsx: Optional boolean to automatically convert your result to xlsx
convert_to_mmd_overlay_pdf: Optional boolean to return your submitted PDF with an invisible, screen-reader-ready text layer added (PDF/UA tagged by default; PDF input only). Configure via conversion_options["mmd.overlay.pdf"]
improve_mathpix: Optional boolean to enable Mathpix to retain user output. Default is true
file_batch_id: Optional batch ID to associate this file with.
callback_url: Optional HTTPS URL to receive this request's webhook deliveries. A request without one is not notified at all
Expand All @@ -505,6 +511,7 @@ def pdf_new(
disable_lstlisting: Optional boolean to disable the lstlisting environment for code blocks
include_page_info: Optional boolean to include page info in the output
include_page_breaks: Optional boolean to include page break markers in the output
include_hyperlinks: Optional boolean to extract PDF link annotations and attach them to line data (adds a per-line "links" field)
conversion_options: Optional dict of per-format conversion options (see https://docs.mathpix.com/reference/shared-types#conversion-options)
extra_options: Optional dict of unmodeled request options. Modeled request fields are rejected; values are validated server-side

Expand Down Expand Up @@ -588,6 +595,8 @@ def pdf_new(
options["include_page_info"] = include_page_info
if include_page_breaks is not None:
options["include_page_breaks"] = include_page_breaks
if include_hyperlinks is not None:
options["include_hyperlinks"] = include_hyperlinks
if conversion_options is not None:
options["conversion_options"] = conversion_options
if file_batch_id:
Expand All @@ -613,6 +622,10 @@ def pdf_new(
options["conversion_formats"]['mmd.zip'] = True
if convert_to_html_zip:
options["conversion_formats"]['html.zip'] = True
if convert_to_xlsx:
options["conversion_formats"]['xlsx'] = True
if convert_to_mmd_overlay_pdf:
options["conversion_formats"]['mmd.overlay.pdf'] = True
if extra_options:
options.update(extra_options)
data = {
Expand Down Expand Up @@ -647,6 +660,8 @@ def pdf_new(
convert_to_mmd_zip=convert_to_mmd_zip,
convert_to_pptx=convert_to_pptx,
convert_to_html_zip=convert_to_html_zip,
convert_to_xlsx=convert_to_xlsx,
convert_to_mmd_overlay_pdf=convert_to_mmd_overlay_pdf,
improve_mathpix=improve_mathpix,
file_batch_id=file_batch_id,
callback_url=callback_url,
Expand Down Expand Up @@ -682,6 +697,8 @@ def pdf_new(
convert_to_mmd_zip=convert_to_mmd_zip,
convert_to_pptx=convert_to_pptx,
convert_to_html_zip=convert_to_html_zip,
convert_to_xlsx=convert_to_xlsx,
convert_to_mmd_overlay_pdf=convert_to_mmd_overlay_pdf,
improve_mathpix=improve_mathpix,
file_batch_id=file_batch_id,
callback_url=callback_url,
Expand Down Expand Up @@ -765,6 +782,7 @@ def conversion_new(
convert_to_mmd_zip: Optional[bool] = False,
convert_to_pptx: Optional[bool] = False,
convert_to_html_zip: Optional[bool] = False,
convert_to_xlsx: Optional[bool] = False,
conversion_options: Optional[Dict[str, Any]] = None,
extra_options: Optional[Dict[str, Any]] = None,
):
Expand All @@ -782,6 +800,7 @@ def conversion_new(
convert_to_mmd_zip: Optional boolean to automatically convert your result to mmd.zip
convert_to_pptx: Optional boolean to automatically convert your result to pptx
convert_to_html_zip: Optional boolean to automatically convert your result to html.zip
convert_to_xlsx: Optional boolean to automatically convert your result to xlsx
conversion_options: Optional dict of per-format conversion options (see https://docs.mathpix.com/reference/shared-types#conversion-options)
extra_options: Optional dict of unmodeled request options. Modeled request fields are rejected; values are validated server-side

Expand Down Expand Up @@ -818,6 +837,8 @@ def conversion_new(
options["formats"]['mmd.zip'] = True
if convert_to_html_zip:
options["formats"]['html.zip'] = True
if convert_to_xlsx:
options["formats"]['xlsx'] = True
if conversion_options is not None:
options["conversion_options"] = conversion_options
if extra_options:
Expand Down Expand Up @@ -847,6 +868,7 @@ def conversion_new(
convert_to_mmd_zip=convert_to_mmd_zip,
convert_to_pptx=convert_to_pptx,
convert_to_html_zip=convert_to_html_zip,
convert_to_xlsx=convert_to_xlsx,
request_options=self.request_options,
)
except Exception as e:
Expand Down
Loading
Loading