mirror of
				https://github.com/RGBCube/GitHubWrapper
				synced 2025-10-30 21:42:45 +00:00 
			
		
		
		
	Add tools/unschema: This generates TypedDicts from JSON response schemas
This commit is contained in:
		
							parent
							
								
									ce6d3c5b5b
								
							
						
					
					
						commit
						7c9d7d4862
					
				
					 3 changed files with 183 additions and 0 deletions
				
			
		
							
								
								
									
										1
									
								
								tools/unschema/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tools/unschema/__init__.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | ||||||
|  | from .parser import generate | ||||||
							
								
								
									
										50
									
								
								tools/unschema/__main__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								tools/unschema/__main__.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,50 @@ | ||||||
|  | import json | ||||||
|  | import time | ||||||
|  | from argparse import ArgumentParser | ||||||
|  | 
 | ||||||
|  | from .parser import generate | ||||||
|  | 
 | ||||||
|  | parser = ArgumentParser(description="Generate TypedDicts from a json schema.") | ||||||
|  | parser.add_argument("-f", "--from", required=True, help="The json schema to generate from.") | ||||||
|  | parser.add_argument("-t", "--to", help="The file to write the TypedDicts to.") | ||||||
|  | parser.add_argument( | ||||||
|  |     "-ne", "--no-examples", action="store_true", help="If given, examples wont be added." | ||||||
|  | ) | ||||||
|  | parser.add_argument( | ||||||
|  |     "-nf", "--no-formats", action="store_true", help="If given, formats wont be added." | ||||||
|  | ) | ||||||
|  | parser.add_argument( | ||||||
|  |     "-p", | ||||||
|  |     "--print", | ||||||
|  |     action="store_true", | ||||||
|  |     help="If given, the result will be printed instead of being writing to a file", | ||||||
|  | ) | ||||||
|  | args = parser.parse_args() | ||||||
|  | 
 | ||||||
|  | with open(args.__getattribute__("from")) as g: | ||||||
|  |     schema = json.load(g) | ||||||
|  | 
 | ||||||
