1010Options:
1111 --force Regenerate even if spec hasn't changed
1212 --check Check if spec has changed without regenerating (exit 1 if changed)
13- --version Show current spec hash and exit
13+ --version Show current spec version and hash, then exit
1414"""
1515
1616import argparse
1717import hashlib
18+ import json
1819import re
1920import subprocess
2021import sys
2829SPEC_CACHE_DIR = PROJECT_ROOT / "src" / "openresponses_types" / "_spec_cache"
2930SPEC_CACHE_FILE = SPEC_CACHE_DIR / "openapi.json"
3031SPEC_HASH_FILE = SPEC_CACHE_DIR / "openapi.sha256"
32+ SPEC_VERSION_FILE = SPEC_CACHE_DIR / "openapi.version"
3133OUTPUT_FILE = PROJECT_ROOT / "src" / "openresponses_types" / "types.py"
3234INIT_FILE = PROJECT_ROOT / "src" / "openresponses_types" / "__init__.py"
35+ PYPROJECT_FILE = PROJECT_ROOT / "pyproject.toml"
3336
3437GENERATION_HEADER = '''\
3538 """Auto-generated Pydantic models from OpenResponses OpenAPI specification.
3841
3942This file is generated by: scripts/generate_types.py
4043Source: {spec_url}
44+ Spec Version: {spec_version}
4145Spec Hash: {spec_hash}
4246
4347To regenerate:
@@ -54,6 +58,12 @@ def fetch_spec() -> bytes:
5458 return response .read ()
5559
5660
61+ def extract_spec_version (content : bytes ) -> str :
62+ """Extract the version from the OpenAPI spec."""
63+ spec = json .loads (content )
64+ return spec .get ("info" , {}).get ("version" , "0.0.0" )
65+
66+
5767def compute_hash (content : bytes ) -> str :
5868 """Compute SHA-256 hash of content."""
5969 return hashlib .sha256 (content ).hexdigest ()
@@ -66,11 +76,19 @@ def get_cached_hash() -> str | None:
6676 return None
6777
6878
69- def save_spec_cache (content : bytes , spec_hash : str ) -> None :
70- """Save the spec and its hash to the cache directory."""
79+ def get_cached_version () -> str | None :
80+ """Get the previously stored spec version, if any."""
81+ if SPEC_VERSION_FILE .exists ():
82+ return SPEC_VERSION_FILE .read_text ().strip ()
83+ return None
84+
85+
86+ def save_spec_cache (content : bytes , spec_hash : str , spec_version : str ) -> None :
87+ """Save the spec, its hash, and version to the cache directory."""
7188 SPEC_CACHE_DIR .mkdir (parents = True , exist_ok = True )
7289 SPEC_CACHE_FILE .write_bytes (content )
7390 SPEC_HASH_FILE .write_text (spec_hash )
91+ SPEC_VERSION_FILE .write_text (spec_version )
7492
7593
7694def fix_discriminator_issues () -> None :
@@ -113,7 +131,7 @@ def format_output() -> None:
113131 print (f"Warning: ruff formatting failed: { result .stderr } " , file = sys .stderr )
114132
115133
116- def generate_models (spec_hash : str ) -> None :
134+ def generate_models (spec_hash : str , spec_version : str ) -> None :
117135 """Run datamodel-codegen to generate Pydantic models."""
118136 print ("Generating Pydantic models..." )
119137
@@ -141,17 +159,21 @@ def generate_models(spec_hash: str) -> None:
141159 print (f"Error generating models: { result .stderr } " , file = sys .stderr )
142160 sys .exit (1 )
143161
144- prepend_header (spec_hash )
162+ prepend_header (spec_hash , spec_version )
145163 fix_discriminator_issues ()
146164 format_output ()
147- update_init_spec_hash (spec_hash )
165+ update_init_metadata (spec_hash , spec_version )
148166 print (f"Generated: { OUTPUT_FILE } " )
149167
150168
151- def prepend_header (spec_hash : str ) -> None :
169+ def prepend_header (spec_hash : str , spec_version : str ) -> None :
152170 """Prepend generation header to the output file."""
153171 content = OUTPUT_FILE .read_text ()
154- header = GENERATION_HEADER .format (spec_url = OPENRESPONSES_SPEC_URL , spec_hash = spec_hash )
172+ header = GENERATION_HEADER .format (
173+ spec_url = OPENRESPONSES_SPEC_URL ,
174+ spec_hash = spec_hash ,
175+ spec_version = spec_version ,
176+ )
155177
156178 lines = content .split ("\n " )
157179 filtered_lines = []
@@ -166,16 +188,31 @@ def prepend_header(spec_hash: str) -> None:
166188 OUTPUT_FILE .write_text (header + "\n " .join (filtered_lines ))
167189
168190
169- def update_init_spec_hash (spec_hash : str ) -> None :
170- """Update the __spec_hash__ in __init__.py."""
191+ def update_init_metadata (spec_hash : str , spec_version : str ) -> None :
192+ """Update the __spec_hash__ and __spec_version__ in __init__.py."""
171193 if not INIT_FILE .exists ():
172194 return
173195
174196 content = INIT_FILE .read_text ()
197+
175198 # Update __spec_hash__ if it exists
176199 if "__spec_hash__" in content :
177200 content = re .sub (r'__spec_hash__\s*=\s*"[^"]*"' , f'__spec_hash__ = "{ spec_hash } "' , content )
178- INIT_FILE .write_text (content )
201+
202+ # Update __spec_version__ if it exists
203+ if "__spec_version__" in content :
204+ content = re .sub (r'__spec_version__\s*=\s*"[^"]*"' , f'__spec_version__ = "{ spec_version } "' , content )
205+
206+ INIT_FILE .write_text (content )
207+
208+
209+ def suggest_package_version (spec_version : str ) -> str :
210+ """Suggest a package version based on the spec version.
211+
212+ Package version matches the spec version directly.
213+ For hotfixes, use PEP 440 post-releases (e.g., 2.3.0.post1).
214+ """
215+ return spec_version
179216
180217
181218def main () -> None :
@@ -184,43 +221,51 @@ def main() -> None:
184221 parser .add_argument (
185222 "--check" , action = "store_true" , help = "Check if spec has changed without regenerating (exit 1 if changed)"
186223 )
187- parser .add_argument ("--version" , action = "store_true" , help = "Show current spec hash and exit" )
224+ parser .add_argument ("--version" , action = "store_true" , help = "Show current spec version and hash, then exit" )
188225 args = parser .parse_args ()
189226
190227 if args .version :
228+ cached_version = get_cached_version ()
191229 cached_hash = get_cached_hash ()
192- if cached_hash :
193- print (f"Current spec hash: { cached_hash } " )
230+ if cached_version and cached_hash :
231+ print (f"Spec version: { cached_version } " )
232+ print (f"Spec hash: { cached_hash } " )
233+ print (f"Suggested package version: { suggest_package_version (cached_version )} " )
194234 else :
195- print ("No spec hash cached yet. Run without --version to generate." )
235+ print ("No spec cached yet. Run without --version to generate." )
196236 return
197237
198238 spec_content = fetch_spec ()
199239 current_hash = compute_hash (spec_content )
240+ current_version = extract_spec_version (spec_content )
200241 cached_hash = get_cached_hash ()
242+ cached_version = get_cached_version ()
201243
202244 if cached_hash == current_hash and not args .force :
203- print (f"OpenResponses spec unchanged (hash: { current_hash [:12 ]} ...)" )
245+ print (f"OpenResponses spec unchanged (v { current_version } , hash: { current_hash [:12 ]} ...)" )
204246 if args .check :
205247 sys .exit (0 )
206248 print ("Use --force to regenerate anyway." )
207249 return
208250
209251 if cached_hash is None :
210- print ("First time generating OpenResponses types." )
252+ print (f "First time generating OpenResponses types (spec v { current_version } ) ." )
211253 else :
212254 print ("OpenResponses spec has CHANGED!" )
213- print (f" Previous: { cached_hash [:12 ]} ..." )
214- print (f" Current: { current_hash [:12 ]} ..." )
255+ print (f" Previous: v { cached_version } (hash: { cached_hash [:12 ]} ...) " )
256+ print (f" Current: v { current_version } (hash: { current_hash [:12 ]} ...) " )
215257
216258 if args .check :
217259 print ("\n Spec change detected. Run without --check to regenerate." )
218260 sys .exit (1 )
219261
220- save_spec_cache (spec_content , current_hash )
221- generate_models (current_hash )
262+ save_spec_cache (spec_content , current_hash , current_version )
263+ generate_models (current_hash , current_version )
222264
223- print ("\n Generation complete!" )
265+ print (f"\n Generation complete!" )
266+ print (f"Spec version: { current_version } " )
267+ print (f"Suggested package version: { suggest_package_version (current_version )} " )
268+ print (f"\n To release, update pyproject.toml version and tag: git tag v{ suggest_package_version (current_version )} " )
224269
225270
226271if __name__ == "__main__" :
0 commit comments