Skip to content

Commit c1967c2

Browse files
esteiningerclaude
andcommitted
Migrate to OpenAPI Generator with modern Mixpeek client wrapper
- Replace Speakeasy SDK with OpenAPI Generator - Add modern client wrapper (mixpeek/_client/) following Stripe/OpenAI patterns - Simplify SDK usage: from mixpeek import Mixpeek; client = Mixpeek() - Auto-sync workflow preserves wrapper across regenerations - Update quickstart example with new API 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
0 parents  commit c1967c2

833 files changed

Lines changed: 113252 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitattributes

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Auto detect text files and perform LF normalization
2+
* text=auto
3+
4+
# Python files
5+
*.py text eol=lf
6+
7+
# Shell scripts
8+
*.sh text eol=lf
9+
10+
# YAML files
11+
*.yml text eol=lf
12+
*.yaml text eol=lf
13+
14+
# Markdown files
15+
*.md text eol=lf
16+
17+
# JSON files
18+
*.json text eol=lf
19+

.github/workflows/python.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# NOTE: This file is auto generated by OpenAPI Generator.
2+
# URL: https://openapi-generator.tech
3+
#
4+
# ref: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
5+
6+
name: mixpeek Python package
7+
8+
on: [push, pull_request]
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
build:
15+
16+
runs-on: ubuntu-latest
17+
strategy:
18+
matrix:
19+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
20+
21+
steps:
22+
- uses: actions/checkout@v4
23+
- name: Set up Python ${{ matrix.python-version }}
24+
uses: actions/setup-python@v4
25+
with:
26+
python-version: ${{ matrix.python-version }}
27+
- name: Install dependencies
28+
run: |
29+
python -m pip install --upgrade pip
30+
pip install -r requirements.txt
31+
pip install -r test-requirements.txt
32+
- name: Test with pytest
33+
run: |
34+
pytest --cov=mixpeek
Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
name: Sync OpenAPI and Publish to PyPI
2+
3+
on:
4+
# Trigger when the main server repo pushes changes
5+
repository_dispatch:
6+
types: [openapi-update]
7+
8+
# Manual trigger
9+
workflow_dispatch:
10+
11+
# Also trigger on push to main (for testing)
12+
push:
13+
branches:
14+
- main
15+
16+
jobs:
17+
generate-and-publish:
18+
runs-on: ubuntu-latest
19+
20+
steps:
21+
- name: Checkout python-sdk repo
22+
uses: actions/checkout@v4
23+
with:
24+
token: ${{ secrets.GITHUB_TOKEN }}
25+
26+
- name: Set up Node.js
27+
uses: actions/setup-node@v4
28+
with:
29+
node-version: '20'
30+
31+
- name: Install OpenAPI Generator CLI
32+
run: npm install -g @openapitools/openapi-generator-cli
33+
34+
- name: Set up Python
35+
uses: actions/setup-python@v5
36+
with:
37+
python-version: '3.9'
38+
39+
- name: Install build dependencies
40+
run: |
41+
python -m pip install --upgrade pip
42+
pip install build twine setuptools wheel
43+
44+
- name: Download OpenAPI Spec
45+
run: curl -s https://api.mixpeek.com/docs/openapi.json -o openapi.json
46+
47+
- name: Extract version from OpenAPI spec
48+
id: get-version
49+
run: |
50+
VERSION=$(python -c "import json; print(json.load(open('openapi.json'))['info']['version'])")
51+
echo "version=$VERSION" >> $GITHUB_OUTPUT
52+
echo "📦 Version: $VERSION"
53+
54+
- name: Check if version exists on PyPI
55+
id: check-pypi
56+
continue-on-error: true
57+
run: |
58+
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" https://pypi.org/pypi/mixpeek/${{ steps.get-version.outputs.version }}/json)
59+
if [ $RESPONSE -eq 200 ]; then
60+
echo "exists=true" >> $GITHUB_OUTPUT
61+
echo "⚠️ Version ${{ steps.get-version.outputs.version }} already exists on PyPI"
62+
else
63+
echo "exists=false" >> $GITHUB_OUTPUT
64+
echo "✅ Version ${{ steps.get-version.outputs.version }} is new"
65+
fi
66+
67+
- name: Clean OpenAPI spec
68+
if: steps.check-pypi.outputs.exists != 'true'
69+
run: |
70+
echo "🧼 Cleaning OpenAPI specification..."
71+
python3 << 'PYTHON_SCRIPT'
72+
import json
73+
import re
74+
75+
with open('openapi.json', 'r') as f:
76+
spec = json.load(f)
77+
78+
def clean_schema(schema):
79+
"""Recursively clean anyOf/oneOf that include null type"""
80+
if isinstance(schema, dict):
81+
if 'anyOf' in schema and isinstance(schema['anyOf'], list):
82+
cleaned = [s for s in schema['anyOf'] if s.get('type') != 'null']
83+
if len(cleaned) == 1 and len(schema['anyOf']) > 1:
84+
for key, value in cleaned[0].items():
85+
schema[key] = value
86+
del schema['anyOf']
87+
schema['nullable'] = True
88+
elif len(cleaned) < len(schema['anyOf']):
89+
schema['anyOf'] = cleaned
90+
schema['nullable'] = True
91+
if 'oneOf' in schema and isinstance(schema['oneOf'], list):
92+
cleaned = [s for s in schema['oneOf'] if s.get('type') != 'null']
93+
if len(cleaned) == 1 and len(schema['oneOf']) > 1:
94+
for key, value in cleaned[0].items():
95+
schema[key] = value
96+
del schema['oneOf']
97+
schema['nullable'] = True
98+
elif len(cleaned) < len(schema['oneOf']):
99+
schema['oneOf'] = cleaned
100+
schema['nullable'] = True
101+
for key, value in list(schema.items()):
102+
if isinstance(value, (dict, list)):
103+
clean_schema(value)
104+
elif isinstance(schema, list):
105+
for item in schema:
106+
clean_schema(item)
107+
return schema
108+
109+
def simplify_operation_id(operation_id):
110+
"""Simplify operation IDs to be more developer-friendly"""
111+
if not operation_id:
112+
return operation_id
113+
114+
# Remove version prefix (v1, v2, etc.)
115+
operation_id = re.sub(r'_v\d+_', '_', operation_id)
116+
117+
# Remove HTTP method suffix
118+
operation_id = re.sub(r'_(post|get|put|delete|patch)$', '', operation_id)
119+
120+
# Remove common path patterns
121+
operation_id = re.sub(r'_identifier_', '_', operation_id)
122+
operation_id = re.sub(r'__{1,}', '_', operation_id)
123+
124+
# Split into parts
125+
parts = operation_id.split('_')
126+
127+
# Extract action verb
128+
action_verbs = ['create', 'get', 'list', 'update', 'delete', 'patch', 'execute',
129+
'upsert', 'describe', 'add', 'remove', 'set']
130+
131+
action = None
132+
if parts and parts[0] in action_verbs:
133+
action = parts[0]
134+
parts = parts[1:]
135+
136+
# Remove generic/redundant words
137+
skip_words = ['route', 'endpoint', 'api', 'private', 'public', 'v1', 'v2', 'v3']
138+
139+
# Track singular/plural forms to avoid duplication
140+
seen_roots = set()
141+
cleaned_parts = []
142+
143+
for part in parts:
144+
if part in skip_words:
145+
continue
146+
147+
# Get root form (simple pluralization check)
148+
root = part.rstrip('s') if part.endswith('s') and len(part) > 3 else part
149+
150+
# Skip if we've seen this root (or very similar)
151+
if root.lower() in seen_roots:
152+
continue
153+
154+
cleaned_parts.append(part)
155+
seen_roots.add(root.lower())
156+
157+
# Rebuild operation ID
158+
if action:
159+
result = action + ('_' + '_'.join(cleaned_parts) if cleaned_parts else '')
160+
else:
161+
result = '_'.join(cleaned_parts)
162+
163+
# Final cleanup
164+
result = result.strip('_')
165+
166+
# Remove action duplication at the end
167+
for verb in action_verbs:
168+
pattern = f'_{verb}$'
169+
if result.startswith(verb + '_') and result.endswith(f'_{verb}'):
170+
result = re.sub(pattern, '', result)
171+
172+
# Remove duplicate nouns
173+
parts_to_check = result.split('_')
174+
if len(parts_to_check) > 2:
175+
generic_suffixes = ['collections', 'list', 'id', 'identifier', 'document', 'documents']
176+
while len(parts_to_check) > 2 and parts_to_check[-1] in generic_suffixes:
177+
parts_to_check = parts_to_check[:-1]
178+
result = '_'.join(parts_to_check)
179+
180+
return result
181+
182+
def clean_operation_ids(spec):
183+
"""Clean all operation IDs in the spec"""
184+
if 'paths' not in spec:
185+
return
186+
187+
for path, path_item in spec['paths'].items():
188+
if not isinstance(path_item, dict):
189+
continue
190+
191+
for method in ['get', 'post', 'put', 'delete', 'patch', 'options', 'head']:
192+
if method in path_item and isinstance(path_item[method], dict):
193+
operation = path_item[method]
194+
if 'operationId' in operation:
195+
old_id = operation['operationId']
196+
new_id = simplify_operation_id(old_id)
197+
operation['operationId'] = new_id
198+
199+
if 'components' in spec and 'schemas' in spec['components']:
200+
clean_schema(spec['components']['schemas'])
201+
if 'paths' in spec:
202+
clean_schema(spec['paths'])
203+
204+
clean_operation_ids(spec)
205+
206+
with open('openapi-cleaned.json', 'w') as f:
207+
json.dump(spec, f, indent=2)
208+
print("✅ Cleaned OpenAPI spec with simplified method names")
209+
PYTHON_SCRIPT
210+
211+
- name: Generate SDK
212+
if: steps.check-pypi.outputs.exists != 'true'
213+
run: |
214+
echo "🚀 Generating SDK..."
215+
216+
# Backup custom wrapper code (preserved across regenerations)
217+
if [ -d "mixpeek/_client" ]; then
218+
echo "📦 Backing up custom wrapper..."
219+
cp -r mixpeek/_client /tmp/_client_backup
220+
fi
221+
222+
# Clean previous generation (except important files)
223+
rm -rf mixpeek test docs .openapi-generator-ignore .gitlab-ci.yml git_push.sh tox.ini test-requirements.txt .travis.yml || true
224+
225+
# Generate the SDK
226+
openapi-generator-cli generate \
227+
-i openapi-cleaned.json \
228+
-g python \
229+
-o . \
230+
--skip-validate-spec \
231+
--package-name mixpeek \
232+
--additional-properties=projectName=mixpeek,packageVersion=${{ steps.get-version.outputs.version }},packageUrl=https://github.com/mixpeek/python-sdk,library=urllib3
233+
234+
# Cleanup unnecessary files
235+
rm -f .travis.yml git_push.sh .gitlab-ci.yml || true
236+
237+
# Restore custom wrapper code
238+
if [ -d "/tmp/_client_backup" ]; then
239+
echo "📦 Restoring custom wrapper..."
240+
cp -r /tmp/_client_backup mixpeek/_client
241+
fi
242+
243+
- name: Inject modern client wrapper
244+
if: steps.check-pypi.outputs.exists != 'true'
245+
run: |
246+
echo "🔧 Injecting modern Mixpeek client..."
247+
python3 -c "
248+
# Read the generated __init__.py
249+
with open('mixpeek/__init__.py', 'r') as f:
250+
content = f.read()
251+
252+
# Add Mixpeek to __all__ (at the beginning)
253+
content = content.replace(
254+
'__all__ = [',
255+
'__all__ = [\n \"Mixpeek\",'
256+
)
257+
258+
# Add the import at the end of the file
259+
wrapper_import = '\n# Modern client wrapper (preserved across regenerations)\nfrom mixpeek._client import Mixpeek as Mixpeek\n'
260+
if 'from mixpeek._client import Mixpeek' not in content:
261+
content += wrapper_import
262+
263+
with open('mixpeek/__init__.py', 'w') as f:
264+
f.write(content)
265+
266+
print('✅ Injected Mixpeek client wrapper')
267+
"
268+
269+
- name: Build package
270+
if: steps.check-pypi.outputs.exists != 'true'
271+
run: python -m build
272+
273+
- name: Publish to PyPI
274+
if: steps.check-pypi.outputs.exists != 'true'
275+
env:
276+
TWINE_USERNAME: __token__
277+
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
278+
run: |
279+
twine upload dist/* --verbose
280+
281+
- name: Commit and push changes
282+
if: steps.check-pypi.outputs.exists != 'true'
283+
run: |
284+
git config --local user.email "github-actions[bot]@users.noreply.github.com"
285+
git config --local user.name "github-actions[bot]"
286+
git add -A
287+
git diff --staged --quiet || git commit -m "🤖 Auto-generate SDK v${{ steps.get-version.outputs.version }}"
288+
git push
289+
290+
- name: Create GitHub Release
291+
if: steps.check-pypi.outputs.exists != 'true'
292+
uses: actions/create-release@v1
293+
env:
294+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
295+
with:
296+
tag_name: v${{ steps.get-version.outputs.version }}
297+
release_name: Release v${{ steps.get-version.outputs.version }}
298+
body: |
299+
🎉 Auto-generated SDK from OpenAPI specification
300+
301+
Version: ${{ steps.get-version.outputs.version }}
302+
303+
Install via pip:
304+
```bash
305+
pip install mixpeek==${{ steps.get-version.outputs.version }}
306+
```
307+
draft: false
308+
prerelease: false
309+
310+
- name: Skip - Version already published
311+
if: steps.check-pypi.outputs.exists == 'true'
312+
run: |
313+
echo "⏭️ Skipping: Version ${{ steps.get-version.outputs.version }} already exists on PyPI"
314+

0 commit comments

Comments
 (0)