|  | start = time.monotonic() | ||||||
|  | text = f""" | ||||||
|  | from __future__ import annotations | ||||||
|  | 
 | ||||||
|  | from typing import List, Optional, TypedDict, Union, TYPE_CHECKING | ||||||
|  | 
 | ||||||
|  | if TYPE_CHECKING: | ||||||
|  |     from typing_extensions import NotRequired{generate(schema, no_examples=args.no_examples, no_formats=args.no_formats)}  # Rename this to your liking. | ||||||
|  | """[ | ||||||
|  |     1: | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | if args.print: | ||||||
|  |     print(text) | ||||||
|  | else: | ||||||
|  |     if not args.to: | ||||||
|  |         raise ValueError("-t/--to is required when writing to a file.") | ||||||
|  | 
 | ||||||
|  |     with open(args.to, "w") as f: | ||||||
|  |         f.write(text) | ||||||
|  | 
 | ||||||
|  | end = time.monotonic() - start | ||||||
|  | 
 | ||||||
|  | print(f"Success! Finished in {end*1000:.3} milliseconds.") | ||||||
							
								
								
									
										132
									
								
								tools/unschema/parser.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								tools/unschema/parser.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,132 @@ | ||||||
|  | from __future__ import annotations | ||||||
|  | 
 | ||||||
|  | from typing import Iterable, Tuple | ||||||
|  | 
 | ||||||
|  | types = { | ||||||
|  |     "string": "str", | ||||||
|  |     "integer": "int", | ||||||
|  |     "number": "float", | ||||||
|  |     "boolean": "bool", | ||||||
|  |     "object": "dict", | ||||||
|  |     "array": "list", | ||||||
|  |     "null": "None", | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | untypes = {v: k for k, v in types.items()} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def unspace(s: str) -> str: | ||||||
|  |     return s.replace(" ", "") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def mklist(s: Iterable) -> str: | ||||||
|  |     return f"[{', '.join(s)}]" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def generate( | ||||||
|  |     _schema: dict, | ||||||
|  |     /, | ||||||
|  |     *, | ||||||
|  |     no_examples: bool = False, | ||||||
|  |     no_formats: bool = False, | ||||||
|  |     title: str = "GeneratedObject", | ||||||
|  | ) -> str: | ||||||
|  |     def inner_generate( | ||||||
|  |         schema: dict, | ||||||
|  |         /, | ||||||
|  |         *, | ||||||
|  |         _no_examples: bool = no_examples, | ||||||
|  |         _no_formats: bool = no_formats, | ||||||
|  |         _title: str = title, | ||||||
|  |     ) -> Tuple[str, str]:  # sourcery skip: low-code-quality | ||||||
|  | 
 | ||||||
|  |         # GeneratedObject = Union[..., ..., ...] | ||||||
|  |         if objects := schema.get("oneOf"): | ||||||
|  |             text = [] | ||||||
|  |             union_items = [] | ||||||
|  | 
 | ||||||
|  |             for item_schema in objects: | ||||||
|  |                 generated_text, union_item = generate(item_schema, title=_title) | ||||||
|  | 
 | ||||||
|  |                 text.append(generated_text) | ||||||
|  |                 union_items.append(union_item) | ||||||
|  | 
 | ||||||
|  |             text.append(f"{_title} = Union{mklist(union_items)}") | ||||||
|  |             return "\n\n".join(_title), _title | ||||||
|  |         del objects | ||||||
|  | 
 | ||||||
|  |         # class GeneratedObject(TypedDict): | ||||||
|  |         if (object_type := schema["type"]) == untypes["dict"]: | ||||||
|  |             _title = unspace(schema["title"]) | ||||||
|  | 
 | ||||||
|  |             dependencies = [] | ||||||
|  |             current = [f'class {_title}(TypedDict):\n    """{schema["description"]}"""'] | ||||||
|  | 
 | ||||||
|  |             for key, value in schema["properties"].items(): | ||||||
|  |                 if isinstance(value_type := value["type"], str): | ||||||
|  |                     param_type = types[value_type] | ||||||
|  | 
 | ||||||
|  |                 elif isinstance(value_type, list): | ||||||
|  |                     combiner = "Union" | ||||||
|  |                     contents = [] | ||||||
|  | 
 | ||||||
|  |                     for type in value_type: | ||||||
|  |                         if type == untypes["None"]: | ||||||
|  |                             combiner = "Optional" | ||||||
|  |                         else: | ||||||
|  |                             contents.append(types[type]) | ||||||
|  | 
 | ||||||
|  |                     param_type = f"{combiner}{mklist(contents)}" | ||||||
|  | 
 | ||||||
|  |                 elif isinstance(value_type, dict): | ||||||
|  |                     text, target = inner_generate( | ||||||
|  |                         value_type, _title=key, _no_examples=_no_examples, _no_formats=_no_formats | ||||||
|  |                     ) | ||||||
|  |                     dependencies.append(text) | ||||||
|  |                     param_type = target | ||||||
|  | 
 | ||||||
|  |                 else: | ||||||
|  |                     param_type = f"Unknown[{value_type}]" | ||||||
|  | 
 | ||||||
|  |                 if key not in schema["required"]: | ||||||
|  |                     param_type = f"NotRequired[{param_type}]" | ||||||
|  | 
 | ||||||
|  |                 eg = ( | ||||||
|  |                     "" | ||||||
|  |                     if no_examples | ||||||
|  |                     else f"    # example: {mklist(str(eg) for eg in egs)[1:-1]}\n" | ||||||
|  |                     if (egs := value.get("examples")) | ||||||
|  |                     else "" | ||||||
|  |                 ) | ||||||
|  | 
 | ||||||
|  |                 fmt = ( | ||||||
|  |                     "" | ||||||
|  |                     if no_formats | ||||||
|  |                     else f"    # format: {fmt}\n" | ||||||
|  |                     if (fmt := value.get("format")) | ||||||
|  |                     else "" | ||||||
|  |                 ) | ||||||
|  | 
 | ||||||
|  |                 sep = "\n" if eg or fmt else "" | ||||||
|  | 
 | ||||||
|  |                 com = "# " if param_type.startswith("Unknown") else "" | ||||||
|  | 
 | ||||||
|  |                 current.append(f"{sep}{eg}{fmt}    {com}{key}: {param_type}") | ||||||
|  | 
 | ||||||
|  |             dependencies.append("\n".join(current)) | ||||||
|  |             result = "\n\n".join(dependencies[:-1]) + "\n\n\n" + dependencies[-1] | ||||||
|  | 
 | ||||||
|  |             return result, _title | ||||||
|  | 
 | ||||||
|  |         # GeneratedObject = List[...] | ||||||
|  |         elif object_type == untypes["list"]: | ||||||
|  |             generated, target = inner_generate( | ||||||
|  |                 schema["items"], _title=_title, _no_examples=_no_examples, _no_formats=_no_formats | ||||||
|  |             ) | ||||||
|  |             return f"{generated}\n\n\n{_title} = List[{target}]", _title | ||||||
|  | 
 | ||||||
|  |         else: | ||||||
|  |             print("Unknown/Unimplemented Object Type:", object_type) | ||||||
|  |             return "", "" | ||||||
|  | 
 | ||||||
|  |     return inner_generate(_schema)[0] | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 RGBCube
						RGBCube