├── src └── pydantype │ ├── __init__.py │ ├── utils.py │ └── converter.py ├── requirements.txt ├── examples ├── basic.py ├── gemini_example.py ├── advance.py └── Gemini_Example_with_pydantic_to_typeddict.ipynb ├── LICENSE ├── setup.py ├── .gitignore ├── README.md └── tests └── test_converter.py /src/pydantype/__init__.py: -------------------------------------------------------------------------------- 1 | from .converter import convert 2 | 3 | __all__ = ['convert'] 4 | __version__ = '0.1.0' -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | protobuf==5.27.3 2 | pydantic==2.8.2 3 | python-dotenv==1.0.1 4 | setuptools==65.5.0 5 | typing_extensions==4.12.2 6 | -------------------------------------------------------------------------------- /src/pydantype/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Union, get_origin, get_args 2 | from pydantic import BaseModel 3 | 4 | def is_pydantic_model(obj: Any) -> bool: 5 | return isinstance(obj, type) and issubclass(obj, BaseModel) 6 | 7 | def is_optional(annotation: Any) -> bool: 8 | return get_origin(annotation) is Union and type(None) in get_args(annotation) -------------------------------------------------------------------------------- /examples/basic.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from typing import List, Optional 3 | from pydantype import convert 4 | 5 | class Address(BaseModel): 6 | street: str 7 | city: str 8 | country: str 9 | 10 | class Person(BaseModel): 11 | name: str 12 | age: int 13 | address: Address 14 | hobbies: List[str] 15 | nickname: Optional[str] 16 | 17 | PersonDict = convert(Person) 18 | 19 | print("PersonDict structure:") 20 | for field, type_hint in PersonDict.__annotations__.items(): 21 | print(f"{field}: {type_hint}") 22 | 23 | # Create a Person instance 24 | person = Person( 25 | name="John Doe", 26 | age=30, 27 | address=Address(street="123 Main St", city="Anytown", country="USA"), 28 | hobbies=["reading", "swimming"], 29 | nickname="Johnny" 30 | ) 31 | 32 | # Convert to dict and verify it matches PersonDict 33 | person_dict = person.model_dump() 34 | assert isinstance(person_dict, dict) 35 | print("\nPerson instance convertted to dict successfully and matches PersonDict structure.") 36 | print(person_dict) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2024 Scott Chacon and others 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import re 2 | from setuptools import setup, find_packages 3 | 4 | # Read the version from __init__.py 5 | def get_version(): 6 | init_py = open('src/pydantype/__init__.py').read() 7 | return re.search(r"__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1) 8 | 9 | # Read the contents of README.md 10 | with open("README.md", "r", encoding="utf-8") as fh: 11 | long_description = fh.read() 12 | 13 | # Read the requirements from requirements.txt 14 | with open('requirements.txt') as f: 15 | requirements = f.read().splitlines() 16 | 17 | setup( 18 | name="pydantype", 19 | version=get_version(), 20 | author="Unclecode", 21 | author_email="unclecode@kidocode.com", 22 | description="A library to convertt Pydantic models to TypedDict", 23 | long_description=long_description, 24 | long_description_content_type="text/markdown", 25 | url="https://github.com/unclecode/pydantype", 26 | packages=find_packages(where="src"), 27 | package_dir={"": "src"}, 28 | install_requires=requirements, 29 | classifiers=[ 30 | "Development Status :: 3 - Alpha", 31 | "Intended Audience :: Developers", 32 | "License :: OSI Approved :: MIT License", 33 | "Programming Language :: Python :: 3", 34 | "Programming Language :: Python :: 3.7", 35 | "Programming Language :: Python :: 3.8", 36 | "Programming Language :: Python :: 3.9", 37 | "Programming Language :: Python :: 3.10", 38 | "Programming Language :: Python :: 3.11", 39 | ], 40 | python_requires=">=3.7", 41 | ) -------------------------------------------------------------------------------- /examples/gemini_example.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import google.generativeai as genai 4 | import typing_extensions as typing 5 | import google.generativeai as genai 6 | from pydantic import BaseModel 7 | from typing import List 8 | from pydantype import convert 9 | from dotenv import load_dotenv 10 | load_dotenv() 11 | 12 | print("Example: Using TypedDict and Pydantic with Google Generative AI (Gemini 1.5 Pro)") 13 | 14 | # Part 1: Using TypedDict directly 15 | print("\nPart 1: Using TypedDict directly") 16 | 17 | class Recipe(typing.TypedDict): 18 | recipe_name: str 19 | ingredients: str 20 | 21 | # Set up the model with TypedDict schema 22 | model_typeddict = genai.GenerativeModel('gemini-1.5-pro', 23 | generation_config={ 24 | "response_mime_type": "application/json", 25 | "response_schema": List[Recipe] 26 | }) 27 | 28 | prompt = "List 3 popular cookie recipes" 29 | 30 | response_typeddict = model_typeddict.generate_content(prompt) 31 | print("Response using TypedDict:") 32 | print(response_typeddict.text) 33 | 34 | # Part 2: Using Pydantic and convertting to TypedDict 35 | print("\nPart 2: Using Pydantic and convertting to TypedDict") 36 | 37 | class RecipePydantic(BaseModel): 38 | recipe_name: str 39 | ingredients: str 40 | 41 | class RecipeList(BaseModel): 42 | recipes: List[RecipePydantic] 43 | 44 | # Convert Pydantic models to TypedDict 45 | RecipeDict = convert(RecipePydantic) 46 | RecipeListDict = convert(RecipeList) 47 | 48 | # Set up the model with convertted Pydantic schema 49 | model_pydantic = genai.GenerativeModel('gemini-1.5-pro', 50 | generation_config={ 51 | "response_mime_type": "application/json", 52 | "response_schema": RecipeListDict 53 | }) 54 | 55 | response_pydantic = model_pydantic.generate_content(prompt) 56 | print("Response using convertted Pydantic model:") 57 | print(response_pydantic.text) 58 | 59 | print("\nComparison:") 60 | print("As you can see, both approaches produce the same structure of response.") 61 | print("The advantage of using Pydantic is that you can define more complex models") 62 | print("with validation, default values, and other features, and then convertt them") 63 | print("to TypedDict for use with APIs that require it, like Gemini 1.5 Pro.") -------------------------------------------------------------------------------- /src/pydantype/converter.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import get_args, get_origin, Any, Union, List, Dict, Optional, Type, ForwardRef 3 | from typing_extensions import TypedDict 4 | from pydantic import BaseModel 5 | 6 | from .utils import is_pydantic_model, is_optional 7 | 8 | def convertt_type(annotation: Any, processed_models: set = None) -> Any: 9 | if processed_models is None: 10 | processed_models = set() 11 | 12 | if isinstance(annotation, ForwardRef): 13 | return annotation.__forward_arg__ 14 | 15 | if is_pydantic_model(annotation): 16 | if annotation.__name__ in processed_models: 17 | return f"ForwardRef('{annotation.__name__}Dict')" 18 | processed_models.add(annotation.__name__) 19 | return convert(annotation, processed_models) 20 | 21 | origin = get_origin(annotation) 22 | if origin is None: 23 | return annotation 24 | 25 | args = get_args(annotation) 26 | 27 | if origin is Union: 28 | non_none_args = [arg for arg in args if arg is not type(None)] 29 | if len(non_none_args) == 1: 30 | return Optional[convertt_type(non_none_args[0], processed_models)] 31 | return Union[tuple(convertt_type(arg, processed_models) for arg in non_none_args)] 32 | 33 | if origin in (list, List): 34 | item_type = convertt_type(args[0], processed_models) if args else Any 35 | return List[item_type] 36 | 37 | if origin in (dict, Dict): 38 | key_type = convertt_type(args[0], processed_models) if len(args) > 0 else Any 39 | value_type = convertt_type(args[1], processed_models) if len(args) > 1 else Any 40 | return Dict[key_type, value_type] 41 | 42 | if hasattr(origin, '__origin__'): # Handle subscripted generics 43 | convertted_args = tuple(convertt_type(arg, processed_models) for arg in args) 44 | return origin[convertted_args] 45 | 46 | return annotation 47 | 48 | def convert(model: Type[BaseModel], processed_models: set = None) -> Type[TypedDict]: 49 | if not is_pydantic_model(model): 50 | raise ValueError(f"Expected a Pydantic model, got {type(model)}") 51 | 52 | if processed_models is None: 53 | processed_models = set() 54 | 55 | fields = {} 56 | for name, field in model.model_fields.items(): 57 | convertted_type = convertt_type(field.annotation, processed_models) 58 | fields[name] = convertted_type 59 | 60 | return TypedDict(f"{model.__name__}Dict", fields) -------------------------------------------------------------------------------- /examples/advance.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Optional, Union, Any 2 | from pydantic import BaseModel, Field 3 | from pydantype import convert 4 | 5 | print("Welcome to the pydantype Converter Tutorial!") 6 | print("This example will demonstrate various complex cases and how they're handled.") 7 | 8 | print("\n1. Simple Model") 9 | class SimpleModel(BaseModel): 10 | integer_field: int 11 | string_field: str 12 | float_field: float 13 | boolean_field: bool 14 | 15 | SimpleDict = convert(SimpleModel) 16 | print(f"SimpleDict annotations: {SimpleDict.__annotations__}") 17 | 18 | print("\n2. Nested Model") 19 | class Address(BaseModel): 20 | street: str 21 | city: str 22 | country: str 23 | 24 | class NestedModel(BaseModel): 25 | name: str 26 | address: Address 27 | 28 | NestedDict = convert(NestedModel) 29 | print(f"NestedDict annotations: {NestedDict.__annotations__}") 30 | 31 | print("\n3. List Model") 32 | class ListModel(BaseModel): 33 | items: List[str] 34 | 35 | ListDict = convert(ListModel) 36 | print(f"ListDict annotations: {ListDict.__annotations__}") 37 | 38 | print("\n4. Dict Model") 39 | class DictModel(BaseModel): 40 | metadata: Dict[str, Any] 41 | 42 | DictDict = convert(DictModel) 43 | print(f"DictDict annotations: {DictDict.__annotations__}") 44 | 45 | print("\n5. Optional Model") 46 | class OptionalModel(BaseModel): 47 | maybe_string: Optional[str] 48 | 49 | OptionalDict = convert(OptionalModel) 50 | print(f"OptionalDict annotations: {OptionalDict.__annotations__}") 51 | 52 | print("\n6. Union Model") 53 | class UnionModel(BaseModel): 54 | union_field: Union[int, str, float] 55 | 56 | UnionDict = convert(UnionModel) 57 | print(f"UnionDict annotations: {UnionDict.__annotations__}") 58 | 59 | print("\n7. Complex Model") 60 | class Department(BaseModel): 61 | name: str 62 | code: int 63 | 64 | class ComplexModel(BaseModel): 65 | name: str 66 | age: int 67 | address: Address 68 | departments: List[Department] 69 | hobbies: List[str] 70 | metadata: Dict[str, Any] 71 | nickname: Optional[str] 72 | status: Union[str, int] 73 | 74 | ComplexDict = convert(ComplexModel) 75 | print(f"ComplexDict annotations: {ComplexDict.__annotations__}") 76 | 77 | print("\n8. Generic Model") 78 | class GenericModel(BaseModel): 79 | generic_field: List[Union[int, str]] 80 | 81 | GenericDict = convert(GenericModel) 82 | print(f"GenericDict annotations: {GenericDict.__annotations__}") 83 | 84 | print("\n9. Recursive Model") 85 | class RecursiveModel(BaseModel): 86 | value: int 87 | next: Optional['RecursiveModel'] = None 88 | 89 | RecursiveModel.model_rebuild() 90 | RecursiveDict = convert(RecursiveModel) 91 | print(f"RecursiveDict annotations: {RecursiveDict.__annotations__}") 92 | 93 | print("\n10. Model with Field constraints") 94 | class ModelWithField(BaseModel): 95 | constrained_string: str = Field(min_length=3, max_length=50) 96 | constrained_integer: int = Field(ge=0, le=100) 97 | 98 | FieldDict = convert(ModelWithField) 99 | print(f"FieldDict annotations: {FieldDict.__annotations__}") 100 | 101 | print("\nTutorial complete! You've seen how pydantype handles various complex cases.") 102 | print("Remember, the resulting TypedDict retains the structure of your Pydantic model,") 103 | print("but doesn't include validation logic. Use it for type hinting and static analysis.") -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | 164 | .pytest_cache/ 165 | .env 166 | main.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 PydanType: Pydantic to TypedDict Converter 2 | [![GitHub stars](https://img.shields.io/github/stars/unclecode/PyDanType.svg?style=social&label=Star&maxAge=2592000)](https://GitHub.com/unclecode/PyDanType/stargazers/) 3 | ![GitHub forks](https://img.shields.io/github/forks/unclecode/PyDanType.svg?style=social&label=Fork&maxAge=2592000) 4 | ![GitHub watchers](https://img.shields.io/github/watchers/unclecode/PyDanType.svg?style=social&label=Watch&maxAge=2592000) 5 | ![License](https://img.shields.io/github/license/unclecode/PyDanType) 6 | 7 | Convert your Pydantic models to TypedDict with ease! 🎉 8 | 9 | ## 🌟 Motivation 10 | 11 | Recently, Google Gemini introduced the ability to generate structured output, but here's the catch: unlike many environments that accept Pydantic models, they require TypeDict. It was tricky for me since I had a lot of Pydantic models in other projects, and I figured I wasn’t the only one. So, I created a simple utility that converts any Pydantic model to TypeDict, making it compatible with Gemini. Hopefully, this helps you as well! 💡 12 | 13 | That's when this utility was born! Now you can: 14 | 1. Define your models in Pydantic (with all its validation goodness) 👍 15 | 2. Convert them to TypedDict when needed (for APIs like Gemini) 🔄 16 | 3. Enjoy the benefits of both! 🎊 17 | 18 | ## 🚀 Quick Start 19 | 20 | Try it out instantly in our Colab notebook: 21 | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1GAzvhDxhMbeBP48bXFWyrS5SoLjLxufc#scrollTo=welcome_markdown) 22 | 23 | Install the package: 24 | 25 | ```bash 26 | pip install pip install git+https://github.com/unclecode/pydantype.git 27 | ``` 28 | 29 | Use it in your code: 30 | 31 | ```python 32 | from pydantic import BaseModel 33 | from pydantype import convert 34 | 35 | class MyModel(BaseModel): 36 | name: str 37 | age: int 38 | 39 | MyTypedDict = convert(MyModel) 40 | ``` 41 | 42 | ## 🌈 Gemini 1.5 Pro Example 43 | 44 | Here's how you can use this utility with Google's Gemini 1.5 Pro: 45 | 46 | ```python 47 | import google.generativeai as genai 48 | from pydantic import BaseModel 49 | from typing import List 50 | from pydantype import convert 51 | 52 | class Recipe(BaseModel): 53 | recipe_name: str 54 | ingredients: str 55 | 56 | class RecipeList(BaseModel): 57 | recipes: List[Recipe] 58 | 59 | RecipeListDict = convert(RecipeList) 60 | 61 | model = genai.GenerativeModel('gemini-1.5-pro', 62 | generation_config={ 63 | "response_mime_type": "application/json", 64 | "response_schema": RecipeListDict 65 | }) 66 | 67 | prompt = "List 3 popular cookie recipes" 68 | response = model.generate_content(prompt) 69 | print(response.text) 70 | ``` 71 | 72 | ## 🎨 General Example 73 | 74 | Here's a more general example showcasing various Pydantic features: 75 | 76 | ```python 77 | from typing import List, Optional 78 | from pydantic import BaseModel, Field 79 | from pydantype import convert 80 | 81 | class Address(BaseModel): 82 | street: str 83 | city: str 84 | country: str = Field(default="Unknown") 85 | 86 | class Person(BaseModel): 87 | name: str 88 | age: int 89 | address: Address 90 | hobbies: List[str] = [] 91 | nickname: Optional[str] = None 92 | 93 | PersonDict = convert(Person) 94 | 95 | # PersonDict is now a TypedDict with the same structure as Person 96 | ``` 97 | 98 | ## 🛠 Features 99 | 100 | - Converts simple and complex Pydantic models 🏗 101 | - Handles nested models, lists, and dictionaries 🔄 102 | - Supports optional fields and unions 🤝 103 | - Preserves type hints for better static analysis 🔍 104 | 105 | ## 🤝 Contributing 106 | 107 | Contributions are welcome! Feel free to open issues or submit pull requests. 🙌 108 | 109 | ## 📜 License 110 | 111 | This project is licensed under the MIT License. See the LICENSE file for details. 112 | 113 | --- 114 | 115 | Happy coding! 🎈🎊 -------------------------------------------------------------------------------- /tests/test_converter.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from typing import List, Dict, Optional, Union, Any, ForwardRef 3 | from pydantic import BaseModel, Field 4 | from pydantype import convert 5 | 6 | class Address(BaseModel): 7 | street: str 8 | city: str 9 | country: str 10 | 11 | class Department(BaseModel): 12 | name: str 13 | code: int 14 | 15 | class SimpleModel(BaseModel): 16 | integer_field: int 17 | string_field: str 18 | float_field: float 19 | boolean_field: bool 20 | 21 | class NestedModel(BaseModel): 22 | name: str 23 | address: Address 24 | 25 | class ListModel(BaseModel): 26 | items: List[str] 27 | 28 | class DictModel(BaseModel): 29 | metadata: Dict[str, Any] 30 | 31 | class OptionalModel(BaseModel): 32 | maybe_string: Optional[str] 33 | 34 | class UnionModel(BaseModel): 35 | union_field: Union[int, str, float] 36 | 37 | class ComplexModel(BaseModel): 38 | name: str 39 | age: int 40 | address: Address 41 | departments: List[Department] 42 | hobbies: List[str] 43 | metadata: Dict[str, Any] 44 | nickname: Optional[str] 45 | status: Union[str, int] 46 | 47 | class GenericModel(BaseModel): 48 | generic_field: List[Union[int, str]] 49 | 50 | class RecursiveModel(BaseModel): 51 | value: int 52 | next: Optional['RecursiveModel'] = None 53 | 54 | RecursiveModel.model_rebuild() 55 | 56 | class ModelWithField(BaseModel): 57 | constrained_string: str = Field(min_length=3, max_length=50) 58 | constrained_integer: int = Field(ge=0, le=100) 59 | 60 | class TestConverter(unittest.TestCase): 61 | 62 | def assert_typeddict_field(self, typeddict, field_name, expected_type): 63 | self.assertIn(field_name, typeddict.__annotations__) 64 | self.assertEqual(typeddict.__annotations__[field_name], expected_type) 65 | 66 | def test_simple_model(self): 67 | SimpleDict = convert(SimpleModel) 68 | self.assert_typeddict_field(SimpleDict, 'integer_field', int) 69 | self.assert_typeddict_field(SimpleDict, 'string_field', str) 70 | self.assert_typeddict_field(SimpleDict, 'float_field', float) 71 | self.assert_typeddict_field(SimpleDict, 'boolean_field', bool) 72 | 73 | def test_nested_model(self): 74 | NestedDict = convert(NestedModel) 75 | self.assert_typeddict_field(NestedDict, 'name', str) 76 | self.assertTrue(isinstance(NestedDict.__annotations__['address'], type)) 77 | 78 | def test_list_model(self): 79 | ListDict = convert(ListModel) 80 | self.assertTrue(ListDict.__annotations__['items'].__origin__ is list) 81 | self.assertEqual(ListDict.__annotations__['items'].__args__[0], str) 82 | 83 | def test_dict_model(self): 84 | DictDict = convert(DictModel) 85 | self.assertTrue(DictDict.__annotations__['metadata'].__origin__ is dict) 86 | self.assertEqual(DictDict.__annotations__['metadata'].__args__[0], str) 87 | self.assertEqual(DictDict.__annotations__['metadata'].__args__[1], Any) 88 | 89 | def test_optional_model(self): 90 | OptionalDict = convert(OptionalModel) 91 | self.assertTrue(OptionalDict.__annotations__['maybe_string'].__origin__ is Union) 92 | self.assertEqual(OptionalDict.__annotations__['maybe_string'].__args__, (str, type(None))) 93 | 94 | def test_union_model(self): 95 | UnionDict = convert(UnionModel) 96 | self.assertTrue(UnionDict.__annotations__['union_field'].__origin__ is Union) 97 | self.assertEqual(set(UnionDict.__annotations__['union_field'].__args__), {int, str, float}) 98 | 99 | def test_complex_model(self): 100 | ComplexDict = convert(ComplexModel) 101 | self.assert_typeddict_field(ComplexDict, 'name', str) 102 | self.assert_typeddict_field(ComplexDict, 'age', int) 103 | self.assertTrue(isinstance(ComplexDict.__annotations__['address'], type)) 104 | self.assertTrue(ComplexDict.__annotations__['departments'].__origin__ is list) 105 | self.assertTrue(isinstance(ComplexDict.__annotations__['departments'].__args__[0], type)) 106 | self.assertTrue(ComplexDict.__annotations__['hobbies'].__origin__ is list) 107 | self.assertEqual(ComplexDict.__annotations__['hobbies'].__args__[0], str) 108 | self.assertTrue(ComplexDict.__annotations__['metadata'].__origin__ is dict) 109 | self.assertEqual(ComplexDict.__annotations__['metadata'].__args__[0], str) 110 | self.assertEqual(ComplexDict.__annotations__['metadata'].__args__[1], Any) 111 | self.assertTrue(ComplexDict.__annotations__['nickname'].__origin__ is Union) 112 | self.assertEqual(ComplexDict.__annotations__['nickname'].__args__, (str, type(None))) 113 | self.assertTrue(ComplexDict.__annotations__['status'].__origin__ is Union) 114 | self.assertEqual(set(ComplexDict.__annotations__['status'].__args__), {str, int}) 115 | 116 | def test_generic_model(self): 117 | GenericDict = convert(GenericModel) 118 | self.assertTrue(GenericDict.__annotations__['generic_field'].__origin__ is list) 119 | self.assertTrue(GenericDict.__annotations__['generic_field'].__args__[0].__origin__ is Union) 120 | self.assertEqual(set(GenericDict.__annotations__['generic_field'].__args__[0].__args__), {int, str}) 121 | 122 | def test_recursive_model(self): 123 | RecursiveDict = convert(RecursiveModel) 124 | self.assert_typeddict_field(RecursiveDict, 'value', int) 125 | self.assertTrue(RecursiveDict.__annotations__['next'].__origin__ is Union) 126 | 127 | # Check that the first argument is the RecursiveModelDict 128 | self.assertEqual(RecursiveDict.__annotations__['next'].__args__[0].__name__, 'RecursiveModelDict') 129 | 130 | # Check that the second argument is NoneType 131 | self.assertEqual(RecursiveDict.__annotations__['next'].__args__[1], type(None)) 132 | 133 | def test_model_with_field(self): 134 | FieldDict = convert(ModelWithField) 135 | self.assert_typeddict_field(FieldDict, 'constrained_string', str) 136 | self.assert_typeddict_field(FieldDict, 'constrained_integer', int) 137 | 138 | if __name__ == '__main__': 139 | unittest.main() -------------------------------------------------------------------------------- /examples/Gemini_Example_with_pydantic_to_typeddict.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "id": "welcome_markdown" 7 | }, 8 | "source": [ 9 | "# 🚀 PydanType: Pydantic to TypedDict Converter with Google Gemini Example\n", 10 | "\n", 11 | "This notebook demonstrates how to use the `pydantype` library with Google's Generative AI (Gemini 1.5 Pro).\n", 12 | "\n", 13 | "## Setup Steps:\n", 14 | "1. Install required libraries\n", 15 | "2. Set up API token\n", 16 | "3. Import necessary modules\n", 17 | "4. Define Pydantic models\n", 18 | "5. Convert Pydantic models to TypedDict\n", 19 | "6. Use with Gemini 1.5 Pro\n", 20 | "\n", 21 | "Let's get started! 🎉" 22 | ] 23 | }, 24 | { 25 | "cell_type": "markdown", 26 | "metadata": { 27 | "id": "install_libraries_markdown" 28 | }, 29 | "source": [ 30 | "## 1. Install Required Libraries\n", 31 | "\n", 32 | "First, let's install the necessary libraries:" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": null, 38 | "metadata": { 39 | "id": "install_libraries" 40 | }, 41 | "outputs": [], 42 | "source": [ 43 | "!pip -q install google-generativeai\n", 44 | "!pip install git+https://github.com/unclecode/pydantype.git" 45 | ] 46 | }, 47 | { 48 | "cell_type": "markdown", 49 | "source": [ 50 | "## 2. Basic Example of Conversion using pydantic_to_typeddict" 51 | ], 52 | "metadata": { 53 | "id": "FLkSSlHvAVHF" 54 | } 55 | }, 56 | { 57 | "cell_type": "code", 58 | "source": [ 59 | "from pydantic import BaseModel\n", 60 | "from typing import List, Optional\n", 61 | "from pydantype import convert\n", 62 | "class Address(BaseModel):\n", 63 | " street: str\n", 64 | " city: str\n", 65 | " country: str\n", 66 | "\n", 67 | "class Person(BaseModel):\n", 68 | " name: str\n", 69 | " age: int\n", 70 | " address: Address\n", 71 | " hobbies: List[str]\n", 72 | " nickname: Optional[str]\n", 73 | "\n", 74 | "PersonDict = convert(Person)\n", 75 | "\n", 76 | "print(\"PersonDict structure:\")\n", 77 | "for field, type_hint in PersonDict.__annotations__.items():\n", 78 | " print(f\"{field}: {type_hint}\")\n", 79 | "\n", 80 | "# Create a Person instance\n", 81 | "person = Person(\n", 82 | " name=\"John Doe\",\n", 83 | " age=30,\n", 84 | " address=Address(street=\"123 Main St\", city=\"Anytown\", country=\"USA\"),\n", 85 | " hobbies=[\"reading\", \"swimming\"],\n", 86 | " nickname=\"Johnny\"\n", 87 | ")\n", 88 | "\n", 89 | "# Convert to dict and verify it matches PersonDict\n", 90 | "person_dict = person.model_dump()\n", 91 | "assert isinstance(person_dict, dict)\n", 92 | "print(\"\\nPerson instance converted to dict successfully and matches PersonDict structure.\")\n", 93 | "print(person_dict)" 94 | ], 95 | "metadata": { 96 | "colab": { 97 | "base_uri": "https://localhost:8080/" 98 | }, 99 | "id": "M0k_jyb-AWLO", 100 | "outputId": "2969c962-e67d-4db0-a32b-c6a594c671ce" 101 | }, 102 | "execution_count": 11, 103 | "outputs": [ 104 | { 105 | "output_type": "stream", 106 | "name": "stdout", 107 | "text": [ 108 | "PersonDict structure:\n", 109 | "name: \n", 110 | "age: \n", 111 | "address: \n", 112 | "hobbies: typing.List[str]\n", 113 | "nickname: typing.Optional[str]\n", 114 | "\n", 115 | "Person instance converted to dict successfully and matches PersonDict structure.\n", 116 | "{'name': 'John Doe', 'age': 30, 'address': {'street': '123 Main St', 'city': 'Anytown', 'country': 'USA'}, 'hobbies': ['reading', 'swimming'], 'nickname': 'Johnny'}\n" 117 | ] 118 | } 119 | ] 120 | }, 121 | { 122 | "cell_type": "markdown", 123 | "metadata": { 124 | "id": "setup_api_token_markdown" 125 | }, 126 | "source": [ 127 | "## 3. Set up API Token\n", 128 | "\n", 129 | "Now, let's set up the API token using Colab's userdata feature. Make sure you've added your Google AI Studio API key to Colab's secrets with the name 'GOOGLE_AI_STUDIO'." 130 | ] 131 | }, 132 | { 133 | "cell_type": "code", 134 | "execution_count": 12, 135 | "metadata": { 136 | "id": "setup_api_token", 137 | "colab": { 138 | "base_uri": "https://localhost:8080/" 139 | }, 140 | "outputId": "4963c3cc-f88b-47b8-b09b-ebe007e6bf19" 141 | }, 142 | "outputs": [ 143 | { 144 | "output_type": "stream", 145 | "name": "stdout", 146 | "text": [ 147 | "API token set successfully!\n" 148 | ] 149 | } 150 | ], 151 | "source": [ 152 | "import os\n", 153 | "from google.colab import userdata\n", 154 | "\n", 155 | "os.environ[\"GOOGLE_AI_STUDIO\"] = userdata.get('GOOGLE_AI_STUDIO')\n", 156 | "print(\"API token set successfully!\")" 157 | ] 158 | }, 159 | { 160 | "cell_type": "markdown", 161 | "metadata": { 162 | "id": "import_modules_markdown" 163 | }, 164 | "source": [ 165 | "## 4. Import Necessary Modules\n", 166 | "\n", 167 | "Let's import the required modules for our example:" 168 | ] 169 | }, 170 | { 171 | "cell_type": "code", 172 | "execution_count": 13, 173 | "metadata": { 174 | "id": "import_modules" 175 | }, 176 | "outputs": [], 177 | "source": [ 178 | "import google.generativeai as genai\n", 179 | "from pydantic import BaseModel\n", 180 | "from typing import List\n", 181 | "from pydantype import convert\n", 182 | "genai.configure(api_key=os.environ[\"GOOGLE_AI_STUDIO\"])" 183 | ] 184 | }, 185 | { 186 | "cell_type": "markdown", 187 | "metadata": { 188 | "id": "define_models_markdown" 189 | }, 190 | "source": [ 191 | "## 5. Define Pydantic Models\n", 192 | "\n", 193 | "Now, let's define our Pydantic models for the recipe example:" 194 | ] 195 | }, 196 | { 197 | "cell_type": "code", 198 | "execution_count": 14, 199 | "metadata": { 200 | "id": "define_models", 201 | "colab": { 202 | "base_uri": "https://localhost:8080/" 203 | }, 204 | "outputId": "6177b985-bbce-4226-f1e3-e8d7526eabb6" 205 | }, 206 | "outputs": [ 207 | { 208 | "output_type": "stream", 209 | "name": "stdout", 210 | "text": [ 211 | "Pydantic models defined successfully!\n" 212 | ] 213 | } 214 | ], 215 | "source": [ 216 | "class Recipe(BaseModel):\n", 217 | " recipe_name: str\n", 218 | " ingredients: str\n", 219 | "\n", 220 | "class RecipeList(BaseModel):\n", 221 | " recipes: List[Recipe]\n", 222 | "\n", 223 | "print(\"Pydantic models defined successfully!\")" 224 | ] 225 | }, 226 | { 227 | "cell_type": "markdown", 228 | "metadata": { 229 | "id": "convert_models_markdown" 230 | }, 231 | "source": [ 232 | "## 6. Convert Pydantic Models to TypedDict\n", 233 | "\n", 234 | "Let's convert our Pydantic models to TypedDict using the `pydantype` converter:" 235 | ] 236 | }, 237 | { 238 | "cell_type": "code", 239 | "execution_count": 18, 240 | "metadata": { 241 | "id": "convert_models", 242 | "colab": { 243 | "base_uri": "https://localhost:8080/" 244 | }, 245 | "outputId": "58320070-3cbf-415f-86cf-6b2ca63504a2" 246 | }, 247 | "outputs": [ 248 | { 249 | "output_type": "stream", 250 | "name": "stdout", 251 | "text": [ 252 | "Pydantic models convertted to TypedDict successfully!\n", 253 | "{'recipes': [Recipe(recipe_name='test', ingredients='test')]}\n" 254 | ] 255 | } 256 | ], 257 | "source": [ 258 | "RecipeListDict = convert(RecipeList)\n", 259 | "print(\"Pydantic models convertted to TypedDict successfully!\")\n", 260 | "example = RecipeListDict(recipes=[Recipe(recipe_name=\"test\", ingredients=\"test\")])\n", 261 | "assert isinstance(person_dict, dict)\n", 262 | "print(example)" 263 | ] 264 | }, 265 | { 266 | "cell_type": "markdown", 267 | "metadata": { 268 | "id": "use_with_gemini_markdown" 269 | }, 270 | "source": [ 271 | "## 6. Use with Gemini 1.5 Pro\n", 272 | "\n", 273 | "Finally, let's use our converted TypedDict with Gemini 1.5 Pro to generate some cookie recipes:" 274 | ] 275 | }, 276 | { 277 | "cell_type": "code", 278 | "execution_count": 19, 279 | "metadata": { 280 | "id": "use_with_gemini", 281 | "colab": { 282 | "base_uri": "https://localhost:8080/", 283 | "height": 106 284 | }, 285 | "outputId": "14e8791e-0b2b-49d2-f6fc-598e6f87efb7" 286 | }, 287 | "outputs": [ 288 | { 289 | "output_type": "stream", 290 | "name": "stdout", 291 | "text": [ 292 | "Generated Recipes:\n", 293 | "{\n", 294 | "\"recipes\": [{\"ingredients\": \"1 cup (2 sticks) unsalted butter, softened\\n1 cup granulated sugar\\n1/2 cup packed light brown sugar\\n2 large eggs\\n1 teaspoon pure vanilla extract\\n2 1/4 cups all-purpose flour\\n1 teaspoon baking soda\\n1/2 teaspoon salt\\n1 cup chocolate chips\", \"recipe_name\": \"Chocolate Chip Cookies\"}, {\"ingredients\": \"1 cup (2 sticks) unsalted butter, softened\\n1 cup granulated sugar\\n1/2 cup packed light brown sugar\\n2 large eggs\\n1 teaspoon pure vanilla extract\\n2 1/4 cups all-purpose flour\\n1 teaspoon baking soda\\n1 teaspoon ground cinnamon\\n1/4 teaspoon ground nutmeg\\n1/2 teaspoon salt\\n1 cup raisins\", \"recipe_name\": \"Oatmeal Raisin Cookies\"}, {\"ingredients\": \"1 cup (2 sticks) unsalted butter, softened\\n1 cup granulated sugar\\n1/2 cup packed light brown sugar\\n2 large eggs\\n1 teaspoon pure vanilla extract\\n2 1/4 cups all-purpose flour\\n1 teaspoon baking soda\\n1/2 teaspoon salt\\n1 cup white chocolate chips\\n1 cup dried cranberries\", \"recipe_name\": \"White Chocolate Cranberry Cookies\"}]\n", 295 | "} \n" 296 | ] 297 | } 298 | ], 299 | "source": [ 300 | "# Configure the model\n", 301 | "model = genai.GenerativeModel('gemini-1.5-pro',\n", 302 | " generation_config={\n", 303 | " \"response_mime_type\": \"application/json\",\n", 304 | " \"response_schema\": RecipeListDict\n", 305 | " })\n", 306 | "\n", 307 | "# Generate content\n", 308 | "prompt = \"List 3 popular cookie recipes\"\n", 309 | "response = model.generate_content(prompt)\n", 310 | "\n", 311 | "# Print the response\n", 312 | "print(\"Generated Recipes:\")\n", 313 | "print(response.text)" 314 | ] 315 | }, 316 | { 317 | "cell_type": "markdown", 318 | "metadata": { 319 | "id": "conclusion_markdown" 320 | }, 321 | "source": [ 322 | "## Conclusion\n", 323 | "\n", 324 | "Congratulations! 🎉 You've successfully used the `pydantype` converter with Google's Generative AI to generate cookie recipes.\n", 325 | "\n", 326 | "This example demonstrates how you can leverage the power of Pydantic for model definition and validation, while still being able to use APIs that require TypedDict, like Gemini 1.5 Pro.\n", 327 | "\n", 328 | "Feel free to modify the Pydantic models or the prompt to experiment further! Happy coding! 💻🍪" 329 | ] 330 | } 331 | ], 332 | "metadata": { 333 | "colab": { 334 | "provenance": [] 335 | }, 336 | "kernelspec": { 337 | "display_name": "Python 3", 338 | "name": "python3" 339 | }, 340 | "language_info": { 341 | "name": "python" 342 | } 343 | }, 344 | "nbformat": 4, 345 | "nbformat_minor": 0 346 | } --------------------------------------------------------------------------------