Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
57d9e58
feat(docs): add Sphinx configuration and index for API documentation
marcos-sinch Jun 8, 2026
ffbbf89
feat(docs): add docs requirements to requirements-dev
marcos-sinch Jun 8, 2026
5f4809f
feat(docs): remove pin for sphinx dev dependency
marcos-sinch Jun 8, 2026
563102f
feat(docs): add push documentation workflow
marcos-sinch Jun 9, 2026
dafdd17
feat(docs): add push documentation workflow
marcos-sinch Jun 9, 2026
5070abc
feat(docs): add version validation
marcos-sinch Jun 11, 2026
a124798
feat(docs): enhance documentation generation and styling
marcos-sinch Jun 12, 2026
f10c546
feat(docs): update documentation templates and add new init file
marcos-sinch Jun 12, 2026
0c20c02
feat(docs): remove documentation generation step from CI workflow
marcos-sinch Jun 12, 2026
5b24b47
fix(docs): update copyright year in project information
marcos-sinch Jun 12, 2026
d5d7bab
feat(docs): add documentation generation step to CI workflow
marcos-sinch Jun 15, 2026
409f89f
feat(docs): add missing __init__.py file for message models in conver…
marcos-sinch Jun 15, 2026
38fd8dc
feat(docs): add __init__.py file for sms sinch_events module
marcos-sinch Jun 15, 2026
4e6a430
feat(docs): enhance documentation generation by removing internal imp…
marcos-sinch Jun 15, 2026
3e114ad
feat(docs): update documentation generation to include additional mod…
marcos-sinch Jun 17, 2026
9f9cda7
feat(docs): update documentation generation to include sinch_events m…
marcos-sinch Jun 17, 2026
24a7ab1
feat(docs): enhance Paginator class documentation
marcos-sinch Jun 19, 2026
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 .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ jobs:
run: |
pip install python-dotenv
python scripts/check_snippet_coverage.py

- name: Generate Documentation
run: |
make docs

- name: Lint and format check with Ruff
run: |
Expand Down
82 changes: 82 additions & 0 deletions .github/workflows/documentation-upload.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Generate and upload documentation

on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: 'Version to use for the documentation package in semver format (e.g. 1.2.3)'
required: true

jobs:
upload-documentation:
runs-on: ubuntu-latest
env:
SDK_NAME: sinch-sdk-python

steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Resolve Version
id: version
run: |
if [ "${{ github.event_name }}" = "release" ]; then
VERSION="${{ github.event.release.tag_name }}"
else
VERSION="${{ inputs.version }}"
fi
# Strip leading 'v' if present (e.g. v1.2.3 → 1.2.3)
VERSION="${VERSION#v}"
echo "value=${VERSION}" >> "$GITHUB_OUTPUT"

- name: Validate Version Format
run: |
VERSION="${{ steps.version.outputs.value }}"
SEMVER_REGEX='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((alpha|beta|preview)(\.[0-9]+)?))?$'
if [[ ! "$VERSION" =~ $SEMVER_REGEX ]]; then
echo "::error::Invalid version format: '$VERSION'. Expected semver (e.g. 1.2.3, 1.2.3-alpha, 1.2.3-beta.1, 1.2.3-preview)"
exit 1
fi
echo "Version '$VERSION' is valid"

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

- name: Install dev dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt

- name: Generate Documentation
run: |
make docs
Comment thread
JPPortier marked this conversation as resolved.

- name: Package Documentation
run: |
cd docs/build/html
zip -r "../../../${{ env.SDK_NAME }}-${{ steps.version.outputs.value }}.zip" .

- name: Upload to GitLab Registry
run: |
echo "Uploading documentation package to GitLab Registry..."
VERSION="${{ steps.version.outputs.value }}"
curl --fail --show-error --location --header "PRIVATE-TOKEN: ${{ secrets.GITLAB_REGISTRY_UPLOAD_DOC_TOKEN }}" \
--upload-file "./${{ env.SDK_NAME }}-${VERSION}.zip" \
"https://gitlab.com/api/v4/projects/63164411/packages/generic/${{ env.SDK_NAME }}/${VERSION}/${{ env.SDK_NAME }}-${VERSION}.zip"
echo "Documentation package for version ${VERSION} uploaded to GitLab Registry"

