diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0534969d..ed8f35ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: | diff --git a/.github/workflows/documentation-upload.yaml b/.github/workflows/documentation-upload.yaml new file mode 100644 index 00000000..05f3ce93 --- /dev/null +++ b/.github/workflows/documentation-upload.yaml @@ -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 + + - 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}" diff --git a/.gitignore b/.gitignore index 3246a258..c11d9513 100644 --- a/.gitignore +++ b/.gitignore @@ -73,7 +73,9 @@ instance/ .scrapy # Sphinx documentation -docs/_build/ +docs/build/ +docs/api/ + # PyBuilder .pybuilder/ diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..9d123075 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +.PHONY: docs + +docs: + rm -rf docs/build + rm -rf docs/api + sphinx-apidoc --force --separate --no-toc --maxdepth 2 \ + --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 diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 00000000..91609f05 --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,21 @@ +/* --------------------------------------------------------------------------- + Multi-line signatures (one parameter per line). + + When a signature is wrapped, Sphinx renders each parameter as a
inside + a nested
. The Read the Docs theme styles every
/
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; +} diff --git a/docs/_templates/apidoc/module.rst.jinja b/docs/_templates/apidoc/module.rst.jinja new file mode 100644 index 00000000..c694aaee --- /dev/null +++ b/docs/_templates/apidoc/module.rst.jinja @@ -0,0 +1,8 @@ +{%- if show_headings %} +{{- [basename, "module"] | join(" ") | e | heading }} + +{% endif -%} +.. automodule:: {{ qualname }} +{%- for option in automodule_options %} + :{{ option }}: +{%- endfor %} diff --git a/docs/_templates/apidoc/package.rst.jinja b/docs/_templates/apidoc/package.rst.jinja new file mode 100644 index 00000000..4e34d501 --- /dev/null +++ b/docs/_templates/apidoc/package.rst.jinja @@ -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 %} diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..5d8b159e --- /dev/null +++ b/docs/conf.py @@ -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"] \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..9cb6c3fe --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,8 @@ +Sinch Python SDK +================ + +.. toctree:: + :maxdepth: 3 + :caption: API Reference + + api/sinch diff --git a/requirements-dev.txt b/requirements-dev.txt index 384715db..5dd21212 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,4 +11,9 @@ ruff requests # Data Validation -pydantic >= 2.0.0 \ No newline at end of file +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 diff --git a/sinch/domains/sms/enums.py b/sinch/domains/conversation/models/v1/messages/__init__.py similarity index 100% rename from sinch/domains/sms/enums.py rename to sinch/domains/conversation/models/v1/messages/__init__.py diff --git a/sinch/domains/conversation/models/v1/sinch_events/events/__init__.py b/sinch/domains/conversation/models/v1/sinch_events/events/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sinch/domains/number_lookup/api/__init__.py b/sinch/domains/number_lookup/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sinch/domains/number_lookup/models/v1/__init__.py b/sinch/domains/number_lookup/models/v1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sinch/domains/sms/sinch_events/__init__.py b/sinch/domains/sms/sinch_events/__init__.py new file mode 100644 index 00000000..e69de29b