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
11 changes: 9 additions & 2 deletions .github/workflows/sdk_generation.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
name: Generate
run-name: ${{ github.event_name == 'repository_dispatch' && format('Generate Python SDK for {0}', github.event.client_payload.deployment_tag) || 'Generate Python SDK' }}
permissions:
checks: write
contents: write
Expand All @@ -15,17 +16,23 @@ permissions:
set_version:
description: optionally set a specific SDK version
type: string
repository_dispatch:
types:
- production-openapi-published
pull_request:
types:
- labeled
- unlabeled
concurrency:
group: sdk-generation
cancel-in-progress: false
jobs:
generate:
uses: speakeasy-api/sdk-generation-action/.github/workflows/workflow-executor.yaml@v15
with:
force: ${{ github.event.inputs.force }}
force: ${{ github.event_name == 'repository_dispatch' || inputs.force }}
mode: pr
set_version: ${{ github.event.inputs.set_version }}
set_version: ${{ inputs.set_version }}
secrets:
github_access_token: ${{ secrets.GH_PAT }}
pypi_token: ${{ secrets.PYPI_TOKEN }}
Expand Down
67 changes: 67 additions & 0 deletions .github/workflows/sdk_publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,70 @@ jobs:
github_access_token: ${{ secrets.GH_PAT }}
pypi_token: ${{ secrets.PYPI_TOKEN }}
speakeasy_api_key: ${{ secrets.SPEAKEASY_API_KEY }}

sync-python-sdk-docs:
needs: [publish]
runs-on: ubuntu-latest
steps:
- name: Checkout Python SDK
uses: actions/checkout@v4
with:
path: sdk

- name: Checkout TextQL docs
uses: actions/checkout@v4
with:
repository: TextQLLabs/demo2
token: ${{ secrets.GH_PAT }}
path: demo2

- uses: actions/setup-python@v5
with:
python-version: "3.x"

- name: Snapshot combined OpenAPI with SDK samples
run: |
curl --fail --silent --show-error --location \
--header 'Cache-Control: no-cache' \
--retry 10 --retry-all-errors --retry-delay 10 \
https://spec.speakeasy.com/textql/home/textql-api-with-code-samples \
--output demo2/docs/public/api-reference/speakeasy/textql-api-with-code-samples.yaml

- name: Render Mintlify API reference
run: |
python3 -m pip install --disable-pip-version-check PyYAML==6.0.3
python3 demo2/scripts/render_sdk_api_reference.py

- name: Render Mintlify release log
run: |
python3 sdk/scripts/render_mintlify_releases.py \
sdk/RELEASES.md \
demo2/docs/public/api-reference/sdk/changelog.mdx

- name: Create or update docs pull request
id: docs-pr
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GH_PAT }}
path: demo2
branch: automation/python-sdk-docs
delete-branch: true
commit-message: "docs: sync Python SDK reference"
title: "docs: sync Python SDK reference"
body: |
Automated after a successful Python SDK publish.

- Snapshots the combined Speakeasy OpenAPI document and SDK code samples.
- Regenerates stable Mintlify API-reference wrappers.
- Updates the Python SDK release log from `TextQLLabs/textql-python-v3/RELEASES.md`.

- name: Auto-merge docs pull request after checks pass
if: steps.docs-pr.outputs.pull-request-number != ''
env:
GH_TOKEN: ${{ secrets.GH_PAT }}
DOCS_PR_NUMBER: ${{ steps.docs-pr.outputs.pull-request-number }}
run: |
gh pr merge "$DOCS_PR_NUMBER" \
--repo TextQLLabs/demo2 \
--auto \
--squash
2 changes: 1 addition & 1 deletion .speakeasy/gen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ generation:
schemas:
allOfMergeStrategy: shallowMerge
requestBodyFieldName: body
versioningStrategy: manual
versioningStrategy: automatic
persistentEdits: {}
tests:
generateTests: false
Expand Down
2 changes: 1 addition & 1 deletion .speakeasy/workflow.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ targets:
location: registry.speakeasyapi.dev/textql/home/textql-api-python-code-samples
labelOverride:
fixedValue: Python (SDK)
blocking: false
blocking: true
125 changes: 125 additions & 0 deletions scripts/render_mintlify_releases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""Render Speakeasy's RELEASES.md as a Mintlify Python SDK release log."""

from __future__ import annotations

import argparse
import re
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path


FRONTMATTER = """---
title: "Python SDK release log"
description: "Release history for the TextQL Python SDK"
icon: "python"
---

Install the latest release from [PyPI](https://pypi.org/project/textql-sdk/):

```bash
pip install textql-sdk
```
"""

SECTION = re.compile(
r"^## (?P<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s*$",
re.MULTILINE,
)
PYPI_LINK = re.compile(
r"\[PyPI v(?P<version>[^\]]+)\](?:\((?P<link_url>https?://[^)]+)\)|\s+(?P<plain_url>https?://\S+))"
)
SPEAKEASY = re.compile(
r"^- Speakeasy CLI (?P<cli>\S+)(?: \((?P<generator>[^)]+)\))?(?: (?P<url>https?://\S+))?\s*$",
re.MULTILINE,
)