- name: Trigger Downstream GitLab Pipeline
run: |
echo "Triggering downstream GitLab pipeline to notify about new documentation package version..."
VERSION="${{ steps.version.outputs.value }}"
curl --fail --show-error --location --request POST \
--form "token=${{ secrets.GITLAB_NOTIFY_REGISTRY_UPLOADED_DOC_TOKEN }}" \
--form "ref=main" \
--form "variables[UPSTREAM_PACKAGE_NAME]=${{ env.SDK_NAME }}" \
--form "variables[UPSTREAM_PACKAGE_VERSION]=${VERSION}" \
"https://gitlab.com/api/v4/projects/63164411/trigger/pipeline"
echo "Documentation repo notified about new package version ${VERSION}"
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ instance/
.scrapy

# Sphinx documentation
docs/_build/
docs/build/
docs/api/


# PyBuilder
.pybuilder/
Expand Down
21 changes: 21 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.PHONY: docs

docs:
rm -rf docs/build
rm -rf docs/api
sphinx-apidoc --force --separate --no-toc --maxdepth 2 \

@JPPortier JPPortier Jun 19, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be, for a next iteration, we can imagine having another pattern for directories and rules about exclusion (e.g.: nearly duplicate of onto "sinch/*/internal" "sinch/*/internal/*"
If we can have a simple **internal** 🙏

--templatedir docs/_templates/apidoc -o docs/api sinch \
"sinch/core/models/base_model.py" \
"sinch/core/models/utils.py" \
"sinch/core/deserializers.py" \
"sinch/core/endpoint.py" \
"sinch/core/enums.py" \
"sinch/core/types.py" \
"sinch/domains/sms/enums.py" \
"sinch/*/internal" "sinch/*/internal/*" \
"sinch/*/api/v1/base" "sinch/*/api/v1/base/*" \
"sinch/*/api/v1/utils" "sinch/*/api/v1/utils/*" \
"sinch/domains/authentication/endpoints" "sinch/domains/authentication/endpoints/*" \
"sinch/domains/authentication/sinch_events" "sinch/domains/authentication/sinch_events/*" \
"sinch/domains/numbers/models/v1/utils" "sinch/domains/numbers/models/v1/utils/*"
sphinx-build -b html docs docs/build/html
21 changes: 21 additions & 0 deletions docs/_static/custom.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* ---------------------------------------------------------------------------
Multi-line signatures (one parameter per line).

When a signature is wrapped, Sphinx renders each parameter as a <dd> inside
a nested <dl>. The Read the Docs theme styles every <dl>/<dd> with borders,
background tint and vertical margins, which leak into the signature and draw
ugly "lines" between parameters. Flatten that nested list so the parameters
read as a clean indented column.
--------------------------------------------------------------------------- */
.rst-content .sig dl,
.rst-content .sig dd {
margin: 0;
padding: 0;
border: none;
background: none;
}

/* Keep each parameter indented under the opening parenthesis. */
.rst-content .sig dd {
margin-left: 2em;
}
8 changes: 8 additions & 0 deletions docs/_templates/apidoc/module.rst.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{%- if show_headings %}
{{- [basename, "module"] | join(" ") | e | heading }}

{% endif -%}
.. automodule:: {{ qualname }}
{%- for option in automodule_options %}
:{{ option }}:
{%- endfor %}
39 changes: 39 additions & 0 deletions docs/_templates/apidoc/package.rst.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{%- macro automodule(modname, options) -%}
.. automodule:: {{ modname }}
{%- for option in options %}
:{{ option }}:
{%- endfor %}
{%- endmacro %}

{%- macro toctree(docnames) -%}
.. toctree::
:maxdepth: {{ maxdepth }}
{% for docname in docnames %}
{{ docname }}
{%- endfor %}
{%- endmacro %}

{{- [pkgname, "package"] | join(" ") | e | heading }}

{%- if is_namespace %}
.. py:module:: {{ pkgname }}
{% endif %}

{%- if subpackages %}
{{ toctree(subpackages) }}
{% endif %}

{%- if submodules %}
{% if separatemodules %}
{{ toctree(submodules) }}
{% else %}
{% for submodule in submodules %}
{{ [submodule.split(".")[-1], "module"] | join(" ") | e | heading(2) }}
{{ automodule(submodule, automodule_options) }}
{% endfor %}
{%- endif %}
{%- endif %}

{%- if not is_namespace %}
{{ automodule(pkgname, automodule_options) }}
{% endif %}
62 changes: 62 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import os
import sys

# Allow autodoc to import the sinch package from the project root
sys.path.insert(0, os.path.abspath(".."))
from sinch import __version__

# -- Project information -------------------------------------------------------

project = "Sinch Python SDK"
copyright = "2026, Sinch Developer Experience Team"
author = "Sinch Developer Experience Team"
release = __version__

# -- General configuration -----------------------------------------------------

extensions = [
# Pulls docstrings from Python source into the generated .rst files
"sphinx.ext.autodoc",
# Adds [source] links that open the highlighted source file
"sphinx.ext.viewcode",
]

# The .rst files under api/ are generated by the `sphinx-apidoc` CLI invoked
# from the Makefile (`make docs`), using the custom templates in
# _templates/apidoc/ to strip the "package" suffix and the
# "Subpackages"/"Submodules"/"Module contents" headings. The
# sphinx.ext.apidoc extension is intentionally NOT used because it ignores
# the template directory.

# -- sphinx.ext.autodoc --------------------------------------------------------

autodoc_default_options = {
# Document all public members (methods, attributes, nested classes)
"members": True,
'undoc-members': True,
# Show the class inheritance chain
"show-inheritance": True,
# Preserve the order in which members appear in the source file
"member-order": "bysource",
}

# Render type hints as part of the parameter/return descriptions, not the signature
autodoc_typehints = "both"
python_maximum_signature_line_length = 88

# -- HTML output ---------------------------------------------------------------

html_theme = "sphinx_rtd_theme"

html_theme_options = {
# Maximum depth of the navigation sidebar (-1 = unlimited)
"navigation_depth": -1,
# Keep all navigation entries expanded by default
"collapse_navigation": False,
}

# Directory with extra static files (custom CSS, etc.), relative to this conf.py
html_static_path = ["_static"]

# Extra stylesheets loaded after the theme's own CSS
html_css_files = ["custom.css"]
8 changes: 8 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Sinch Python SDK
================

.. toctree::
:maxdepth: 3
:caption: API Reference

api/sinch
7 changes: 6 additions & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,9 @@ ruff
requests

# Data Validation
pydantic >= 2.0.0
pydantic >= 2.0.0

# Documentation
# Sphinx 7.1 introduced python_maximum_signature_line_length and 7.x is also the last series supporting Python 3.9.
sphinx >= 7.1
sphinx-rtd-theme >= 2.0
35 changes: 31 additions & 4 deletions sinch/core/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@

class Paginator(ABC, Generic[BM]):
"""
Pagination response object.
Public interface for paginated list responses.

Use :meth:`content`, :meth:`next_page` and :meth:`iterator` to traverse the
result set without dealing with the underlying pagination scheme.
"""
def __init__(self, sinch, endpoint, result: BM):
self._sinch = sinch
Expand All @@ -19,16 +22,34 @@ def __repr__(self):

# TODO: Make content() method abstract in Parent class as we implement in the other domains:
# - Refactor pydantic models in other domains to have a content property.
def content(self):
def content(self) -> list[BM]:
"""
Return the items contained in the current page.

:returns: The list of items in the current page.
:rtype: list[BM]
"""
pass

# TODO: Make iterator() method abstract in Parent class as we implement in the other domains:
# - Refactor pydantic models in other domains to have a content property.
def iterator(self) -> Iterator[BM]:
"""
Iterate over individual items across all pages, fetching each page on demand.

:returns: An iterator over every item in the result set.
:rtype: Iterator[BM]
"""
pass

@abstractmethod
def next_page(self):
"""
Advance to the next page of results.

:returns: This paginator positioned on the next page, or ``None`` if there is no further page.
:rtype: Paginator | None
"""
pass

@abstractmethod
Expand All @@ -42,7 +63,10 @@ def _initialize(cls, sinch, endpoint):


class SMSPaginator(Paginator[BM]):
"""Base paginator for integer-based pagination with explicit page navigation and metadata."""
"""Base paginator for integer-based pagination with explicit page navigation and metadata.

:meta private:
"""

def __init__(self, sinch, endpoint, result=None):
super().__init__(sinch, endpoint, result or sinch.configuration.transport.request(endpoint))
Expand Down Expand Up @@ -107,7 +131,10 @@ def _initialize(cls, sinch, endpoint):


class TokenBasedPaginator(Paginator[BM]):
"""Base paginator for token-based pagination with explicit page navigation and metadata."""
"""Base paginator for token-based pagination with explicit page navigation and metadata.

:meta private:
"""

def __init__(self, sinch, endpoint, result=None):
super().__init__(sinch, endpoint, result or sinch.configuration.transport.request(endpoint))
Expand Down
Empty file.
Empty file.
Empty file.
Empty file.
Loading