@dataclass(frozen=True)
class Release:
published_at: datetime
version: str
pypi_url: str
speakeasy_cli: str | None = None
generator_version: str | None = None
speakeasy_url: str | None = None


def parse_releases(markdown: str) -> list[Release]:
"""Parse the release records generated by Speakeasy."""
matches = list(SECTION.finditer(markdown))
if not matches:
raise ValueError("RELEASES.md does not contain any timestamped releases")

releases: list[Release] = []
for index, match in enumerate(matches):
end = matches[index + 1].start() if index + 1 < len(matches) else len(markdown)
body = markdown[match.end() : end]
pypi = PYPI_LINK.search(body)
if pypi is None:
raise ValueError(f"release {match.group('timestamp')} has no PyPI link")

speakeasy = SPEAKEASY.search(body)
releases.append(
Release(
published_at=datetime.strptime(
match.group("timestamp"), "%Y-%m-%d %H:%M:%S"
),
version=pypi.group("version"),
pypi_url=pypi.group("link_url") or pypi.group("plain_url"),
speakeasy_cli=speakeasy.group("cli") if speakeasy else None,
generator_version=speakeasy.group("generator") if speakeasy else None,
speakeasy_url=speakeasy.group("url") if speakeasy else None,
)
)
return releases


def render_release(release: Release) -> str:
date = (
f"{release.published_at.strftime('%B')} "
f"{release.published_at.day}, {release.published_at.year}"
)
lines = [
f'<Update label="v{release.version}" description="{date}">',
"",
f"[View `textql-sdk` v{release.version} on PyPI]({release.pypi_url}).",
"",
"```bash",
f"pip install textql-sdk=={release.version}",
"```",
]
if release.speakeasy_cli:
label = f"Speakeasy CLI {release.speakeasy_cli}"
if release.generator_version:
label += f" (generator {release.generator_version})"
if release.speakeasy_url:
lines.extend(["", f"Generated with [{label}]({release.speakeasy_url})."])
else:
lines.extend(["", f"Generated with {label}."])
lines.extend(["", "</Update>"])
return "\n".join(lines)


def render(markdown: str) -> str:
releases = parse_releases(markdown)
return FRONTMATTER.rstrip() + "\n\n" + "\n\n".join(
render_release(release) for release in releases
) + "\n"


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("source", type=Path, help="Speakeasy RELEASES.md")
parser.add_argument("destination", type=Path, help="Mintlify MDX output")
args = parser.parse_args()

result = render(args.source.read_text())
args.destination.parent.mkdir(parents=True, exist_ok=True)
args.destination.write_text(result)
print(f"Rendered {len(parse_releases(args.source.read_text()))} Python SDK release(s)")


if __name__ == "__main__":
main()
51 changes: 51 additions & 0 deletions tests/test_render_mintlify_releases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import unittest

from scripts.render_mintlify_releases import parse_releases, render


RELEASE = """
## 2026-07-17 21:45:13
### Changes
Based on:
- OpenAPI Doc
- Speakeasy CLI 1.790.2 (2.918.3) https://github.com/speakeasy-api/speakeasy
### Generated
- [python v1.0.0] .
### Releases
- [PyPI v1.0.0] https://pypi.org/project/textql-sdk/1.0.0 - .
"""


class RenderMintlifyReleasesTest(unittest.TestCase):
def test_parses_speakeasy_release_format(self) -> None:
release = parse_releases(RELEASE)[0]
self.assertEqual(release.version, "1.0.0")
self.assertEqual(
release.pypi_url, "https://pypi.org/project/textql-sdk/1.0.0"
)
self.assertEqual(release.speakeasy_cli, "1.790.2")
self.assertEqual(release.generator_version, "2.918.3")

def test_accepts_standard_markdown_pypi_link(self) -> None:
markdown = RELEASE.replace(
"[PyPI v1.0.0] https://pypi.org/project/textql-sdk/1.0.0 - .",
"[PyPI v1.0.0](https://pypi.org/project/textql-sdk/1.0.0)",
)
self.assertEqual(parse_releases(markdown)[0].version, "1.0.0")

def test_renders_mintlify_updates(self) -> None:
output = render(RELEASE)
self.assertIn('title: "Python SDK release log"', output)
self.assertIn(
'<Update label="v1.0.0" description="July 17, 2026">', output
)
self.assertIn("pip install textql-sdk==1.0.0", output)
self.assertIn("[Speakeasy CLI 1.790.2 (generator 2.918.3)]", output)

def test_rejects_release_without_pypi_link(self) -> None:
with self.assertRaisesRegex(ValueError, "no PyPI link"):
parse_releases(RELEASE.replace("PyPI", "Package"))


if __name__ == "__main__":
unittest.main()