Repository: allmonday/pydantic-resolve (language: python)

Issue:
Support pydantic v2 
add internal support of pydantic v2

after that, remove pydantic2-resolve  https://github.com/allmonday/pydantic2-resolve


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 980f0a7..e768945 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -38,28 +38,7 @@ jobs:
         if: steps.cache-deps.outputs.cache-hit != 'true'
 
       - run: poetry install --no-interaction
+      - run: pip install pydantic==1.*
 
       - name: Test with pytest
-        run: poetry run pytest
-
-      - name: Test with pytest coverage
-        run: poetry run coverage run -m pytest
-
-      - name: "Combine"
-        run: |
-          poetry run coverage json
-          export TOTAL=$(python -c "import json;print(json.load(open('coverage.json'))['totals']['percent_covered_display'])")
-          echo "total=$TOTAL" >> $GITHUB_ENV
-          echo "### Total coverage: ${TOTAL}%" >> $GITHUB_STEP_SUMMARY
-
-      - name: "Make badge"
-        uses: schneegans/dynamic-badges-action@v1.6.0
-        with:
-          auth: ${{ secrets.GIST_TOKEN }}
-          gistID: 6f1661c6310e1b31c9a10b0d09d52d11 # replace with your real Gist id.
-          filename: covbadge.json
-          label: Coverage
-          message: ${{ env.total }}%
-          minColorRange: 50
-          maxColorRange: 90
-          valColorRange: ${{ env.total }}
+        run: poetry run pytest tests/pydantic_v1
diff --git a/.gitignore b/.gitignore
index ffd1fa3..febed10 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,5 @@ coverage.json
 .venv
 .tox
 docs/_build
-.vscode
\ No newline at end of file
+.vscode
+cov
\ No newline at end of file
diff --git a/README.md b/README.md
index 413054b..52540a3 100644
--- a/README.md
+++ b/README.md
@@ -1,26 +1,21 @@
 [![pypi](https://img.shields.io/pypi/v/pydantic-resolve.svg)](https://pypi.python.org/pypi/pydantic-resolve)
 [![Downloads](https://static.pepy.tech/personalized-badge/pydantic-resolve?period=month&units=abbreviation&left_color=grey&right_color=orange&left_text=Downloads)](https://pepy.tech/project/pydantic-resolve)
 ![Python Versions](https://img.shields.io/pypi/pyversions/pydantic-resolve)
-![Test Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/allmonday/6f1661c6310e1b31c9a10b0d09d52d11/raw/covbadge.json)
 [![CI](https://github.com/allmonday/pydantic_resolve/actions/workflows/ci.yml/badge.svg)](https://github.com/allmonday/pydantic_resolve/actions/workflows/ci.yml)
 
-Pydantic-resolve is a schema based, hierarchical solution for data fetching and crafting.
+Pydantic-resolve is a schema based solution for data management.
 
-It can compose schemas together with resolver (and dataloader), and then expose typescript types and methods to client.
-
-It can be a super simple alternative solution for GraphQL
+1. manage the deep data inside it's schema, instead of visiting if outside by manual traversal.
+2. pydantic-resolve runs a Level Order Traversal (BFS) inside and execute `resolve` and `post` during this process.
+3. describe the relationship between data in a way close to ERD (entity relationship diagram)
 
 ![](./doc/imgs/concept.png)
 
-**Features**:
-
-1. with pydantic schema and instances, resolver recursively resolve uncertain nodes and their descendants.
-2. nodes could be modified during post-process
-3. plugable, easy to combine together and reuse.
-
 ## Install
 
-> User of pydantic v2, please use [pydantic2-resolve](https://github.com/allmonday/pydantic2-resolve) instead.
+~~User of pydantic v2, please use [pydantic2-resolve](https://github.com/allmonday/pydantic2-resolve) instead.~~
+
+This lib now supports both pydantic v1 and v2 starts from v1.11.0
 
 ```shell
 pip install pydantic-resolve
@@ -28,195 +23,77 @@ pip install pydantic-resolve
 
 ## Hello world
 
-build blogs with comments
+manage your data inside the schema.
 
 ```python
-import asyncio
-import json
-from pydantic import BaseModel
-from pydantic_resolve import Resolver
-
-class Comment(BaseModel):
-    id: int
-    content: str
-
-class Blog(BaseModel):
-    id: int
-    title: str
-    content: str
-
-    comments: list[Comment] = []
-    def resolve_comments(self):
-        return get_comments(self.id)
-
-
-def get_blogs():
-    return [
-        dict(id=1, title="hello world", content="hello world detail"),
-        dict(id=2, title="hello Mars", content="hello Mars detail"),
-    ]
-
-def get_comments(id):
-    comments = [
-        dict(id=1, content="world is beautiful", blog_id=1),
-        dict(id=2, content="Mars is beautiful", blog_id=2),
-        dict(id=3, content="I love Mars", blog_id=2),
+class Tree(BaseModel):
+    name: str
+    number: int
+    description: str = ''
+    def resolve_description(self):
+        return f"I'm {self.name}, my number is {self.number}"
+    children: list['Tree'] = []
+
+
+tree = dict(
+    name='root',
+    number=1,
+    children=[
+        dict(
+            name='child1',
+            number=2,
+            children=[
+                dict(
+                    name='child1-1',
+                    number=3,
+                ),
+                dict(
+                    name='child1-2',
+                    number=4,
+                ),
+            ]
+        )
     ]
-    return [c for c in comments if c['blog_id'] == id]
-
+)
 
 async def main():
-    blogs = [Blog.parse_obj(blog) for blog in get_blogs()]
-    blogs = await Resolver().resolve(blogs)
-    print(json.dumps(blogs, indent=2, default=lambda o: o.dict()))
+    t = Tree.parse_obj(tree)
+    t = await Resolver().resolve(t)
+    print(t.json(indent=4))
 
+import asyncio
 asyncio.run(main())
 ```
 
-```json
-[
-  {
-    "id": 1,
-    "title": "hello world",
-    "content": "hello world detail",
-    "comments": [
-      {
-        "id": 1,
-        "content": "world is beautiful"
-      }
-    ]
-  },
-  {
-    "id": 2,
-    "title": "hello Mars",
-    "content": "hello Mars detail",
-    "comments": [
-      {
-        "id": 2,
-        "content": "Mars is beautiful"
-      },
-      {
-        "id": 3,
-        "content": "I love Mars"
-      }
-    ]
-  }
-]
-```
-
-## Communicating between ancestor and descendents
-
-getting sum of deep field without writing for loops.
-
-```python
-import asyncio
-from pydantic_resolve import Resolver, ICollector
-from pydantic import BaseModel
-
-class TotalEstimateCollector(ICollector):
-    def __init__(self, alias):
-        self.alias = alias
-        self.counter = 0
-
-    def add(self, val):
-        self.counter = self.counter + val
-
-    def values(self):
-        return self.counter
-
-class TotalDoneEstimateCollector(ICollector):
-    def __init__(self, alias):
-        self.alias = alias
-        self.counter = 0
-
-    def add(self, val):
-        done, estimate = val
-        if done:
-            self.counter = self.counter + estimate
+output
 
-    def values(self):
-        return self.counter
-
-class Task(BaseModel):
-    __pydantic_resolve_collect__ = {
-        'estimated': 'total_estimate',
-        ('done', 'estimated'): 'done_estimate'
-    }
-    name: str
-    estimated: int
-    done: bool
-
-class Story(BaseModel):
-    name: str
-    tasks: list[Task]
-
-class Sprint(BaseModel):
-    name: str
-    stories: list[Story]
-
-class Team(BaseModel):
-    name: str
-    sprints: list[Sprint]
-
-    total_estimated: int = 0
-    def post_total_estimated(self, counter=TotalEstimateCollector('total_estimate')):
-        return counter.values()
-
-    total_done_estimated: int = 0
-    def post_total_done_estimated(self, counter=TotalDoneEstimateCollector('done_estimate')):
-        return counter.values()
-
-input = {
-    "name": "Team A",
-    "sprints": [
+```json
+{
+  "name": "root",
+  "number": 1,
+  "description": "I'm root, my number is 1",
+  "children": [
+    {
+      "name": "child1",
+      "number": 2,
+      "description": "I'm child1, my number is 2",
+      "children": [
         {
-            "name": "Sprint 1",
-            "stories": [
-                {
-                    "name": "Story 1",
-                    "tasks": [
-                        {"name": "Task 1", "estimated": 5, "done": False},
-                        {"name": "Task 2", "estimated": 3, "done": True},
-                    ]
-                },
-                {
-                    "name": "Story 2",
-                    "tasks": [
-                        {"name": "Task 3", "estimated": 2, "done": True},
-                        {"name": "Task 4", "estimated": 1, "done": True},
-                    ]
-                }
-            ]
+          "name": "child1-1",
+          "number": 3,
+          "description": "I'm child1-1, my number is 3",
+          "children": []
         },
         {
-            "name": "Sprint 2",
-            "stories": [
-                {
-                    "name": "Story 3",
-                    "tasks": [
-                        {"name": "Task 5", "estimated": 3, "done": False},
-                        {"name": "Task 6", "estimated": 2, "done": False},
-                    ]
-                },
-                {
-                    "name": "Story 4",
-                    "tasks": [
-                        {"name": "Task 7", "estimated": 1, "done": False},
-                        {"name": "Task 8", "estimated": 3, "done": False},
-                    ]
-                }
-            ]
+          "name": "child1-2",
+          "number": 4,
+          "description": "I'm child1-2, my number is 4",
+          "children": []
         }
-    ]
+      ]
+    }
+  ]
 }
-
-async def main():
-    team = Team.parse_obj(input)
-    team = await Resolver().resolve(team)
-    print(team.total_estimated)  # 20
-    print(team.total_done_estimated)  # 6
-
-asyncio.run(main())
 ```
 
 ## Documents
@@ -226,6 +103,19 @@ asyncio.run(main())
 - **Demo**: https://github.com/allmonday/pydantic-resolve-demo
 - **Composition oriented pattern**: https://github.com/allmonday/composition-oriented-development-pattern
 
+## Test and coverage
+
+```shell
+tox
+```
+
+```shell
+tox -e coverage
+python -m http.server
+```
+
+latest coverage: 98%
+
 ## Sponsor
 
 If this code helps and you wish to support me
diff --git a/docs/changelog.en.md b/docs/changelog.en.md
index ffc13f0..9b94ac3 100644
--- a/docs/changelog.en.md
+++ b/docs/changelog.en.md
@@ -1,5 +1,10 @@
 # Changelog
 
+## v.1.11.0 (2024.10.24)
+
+- support pydantic v2
+- remove hidden_fields in model_config
+
 ## v.1.10.8 (2024.8.27)
 
 - collector suppor tuple for value
diff --git a/docs/changelog.zh.md b/docs/changelog.zh.md
index ffc13f0..9b94ac3 100644
--- a/docs/changelog.zh.md
+++ b/docs/changelog.zh.md
@@ -1,5 +1,10 @@
 # Changelog
 
+## v.1.11.0 (2024.10.24)
+
+- support pydantic v2
+- remove hidden_fields in model_config
+
 ## v.1.10.8 (2024.8.27)
 
 - collector suppor tuple for value
diff --git a/examples/13_for_loop.py b/examples/13_for_loop.py
index d5a3789..a06f9e3 100644
--- a/examples/13_for_loop.py
+++ b/examples/13_for_loop.py
@@ -85,6 +85,14 @@ class Story(BaseModel):
     name: str
     tasks: list[Task]
 
+    total_estimated: int = 0
+    def post_total_estimated(self, counter=TotalEstimateCollector('total_estimate')):
+        return counter.values()
+
+    total_done_estimated: int = 0
+    def post_total_done_estimated(self, counter=TotalDoneEstimateCollector('done_estimate')):
+        return counter.values()
+
 class Sprint(BaseModel):
     name: str
     stories: list[Story]
@@ -105,6 +113,10 @@ class Team(BaseModel):
     def post_total_estimated(self, counter=TotalEstimateCollector('total_estimate')):
         return counter.values()
 
+    total_estimated2: int = 0
+    def post_total_estimated2(self, counter=TotalEstimateCollector('total_estimate')):
+        return counter.values()
+
     total_done_estimated: int = 0
     def post_total_done_estimated(self, counter=TotalDoneEstimateCollector('done_estimate')):
         return counter.values()
diff --git a/examples/14_simple.py b/examples/14_simple.py
new file mode 100644
index 0000000..c096c5d
--- /dev/null
+++ b/examples/14_simple.py
@@ -0,0 +1,40 @@
+from pydantic import BaseModel
+from pydantic_resolve import Resolver
+
+class Tree(BaseModel):
+    name: str
+    number: int
+    description: str = ''
+    def resolve_description(self):
+        return f"I'm {self.name}, my number is {self.number}"
+    children: list['Tree'] = []
+
+
+tree = dict(
+    name='root',
+    number=1,
+    children=[
+        dict(
+            name='child1',
+            number=2,
+            children=[
+                dict(
+                    name='child1-1',
+                    number=3,
+                ),
+                dict(
+                    name='child1-2',
+                    number=4,
+                ),
+            ]
+        )
+    ]
+)
+
+async def main():
+    t = Tree.parse_obj(tree)
+    t = await Resolver().resolve(t)
+    print(t.json(indent=4))
+
+import asyncio
+asyncio.run(main())
\ No newline at end of file
diff --git a/poetry.lock b/poetry.lock
index 1f0d930..2bc1822 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
 
 [[package]]
 name = "aiodataloader"
@@ -73,24 +73,24 @@ dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
 
 [[package]]
 name = "cachetools"
-version = "5.3.3"
+version = "5.5.0"
 description = "Extensible memoizing collections and decorators"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"},
-    {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"},
+    {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"},
+    {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"},
 ]
 
 [[package]]
 name = "certifi"
-version = "2024.7.4"
+version = "2024.8.30"
 description = "Python package for providing Mozilla's CA Bundle."
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
-    {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
+    {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"},
+    {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"},
 ]
 
 [[package]]
@@ -106,101 +106,116 @@ files = [
 
 [[package]]
 name = "charset-normalizer"
-version = "3.3.2"
+version = "3.4.0"
 description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
 optional = false
 python-versions = ">=3.7.0"
 files = [
-    {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
-    {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
-    {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
-    {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
-    {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
-    {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
+    {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"},
+    {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"},
+    {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"},
+    {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"},
+    {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"},
+    {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"},
+    {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"},
+    {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"},
+    {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"},
+    {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"},
+    {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"},
+    {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"},
+    {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"},
+    {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"},
+    {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"},
+    {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"},
+    {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"},
+    {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"},
+    {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"},
+    {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"},
+    {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"},
+    {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"},
+    {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"},
+    {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"},
+    {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"},
+    {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"},
+    {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"},
+    {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"},
+    {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"},
+    {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"},
+    {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"},
+    {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"},
+    {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"},
+    {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"},
+    {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"},
+    {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"},
+    {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"},
+    {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"},
+    {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"},
+    {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"},
+    {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"},
+    {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"},
+    {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"},
+    {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"},
+    {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"},
+    {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"},
+    {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"},
+    {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"},
+    {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"},
+    {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"},
+    {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"},
+    {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"},
+    {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"},
+    {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"},
+    {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"},
+    {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"},
+    {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"},
+    {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"},
+    {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"},
+    {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"},
+    {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"},
+    {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"},
+    {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"},
+    {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"},
+    {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"},
+    {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"},
+    {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"},
+    {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"},
+    {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"},
+    {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"},
+    {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"},
+    {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"},
+    {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"},
+    {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"},
+    {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"},
+    {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"},
+    {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"},
+    {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"},
+    {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"},
+    {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"},
+    {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"},
+    {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"},
+    {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"},
+    {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"},
+    {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"},
+    {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"},
+    {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"},
+    {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"},
+    {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"},
+    {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"},
+    {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"},
+    {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"},
+    {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"},
+    {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"},
+    {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"},
+    {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"},
+    {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"},
+    {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"},
+    {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"},
+    {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"},
+    {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"},
+    {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"},
+    {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"},
+    {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"},
+    {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"},
 ]
 
 [[package]]
@@ -306,13 +321,13 @@ toml = ["tomli"]
 
 [[package]]
 name = "distlib"
-version = "0.3.8"
+version = "0.3.9"
 description = "Distribution utilities"
 optional = false
 python-versions = "*"
 files = [
-    {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
-    {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
+    {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"},
+    {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"},
 ]
 
 [[package]]
@@ -331,24 +346,22 @@ test = ["pytest (>=6)"]
 
 [[package]]
 name = "fastapi"
-version = "0.96.1"
+version = "0.101.1"
 description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "fastapi-0.96.1-py3-none-any.whl", hash = "sha256:22d773ce95f14f04f8f37a0c8998fc163e67af83b65510d2879de6cbaaa10215"},
-    {file = "fastapi-0.96.1.tar.gz", hash = "sha256:5c1d243030e63089ccfc0aec69c2da6d619943917727e8e82ee502358d5119bf"},
+    {file = "fastapi-0.101.1-py3-none-any.whl", hash = "sha256:aef5f8676eb1b8389952e1fe734abe20f04b71f6936afcc53b320ba79b686a4b"},
+    {file = "fastapi-0.101.1.tar.gz", hash = "sha256:7b32000d14ca9992f7461117b81e4ef9ff0c07936af641b4fe40e67d5f9d63cb"},
 ]
 
 [package.dependencies]
-pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0"
+pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
 starlette = ">=0.27.0,<0.28.0"
+typing-extensions = ">=4.5.0"
 
 [package.extras]
-all = ["email-validator (>=1.1.1)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
-dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.138)", "uvicorn[standard] (>=0.12.0,<0.21.0)"]
-doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer-cli (>=0.0.13,<0.0.14)", "typer[all] (>=0.6.1,<0.8.0)"]
-test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==23.1.0)", "coverage[toml] (>=6.5.0,<8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.7)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.138)", "sqlalchemy (>=1.3.18,<1.4.43)", "types-orjson (==3.6.2)", "types-ujson (==5.7.0.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"]
+all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
 
 [[package]]
 name = "filelock"
@@ -384,69 +397,84 @@ dev = ["flake8", "markdown", "twine", "wheel"]
 
 [[package]]
 name = "greenlet"
-version = "3.0.3"
+version = "3.1.1"
 description = "Lightweight in-process concurrent programming"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"},
-    {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"},
-    {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"},
-    {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"},
-    {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"},
-    {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"},
-    {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"},
-    {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"},
-    {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"},
-    {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"},
-    {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"},
-    {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"},
-    {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"},
-    {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"},
-    {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"},
-    {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"},
-    {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"},
-    {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"},
-    {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"},
-    {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"},
-    {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"},
-    {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"},
-    {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"},
-    {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"},
-    {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"},
-    {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"},
-    {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"},
-    {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"},
-    {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"},
-    {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"},
-    {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"},
-    {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"},
-    {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"},
-    {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"},
-    {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"},
-    {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"},
-    {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"},
-    {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"},
-    {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"},
-    {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"},
-    {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"},
-    {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"},
-    {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"},
-    {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"},
-    {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"},
-    {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"},
-    {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"},
-    {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"},
-    {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"},
-    {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"},
-    {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"},
-    {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"},
-    {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"},
-    {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"},
-    {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"},
-    {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"},
-    {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"},
-    {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"},
+    {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"},
+    {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"},
+    {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0"},
+    {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120"},
+    {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc"},
+    {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617"},
+    {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7"},
+    {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6"},
+    {file = "greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80"},
+    {file = "greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70"},
+    {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159"},
+    {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e"},
+    {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1"},
+    {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383"},
+    {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a"},
+    {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511"},
+    {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395"},
+    {file = "greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39"},
+    {file = "greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d"},
+    {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79"},
+    {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa"},
+    {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441"},
+    {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36"},
+    {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9"},
+    {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0"},
+    {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942"},
+    {file = "greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01"},
+    {file = "greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1"},
+    {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff"},
+    {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a"},
+    {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e"},
+    {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4"},
+    {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e"},
+    {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1"},
+    {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c"},
+    {file = "greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761"},
+    {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011"},
+    {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13"},
+    {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475"},
+    {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b"},
+    {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822"},
+    {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01"},
+    {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6"},
+    {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291"},
+    {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981"},
+    {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803"},
+    {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc"},
+    {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de"},
+    {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa"},
+    {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af"},
+    {file = "greenlet-3.1.1-cp37-cp37m-win32.whl", hash = "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798"},
+    {file = "greenlet-3.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef"},
+    {file = "greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9"},
+    {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111"},
+    {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81"},
+    {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba"},
+    {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8"},
+    {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1"},
+    {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd"},
+    {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7"},
+    {file = "greenlet-3.1.1-cp38-cp38-win32.whl", hash = "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef"},
+    {file = "greenlet-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d"},
+    {file = "greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3"},
+    {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42"},
+    {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f"},
+    {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437"},
+    {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145"},
+    {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c"},
+    {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e"},
+    {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e"},
+    {file = "greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c"},
+    {file = "greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22"},
+    {file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"},
 ]
 
 [package.extras]
@@ -469,15 +497,18 @@ typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
 
 [[package]]
 name = "idna"
-version = "3.7"
+version = "3.10"
 description = "Internationalized Domain Names in Applications (IDNA)"
 optional = false
-python-versions = ">=3.5"
+python-versions = ">=3.6"
 files = [
-    {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
-    {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
+    {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
+    {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
 ]
 
+[package.extras]
+all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
+
 [[package]]
 name = "importlib-metadata"
 version = "6.7.0"
@@ -722,14 +753,19 @@ files = [
 
 [[package]]
 name = "paginate"
-version = "0.5.6"
+version = "0.5.7"
 description = "Divides large result sets into pages for easier browsing"
 optional = false
 python-versions = "*"
 files = [
-    {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"},
+    {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"},
+    {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"},
 ]
 
+[package.extras]
+dev = ["pytest", "tox"]
+lint = ["black"]
+
 [[package]]
 name = "pathspec"
 version = "0.11.2"
@@ -779,54 +815,54 @@ testing = ["pytest", "pytest-benchmark"]
 
 [[package]]
 name = "pydantic"
-version = "1.10.17"
+version = "1.10.18"
 description = "Data validation and settings management using python type hints"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "pydantic-1.10.17-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fa51175313cc30097660b10eec8ca55ed08bfa07acbfe02f7a42f6c242e9a4b"},
-    {file = "pydantic-1.10.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7e8988bb16988890c985bd2093df9dd731bfb9d5e0860db054c23034fab8f7a"},
-    {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:371dcf1831f87c9e217e2b6a0c66842879a14873114ebb9d0861ab22e3b5bb1e"},
-    {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4866a1579c0c3ca2c40575398a24d805d4db6cb353ee74df75ddeee3c657f9a7"},
-    {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:543da3c6914795b37785703ffc74ba4d660418620cc273490d42c53949eeeca6"},
-    {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7623b59876f49e61c2e283551cc3647616d2fbdc0b4d36d3d638aae8547ea681"},
-    {file = "pydantic-1.10.17-cp310-cp310-win_amd64.whl", hash = "sha256:409b2b36d7d7d19cd8310b97a4ce6b1755ef8bd45b9a2ec5ec2b124db0a0d8f3"},
-    {file = "pydantic-1.10.17-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fa43f362b46741df8f201bf3e7dff3569fa92069bcc7b4a740dea3602e27ab7a"},
-    {file = "pydantic-1.10.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a72d2a5ff86a3075ed81ca031eac86923d44bc5d42e719d585a8eb547bf0c9b"},
-    {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4ad32aed3bf5eea5ca5decc3d1bbc3d0ec5d4fbcd72a03cdad849458decbc63"},
-    {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb4e741782e236ee7dc1fb11ad94dc56aabaf02d21df0e79e0c21fe07c95741"},
-    {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d2f89a719411cb234105735a520b7c077158a81e0fe1cb05a79c01fc5eb59d3c"},
-    {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db3b48d9283d80a314f7a682f7acae8422386de659fffaba454b77a083c3937d"},
-    {file = "pydantic-1.10.17-cp311-cp311-win_amd64.whl", hash = "sha256:9c803a5113cfab7bbb912f75faa4fc1e4acff43e452c82560349fff64f852e1b"},
-    {file = "pydantic-1.10.17-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:820ae12a390c9cbb26bb44913c87fa2ff431a029a785642c1ff11fed0a095fcb"},
-    {file = "pydantic-1.10.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c1e51d1af306641b7d1574d6d3307eaa10a4991542ca324f0feb134fee259815"},
-    {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e53fb834aae96e7b0dadd6e92c66e7dd9cdf08965340ed04c16813102a47fab"},
-    {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e2495309b1266e81d259a570dd199916ff34f7f51f1b549a0d37a6d9b17b4dc"},
-    {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:098ad8de840c92ea586bf8efd9e2e90c6339d33ab5c1cfbb85be66e4ecf8213f"},
-    {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:525bbef620dac93c430d5d6bdbc91bdb5521698d434adf4434a7ef6ffd5c4b7f"},
-    {file = "pydantic-1.10.17-cp312-cp312-win_amd64.whl", hash = "sha256:6654028d1144df451e1da69a670083c27117d493f16cf83da81e1e50edce72ad"},
-    {file = "pydantic-1.10.17-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c87cedb4680d1614f1d59d13fea353faf3afd41ba5c906a266f3f2e8c245d655"},
-    {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11289fa895bcbc8f18704efa1d8020bb9a86314da435348f59745473eb042e6b"},
-    {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94833612d6fd18b57c359a127cbfd932d9150c1b72fea7c86ab58c2a77edd7c7"},
-    {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d4ecb515fa7cb0e46e163ecd9d52f9147ba57bc3633dca0e586cdb7a232db9e3"},
-    {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7017971ffa7fd7808146880aa41b266e06c1e6e12261768a28b8b41ba55c8076"},
-    {file = "pydantic-1.10.17-cp37-cp37m-win_amd64.whl", hash = "sha256:e840e6b2026920fc3f250ea8ebfdedf6ea7a25b77bf04c6576178e681942ae0f"},
-    {file = "pydantic-1.10.17-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bfbb18b616abc4df70591b8c1ff1b3eabd234ddcddb86b7cac82657ab9017e33"},
-    {file = "pydantic-1.10.17-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebb249096d873593e014535ab07145498957091aa6ae92759a32d40cb9998e2e"},
-    {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c209af63ccd7b22fba94b9024e8b7fd07feffee0001efae50dd99316b27768"},
-    {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b40c9e13a0b61583e5599e7950490c700297b4a375b55b2b592774332798b7"},
-    {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c31d281c7485223caf6474fc2b7cf21456289dbaa31401844069b77160cab9c7"},
-    {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae5184e99a060a5c80010a2d53c99aee76a3b0ad683d493e5f0620b5d86eeb75"},
-    {file = "pydantic-1.10.17-cp38-cp38-win_amd64.whl", hash = "sha256:ad1e33dc6b9787a6f0f3fd132859aa75626528b49cc1f9e429cdacb2608ad5f0"},
-    {file = "pydantic-1.10.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e17c0ee7192e54a10943f245dc79e36d9fe282418ea05b886e1c666063a7b54"},
-    {file = "pydantic-1.10.17-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cafb9c938f61d1b182dfc7d44a7021326547b7b9cf695db5b68ec7b590214773"},
-    {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95ef534e3c22e5abbdbdd6f66b6ea9dac3ca3e34c5c632894f8625d13d084cbe"},
-    {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d96b8799ae3d782df7ec9615cb59fc32c32e1ed6afa1b231b0595f6516e8ab"},
-    {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ab2f976336808fd5d539fdc26eb51f9aafc1f4b638e212ef6b6f05e753c8011d"},
-    {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8ad363330557beac73159acfbeed220d5f1bfcd6b930302a987a375e02f74fd"},
-    {file = "pydantic-1.10.17-cp39-cp39-win_amd64.whl", hash = "sha256:48db882e48575ce4b39659558b2f9f37c25b8d348e37a2b4e32971dd5a7d6227"},
-    {file = "pydantic-1.10.17-py3-none-any.whl", hash = "sha256:e41b5b973e5c64f674b3b4720286ded184dcc26a691dd55f34391c62c6934688"},
-    {file = "pydantic-1.10.17.tar.gz", hash = "sha256:f434160fb14b353caf634149baaf847206406471ba70e64657c1e8330277a991"},
+    {file = "pydantic-1.10.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e405ffcc1254d76bb0e760db101ee8916b620893e6edfbfee563b3c6f7a67c02"},
+    {file = "pydantic-1.10.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e306e280ebebc65040034bff1a0a81fd86b2f4f05daac0131f29541cafd80b80"},
+    {file = "pydantic-1.10.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11d9d9b87b50338b1b7de4ebf34fd29fdb0d219dc07ade29effc74d3d2609c62"},
+    {file = "pydantic-1.10.18-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b661ce52c7b5e5f600c0c3c5839e71918346af2ef20062705ae76b5c16914cab"},
+    {file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c20f682defc9ef81cd7eaa485879ab29a86a0ba58acf669a78ed868e72bb89e0"},
+    {file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c5ae6b7c8483b1e0bf59e5f1843e4fd8fd405e11df7de217ee65b98eb5462861"},
+    {file = "pydantic-1.10.18-cp310-cp310-win_amd64.whl", hash = "sha256:74fe19dda960b193b0eb82c1f4d2c8e5e26918d9cda858cbf3f41dd28549cb70"},
+    {file = "pydantic-1.10.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72fa46abace0a7743cc697dbb830a41ee84c9db8456e8d77a46d79b537efd7ec"},
+    {file = "pydantic-1.10.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef0fe7ad7cbdb5f372463d42e6ed4ca9c443a52ce544472d8842a0576d830da5"},
+    {file = "pydantic-1.10.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a00e63104346145389b8e8f500bc6a241e729feaf0559b88b8aa513dd2065481"},
+    {file = "pydantic-1.10.18-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae6fa2008e1443c46b7b3a5eb03800121868d5ab6bc7cda20b5df3e133cde8b3"},
+    {file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9f463abafdc92635da4b38807f5b9972276be7c8c5121989768549fceb8d2588"},
+    {file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3445426da503c7e40baccefb2b2989a0c5ce6b163679dd75f55493b460f05a8f"},
+    {file = "pydantic-1.10.18-cp311-cp311-win_amd64.whl", hash = "sha256:467a14ee2183bc9c902579bb2f04c3d3dac00eff52e252850509a562255b2a33"},
+    {file = "pydantic-1.10.18-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:efbc8a7f9cb5fe26122acba1852d8dcd1e125e723727c59dcd244da7bdaa54f2"},
+    {file = "pydantic-1.10.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24a4a159d0f7a8e26bf6463b0d3d60871d6a52eac5bb6a07a7df85c806f4c048"},
+    {file = "pydantic-1.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b74be007703547dc52e3c37344d130a7bfacca7df112a9e5ceeb840a9ce195c7"},
+    {file = "pydantic-1.10.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcb20d4cb355195c75000a49bb4a31d75e4295200df620f454bbc6bdf60ca890"},
+    {file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:46f379b8cb8a3585e3f61bf9ae7d606c70d133943f339d38b76e041ec234953f"},
+    {file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbfbca662ed3729204090c4d09ee4beeecc1a7ecba5a159a94b5a4eb24e3759a"},
+    {file = "pydantic-1.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:c6d0a9f9eccaf7f438671a64acf654ef0d045466e63f9f68a579e2383b63f357"},
+    {file = "pydantic-1.10.18-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3d5492dbf953d7d849751917e3b2433fb26010d977aa7a0765c37425a4026ff1"},
+    {file = "pydantic-1.10.18-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe734914977eed33033b70bfc097e1baaffb589517863955430bf2e0846ac30f"},
+    {file = "pydantic-1.10.18-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15fdbe568beaca9aacfccd5ceadfb5f1a235087a127e8af5e48df9d8a45ae85c"},
+    {file = "pydantic-1.10.18-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c3e742f62198c9eb9201781fbebe64533a3bbf6a76a91b8d438d62b813079dbc"},
+    {file = "pydantic-1.10.18-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:19a3bd00b9dafc2cd7250d94d5b578edf7a0bd7daf102617153ff9a8fa37871c"},
+    {file = "pydantic-1.10.18-cp37-cp37m-win_amd64.whl", hash = "sha256:2ce3fcf75b2bae99aa31bd4968de0474ebe8c8258a0110903478bd83dfee4e3b"},
+    {file = "pydantic-1.10.18-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:335a32d72c51a313b33fa3a9b0fe283503272ef6467910338e123f90925f0f03"},
+    {file = "pydantic-1.10.18-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:34a3613c7edb8c6fa578e58e9abe3c0f5e7430e0fc34a65a415a1683b9c32d9a"},
+    {file = "pydantic-1.10.18-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9ee4e6ca1d9616797fa2e9c0bfb8815912c7d67aca96f77428e316741082a1b"},
+    {file = "pydantic-1.10.18-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23e8ec1ce4e57b4f441fc91e3c12adba023fedd06868445a5b5f1d48f0ab3682"},
+    {file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:44ae8a3e35a54d2e8fa88ed65e1b08967a9ef8c320819a969bfa09ce5528fafe"},
+    {file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5389eb3b48a72da28c6e061a247ab224381435256eb541e175798483368fdd3"},
+    {file = "pydantic-1.10.18-cp38-cp38-win_amd64.whl", hash = "sha256:069b9c9fc645474d5ea3653788b544a9e0ccd3dca3ad8c900c4c6eac844b4620"},
+    {file = "pydantic-1.10.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:80b982d42515632eb51f60fa1d217dfe0729f008e81a82d1544cc392e0a50ddf"},
+    {file = "pydantic-1.10.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aad8771ec8dbf9139b01b56f66386537c6fe4e76c8f7a47c10261b69ad25c2c9"},
+    {file = "pydantic-1.10.18-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941a2eb0a1509bd7f31e355912eb33b698eb0051730b2eaf9e70e2e1589cae1d"},
+    {file = "pydantic-1.10.18-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65f7361a09b07915a98efd17fdec23103307a54db2000bb92095457ca758d485"},
+    {file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6951f3f47cb5ca4da536ab161ac0163cab31417d20c54c6de5ddcab8bc813c3f"},
+    {file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7a4c5eec138a9b52c67f664c7d51d4c7234c5ad65dd8aacd919fb47445a62c86"},
+    {file = "pydantic-1.10.18-cp39-cp39-win_amd64.whl", hash = "sha256:49e26c51ca854286bffc22b69787a8d4063a62bf7d83dc21d44d2ff426108518"},
+    {file = "pydantic-1.10.18-py3-none-any.whl", hash = "sha256:06a189b81ffc52746ec9c8c007f16e5167c8b0a696e1a726369327e3db7b2a82"},
+    {file = "pydantic-1.10.18.tar.gz", hash = "sha256:baebdff1907d1d96a139c25136a9bb7d17e118f133a76a2ef3b845e831e3403a"},
 ]
 
 [package.dependencies]
@@ -964,13 +1000,13 @@ six = ">=1.5"
 
 [[package]]
 name = "pytz"
-version = "2024.1"
+version = "2024.2"
 description = "World timezone definitions, modern and historical"
 optional = false
 python-versions = "*"
 files = [
-    {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"},
-    {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"},
+    {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"},
+    {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"},
 ]
 
 [[package]]
@@ -985,6 +1021,7 @@ files = [
     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
+    {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
     {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
     {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
     {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
@@ -992,8 +1029,16 @@ files = [
     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
+    {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
     {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
     {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
+    {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
+    {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
+    {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
+    {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
+    {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
+    {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
+    {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
     {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
     {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
     {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
@@ -1010,6 +1055,7 @@ files = [
     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
+    {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
     {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
     {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
     {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
@@ -1017,6 +1063,7 @@ files = [
     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
+    {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
     {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
     {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
     {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
@@ -1178,64 +1225,72 @@ files = [
 
 [[package]]
 name = "sqlalchemy"
-version = "2.0.31"
+version = "2.0.36"
 description = "Database Abstraction Library"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "SQLAlchemy-2.0.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f2a213c1b699d3f5768a7272de720387ae0122f1becf0901ed6eaa1abd1baf6c"},
-    {file = "SQLAlchemy-2.0.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9fea3d0884e82d1e33226935dac990b967bef21315cbcc894605db3441347443"},
-    {file = "SQLAlchemy-2.0.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ad7f221d8a69d32d197e5968d798217a4feebe30144986af71ada8c548e9fa"},
-    {file = "SQLAlchemy-2.0.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2bee229715b6366f86a95d497c347c22ddffa2c7c96143b59a2aa5cc9eebbc"},
-    {file = "SQLAlchemy-2.0.31-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cd5b94d4819c0c89280b7c6109c7b788a576084bf0a480ae17c227b0bc41e109"},
-    {file = "SQLAlchemy-2.0.31-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:750900a471d39a7eeba57580b11983030517a1f512c2cb287d5ad0fcf3aebd58"},
-    {file = "SQLAlchemy-2.0.31-cp310-cp310-win32.whl", hash = "sha256:7bd112be780928c7f493c1a192cd8c5fc2a2a7b52b790bc5a84203fb4381c6be"},
-    {file = "SQLAlchemy-2.0.31-cp310-cp310-win_amd64.whl", hash = "sha256:5a48ac4d359f058474fadc2115f78a5cdac9988d4f99eae44917f36aa1476327"},
-    {file = "SQLAlchemy-2.0.31-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f68470edd70c3ac3b6cd5c2a22a8daf18415203ca1b036aaeb9b0fb6f54e8298"},
-    {file = "SQLAlchemy-2.0.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e2c38c2a4c5c634fe6c3c58a789712719fa1bf9b9d6ff5ebfce9a9e5b89c1ca"},
-    {file = "SQLAlchemy-2.0.31-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd15026f77420eb2b324dcb93551ad9c5f22fab2c150c286ef1dc1160f110203"},
-    {file = "SQLAlchemy-2.0.31-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2196208432deebdfe3b22185d46b08f00ac9d7b01284e168c212919891289396"},
-    {file = "SQLAlchemy-2.0.31-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:352b2770097f41bff6029b280c0e03b217c2dcaddc40726f8f53ed58d8a85da4"},
-    {file = "SQLAlchemy-2.0.31-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:56d51ae825d20d604583f82c9527d285e9e6d14f9a5516463d9705dab20c3740"},
-    {file = "SQLAlchemy-2.0.31-cp311-cp311-win32.whl", hash = "sha256:6e2622844551945db81c26a02f27d94145b561f9d4b0c39ce7bfd2fda5776dac"},
-    {file = "SQLAlchemy-2.0.31-cp311-cp311-win_amd64.whl", hash = "sha256:ccaf1b0c90435b6e430f5dd30a5aede4764942a695552eb3a4ab74ed63c5b8d3"},
-    {file = "SQLAlchemy-2.0.31-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3b74570d99126992d4b0f91fb87c586a574a5872651185de8297c6f90055ae42"},
-    {file = "SQLAlchemy-2.0.31-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f77c4f042ad493cb8595e2f503c7a4fe44cd7bd59c7582fd6d78d7e7b8ec52c"},
-    {file = "SQLAlchemy-2.0.31-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd1591329333daf94467e699e11015d9c944f44c94d2091f4ac493ced0119449"},
-    {file = "SQLAlchemy-2.0.31-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74afabeeff415e35525bf7a4ecdab015f00e06456166a2eba7590e49f8db940e"},
-    {file = "SQLAlchemy-2.0.31-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b9c01990d9015df2c6f818aa8f4297d42ee71c9502026bb074e713d496e26b67"},
-    {file = "SQLAlchemy-2.0.31-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:66f63278db425838b3c2b1c596654b31939427016ba030e951b292e32b99553e"},
-    {file = "SQLAlchemy-2.0.31-cp312-cp312-win32.whl", hash = "sha256:0b0f658414ee4e4b8cbcd4a9bb0fd743c5eeb81fc858ca517217a8013d282c96"},
-    {file = "SQLAlchemy-2.0.31-cp312-cp312-win_amd64.whl", hash = "sha256:fa4b1af3e619b5b0b435e333f3967612db06351217c58bfb50cee5f003db2a5a"},
-    {file = "SQLAlchemy-2.0.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f43e93057cf52a227eda401251c72b6fbe4756f35fa6bfebb5d73b86881e59b0"},
-    {file = "SQLAlchemy-2.0.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d337bf94052856d1b330d5fcad44582a30c532a2463776e1651bd3294ee7e58b"},
-    {file = "SQLAlchemy-2.0.31-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c06fb43a51ccdff3b4006aafee9fcf15f63f23c580675f7734245ceb6b6a9e05"},
-    {file = "SQLAlchemy-2.0.31-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:b6e22630e89f0e8c12332b2b4c282cb01cf4da0d26795b7eae16702a608e7ca1"},
-    {file = "SQLAlchemy-2.0.31-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:79a40771363c5e9f3a77f0e28b3302801db08040928146e6808b5b7a40749c88"},
-    {file = "SQLAlchemy-2.0.31-cp37-cp37m-win32.whl", hash = "sha256:501ff052229cb79dd4c49c402f6cb03b5a40ae4771efc8bb2bfac9f6c3d3508f"},
-    {file = "SQLAlchemy-2.0.31-cp37-cp37m-win_amd64.whl", hash = "sha256:597fec37c382a5442ffd471f66ce12d07d91b281fd474289356b1a0041bdf31d"},
-    {file = "SQLAlchemy-2.0.31-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dc6d69f8829712a4fd799d2ac8d79bdeff651c2301b081fd5d3fe697bd5b4ab9"},
-    {file = "SQLAlchemy-2.0.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:23b9fbb2f5dd9e630db70fbe47d963c7779e9c81830869bd7d137c2dc1ad05fb"},
-    {file = "SQLAlchemy-2.0.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a21c97efcbb9f255d5c12a96ae14da873233597dfd00a3a0c4ce5b3e5e79704"},
-    {file = "SQLAlchemy-2.0.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26a6a9837589c42b16693cf7bf836f5d42218f44d198f9343dd71d3164ceeeac"},
-    {file = "SQLAlchemy-2.0.31-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc251477eae03c20fae8db9c1c23ea2ebc47331bcd73927cdcaecd02af98d3c3"},
-    {file = "SQLAlchemy-2.0.31-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2fd17e3bb8058359fa61248c52c7b09a97cf3c820e54207a50af529876451808"},
-    {file = "SQLAlchemy-2.0.31-cp38-cp38-win32.whl", hash = "sha256:c76c81c52e1e08f12f4b6a07af2b96b9b15ea67ccdd40ae17019f1c373faa227"},
-    {file = "SQLAlchemy-2.0.31-cp38-cp38-win_amd64.whl", hash = "sha256:4b600e9a212ed59355813becbcf282cfda5c93678e15c25a0ef896b354423238"},
-    {file = "SQLAlchemy-2.0.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b6cf796d9fcc9b37011d3f9936189b3c8074a02a4ed0c0fbbc126772c31a6d4"},
-    {file = "SQLAlchemy-2.0.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:78fe11dbe37d92667c2c6e74379f75746dc947ee505555a0197cfba9a6d4f1a4"},
-    {file = "SQLAlchemy-2.0.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fc47dc6185a83c8100b37acda27658fe4dbd33b7d5e7324111f6521008ab4fe"},
-    {file = "SQLAlchemy-2.0.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a41514c1a779e2aa9a19f67aaadeb5cbddf0b2b508843fcd7bafdf4c6864005"},
-    {file = "SQLAlchemy-2.0.31-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:afb6dde6c11ea4525318e279cd93c8734b795ac8bb5dda0eedd9ebaca7fa23f1"},
-    {file = "SQLAlchemy-2.0.31-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3f9faef422cfbb8fd53716cd14ba95e2ef655400235c3dfad1b5f467ba179c8c"},
-    {file = "SQLAlchemy-2.0.31-cp39-cp39-win32.whl", hash = "sha256:fc6b14e8602f59c6ba893980bea96571dd0ed83d8ebb9c4479d9ed5425d562e9"},
-    {file = "SQLAlchemy-2.0.31-cp39-cp39-win_amd64.whl", hash = "sha256:3cb8a66b167b033ec72c3812ffc8441d4e9f5f78f5e31e54dcd4c90a4ca5bebc"},
-    {file = "SQLAlchemy-2.0.31-py3-none-any.whl", hash = "sha256:69f3e3c08867a8e4856e92d7afb618b95cdee18e0bc1647b77599722c9a28911"},
-    {file = "SQLAlchemy-2.0.31.tar.gz", hash = "sha256:b607489dd4a54de56984a0c7656247504bd5523d9d0ba799aef59d4add009484"},
+    {file = "SQLAlchemy-2.0.36-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59b8f3adb3971929a3e660337f5dacc5942c2cdb760afcabb2614ffbda9f9f72"},
+    {file = "SQLAlchemy-2.0.36-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37350015056a553e442ff672c2d20e6f4b6d0b2495691fa239d8aa18bb3bc908"},
+    {file = "SQLAlchemy-2.0.36-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8318f4776c85abc3f40ab185e388bee7a6ea99e7fa3a30686580b209eaa35c08"},
+    {file = "SQLAlchemy-2.0.36-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c245b1fbade9c35e5bd3b64270ab49ce990369018289ecfde3f9c318411aaa07"},
+    {file = "SQLAlchemy-2.0.36-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:69f93723edbca7342624d09f6704e7126b152eaed3cdbb634cb657a54332a3c5"},
+    {file = "SQLAlchemy-2.0.36-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f9511d8dd4a6e9271d07d150fb2f81874a3c8c95e11ff9af3a2dfc35fe42ee44"},
+    {file = "SQLAlchemy-2.0.36-cp310-cp310-win32.whl", hash = "sha256:c3f3631693003d8e585d4200730616b78fafd5a01ef8b698f6967da5c605b3fa"},
+    {file = "SQLAlchemy-2.0.36-cp310-cp310-win_amd64.whl", hash = "sha256:a86bfab2ef46d63300c0f06936bd6e6c0105faa11d509083ba8f2f9d237fb5b5"},
+    {file = "SQLAlchemy-2.0.36-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fd3a55deef00f689ce931d4d1b23fa9f04c880a48ee97af488fd215cf24e2a6c"},
+    {file = "SQLAlchemy-2.0.36-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f5e9cd989b45b73bd359f693b935364f7e1f79486e29015813c338450aa5a71"},
+    {file = "SQLAlchemy-2.0.36-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ddd9db6e59c44875211bc4c7953a9f6638b937b0a88ae6d09eb46cced54eff"},
+    {file = "SQLAlchemy-2.0.36-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2519f3a5d0517fc159afab1015e54bb81b4406c278749779be57a569d8d1bb0d"},
+    {file = "SQLAlchemy-2.0.36-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59b1ee96617135f6e1d6f275bbe988f419c5178016f3d41d3c0abb0c819f75bb"},
+    {file = "SQLAlchemy-2.0.36-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:39769a115f730d683b0eb7b694db9789267bcd027326cccc3125e862eb03bfd8"},
+    {file = "SQLAlchemy-2.0.36-cp311-cp311-win32.whl", hash = "sha256:66bffbad8d6271bb1cc2f9a4ea4f86f80fe5e2e3e501a5ae2a3dc6a76e604e6f"},
+    {file = "SQLAlchemy-2.0.36-cp311-cp311-win_amd64.whl", hash = "sha256:23623166bfefe1487d81b698c423f8678e80df8b54614c2bf4b4cfcd7c711959"},
+    {file = "SQLAlchemy-2.0.36-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7b64e6ec3f02c35647be6b4851008b26cff592a95ecb13b6788a54ef80bbdd4"},
+    {file = "SQLAlchemy-2.0.36-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46331b00096a6db1fdc052d55b101dbbfc99155a548e20a0e4a8e5e4d1362855"},
+    {file = "SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53"},
+    {file = "SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9dfa18ff2a67b09b372d5db8743c27966abf0e5344c555d86cc7199f7ad83a"},
+    {file = "SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:90812a8933df713fdf748b355527e3af257a11e415b613dd794512461eb8a686"},
+    {file = "SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1bc330d9d29c7f06f003ab10e1eaced295e87940405afe1b110f2eb93a233588"},
+    {file = "SQLAlchemy-2.0.36-cp312-cp312-win32.whl", hash = "sha256:79d2e78abc26d871875b419e1fd3c0bca31a1cb0043277d0d850014599626c2e"},
+    {file = "SQLAlchemy-2.0.36-cp312-cp312-win_amd64.whl", hash = "sha256:b544ad1935a8541d177cb402948b94e871067656b3a0b9e91dbec136b06a2ff5"},
+    {file = "SQLAlchemy-2.0.36-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5cc79df7f4bc3d11e4b542596c03826063092611e481fcf1c9dfee3c94355ef"},
+    {file = "SQLAlchemy-2.0.36-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3c01117dd36800f2ecaa238c65365b7b16497adc1522bf84906e5710ee9ba0e8"},
+    {file = "SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bc633f4ee4b4c46e7adcb3a9b5ec083bf1d9a97c1d3854b92749d935de40b9b"},
+    {file = "SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e46ed38affdfc95d2c958de328d037d87801cfcbea6d421000859e9789e61c2"},
+    {file = "SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b2985c0b06e989c043f1dc09d4fe89e1616aadd35392aea2844f0458a989eacf"},
+    {file = "SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a121d62ebe7d26fec9155f83f8be5189ef1405f5973ea4874a26fab9f1e262c"},
+    {file = "SQLAlchemy-2.0.36-cp313-cp313-win32.whl", hash = "sha256:0572f4bd6f94752167adfd7c1bed84f4b240ee6203a95e05d1e208d488d0d436"},
+    {file = "SQLAlchemy-2.0.36-cp313-cp313-win_amd64.whl", hash = "sha256:8c78ac40bde930c60e0f78b3cd184c580f89456dd87fc08f9e3ee3ce8765ce88"},
+    {file = "SQLAlchemy-2.0.36-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:be9812b766cad94a25bc63bec11f88c4ad3629a0cec1cd5d4ba48dc23860486b"},
+    {file = "SQLAlchemy-2.0.36-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50aae840ebbd6cdd41af1c14590e5741665e5272d2fee999306673a1bb1fdb4d"},
+    {file = "SQLAlchemy-2.0.36-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4557e1f11c5f653ebfdd924f3f9d5ebfc718283b0b9beebaa5dd6b77ec290971"},
+    {file = "SQLAlchemy-2.0.36-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07b441f7d03b9a66299ce7ccf3ef2900abc81c0db434f42a5694a37bd73870f2"},
+    {file = "SQLAlchemy-2.0.36-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:28120ef39c92c2dd60f2721af9328479516844c6b550b077ca450c7d7dc68575"},
+    {file = "SQLAlchemy-2.0.36-cp37-cp37m-win32.whl", hash = "sha256:b81ee3d84803fd42d0b154cb6892ae57ea6b7c55d8359a02379965706c7efe6c"},
+    {file = "SQLAlchemy-2.0.36-cp37-cp37m-win_amd64.whl", hash = "sha256:f942a799516184c855e1a32fbc7b29d7e571b52612647866d4ec1c3242578fcb"},
+    {file = "SQLAlchemy-2.0.36-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3d6718667da04294d7df1670d70eeddd414f313738d20a6f1d1f379e3139a545"},
+    {file = "SQLAlchemy-2.0.36-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:72c28b84b174ce8af8504ca28ae9347d317f9dba3999e5981a3cd441f3712e24"},
+    {file = "SQLAlchemy-2.0.36-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b11d0cfdd2b095e7b0686cf5fabeb9c67fae5b06d265d8180715b8cfa86522e3"},
+    {file = "SQLAlchemy-2.0.36-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e32092c47011d113dc01ab3e1d3ce9f006a47223b18422c5c0d150af13a00687"},
+    {file = "SQLAlchemy-2.0.36-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6a440293d802d3011028e14e4226da1434b373cbaf4a4bbb63f845761a708346"},
+    {file = "SQLAlchemy-2.0.36-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c54a1e53a0c308a8e8a7dffb59097bff7facda27c70c286f005327f21b2bd6b1"},
+    {file = "SQLAlchemy-2.0.36-cp38-cp38-win32.whl", hash = "sha256:1e0d612a17581b6616ff03c8e3d5eff7452f34655c901f75d62bd86449d9750e"},
+    {file = "SQLAlchemy-2.0.36-cp38-cp38-win_amd64.whl", hash = "sha256:8958b10490125124463095bbdadda5aa22ec799f91958e410438ad6c97a7b793"},
+    {file = "SQLAlchemy-2.0.36-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc022184d3e5cacc9579e41805a681187650e170eb2fd70e28b86192a479dcaa"},
+    {file = "SQLAlchemy-2.0.36-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b817d41d692bf286abc181f8af476c4fbef3fd05e798777492618378448ee689"},
+    {file = "SQLAlchemy-2.0.36-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4e46a888b54be23d03a89be510f24a7652fe6ff660787b96cd0e57a4ebcb46d"},
+    {file = "SQLAlchemy-2.0.36-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4ae3005ed83f5967f961fd091f2f8c5329161f69ce8480aa8168b2d7fe37f06"},
+    {file = "SQLAlchemy-2.0.36-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03e08af7a5f9386a43919eda9de33ffda16b44eb11f3b313e6822243770e9763"},
+    {file = "SQLAlchemy-2.0.36-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3dbb986bad3ed5ceaf090200eba750b5245150bd97d3e67343a3cfed06feecf7"},
+    {file = "SQLAlchemy-2.0.36-cp39-cp39-win32.whl", hash = "sha256:9fe53b404f24789b5ea9003fc25b9a3988feddebd7e7b369c8fac27ad6f52f28"},
+    {file = "SQLAlchemy-2.0.36-cp39-cp39-win_amd64.whl", hash = "sha256:af148a33ff0349f53512a049c6406923e4e02bf2f26c5fb285f143faf4f0e46a"},
+    {file = "SQLAlchemy-2.0.36-py3-none-any.whl", hash = "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e"},
+    {file = "sqlalchemy-2.0.36.tar.gz", hash = "sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5"},
 ]
 
 [package.dependencies]
-greenlet = {version = "!=0.4.17", optional = true, markers = "python_version < \"3.13\" and (platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\") or extra == \"asyncio\""}
+greenlet = {version = "!=0.4.17", optional = true, markers = "python_version < \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\") or extra == \"asyncio\""}
 importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
 typing-extensions = ">=4.6.0"
 
@@ -1245,7 +1300,7 @@ aioodbc = ["aioodbc", "greenlet (!=0.4.17)"]
 aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"]
 asyncio = ["greenlet (!=0.4.17)"]
 asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"]
-mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"]
+mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"]
 mssql = ["pyodbc"]
 mssql-pymssql = ["pymssql"]
 mssql-pyodbc = ["pyodbc"]
@@ -1371,13 +1426,13 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
 
 [[package]]
 name = "virtualenv"
-version = "20.26.3"
+version = "20.26.6"
 description = "Virtual Python Environment builder"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"},
-    {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"},
+    {file = "virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2"},
+    {file = "virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48"},
 ]
 
 [package.dependencies]
@@ -1447,4 +1502,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.7"
-content-hash = "3c01d5935b79b8a790d29076499ac97206449a2902cc8f794fe9adf72e3b99df"
+content-hash = "e146ff627670b195f8f512af11f09f2d2e1900c0e2d08866fec443c587f24af6"
diff --git a/pydantic_resolve/compat.py b/pydantic_resolve/compat.py
new file mode 100644
index 0000000..a0e6d49
--- /dev/null
+++ b/pydantic_resolve/compat.py
@@ -0,0 +1,4 @@
+from pydantic.version import VERSION as P_VERSION
+
+PYDANTIC_VERSION = P_VERSION
+PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.")
diff --git a/pydantic_resolve/core.py b/pydantic_resolve/core.py
index 12e1098..c2edcb2 100644
--- a/pydantic_resolve/core.py
+++ b/pydantic_resolve/core.py
@@ -10,6 +10,7 @@
 import pydantic_resolve.util as util
 import pydantic_resolve.constant as const
 from .exceptions import ResolverTargetAttrNotFound, LoaderFieldNotProvidedError, MissingCollector
+from pydantic_resolve.compat import PYDANTIC_V2
 
 def LoaderDepend(  # noqa: N802
     dependency: Optional[Callable[..., Any]] = None,
@@ -56,8 +57,15 @@ def values(self):
 
 
 def _get_pydantic_attrs(kls):
-    for k, v in kls.__fields__.items():
-        shelled_type = util.shelling_type(v.type_)
+    if PYDANTIC_V2:
+        items = kls.model_fields.items()
+    else:
+        items = kls.__fields__.items()
+
+    for k, v in items:
+        t = v.annotation if PYDANTIC_V2 else v.type_
+
+        shelled_type = util.shelling_type(t)
         if is_acceptable_kls(shelled_type):
             yield (k, shelled_type)  # type_ is the most inner type
 
@@ -249,7 +257,10 @@ def scan_and_store_metadata(root_class):
 
     def _get_all_fields_and_object_fields(kls):
         if util.safe_issubclass(kls, BaseModel):
-            all_fields = set(kls.__fields__.keys())
+            if PYDANTIC_V2:
+                all_fields = set(kls.model_fields.keys())
+            else:
+                all_fields = set(kls.__fields__.keys())
             object_fields = list(_get_pydantic_attrs(kls))  # dive and recursively analysis
         elif is_dataclass(kls):
             all_fields = set([f.name for f in dc_fields(kls)])
diff --git a/pydantic_resolve/util.py b/pydantic_resolve/util.py
index f55a128..23b899b 100644
--- a/pydantic_resolve/util.py
+++ b/pydantic_resolve/util.py
@@ -3,12 +3,17 @@
 import functools
 from collections import defaultdict
 from dataclasses import is_dataclass
-from pydantic import BaseModel, parse_obj_as, ValidationError
 from inspect import iscoroutine, isfunction
 from typing import Any, DefaultDict, Sequence, Type, TypeVar, List, Callable, Optional, Mapping, Union, Iterator, Dict, get_type_hints
 import pydantic_resolve.constant as const
 from pydantic_resolve.exceptions import GlobalLoaderFieldOverlappedError
 from aiodataloader import DataLoader
+from pydantic_resolve.compat import PYDANTIC_V2
+
+if PYDANTIC_V2:
+    from pydantic import BaseModel, TypeAdapter, ValidationError
+else:
+    from pydantic import BaseModel, parse_obj_as, ValidationError
 
 def get_class_field_annotations(cls: Type):
     anno = cls.__dict__.get('__annotations__') or {}
@@ -18,6 +23,20 @@ def get_class_field_annotations(cls: Type):
 T = TypeVar("T")
 V = TypeVar("V")
 
+if PYDANTIC_V2:
+    class TypeAdapterManager:
+        apapters = {}
+        
+        @classmethod
+        def get(cls, type):
+            adapter = cls.apapters.get(type)
+            if adapter:
+                return adapter
+            else:
+                new_adapter = TypeAdapter(type)
+                cls.apapters[type] = new_adapter
+                return new_adapter
+
 def safe_issubclass(kls, classinfo):
     try:
         return issubclass(kls, classinfo)
@@ -58,14 +77,26 @@ def replace_method(cls: Type, cls_name: str, func_name: str, func: Callable):
     KLS = type(cls_name, (cls,), {func_name: func})
     return KLS
 
-
 def get_required_fields(kls: BaseModel):
     required_fields = []
 
+    def _is_require(field):
+        if PYDANTIC_V2:
+            return field.is_required()
+        else:
+            return field.required
+
     # 1. get required fields
-    for fname, field in kls.__fields__.items():
-        if field.required:
+    if PYDANTIC_V2:
+        items = kls.model_fields.items()
+    else:
+        items = kls.__fields__.items()
+    
+
+    for fname, field in items:
+        if _is_require(field):
             required_fields.append(fname)
+        
 
     # 2. get resolve_ and post_ target fields
     for f in dir(kls):
@@ -79,7 +110,8 @@ def get_required_fields(kls: BaseModel):
 
     return required_fields
 
-def output(kls):
+
+def output_v1(kls):
     """
     set required as True for all fields, make typescript code gen result friendly to use
     """
@@ -94,7 +126,30 @@ def _schema_extra(schema: Dict[str, Any], model) -> None:
         raise AttributeError(f'target class {kls.__name__} is not BaseModel')
     return kls
 
-def model_config(hidden_fields: Optional[List[str]]=None, default_required: bool = True):
+def output_v2(kls):
+    """
+    set required as True for all fields
+    make typescript code gen result friendly to use
+    """
+
+    if safe_issubclass(kls, BaseModel):
+
+        def build():
+            def schema_extra(schema: Dict[str, Any], model) -> None:
+                fnames = get_required_fields(model)
+                schema['required'] = fnames
+            return schema_extra
+
+        kls.model_config['json_schema_extra'] = staticmethod(build())
+
+    else:
+        raise AttributeError(f'target class {kls.__name__} is not BaseModel')
+    return kls
+
+output = output_v2 if PYDANTIC_V2 else output_v1
+
+
+def model_config_v1(default_required: bool = True):
     """
     - hidden_fields: fields want to hide
     - default_required: 
@@ -103,27 +158,12 @@ def model_config(hidden_fields: Optional[List[str]]=None, default_required: bool
     """
     def wrapper(kls):
         if safe_issubclass(kls, BaseModel):
-            # handle __exclude_fields__ 
-            if hidden_fields:
-                # validate
-                for f in hidden_fields:
-                    if f not in kls.__fields__.keys():
-                        raise KeyError(f'{f} is not valid')
-
-                # exclude in dict()
-                excludes_fields = kls.__exclude_fields__ or {}
-                hiddens_fields = {k: True for k in hidden_fields}
-                kls.__exclude_fields__ = {**excludes_fields, **hiddens_fields}
                 
             # override schema_extra method
             def _schema_extra(schema: Dict[str, Any], model) -> None:
                 # define schema.properties
                 excludes = set()
 
-                if hidden_fields:
-                    for hf in hidden_fields:
-                        excludes.add(hf)
-
                 if kls.__exclude_fields__:
                     for k in kls.__exclude_fields__.keys():
                         excludes.add(k)
@@ -137,8 +177,6 @@ def _schema_extra(schema: Dict[str, Any], model) -> None:
                 # define schema.required
                 if default_required:
                     fnames = get_required_fields(model)
-                    if hidden_fields:
-                        fnames = [n for n in fnames if n not in hidden_fields]
                     schema['required'] = fnames
             kls.__config__.schema_extra = staticmethod(_schema_extra)
         else:
@@ -146,6 +184,48 @@ def _schema_extra(schema: Dict[str, Any], model) -> None:
         return kls
     return wrapper
 
+def model_config_v2(default_required: bool=True):
+    """
+    in pydantic v2, we can not use __exclude_field__ to set hidden field in model_config hidden_field params
+    model_config now is just a simple decorator to remove fields (with exclude=True) from schema.properties
+    and set schema.required for better schema description. 
+    (same like `output` decorator, you can replace output with model_config)
+
+    it keeps the form of model_config(params) in order to extend new features in future
+    """
+    def wrapper(kls):
+        if safe_issubclass(kls, BaseModel):
+            def build():
+                def _schema_extra(schema: Dict[str, Any], model) -> None:
+                    # 1. collect exclude fields and then hide in both schema and dump (default action)
+                    excluded_fields = [k for k, v in kls.model_fields.items() if v.exclude == True]
+                    props = {}
+
+                    # config schema properties
+                    for k, v in schema.get('properties', {}).items():
+                        if k not in excluded_fields:
+                            props[k] = v
+                    schema['properties'] = props
+
+                    # config schema required (fields with default values will not be listed in required field)
+                    # and the generated typescript models will define it as optional, and is troublesome in use
+                    if default_required:
+                        fnames = get_required_fields(model)
+                        if excluded_fields:
+                            fnames = [n for n in fnames if n not in excluded_fields]
+                        schema['required'] = fnames
+
+                return _schema_extra
+
+            kls.model_config['json_schema_extra'] = staticmethod(build())
+        else:
+            raise AttributeError(f'target class {kls.__name__} is not BaseModel')
+        return kls
+    return wrapper
+
+model_config = model_config_v2 if PYDANTIC_V2 else model_config_v1
+
+
 def mapper(func_or_class: Union[Callable, Type]):
     """
     execute post-transform function after the value is reolved
@@ -185,7 +265,7 @@ async def wrap(*args, **kwargs):
         return wrap
     return inner
 
-def _get_mapping_rule(target, source) -> Optional[Callable]:
+def _get_mapping_rule_v1(target, source) -> Optional[Callable]:
     # do noting
     if isinstance(source, target):
         return None
@@ -211,6 +291,41 @@ def _get_mapping_rule(target, source) -> Optional[Callable]:
 
     raise NotImplementedError(f"{type(source)} -> {target.__name__}: faild to get auto mapping rule and execut mapping, use your own rule instead.")
 
+def _get_mapping_rule_v2(target, source) -> Optional[Callable]:
+    # do noting
+    if isinstance(source, target):
+        return None
+
+    # pydantic
+    if safe_issubclass(target, BaseModel):
+        if target.model_config.get('from_attributes'):
+            if isinstance(source, dict):
+                raise AttributeError(f"{type(source)} -> {target.__name__}: pydantic from_orm can't handle dict object")
+            else:
+                return lambda t, s: t.model_validate(s)
+
+        if isinstance(source, dict):
+            return lambda t, s: t.model_validate(s)
+
+        if isinstance(source, BaseModel):
+            if source.model_config.get('from_attributes'):
+                return lambda t, s: t.model_validate(s) 
+            else:
+                return lambda t, s: t(**s.model_dump()) 
+
+        else:
+            raise AttributeError(f"{type(source)} -> {target.__name__}: pydantic can't handle non-dict data")
+    
+    # dataclass
+    if is_dataclass(target):
+        if isinstance(source, dict):
+            return lambda t, s: t(**s)
+    
+    raise NotImplementedError(f"{type(source)} -> {target.__name__}: faild to get auto mapping rule and execut mapping, use your own rule instead.")
+
+_get_mapping_rule = _get_mapping_rule_v2 if PYDANTIC_V2 else _get_mapping_rule_v1
+
+
 def _apply_rule(rule: Optional[Callable], target, source: Any, is_list: bool):
     if not rule:  # no change
         return source
@@ -220,7 +335,7 @@ def _apply_rule(rule: Optional[Callable], target, source: Any, is_list: bool):
     else:
         return rule(target, source)
 
-def ensure_subset(base):
+def ensure_subset_v1(base):
     """
     used with pydantic class to make sure a class's field is 
     subset of target class
@@ -240,8 +355,33 @@ def inner():
                         raise AttributeError(f'type of {k} not consistent with {base.__name__}'  )
             return kls
         return inner()
+    return wrap 
+
+def ensure_subset_v2(base):
+    """
+    used with pydantic class to make sure a class's field is 
+    subset of target class
+    """
+    def wrap(kls):
+        assert safe_issubclass(base, BaseModel), 'base should be pydantic class'
+        assert safe_issubclass(kls, BaseModel), 'class should be pydantic class'
+
+        @functools.wraps(kls)
+        def inner():
+            for k, field in kls.model_fields.items():
+                if field.is_required():
+                    base_field = base.model_fields.get(k)
+                    if not base_field:
+                        raise AttributeError(f'{k} not existed in {base.__name__}.')
+                    if base_field and base_field.annotation != field.annotation:
+                        raise AttributeError(f'type of {k} not consistent with {base.__name__}'  )
+            return  kls
+        return inner()
     return wrap
 
+ensure_subset = ensure_subset_v2 if PYDANTIC_V2 else ensure_subset_v1
+
+
 def update_forward_refs(kls):
     def update_pydantic_forward_refs(kls: Type[BaseModel]):
         """
@@ -249,11 +389,24 @@ def update_pydantic_forward_refs(kls: Type[BaseModel]):
         """
         if getattr(kls, const.PYDANTIC_FORWARD_REF_UPDATED, False):
             return
-        kls.update_forward_refs()
+
+        if PYDANTIC_V2:
+            kls.model_rebuild()
+        else:
+            kls.update_forward_refs()
+
         setattr(kls, const.PYDANTIC_FORWARD_REF_UPDATED, True)
 
-        for field in kls.__fields__.values():
-            shelled_type = shelling_type(field.type_)
+        if PYDANTIC_V2:
+            values = kls.model_fields.values()
+        else:
+            values = kls.__fields__.values()
+
+        for field in values:
+            if PYDANTIC_V2:
+                shelled_type = shelling_type(field.annotation)
+            else:
+                shelled_type = shelling_type(field.type_)
             update_forward_refs(shelled_type)
 
     def update_dataclass_forward_refs(kls):
@@ -272,7 +425,8 @@ def update_dataclass_forward_refs(kls):
     if is_dataclass(kls):
         update_dataclass_forward_refs(kls)
 
-def try_parse_data_to_target_field_type(target, field_name, data):
+
+def try_parse_data_to_target_field_type_v1(target, field_name, data):
     """
     parse to pydantic or dataclass object
     1. get type of target field
@@ -303,6 +457,42 @@ def try_parse_data_to_target_field_type(target, field_name, data):
     else:
         return data  #noqa
 
+def try_parse_data_to_target_field_type_v2(target, field_name, data):
+    """
+    parse to pydantic or dataclass object
+    1. get type of target field
+    2. parse
+    """
+    field_type = None
+
+    # 1. get type of target field
+    if isinstance(target, BaseModel):
+        _fields = target.__class__.model_fields
+        field_type = _fields[field_name].annotation
+
+        # handle optional logic
+        if data is None and _fields[field_name].is_required() == False:
+            return data
+
+    elif is_dataclass(target):
+        field_type = target.__class__.__annotations__[field_name]
+
+    # 2. parse
+    if field_type:
+        try:
+            # https://docs.pydantic.dev/latest/concepts/performance/#typeadapter-instantiated-once
+            adapter = TypeAdapterManager.get(field_type)
+            result = adapter.validate_python(data)  
+            return result
+        except ValidationError as e:
+            print(f'Warning: type mismatch, pls check the return type for "{field_name}", expected: {field_type}')
+            raise e
+    
+    else:
+        return data  #noqa
+
+try_parse_data_to_target_field_type = try_parse_data_to_target_field_type_v2 if PYDANTIC_V2 else try_parse_data_to_target_field_type_v1
+
 def _is_optional(annotation):
     annotation_origin = getattr(annotation, "__origin__", None)
     return annotation_origin == Union \
diff --git a/pyproject.toml b/pyproject.toml
index 5211442..53098dc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,7 @@
 [tool.poetry]
 name = "pydantic-resolve"
-version = "1.10.8"
-description = "create nested data structure easily"
+version = "1.11.0"
+description = "It just provide a pair of pre & post methods around pydantic fields, the rest is up to your imagination"
 authors = ["tangkikodo <allmonday@126.com>"]
 readme = "README.md"
 repository = "https://github.com/allmonday/pydantic_resolve"
@@ -11,7 +11,6 @@ packages = [{include = "pydantic_resolve"}]
 
 [tool.poetry.dependencies]
 python = "^3.7"
-pydantic = "^1.10.4"
 aiodataloader = "^0.4.0"
 
 [tool.poetry.group.dev.dependencies]
@@ -22,7 +21,7 @@ coverage = "^7.2.1"
 aiosqlite = "^0.18.0"
 sqlalchemy = {extras = ["asyncio"], version = "^2.0.7"}
 pytest-asyncio = "^0.21.0"
-fastapi = "^0.96.0"
+fastapi = "^0.101.0"
 uvicorn = "^0.22.0"
 mkdocs-material = "9.*"
 mkdocs-static-i18n = {version = "1.2.0", extras = ["material"]}
diff --git a/tox.ini b/tox.ini
index c8046eb..38756eb 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,14 +1,71 @@
 [tox]
 envlist = 
-    py37.4
-    py38.4
-    py39.4
-    py310
-isolated_build = True
+    py38pyd1
+    py38pyd2
+    py39pyd1
+    py39pyd2
+    py310pyd1
+    py310pyd2
+setenv = VIRTUALENV_DISCOVERY=pyenv
 
 [testenv]
 allowlist_externals = poetry
-commands_pre = 
+
+
+[testenv:py38pyd1]
+basepython = python3.8
+commands_pre =
+    poetry install --no-root --sync
+commands =
+  poetry run pytest tests/pydantic_v1
+
+[testenv:py38pyd2]
+commands_pre =
+  poetry install --no-root --sync
+  pip install pydantic==2.*
+basepython = python3.8
+commands =
+  poetry run pytest tests/pydantic_v2
+
+[testenv:py39pyd1]
+basepython = python3.9
+commands_pre =
+    poetry install --no-root --sync
+commands =
+  poetry run pytest tests/pydantic_v1
+
+[testenv:py39pyd2]
+commands_pre =
   poetry install --no-root --sync
+  pip install pydantic==2.*
+basepython = python3.9
+commands =
+  poetry run pytest tests/pydantic_v2
+
+[testenv:py310pyd1]
+basepython = python3.10
+commands_pre =
+    poetry install --no-root --sync
+commands =
+  ; poetry run pytest tests/pydantic_v1
+  poetry run coverage run --data-file=./cov/.result1 -m pytest tests/pydantic_v1
+  ; poetry run coverage xml -o ./cov/coverage.xml
+
+
+[testenv:py310pyd2]
+basepython = python3.10
+commands_pre =
+  poetry install --no-root --sync
+  pip install pydantic==2.*
+commands =
+  ; poetry run pytest tests/pydantic_v2
+  poetry run coverage run --data-file=./cov/.result2 -m pytest tests/pydantic_v2
+
+[testenv:coverage]
+description = Combine coverage data and generate report
+skip_install = true
+deps = coverage
 commands =
-  poetry run pytest tests/ --import-mode importlib
\ No newline at end of file
+    coverage combine cov/.result1 cov/.result2
+    coverage report -m
+    coverage html
\ No newline at end of file


Test patch:
diff --git a/tests/resolver/__init__.py b/tests/pydantic_v1/__init__.py
similarity index 100%
rename from tests/resolver/__init__.py
rename to tests/pydantic_v1/__init__.py
diff --git a/tests/core/test_field.py b/tests/pydantic_v1/core/test_field.py
similarity index 100%
rename from tests/core/test_field.py
rename to tests/pydantic_v1/core/test_field.py
diff --git a/tests/core/test_field_dataclass.py b/tests/pydantic_v1/core/test_field_dataclass.py
similarity index 100%
rename from tests/core/test_field_dataclass.py
rename to tests/pydantic_v1/core/test_field_dataclass.py
diff --git a/tests/core/test_field_dataclass_anno.py b/tests/pydantic_v1/core/test_field_dataclass_anno.py
similarity index 100%
rename from tests/core/test_field_dataclass_anno.py
rename to tests/pydantic_v1/core/test_field_dataclass_anno.py
diff --git a/tests/core/test_field_mix.py b/tests/pydantic_v1/core/test_field_mix.py
similarity index 100%
rename from tests/core/test_field_mix.py
rename to tests/pydantic_v1/core/test_field_mix.py
diff --git a/tests/core/test_field_pydantic.py b/tests/pydantic_v1/core/test_field_pydantic.py
similarity index 100%
rename from tests/core/test_field_pydantic.py
rename to tests/pydantic_v1/core/test_field_pydantic.py
diff --git a/tests/core/test_field_pydantic_error.py b/tests/pydantic_v1/core/test_field_pydantic_error.py
similarity index 100%
rename from tests/core/test_field_pydantic_error.py
rename to tests/pydantic_v1/core/test_field_pydantic_error.py
diff --git a/tests/core/test_field_validate_and_create_loader_instance.py b/tests/pydantic_v1/core/test_field_validate_and_create_loader_instance.py
similarity index 100%
rename from tests/core/test_field_validate_and_create_loader_instance.py
rename to tests/pydantic_v1/core/test_field_validate_and_create_loader_instance.py
diff --git a/tests/core/test_input.py b/tests/pydantic_v1/core/test_input.py
similarity index 100%
rename from tests/core/test_input.py
rename to tests/pydantic_v1/core/test_input.py
diff --git a/tests/core/test_scan_post_method.py b/tests/pydantic_v1/core/test_scan_post_method.py
similarity index 100%
rename from tests/core/test_scan_post_method.py
rename to tests/pydantic_v1/core/test_scan_post_method.py
diff --git a/tests/core/test_scan_resolve_method.py b/tests/pydantic_v1/core/test_scan_resolve_method.py
similarity index 100%
rename from tests/core/test_scan_resolve_method.py
rename to tests/pydantic_v1/core/test_scan_resolve_method.py
diff --git a/tests/core/test_specific_type.py b/tests/pydantic_v1/core/test_specific_type.py
similarity index 100%
rename from tests/core/test_specific_type.py
rename to tests/pydantic_v1/core/test_specific_type.py
diff --git a/tests/resolver/test_14_deps/__init__.py b/tests/pydantic_v1/resolver/__init__.py
similarity index 100%
rename from tests/resolver/test_14_deps/__init__.py
rename to tests/pydantic_v1/resolver/__init__.py
diff --git a/tests/resolver/test_0_depends.py b/tests/pydantic_v1/resolver/test_0_depends.py
similarity index 100%
rename from tests/resolver/test_0_depends.py
rename to tests/pydantic_v1/resolver/test_0_depends.py
diff --git a/tests/resolver/test_10_sqlalchemy_query_with_change.py b/tests/pydantic_v1/resolver/test_10_sqlalchemy_query_with_change.py
similarity index 100%
rename from tests/resolver/test_10_sqlalchemy_query_with_change.py
rename to tests/pydantic_v1/resolver/test_10_sqlalchemy_query_with_change.py
diff --git a/tests/resolver/test_11_sqlalchemy_query_global_filter.py b/tests/pydantic_v1/resolver/test_11_sqlalchemy_query_global_filter.py
similarity index 100%
rename from tests/resolver/test_11_sqlalchemy_query_global_filter.py
rename to tests/pydantic_v1/resolver/test_11_sqlalchemy_query_global_filter.py
diff --git a/tests/resolver/test_12_loader_global_filter_exception.py b/tests/pydantic_v1/resolver/test_12_loader_global_filter_exception.py
similarity index 100%
rename from tests/resolver/test_12_loader_global_filter_exception.py
rename to tests/pydantic_v1/resolver/test_12_loader_global_filter_exception.py
diff --git a/tests/resolver/test_13_check_wrong_type.py b/tests/pydantic_v1/resolver/test_13_check_wrong_type.py
similarity index 100%
rename from tests/resolver/test_13_check_wrong_type.py
rename to tests/pydantic_v1/resolver/test_13_check_wrong_type.py
diff --git a/tests/resolver/test_14_check_loader_name.py b/tests/pydantic_v1/resolver/test_14_check_loader_name.py
similarity index 91%
rename from tests/resolver/test_14_check_loader_name.py
rename to tests/pydantic_v1/resolver/test_14_check_loader_name.py
index 8c74d54..f6a460e 100644
--- a/tests/resolver/test_14_check_loader_name.py
+++ b/tests/pydantic_v1/resolver/test_14_check_loader_name.py
@@ -3,8 +3,8 @@
 import pytest
 from pydantic import BaseModel
 from pydantic_resolve import Resolver, LoaderDepend
-import tests.resolver.test_14_deps.mod_a as a
-import tests.resolver.test_14_deps.mod_b as b
+import tests.pydantic_v1.resolver.test_14_deps.mod_a as a
+import tests.pydantic_v1.resolver.test_14_deps.mod_b as b
 
 @pytest.mark.asyncio
 async def test_loader_depends():
diff --git a/tests/pydantic_v1/resolver/test_14_deps/__init__.py b/tests/pydantic_v1/resolver/test_14_deps/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/resolver/test_14_deps/mod_a.py b/tests/pydantic_v1/resolver/test_14_deps/mod_a.py
similarity index 100%
rename from tests/resolver/test_14_deps/mod_a.py
rename to tests/pydantic_v1/resolver/test_14_deps/mod_a.py
diff --git a/tests/resolver/test_14_deps/mod_b.py b/tests/pydantic_v1/resolver/test_14_deps/mod_b.py
similarity index 100%
rename from tests/resolver/test_14_deps/mod_b.py
rename to tests/pydantic_v1/resolver/test_14_deps/mod_b.py
diff --git a/tests/resolver/test_15_support_batch_load_fn.py b/tests/pydantic_v1/resolver/test_15_support_batch_load_fn.py
similarity index 100%
rename from tests/resolver/test_15_support_batch_load_fn.py
rename to tests/pydantic_v1/resolver/test_15_support_batch_load_fn.py
diff --git a/tests/resolver/test_16_mapper.py b/tests/pydantic_v1/resolver/test_16_mapper.py
similarity index 100%
rename from tests/resolver/test_16_mapper.py
rename to tests/pydantic_v1/resolver/test_16_mapper.py
diff --git a/tests/resolver/test_16_mapper_1.py b/tests/pydantic_v1/resolver/test_16_mapper_1.py
similarity index 100%
rename from tests/resolver/test_16_mapper_1.py
rename to tests/pydantic_v1/resolver/test_16_mapper_1.py
diff --git a/tests/resolver/test_16_mapper_2.py b/tests/pydantic_v1/resolver/test_16_mapper_2.py
similarity index 100%
rename from tests/resolver/test_16_mapper_2.py
rename to tests/pydantic_v1/resolver/test_16_mapper_2.py
diff --git a/tests/resolver/test_16_mapper_3.py b/tests/pydantic_v1/resolver/test_16_mapper_3.py
similarity index 100%
rename from tests/resolver/test_16_mapper_3.py
rename to tests/pydantic_v1/resolver/test_16_mapper_3.py
diff --git a/tests/resolver/test_16_mapper_4.py b/tests/pydantic_v1/resolver/test_16_mapper_4.py
similarity index 100%
rename from tests/resolver/test_16_mapper_4.py
rename to tests/pydantic_v1/resolver/test_16_mapper_4.py
diff --git a/tests/resolver/test_16_mapper_5.py b/tests/pydantic_v1/resolver/test_16_mapper_5.py
similarity index 100%
rename from tests/resolver/test_16_mapper_5.py
rename to tests/pydantic_v1/resolver/test_16_mapper_5.py
diff --git a/tests/resolver/test_16_mapper_6.py b/tests/pydantic_v1/resolver/test_16_mapper_6.py
similarity index 100%
rename from tests/resolver/test_16_mapper_6.py
rename to tests/pydantic_v1/resolver/test_16_mapper_6.py
diff --git a/tests/resolver/test_17_mapper_deep.py b/tests/pydantic_v1/resolver/test_17_mapper_deep.py
similarity index 100%
rename from tests/resolver/test_17_mapper_deep.py
rename to tests/pydantic_v1/resolver/test_17_mapper_deep.py
diff --git a/tests/resolver/test_18_post_methods.py b/tests/pydantic_v1/resolver/test_18_post_methods.py
similarity index 100%
rename from tests/resolver/test_18_post_methods.py
rename to tests/pydantic_v1/resolver/test_18_post_methods.py
diff --git a/tests/resolver/test_19_post_methods_exception.py b/tests/pydantic_v1/resolver/test_19_post_methods_exception.py
similarity index 100%
rename from tests/resolver/test_19_post_methods_exception.py
rename to tests/pydantic_v1/resolver/test_19_post_methods_exception.py
diff --git a/tests/resolver/test_1_pydantic_resolve.py b/tests/pydantic_v1/resolver/test_1_pydantic_resolve.py
similarity index 100%
rename from tests/resolver/test_1_pydantic_resolve.py
rename to tests/pydantic_v1/resolver/test_1_pydantic_resolve.py
diff --git a/tests/resolver/test_20_loader_instance.py b/tests/pydantic_v1/resolver/test_20_loader_instance.py
similarity index 100%
rename from tests/resolver/test_20_loader_instance.py
rename to tests/pydantic_v1/resolver/test_20_loader_instance.py
diff --git a/tests/resolver/test_21_not_stop_by_idle_level.py b/tests/pydantic_v1/resolver/test_21_not_stop_by_idle_level.py
similarity index 100%
rename from tests/resolver/test_21_not_stop_by_idle_level.py
rename to tests/pydantic_v1/resolver/test_21_not_stop_by_idle_level.py
diff --git a/tests/resolver/test_22_not_stop_by_idle_level_complex_1.py b/tests/pydantic_v1/resolver/test_22_not_stop_by_idle_level_complex_1.py
similarity index 100%
rename from tests/resolver/test_22_not_stop_by_idle_level_complex_1.py
rename to tests/pydantic_v1/resolver/test_22_not_stop_by_idle_level_complex_1.py
diff --git a/tests/resolver/test_22_not_stop_by_idle_level_complex_2.py b/tests/pydantic_v1/resolver/test_22_not_stop_by_idle_level_complex_2.py
similarity index 100%
rename from tests/resolver/test_22_not_stop_by_idle_level_complex_2.py
rename to tests/pydantic_v1/resolver/test_22_not_stop_by_idle_level_complex_2.py
diff --git a/tests/resolver/test_23_parse_to_obj_for_pydantic.py b/tests/pydantic_v1/resolver/test_23_parse_to_obj_for_pydantic.py
similarity index 100%
rename from tests/resolver/test_23_parse_to_obj_for_pydantic.py
rename to tests/pydantic_v1/resolver/test_23_parse_to_obj_for_pydantic.py
diff --git a/tests/resolver/test_24_parse_to_obj_for_dataclass.py b/tests/pydantic_v1/resolver/test_24_parse_to_obj_for_dataclass.py
similarity index 100%
rename from tests/resolver/test_24_parse_to_obj_for_dataclass.py
rename to tests/pydantic_v1/resolver/test_24_parse_to_obj_for_dataclass.py
diff --git a/tests/resolver/test_25_parse_to_obj_for_pydantic_with_annotation.py b/tests/pydantic_v1/resolver/test_25_parse_to_obj_for_pydantic_with_annotation.py
similarity index 100%
rename from tests/resolver/test_25_parse_to_obj_for_pydantic_with_annotation.py
rename to tests/pydantic_v1/resolver/test_25_parse_to_obj_for_pydantic_with_annotation.py
diff --git a/tests/resolver/test_26_tree.py b/tests/pydantic_v1/resolver/test_26_tree.py
similarity index 100%
rename from tests/resolver/test_26_tree.py
rename to tests/pydantic_v1/resolver/test_26_tree.py
diff --git a/tests/resolver/test_27_context.py b/tests/pydantic_v1/resolver/test_27_context.py
similarity index 100%
rename from tests/resolver/test_27_context.py
rename to tests/pydantic_v1/resolver/test_27_context.py
diff --git a/tests/resolver/test_28_parse_to_obj_for_dataclass_with_annotation.py b/tests/pydantic_v1/resolver/test_28_parse_to_obj_for_dataclass_with_annotation.py
similarity index 100%
rename from tests/resolver/test_28_parse_to_obj_for_dataclass_with_annotation.py
rename to tests/pydantic_v1/resolver/test_28_parse_to_obj_for_dataclass_with_annotation.py
diff --git a/tests/resolver/test_29_better_warning_of_list.py b/tests/pydantic_v1/resolver/test_29_better_warning_of_list.py
similarity index 100%
rename from tests/resolver/test_29_better_warning_of_list.py
rename to tests/pydantic_v1/resolver/test_29_better_warning_of_list.py
diff --git a/tests/resolver/test_2_resolve_object.py b/tests/pydantic_v1/resolver/test_2_resolve_object.py
similarity index 100%
rename from tests/resolver/test_2_resolve_object.py
rename to tests/pydantic_v1/resolver/test_2_resolve_object.py
diff --git a/tests/resolver/test_30_loader_in_object.py b/tests/pydantic_v1/resolver/test_30_loader_in_object.py
similarity index 100%
rename from tests/resolver/test_30_loader_in_object.py
rename to tests/pydantic_v1/resolver/test_30_loader_in_object.py
diff --git a/tests/resolver/test_31_dynamic_variable_for_descdant_loader.py b/tests/pydantic_v1/resolver/test_31_dynamic_variable_for_descdant_loader.py
similarity index 100%
rename from tests/resolver/test_31_dynamic_variable_for_descdant_loader.py
rename to tests/pydantic_v1/resolver/test_31_dynamic_variable_for_descdant_loader.py
diff --git a/tests/resolver/test_32_dynamic_variable_for_descdant_loader_exception.py b/tests/pydantic_v1/resolver/test_32_dynamic_variable_for_descdant_loader_exception.py
similarity index 100%
rename from tests/resolver/test_32_dynamic_variable_for_descdant_loader_exception.py
rename to tests/pydantic_v1/resolver/test_32_dynamic_variable_for_descdant_loader_exception.py
diff --git a/tests/resolver/test_33_global_loader_filter.py b/tests/pydantic_v1/resolver/test_33_global_loader_filter.py
similarity index 100%
rename from tests/resolver/test_33_global_loader_filter.py
rename to tests/pydantic_v1/resolver/test_33_global_loader_filter.py
diff --git a/tests/resolver/test_34_filter_rename_deprecation.py b/tests/pydantic_v1/resolver/test_34_filter_rename_deprecation.py
similarity index 100%
rename from tests/resolver/test_34_filter_rename_deprecation.py
rename to tests/pydantic_v1/resolver/test_34_filter_rename_deprecation.py
diff --git a/tests/resolver/test_35_collector.py b/tests/pydantic_v1/resolver/test_35_collector.py
similarity index 100%
rename from tests/resolver/test_35_collector.py
rename to tests/pydantic_v1/resolver/test_35_collector.py
diff --git a/tests/resolver/test_36_collector_level_by_level.py b/tests/pydantic_v1/resolver/test_36_collector_level_by_level.py
similarity index 100%
rename from tests/resolver/test_36_collector_level_by_level.py
rename to tests/pydantic_v1/resolver/test_36_collector_level_by_level.py
diff --git a/tests/resolver/test_37_specific_types.py b/tests/pydantic_v1/resolver/test_37_specific_types.py
similarity index 100%
rename from tests/resolver/test_37_specific_types.py
rename to tests/pydantic_v1/resolver/test_37_specific_types.py
diff --git a/tests/resolver/test_38_parent.py b/tests/pydantic_v1/resolver/test_38_parent.py
similarity index 100%
rename from tests/resolver/test_38_parent.py
rename to tests/pydantic_v1/resolver/test_38_parent.py
diff --git a/tests/resolver/test_39_post_async.py b/tests/pydantic_v1/resolver/test_39_post_async.py
similarity index 100%
rename from tests/resolver/test_39_post_async.py
rename to tests/pydantic_v1/resolver/test_39_post_async.py
diff --git a/tests/resolver/test_3_tuple_list.py b/tests/pydantic_v1/resolver/test_3_tuple_list.py
similarity index 100%
rename from tests/resolver/test_3_tuple_list.py
rename to tests/pydantic_v1/resolver/test_3_tuple_list.py
diff --git a/tests/resolver/test_40_multiple_collect_source.py b/tests/pydantic_v1/resolver/test_40_multiple_collect_source.py
similarity index 100%
rename from tests/resolver/test_40_multiple_collect_source.py
rename to tests/pydantic_v1/resolver/test_40_multiple_collect_source.py
diff --git a/tests/resolver/test_41_validate_collect_relationship.py b/tests/pydantic_v1/resolver/test_41_validate_collect_relationship.py
similarity index 100%
rename from tests/resolver/test_41_validate_collect_relationship.py
rename to tests/pydantic_v1/resolver/test_41_validate_collect_relationship.py
diff --git a/tests/resolver/test_4_resolve_return_types.py b/tests/pydantic_v1/resolver/test_4_resolve_return_types.py
similarity index 100%
rename from tests/resolver/test_4_resolve_return_types.py
rename to tests/pydantic_v1/resolver/test_4_resolve_return_types.py
diff --git a/tests/resolver/test_5_exception.py b/tests/pydantic_v1/resolver/test_5_exception.py
similarity index 100%
rename from tests/resolver/test_5_exception.py
rename to tests/pydantic_v1/resolver/test_5_exception.py
diff --git a/tests/resolver/test_6_resolve_dataclass.py b/tests/pydantic_v1/resolver/test_6_resolve_dataclass.py
similarity index 100%
rename from tests/resolver/test_6_resolve_dataclass.py
rename to tests/pydantic_v1/resolver/test_6_resolve_dataclass.py
diff --git a/tests/resolver/test_7_sqlalchemy_query.py b/tests/pydantic_v1/resolver/test_7_sqlalchemy_query.py
similarity index 100%
rename from tests/resolver/test_7_sqlalchemy_query.py
rename to tests/pydantic_v1/resolver/test_7_sqlalchemy_query.py
diff --git a/tests/resolver/test_8_loader_depend.py b/tests/pydantic_v1/resolver/test_8_loader_depend.py
similarity index 100%
rename from tests/resolver/test_8_loader_depend.py
rename to tests/pydantic_v1/resolver/test_8_loader_depend.py
diff --git a/tests/resolver/test_9_sqlalchemy_query_fix_cache.py b/tests/pydantic_v1/resolver/test_9_sqlalchemy_query_fix_cache.py
similarity index 100%
rename from tests/resolver/test_9_sqlalchemy_query_fix_cache.py
rename to tests/pydantic_v1/resolver/test_9_sqlalchemy_query_fix_cache.py
diff --git a/tests/utils/test_2_ensure_subset.py b/tests/pydantic_v1/utils/test_2_ensure_subset.py
similarity index 100%
rename from tests/utils/test_2_ensure_subset.py
rename to tests/pydantic_v1/utils/test_2_ensure_subset.py
diff --git a/tests/utils/test_merge.py b/tests/pydantic_v1/utils/test_merge.py
similarity index 100%
rename from tests/utils/test_merge.py
rename to tests/pydantic_v1/utils/test_merge.py
diff --git a/tests/utils/test_model_config.py b/tests/pydantic_v1/utils/test_model_config.py
similarity index 87%
rename from tests/utils/test_model_config.py
rename to tests/pydantic_v1/utils/test_model_config.py
index c9ee7f4..e9b8d59 100644
--- a/tests/utils/test_model_config.py
+++ b/tests/pydantic_v1/utils/test_model_config.py
@@ -15,17 +15,17 @@ class X(BaseModel):
     async def loader_fn(keys):
         return keys
 
-    @model_config(hidden_fields=['id', 'password', 'passwords'])
+    @model_config()
     @ensure_subset(X)
     class Y(BaseModel):
-        id: int = 0
+        id: int = Field(0, exclude=True)
         name: str
 
-        password: str = ''
+        password: str = Field('', exclude=True)
         def resolve_password(self, loader=LD(loader_fn)):
             return loader.load('xxx')
             
-        passwords: List[str] = []
+        passwords: List[str] = Field(default_factory=list, exclude=True)
         def resolve_passwords(self):
             return ['a', 'b']
 
@@ -45,15 +45,15 @@ def resolve_passwords(self):
 async def test_schema_config_hidden_with_field():
     """Field(exclude=True) will also work """
 
-    @model_config(hidden_fields=['id', 'password'])
+    @model_config()
     class Y(BaseModel):
-        id: int = 0
+        id: int = Field(0, exclude=True)
         def resolve_id(self):
             return 1
 
         name: str = Field(exclude=True)
 
-        password: str = ''
+        password: str = Field('', exclude=True)
         def resolve_password(self):
             return 'confidential'
 
@@ -111,16 +111,16 @@ async def load_items(keys):
     async def load_details(keys):
         return [['1', '2'] for key in keys]
 
-    @model_config(hidden_fields=['details'])
+    @model_config()
     class Item(BaseModel):
         name: str
-        details: List[str] = []
+        details: List[str] = Field(default_factory=list, exclude=True)
         def resolve_details(self, loader=LD(load_details)):
             return loader.load(self.name)
 
-    @model_config(hidden_fields=['id'])
+    @model_config()
     class Record(BaseModel):
-        id: int = 0
+        id: int = Field(0, exclude=True)
         items: List[Item] = []
         def resolve_items(self, loader=LD(load_items)):
             return loader.load(self.id)
diff --git a/tests/utils/test_output.py b/tests/pydantic_v1/utils/test_output.py
similarity index 100%
rename from tests/utils/test_output.py
rename to tests/pydantic_v1/utils/test_output.py
diff --git a/tests/utils/test_output_2.py b/tests/pydantic_v1/utils/test_output_2.py
similarity index 100%
rename from tests/utils/test_output_2.py
rename to tests/pydantic_v1/utils/test_output_2.py
diff --git a/tests/utils/test_parse.py b/tests/pydantic_v1/utils/test_parse.py
similarity index 100%
rename from tests/utils/test_parse.py
rename to tests/pydantic_v1/utils/test_parse.py
diff --git a/tests/utils/test_parse_forward_ref.py b/tests/pydantic_v1/utils/test_parse_forward_ref.py
similarity index 100%
rename from tests/utils/test_parse_forward_ref.py
rename to tests/pydantic_v1/utils/test_parse_forward_ref.py
diff --git a/tests/utils/test_utils.py b/tests/pydantic_v1/utils/test_utils.py
similarity index 100%
rename from tests/utils/test_utils.py
rename to tests/pydantic_v1/utils/test_utils.py
diff --git a/tests/pydantic_v2/__init__.py b/tests/pydantic_v2/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/pydantic_v2/core/test_field.py b/tests/pydantic_v2/core/test_field.py
new file mode 100644
index 0000000..4ccbd74
--- /dev/null
+++ b/tests/pydantic_v2/core/test_field.py
@@ -0,0 +1,13 @@
+# from __future__ import annotations
+from pydantic import BaseModel
+from pydantic_resolve.core import get_class
+
+def test_get_class():
+    class Student(BaseModel):
+        name: str = 'kikodo'
+
+    stu = Student()
+    stus = [Student(), Student()]
+
+    assert get_class(stu) == Student
+    assert get_class(stus) == Student
diff --git a/tests/pydantic_v2/core/test_field_dataclass.py b/tests/pydantic_v2/core/test_field_dataclass.py
new file mode 100644
index 0000000..adf4e59
--- /dev/null
+++ b/tests/pydantic_v2/core/test_field_dataclass.py
@@ -0,0 +1,78 @@
+# from __future__ import annotations
+from dataclasses import dataclass, field
+from typing import Optional, List
+from pydantic_resolve.core import scan_and_store_metadata
+
+@dataclass
+class Queue:
+    name: str
+
+@dataclass
+class Zone:
+    name: str
+    qs: List[Queue]
+
+@dataclass
+class Zeta:
+    name: str
+
+
+@dataclass
+class Student:
+    __pydantic_resolve_expose__ = {'name': 'student_name'}
+    zone: Optional[Zone] = None
+    name: str = ''
+
+    def resolve_name(self):
+        return '.'
+
+    def post_name(self):
+        return '.'
+
+    zeta2: Optional[List[Zeta]] = None
+    zetas2: List[Optional[Zeta]] = field(default_factory=list)
+
+    zeta: Optional[Zeta] = None
+
+    def resolve_zeta(self):
+        return dict(name='z')
+
+
+def test_get_all_fields():
+    result = scan_and_store_metadata(Student)
+    expect = {
+        'test_field_dataclass.Student': {
+            'resolve': ['resolve_name', 'resolve_zeta'],
+            'post': ['post_name'],
+            'attribute': ['zone', 'zeta2', 'zetas2'],
+            'expose_dict': {'name': 'student_name'},
+            'collect_dict': {},
+            'has_context': False,
+        },
+        'test_field_dataclass.Zone': {
+            'resolve': [],
+            'post': [],
+            'attribute': ['qs'],
+            'expose_dict': {},
+            'collect_dict': {},
+            'has_context': False,
+        },
+        'test_field_dataclass.Queue': {
+            'resolve': [],
+            'post': [],
+            'attribute': [],
+            'expose_dict': {},
+            'collect_dict': {},
+            'has_context': False,
+        },
+        'test_field_dataclass.Zeta': {
+            'resolve': [],
+            'post': [],
+            'attribute': [],
+            'expose_dict': {},
+            'collect_dict': {},
+            'has_context': False,
+        }
+    }
+    for k, v in result.items():
+        assert expect[k].items() <= v.items()
\ No newline at end of file
diff --git a/tests/pydantic_v2/core/test_field_dataclass_anno.py b/tests/pydantic_v2/core/test_field_dataclass_anno.py
new file mode 100644
index 0000000..8a2dd98
--- /dev/null
+++ b/tests/pydantic_v2/core/test_field_dataclass_anno.py
@@ -0,0 +1,114 @@
+from dataclasses import dataclass, field
+from typing import Optional, List
+from pydantic_resolve.core import scan_and_store_metadata
+from pydantic_resolve import LoaderDepend, Collector
+
+
+async def loader_fn(keys):
+    return keys
+
+@dataclass
+class Student:
+    __pydantic_resolve_expose__ = {'name': 'student_name'}
+    zone: Optional['Zone'] = None
+    name: str = ''
+
+    def resolve_name(self, context, ancestor_context, loader=LoaderDepend(loader_fn)):
+        return '.'
+
+    def post_name(self):
+        return '.'
+
+    zeta: Optional['Zeta'] = None
+
+    def resolve_zeta(self):
+        return dict(name='z')
+
+    queue_names: List[str] = field(default_factory=list)
+    def post_queue_names(self, collector=Collector('queue_name')):
+        return collector.values()
+
+@dataclass
+class Queue:
+    __pydantic_resolve_collect__ = {"name": "queue_name"}
+    name: str
+
+@dataclass
+class Zone:
+    name: str
+    qs: List[Queue]
+
+@dataclass
+class Zeta:
+    name: str
+
+
+def test_get_all_fields():
+    result = scan_and_store_metadata(Student)
+    expect = {
+        'test_field_dataclass_anno.Student': {
+            'resolve': ['resolve_name', 'resolve_zeta'],
+            'post': ['post_name', 'post_queue_names'],
+            'attribute': ['zone'],
+            'expose_dict': {'name': 'student_name'},
+            'collect_dict': {},
+            'kls': Student
+        },
+        'test_field_dataclass_anno.Zone': {
+            'resolve': [],
+            'post': [],
+            'attribute': ['qs'],
+            'expose_dict': {},
+            'collect_dict': {},
+            'kls': Zone
+        },
+        'test_field_dataclass_anno.Queue': {
+            'resolve': [],
+            'post': [],
+            'attribute': [],
+            'expose_dict': {},
+            'collect_dict': {'name': 'queue_name'},
+            'kls': Queue
+        },
+        'test_field_dataclass_anno.Zeta': {
+            'resolve': [],
+            'post': [],
+            'attribute': [],
+            'expose_dict': {},
+            'collect_dict': {},
+            'kls': Zeta
+        }
+    }
+    for k, v in result.items():
+        assert expect[k].items() <= v.items()
+
+def test_resolve_params():
+    result = scan_and_store_metadata(Student)
+    expect = {
+        'test_field_dataclass_anno.Student': {
+            'resolve_params': {
+                'resolve_name': {
+                    'trim_field': 'name',
+                    'context': True,
+                    'ancestor_context': True,
+                    'parent': False,
+                    'dataloaders': [
+                        {
+                            'param': 'loader',
+                            'kls': loader_fn,
+                            'path': 'test_field_dataclass_anno.loader_fn'
+                        }
+                    ],
+                }, 
+                'resolve_zeta': {
+                    'trim_field': 'zeta',
+                    'context': False,
+                    'ancestor_context': False,
+                    'parent': False,
+                    'dataloaders': [],
+                }
+            },
+        },
+    }
+    key = 'test_field_dataclass_anno.Student'
+    assert expect[key].items() <= result[key].items()
\ No newline at end of file
diff --git a/tests/pydantic_v2/core/test_field_mix.py b/tests/pydantic_v2/core/test_field_mix.py
new file mode 100644
index 0000000..645a41b
--- /dev/null
+++ b/tests/pydantic_v2/core/test_field_mix.py
@@ -0,0 +1,40 @@
+from __future__ import annotations
+from dataclasses import dataclass
+from typing import Optional, List
+from pydantic import BaseModel
+from pydantic_resolve.core import scan_and_store_metadata
+
+@dataclass
+class Book:
+    name: str
+
+class Student(BaseModel):
+    __pydantic_resolve_expose__ = {'name': 's_name'}
+    name: str = ''
+
+    books: List[Book] = []
+
+    def resolve_books(self):
+        return []
+
+
+def test_get_all_fields():
+    result = scan_and_store_metadata(Student)
+    expect = {
+        'test_field_mix.Student': {
+            'resolve': ['resolve_books'],
+            'post': [],
+            'attribute': [],
+            'expose_dict': {'name': 's_name'},
+            'collect_dict': {}
+        },
+        'test_field_mix.Book': {
+            'resolve': [],
+            'post': [],
+            'attribute': [],
+            'expose_dict': {},
+            'collect_dict': {}
+        },
+    }
+    for k, v in result.items():
+        assert expect[k].items() <= v.items()
\ No newline at end of file
diff --git a/tests/pydantic_v2/core/test_field_pydantic.py b/tests/pydantic_v2/core/test_field_pydantic.py
new file mode 100644
index 0000000..67c532b
--- /dev/null
+++ b/tests/pydantic_v2/core/test_field_pydantic.py
@@ -0,0 +1,162 @@
+from __future__ import annotations
+from typing import Optional, List
+from pydantic import BaseModel
+from pydantic_resolve.core import scan_and_store_metadata, convert_metadata_key_as_kls
+from pydantic_resolve import LoaderDepend
+
+async def loader_fn(keys):
+    return keys
+
+class Student(BaseModel):
+    __pydantic_resolve_expose__ = {'name': 'student_name'}
+    name: str = ''
+    resolve_hello: str = ''
+
+    def resolve_name(self, context, ancestor_context, loader=LoaderDepend(loader_fn)):
+        return '.'
+
+    def post_name(self):
+        return '.'
+
+    zones: List[Optional[Zone]] = [None]
+    zones2: List[Zone] = []
+    zone: Optional[Optional[Zone]] = None
+    z: str # ignored in attributes
+
+    zeta: Optional[Zeta] = None
+
+    def resolve_zeta(self):
+        return dict(name='z')
+
+class Zone(BaseModel):
+    name: str
+    qs: List[Queue]
+
+class Queue(BaseModel):
+    name: str
+
+class Zeta(BaseModel):
+    name: str
+
+
+def test_get_all_fields():
+    result = scan_and_store_metadata(Student)
+    expect = {
+        'test_field_pydantic.Student': {
+            'resolve': ['resolve_name', 'resolve_zeta'],
+            'post': ['post_name'],
+            'attribute': ['zones', 'zones2', 'zone'],
+            'expose_dict': {'name': 'student_name'},
+            'collect_dict': {}
+            # ... others
+        },
+        'test_field_pydantic.Zone': {
+            'resolve': [],
+            'post': [],
+            'attribute': ['qs'],
+            'expose_dict': {},
+            'collect_dict': {}
+            # ... others
+        },
+        'test_field_pydantic.Queue': {
+            'resolve': [],
+            'post': [],
+            'attribute': [],
+            'expose_dict': {},
+            'collect_dict': {}
+            # ... others
+        },
+        'test_field_pydantic.Zeta': {
+            'resolve': [],
+            'post': [],
+            'attribute': [],
+            'expose_dict': {},
+            'collect_dict': {}
+            # ... others
+        }
+    }
+    for k, v in result.items():
+        assert expect[k].items() <= v.items()
+
+
+def test_convert_metadata():
+    result = scan_and_store_metadata(Student)
+    result = convert_metadata_key_as_kls(result)
+    expect = {
+        Student: {
+            'resolve': ['resolve_name', 'resolve_zeta'],
+            'post': ['post_name'],
+            'attribute': ['zones', 'zones2', 'zone'],
+            'expose_dict': {'name': 'student_name'},
+            'collect_dict': {},
+            'kls': Student,
+            'kls_path': 'test_field_pydantic.Student',
+            # ... others
+        },
+        Zone: {
+            'resolve': [],
+            'post': [],
+            'attribute': ['qs'],
+            'expose_dict': {},
+            'collect_dict': {},
+            'kls': Zone,
+            'kls_path': 'test_field_pydantic.Zone',
+            # ... others
+        },
+        Queue: {
+            'resolve': [],
+            'post': [],
+            'attribute': [],
+            'expose_dict': {},
+            'collect_dict': {},
+            'kls': Queue,
+            'kls_path': 'test_field_pydantic.Queue',
+            # ... others
+        },
+        Zeta: {
+            'resolve': [],
+            'post': [],
+            'attribute': [],
+            'expose_dict': {},
+            'collect_dict': {},
+            'kls': Zeta,
+            'kls_path': 'test_field_pydantic.Zeta',
+            # ... others
+        }
+    }
+    for k, v in result.items():
+        assert expect[k].items() <= v.items()
+
+
+def test_resolve_params():
+    result = scan_and_store_metadata(Student)
+    expect = {
+        'test_field_pydantic.Student': {
+            # ... others
+            'has_context': True,
+            'resolve_params': {
+                'resolve_name': {
+                    'trim_field': 'name',
+                    'context': True,
+                    'ancestor_context': True,
+                    'parent': False,
+                    'dataloaders': [
+                        {
+                            'param': 'loader',
+                            'kls': loader_fn,
+                            'path': 'test_field_pydantic.loader_fn'
+                        }
+                    ],
+                }, 
+                'resolve_zeta': {
+                    'trim_field': 'zeta',
+                    'context': False,
+                    'ancestor_context': False,
+                    'parent': False,
+                    'dataloaders': [],
+                }
+            },
+        }
+    }
+    key = 'test_field_pydantic.Student'
+    assert expect[key].items() <= result[key].items()
\ No newline at end of file
diff --git a/tests/pydantic_v2/core/test_field_pydantic_error.py b/tests/pydantic_v2/core/test_field_pydantic_error.py
new file mode 100644
index 0000000..6d14eb9
--- /dev/null
+++ b/tests/pydantic_v2/core/test_field_pydantic_error.py
@@ -0,0 +1,27 @@
+from __future__ import annotations
+import pytest
+from typing import Optional
+from pydantic import BaseModel
+from pydantic_resolve.core import scan_and_store_metadata
+
+class A(BaseModel):
+    __pydantic_resolve_expose__ = {'name': 'A_name'}
+    name: str = ''
+    b: Optional[B] = None
+    def resolve_b(self):
+        return dict(name='b')
+
+class B(BaseModel):
+    __pydantic_resolve_expose__ = {'name': 'A_name'}
+    name: str
+    c: Optional[C] = None
+    def resolve_c(self):
+        return dict(name='c')
+
+class C(BaseModel):
+    name: str
+
+
+def test_raise_exception():
+    with pytest.raises(AttributeError):
+        scan_and_store_metadata(A)
\ No newline at end of file
diff --git a/tests/pydantic_v2/core/test_field_validate_and_create_loader_instance.py b/tests/pydantic_v2/core/test_field_validate_and_create_loader_instance.py
new file mode 100644
index 0000000..969742f
--- /dev/null
+++ b/tests/pydantic_v2/core/test_field_validate_and_create_loader_instance.py
@@ -0,0 +1,90 @@
+from __future__ import annotations
+import pytest
+from typing import Optional, List
+from pydantic import BaseModel
+from aiodataloader import DataLoader
+from pydantic_resolve.core import scan_and_store_metadata, validate_and_create_loader_instance
+from pydantic_resolve import LoaderDepend, LoaderFieldNotProvidedError
+
+async def loader_fn(keys):
+    return keys
+
+class MyLoader(DataLoader):
+    param: str
+    async def batch_load_fn(self, keys):
+        return keys
+
+class Student(BaseModel):
+    __pydantic_resolve_expose__ = {'name': 'student_name'}
+    name: str = ''
+    resolve_hello: str = ''
+
+    def resolve_name(self, context, ancestor_context, loader=LoaderDepend(loader_fn)):
+        return '.'
+
+    def post_name(self, loader=LoaderDepend(loader_fn)):
+        return '.'
+
+    zones: List[Optional[Zone]] = [None]
+    zones2: List[Zone] = []
+    zone: Optional[Optional[Zone]] = None
+
+    zeta: Optional[Zeta] = None
+
+    def resolve_zeta(self):
+        return dict(name='z')
+
+class Zone(BaseModel):
+    name: str
+    qs: List[Queue]
+    def resolve_qs(self, qs_loader=LoaderDepend(MyLoader)):
+        return qs_loader.load(self.name)
+
+class Queue(BaseModel):
+    name: str
+
+class Zeta(BaseModel):
+    name: str
+
+
+loader_params = {
+    MyLoader: {
+        'param': 'aaa'
+    }
+}
+global_loader_param = {
+    'param': 'aaa'
+}
+
+@pytest.mark.asyncio
+async def test_instance_1():
+    metadata = scan_and_store_metadata(Student)
+    loader_instance = validate_and_create_loader_instance(loader_params, {}, {}, metadata)
+
+    assert isinstance(loader_instance['test_field_validate_and_create_loader_instance.loader_fn'] , DataLoader)
+    assert isinstance(loader_instance['test_field_validate_and_create_loader_instance.MyLoader'] , MyLoader)
+
+@pytest.mark.asyncio
+async def test_instance_2():
+    """ test cache works """
+    metadata = scan_and_store_metadata(Student)
+    loader_instance = validate_and_create_loader_instance(loader_params, {}, {}, metadata)
+
+    assert len(loader_instance) == 2
+
+@pytest.mark.asyncio
+async def test_instance_3():
+    """test global param"""
+    metadata = scan_and_store_metadata(Student)
+    loader_instance = validate_and_create_loader_instance({}, global_loader_param, {}, metadata)
+
+    assert isinstance(loader_instance['test_field_validate_and_create_loader_instance.loader_fn'] , DataLoader)
+    assert len(loader_instance) == 2
+
+@pytest.mark.asyncio
+async def test_instance_4():
+    """raise missing param error"""
+    metadata = scan_and_store_metadata(Student)
+
+    with pytest.raises(LoaderFieldNotProvidedError):
+        loader_instance = validate_and_create_loader_instance({}, {}, {}, metadata)
\ No newline at end of file
diff --git a/tests/pydantic_v2/core/test_input.py b/tests/pydantic_v2/core/test_input.py
new file mode 100644
index 0000000..9f54db2
--- /dev/null
+++ b/tests/pydantic_v2/core/test_input.py
@@ -0,0 +1,15 @@
+from pydantic_resolve import Resolver
+import pytest
+
+@pytest.mark.asyncio
+async def test_input():
+    data = []
+    data = await Resolver().resolve(data)
+    assert data == []
+
+
+@pytest.mark.asyncio
+async def test_input_2():
+    data = 'hello' 
+    with pytest.raises(AttributeError):
+        await Resolver().resolve(data)
diff --git a/tests/pydantic_v2/core/test_scan_post_method.py b/tests/pydantic_v2/core/test_scan_post_method.py
new file mode 100644
index 0000000..9c8912f
--- /dev/null
+++ b/tests/pydantic_v2/core/test_scan_post_method.py
@@ -0,0 +1,68 @@
+from pydantic import BaseModel
+from pydantic_resolve.core import _scan_post_method, _scan_post_default_handler
+from pydantic_resolve import Collector
+
+def test_scan_post_method_1():
+    class A(BaseModel):
+        a: str
+        def post_a(self):
+            return 2 * self.a
+        
+    result = _scan_post_method(A.post_a, 'post_a')
+
+    assert result == {
+        'trim_field': 'a',
+        'context': False,
+        'ancestor_context': False,
+        'parent': False,
+        'collectors': []
+    }
+
+
+def test_scan_post_method_2():
+    class A(BaseModel):
+        a: str
+        def post_a(self, context, ancestor_context, parent):
+            return 2 * self.a
+        
+    result = _scan_post_method(A.post_a, 'post_a')
+
+    assert result == {
+        'trim_field': 'a',
+        'context': True,
+        'ancestor_context': True,
+        'parent': True,
+        'collectors': []
+    }
+
+
+def test_scan_post_method_3():
+    class A(BaseModel):
+        a: str
+        def post_a(self, context, ancestor_context, collector=Collector(alias='c_name')):
+            return 2 * self.a
+        
+    result = _scan_post_method(A.post_a, 'post_a')
+
+    assert len(result['collectors']) == 1
+    assert result['collectors'][0]['field'] == 'post_a' 
+    assert result['collectors'][0]['param'] == 'collector' 
+    assert result['collectors'][0]['alias'] == 'c_name'
+    assert isinstance(result['collectors'][0]['instance'], Collector)
+
+
+def test_scan_post_method_4():
+    class A(BaseModel):
+        a: str
+        def post_a(self, context, ancestor_context, collector=Collector(alias='c_name')):
+            return 2 * self.a
+        
+        def post_default_handler(self, context, ancestor_context, parent):
+            return 1
+        
+    result = _scan_post_default_handler(A.post_default_handler)
+
+    assert result['context'] == True
+    assert result['parent'] == True
+    assert result['ancestor_context'] == True
+
diff --git a/tests/pydantic_v2/core/test_scan_resolve_method.py b/tests/pydantic_v2/core/test_scan_resolve_method.py
new file mode 100644
index 0000000..c63ef43
--- /dev/null
+++ b/tests/pydantic_v2/core/test_scan_resolve_method.py
@@ -0,0 +1,74 @@
+import pytest
+from aiodataloader import DataLoader
+from pydantic import BaseModel
+from pydantic_resolve.core import _scan_resolve_method, LoaderDepend, Collector
+
+def test_scan_resolve_method_1():
+    class A(BaseModel):
+        a: str
+        def resolve_a(self):
+            return 2 * self.a
+        
+    result = _scan_resolve_method(A.resolve_a, 'resolve_a')
+
+    assert result == {
+        'trim_field': 'a',
+        'context': False,
+        'ancestor_context': False,
+        'parent': False,
+        'dataloaders': []
+    }
+
+
+def test_scan_resolve_method_2():
+    class A(BaseModel):
+        a: str
+        def resolve_a(self, context, ancestor_context, parent):
+            return 2 * self.a
+        
+    result = _scan_resolve_method(A.resolve_a, 'resolve_a')
+
+    assert result == {
+        'trim_field': 'a',
+        'context': True,
+        'ancestor_context': True,
+        'parent': True,
+        'dataloaders': []
+    }
+
+
+def test_scan_resolve_method_3():
+    class Loader(DataLoader):
+        async def batch_loader_fn(self, keys):
+            return keys
+
+    class A(BaseModel):
+        a: str
+        def resolve_a(self, context, ancestor_context, parent, loader=LoaderDepend(Loader)):
+            return 2 * self.a
+        
+    result = _scan_resolve_method(A.resolve_a, 'resolve_a')
+
+    assert result == {
+        'trim_field': 'a',
+        'context': True,
+        'ancestor_context': True,
+        'parent': True,
+        'dataloaders': [
+            {
+                'param': 'loader',
+                'kls': Loader,
+                'path': 'test_scan_resolve_method.test_scan_resolve_method_3.<locals>.Loader' 
+            }
+        ]
+    }
+
+
+def test_scan_resolve_method_4():
+    class A(BaseModel):
+        a: str
+        def resolve_a(self, c=Collector('some_field')):
+            return c.values()
+        
+    with pytest.raises(AttributeError):
+        _scan_resolve_method(A.resolve_a, 'resolve_a')
diff --git a/tests/pydantic_v2/core/test_specific_type.py b/tests/pydantic_v2/core/test_specific_type.py
new file mode 100644
index 0000000..2ab8cf9
--- /dev/null
+++ b/tests/pydantic_v2/core/test_specific_type.py
@@ -0,0 +1,27 @@
+from __future__ import annotations
+from typing import Tuple
+from pydantic import BaseModel
+from pydantic_resolve.core import scan_and_store_metadata, convert_metadata_key_as_kls
+from pydantic_resolve import LoaderDepend
+
+async def loader_fn(keys):
+    return keys
+
+class Student(BaseModel):
+    zones: Tuple[int, int] = (0, 0)
+
+def test_get_all_fields():
+    # https://github.com/allmonday/pydantic2-resolve/issues/7
+    result = scan_and_store_metadata(Student)
+    expect = {
+        'test_specific_type.Student': {
+            'resolve': [],
+            'post': [],
+            'attribute': [],
+            'expose_dict': {},
+            'collect_dict': {}
+            # ... others
+        },
+    }
+    for k, v in result.items():
+        assert expect[k].items() <= v.items()
diff --git a/tests/pydantic_v2/resolver/__init__.py b/tests/pydantic_v2/resolver/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/pydantic_v2/resolver/test_0_depends.py b/tests/pydantic_v2/resolver/test_0_depends.py
new file mode 100644
index 0000000..520b1df
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_0_depends.py
@@ -0,0 +1,68 @@
+import inspect
+from typing import Any, Callable, Optional
+import pytest
+
+# verify the Feasibility of implement depends 
+class Loader:
+    def load(self):
+        print('load')
+
+def Depend(  # noqa: N802
+    dependency: Optional[Callable[..., Any]] = None 
+) -> Any:
+    return Depends(dependency=dependency)
+
+class Depends:
+    def __init__(
+        self, dependency: Optional[Callable[..., Any]] = None
+    ):
+        self.dependency = dependency
+
+class LoaderA(Loader):
+    def load(self):
+        print('load-a')
+
+class TestClass:
+    def resolve(self, loader = Depend(LoaderA)):
+        loader.load()
+
+def runner_maker():
+    cache = {}
+    counter = {
+        "init_count": 0
+    }
+
+    def exec(t_method):
+        signature = inspect.signature(t_method)
+        params = {}
+
+        for k, v in signature.parameters.items():
+            if v.default == inspect._empty:
+                continue
+
+            if isinstance(v.default, Depends):
+                cache_key = v.default.dependency.__name__
+                hit = cache.get(cache_key)
+                if hit:
+                    instance = hit
+                else:
+                    instance = v.default.dependency()
+                    cache[cache_key] = instance
+                    counter["init_count"] += 1
+                params[k] = instance
+        t_method(**params)
+        return counter
+    return exec
+
+@pytest.mark.asyncio
+async def test_depend():
+    run = runner_maker()
+    t = TestClass()
+    t2 = TestClass()
+    t3 = TestClass()
+
+    run(t.resolve)   # missing
+    run(t2.resolve)  # hit
+    counter = run(t3.resolve)  # hit
+
+    assert counter["init_count"] == 1
\ No newline at end of file
diff --git a/tests/pydantic_v2/resolver/test_10_sqlalchemy_query_with_change.py b/tests/pydantic_v2/resolver/test_10_sqlalchemy_query_with_change.py
new file mode 100644
index 0000000..30b4af4
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_10_sqlalchemy_query_with_change.py
@@ -0,0 +1,197 @@
+import pytest
+from typing import List
+from collections import Counter, defaultdict
+from aiodataloader import DataLoader
+from pydantic import ConfigDict, BaseModel
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import async_sessionmaker
+from sqlalchemy.ext.asyncio import create_async_engine
+from sqlalchemy.orm import DeclarativeBase
+from sqlalchemy.orm import Mapped
+from sqlalchemy.orm import mapped_column
+from pydantic_resolve import Resolver, LoaderDepend
+
+@pytest.mark.asyncio
+async def test_sqlite_and_dataloader():
+    counter = Counter()
+    engine = create_async_engine(
+        "sqlite+aiosqlite://",
+        echo=False,
+    )
+    async_session = async_sessionmaker(engine, expire_on_commit=False)
+
+    class Base(DeclarativeBase):
+        pass
+
+    class Task(Base):
+        __tablename__ = "task"
+
+        id: Mapped[int] = mapped_column(primary_key=True)
+        name: Mapped[str]
+
+    class Comment(Base):
+        __tablename__ = "comment"
+        id: Mapped[int] = mapped_column(primary_key=True)
+        task_id: Mapped[int] = mapped_column()
+        content: Mapped[str]
+
+    class Feedback(Base):
+        __tablename__ = "feedback"
+        id: Mapped[int] = mapped_column(primary_key=True)
+        comment_id: Mapped[int] = mapped_column()
+        content: Mapped[str]
+
+    async def insert_objects() -> None:
+        async with async_session() as session:
+            async with session.begin():
+                session.add_all(
+                    [
+                        Task(id=1, name="task-1"),
+
+                        Comment(id=1, task_id=1, content="comment-1 for task 1"),
+
+                        Feedback(id=1, comment_id=1, content="feedback-1 for comment-1"),
+                        Feedback(id=2, comment_id=1, content="feedback-2 for comment-1"),
+                        Feedback(id=3, comment_id=1, content="feedback-3 for comment-1"),
+                    ]
+                )
+
+    async def insert_and_update_objects() -> None:
+        async with async_session() as session:
+            async with session.begin():
+                task_1 = (await session.execute(select(Task).filter_by(id=1))).scalar_one()
+                task_1.name = 'task-1 xyz'
+
+                comment_1 = (await session.execute(select(Comment).filter_by(id=1))).scalar_one()
+                comment_1.content = 'comment-1 for task 1 (changes)'
+
+                feedback_1 = (await session.execute(select(Feedback).filter_by(id=1))).scalar_one()
+                feedback_1.content = 'feedback-1 for comment-1 (changes)'
+
+                session.add(task_1)
+                session.add(comment_1)
+                session.add(feedback_1)
+                session.add_all(
+                    [
+                        Comment(id=2, task_id=1, content="comment-2 for task 1"),
+
+                        Feedback(id=4, comment_id=2, content="test"),
+                    ]
+                )
+
+
+    # =========================== Pydantic Schema layer =========================
+    class FeedbackLoader(DataLoader):
+        async def batch_load_fn(self, comment_ids):
+            counter['load-feedback'] += 1
+            async with async_session() as session:
+                res = await session.execute(select(Feedback).where(Feedback.comment_id.in_(comment_ids)))
+                rows = res.scalars().all()
+                dct = defaultdict(list)
+                for row in rows:
+                    dct[row.comment_id].append(FeedbackSchema.model_validate(row))
+                return [dct.get(k, []) for k in comment_ids]
+
+    class CommentLoader(DataLoader):
+        async def batch_load_fn(self, task_ids):
+            counter['load-comment'] += 1
+            async with async_session() as session:
+                res = await session.execute(select(Comment).where(Comment.task_id.in_(task_ids)))
+                rows = res.scalars().all()
+
+                dct = defaultdict(list)
+                for row in rows:
+                    dct[row.task_id].append(CommentSchema.model_validate(row))
+                return [dct.get(k, []) for k in task_ids]
+
+    class FeedbackSchema(BaseModel):
+        id: int
+        comment_id: int
+        content: str
+        model_config = ConfigDict(from_attributes=True)
+
+    class CommentSchema(BaseModel):
+        id: int
+        task_id: int
+        content: str
+        feedbacks: List[FeedbackSchema]  = []
+
+        def resolve_feedbacks(self, loader=LoaderDepend(FeedbackLoader)):
+            return loader.load(self.id)
+        model_config = ConfigDict(from_attributes=True)
+
+    class TaskSchema(BaseModel):
+        id: int
+        name: str
+        comments: List[CommentSchema]  = []
+        
+        def resolve_comments(self, loader=LoaderDepend(CommentLoader)):
+            return loader.load(self.id)
+        model_config = ConfigDict(from_attributes=True)
+
+    async def init():
+        async with engine.begin() as conn:
+            await conn.run_sync(Base.metadata.create_all)
+        await insert_objects()
+
+    async def task_query(task_id = 1):
+        async with async_session() as session:
+            tasks = (await session.execute(select(Task).where(Task.id == task_id))).scalars().all()
+            task_objs = [TaskSchema.model_validate(t) for t in tasks]
+            resolved_results = await Resolver().resolve(task_objs)
+            to_dict_arr = [r.model_dump() for r in resolved_results]
+            return to_dict_arr
+
+    await init()
+    result_1 = await task_query()
+    await insert_and_update_objects()
+    result_2 = await task_query()
+
+    expected_1 = [
+        {
+            'id': 1,
+            'name': 'task-1',
+            'comments': [
+                {
+                    'content': 'comment-1 for task 1',
+                    'feedbacks': [
+                        {'comment_id': 1, 'content': 'feedback-1 for comment-1', 'id': 1},
+                        {'comment_id': 1, 'content': 'feedback-2 for comment-1', 'id': 2},
+                        {'comment_id': 1, 'content': 'feedback-3 for comment-1', 'id': 3}
+                        ],
+                    'id': 1,
+                    'task_id': 1},
+                ],
+            }
+        ]
+
+    expected_2 = [
+        {
+            'id': 1,
+            'name': 'task-1 xyz',
+            'comments': [
+                {
+                    'content': 'comment-1 for task 1 (changes)',
+                    'feedbacks': [
+                        {'comment_id': 1, 'content': 'feedback-1 for comment-1 (changes)', 'id': 1},
+                        {'comment_id': 1, 'content': 'feedback-2 for comment-1', 'id': 2},
+                        {'comment_id': 1, 'content': 'feedback-3 for comment-1', 'id': 3}
+                        ],
+                    'id': 1,
+                    'task_id': 1},
+                {
+                    'content': 'comment-2 for task 1',
+                    'feedbacks': [
+                        {'comment_id': 2, 'content': 'test', 'id': 4},
+                    ],
+                    'id': 2,
+                    'task_id': 1}
+                ],
+            }
+        ]
+
+    assert result_1 == expected_1
+    assert result_2 == expected_2
+    
+    assert counter['load-comment'] == 2  # Resolver + LoaderDepend can fix cache issue
+    assert counter['load-feedback'] == 2
diff --git a/tests/pydantic_v2/resolver/test_11_sqlalchemy_query_global_filter.py b/tests/pydantic_v2/resolver/test_11_sqlalchemy_query_global_filter.py
new file mode 100644
index 0000000..5ff07a1
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_11_sqlalchemy_query_global_filter.py
@@ -0,0 +1,206 @@
+from __future__ import annotations
+import pytest
+from typing import List
+from collections import Counter, defaultdict
+from aiodataloader import DataLoader
+from pydantic import ConfigDict, BaseModel
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import async_sessionmaker
+from sqlalchemy.ext.asyncio import create_async_engine
+from sqlalchemy.orm import DeclarativeBase
+from sqlalchemy.orm import Mapped
+from sqlalchemy.orm import mapped_column
+from pydantic_resolve import Resolver, LoaderDepend
+
+
+"""
+CAUTION: using annotations at top
+so all schema should be defined in global scope
+"""
+
+counter = Counter()
+engine = create_async_engine(
+    "sqlite+aiosqlite://",
+    echo=False,
+)
+async_session = async_sessionmaker(engine, expire_on_commit=False)
+
+class Base(DeclarativeBase):
+    pass
+
+class Task(Base):
+    __tablename__ = "task"
+
+    id: Mapped[int] = mapped_column(primary_key=True)
+    name: Mapped[str]
+
+class Comment(Base):
+    __tablename__ = "comment"
+    id: Mapped[int] = mapped_column(primary_key=True)
+    task_id: Mapped[int] = mapped_column(index=True)
+    content: Mapped[str]
+
+class Feedback(Base):
+    __tablename__ = "feedback"
+    id: Mapped[int] = mapped_column(primary_key=True)
+    comment_id: Mapped[int] = mapped_column(index=True)
+    content: Mapped[str]
+    private: Mapped[bool]
+
+async def insert_objects() -> None:
+    async with async_session() as session:
+        async with session.begin():
+            session.add_all(
+                [
+                    Task(id=1, name="task-1"),
+
+                    Comment(id=1, task_id=1, content="comment-1 for task 1"),
+
+                    Feedback(id=1, comment_id=1, content="feedback-1 for comment-1", private=True),
+                    Feedback(id=2, comment_id=1, content="feedback-2 for comment-1", private=False),
+                    Feedback(id=3, comment_id=1, content="feedback-3 for comment-1", private=True),
+                ]
+            )
+
+async def insert_and_update_objects() -> None:
+    async with async_session() as session:
+        async with session.begin():
+            task_1 = (await session.execute(select(Task).filter_by(id=1))).scalar_one()
+            task_1.name = 'task-1 xyz'
+
+            comment_1 = (await session.execute(select(Comment).filter_by(id=1))).scalar_one()
+            comment_1.content = 'comment-1 for task 1 (changes)'
+
+            feedback_1 = (await session.execute(select(Feedback).filter_by(id=1))).scalar_one()
+            feedback_1.content = 'feedback-1 for comment-1 (changes)'
+
+            session.add(task_1)
+            session.add(comment_1)
+            session.add(feedback_1)
+            session.add_all(
+                [
+                    Comment(id=2, task_id=1, content="comment-2 for task 1"),
+
+                    Feedback(id=4, comment_id=2, content="test", private=False),
+                ]
+            )
+
+
+# =========================== Pydantic Schema layer =========================
+class FeedbackLoader(DataLoader):
+    private: bool
+    async def batch_load_fn(self, comment_ids):
+        counter['load-feedback'] += 1
+        async with async_session() as session:
+            res = await session.execute(select(Feedback).where(Feedback.private == self.private).where(Feedback.comment_id.in_(comment_ids)))
+            rows = res.scalars().all()
+            dct = defaultdict(list)
+            for row in rows:
+                dct[row.comment_id].append(FeedbackSchema.model_validate(row))
+            return [dct.get(k, []) for k in comment_ids]
+
+class CommentLoader(DataLoader):
+    async def batch_load_fn(self, task_ids):
+        counter['load-comment'] += 1
+        async with async_session() as session:
+            res = await session.execute(select(Comment).where(Comment.task_id.in_(task_ids)))
+            rows = res.scalars().all()
+
+            dct = defaultdict(list)
+            for row in rows:
+                dct[row.task_id].append(CommentSchema.model_validate(row))
+            return [dct.get(k, []) for k in task_ids]
+
+class FeedbackSchema(BaseModel):
+    id: int
+    comment_id: int
+    content: str
+    model_config = ConfigDict(from_attributes=True)
+
+class CommentSchema(BaseModel):
+    id: int
+    task_id: int
+    content: str
+    feedbacks: List[FeedbackSchema]  = []
+
+    def resolve_feedbacks(self, loader=LoaderDepend(FeedbackLoader)):
+        return loader.load(self.id)
+    model_config = ConfigDict(from_attributes=True)
+
+class TaskSchema(BaseModel):
+    id: int
+    name: str
+    comments: List[CommentSchema]  = []
+    
+    def resolve_comments(self, loader=LoaderDepend(CommentLoader)):
+        return loader.load(self.id)
+    model_config = ConfigDict(from_attributes=True)
+
+async def init():
+    async with engine.begin() as conn:
+        await conn.run_sync(Base.metadata.create_all)
+    await insert_objects()
+
+async def task_query(task_id = 1):
+    async with async_session() as session:
+        tasks = (await session.execute(select(Task).where(Task.id == task_id))).scalars().all()
+        task_objs = [TaskSchema.model_validate(t) for t in tasks]
+        resolved_results = await Resolver(loader_params={FeedbackLoader: {'private': True}}).resolve(task_objs)
+        to_dict_arr = [r.model_dump() for r in resolved_results]
+        return to_dict_arr
+
+@pytest.mark.asyncio
+async def test_sqlite_and_dataloader():
+    await init()
+    result_1 = await task_query()
+    await insert_and_update_objects()
+    result_2 = await task_query()
+
+    expected_1 = [
+        {
+            'id': 1,
+            'name': 'task-1',
+            'comments': [
+                {
+                    'content': 'comment-1 for task 1',
+                    'feedbacks': [
+                        {'comment_id': 1, 'content': 'feedback-1 for comment-1', 'id': 1},
+                        # {'comment_id': 1, 'content': 'feedback-2 for comment-1', 'id': 2},
+                        {'comment_id': 1, 'content': 'feedback-3 for comment-1', 'id': 3}
+                        ],
+                    'id': 1,
+                    'task_id': 1},
+                ],
+            }
+        ]
+
+    expected_2 = [
+        {
+            'id': 1,
+            'name': 'task-1 xyz',
+            'comments': [
+                {
+                    'content': 'comment-1 for task 1 (changes)',
+                    'feedbacks': [
+                        {'comment_id': 1, 'content': 'feedback-1 for comment-1 (changes)', 'id': 1},
+                        # {'comment_id': 1, 'content': 'feedback-2 for comment-1', 'id': 2},
+                        {'comment_id': 1, 'content': 'feedback-3 for comment-1', 'id': 3}
+                        ],
+                    'id': 1,
+                    'task_id': 1},
+                {
+                    'content': 'comment-2 for task 1',
+                    'feedbacks': [
+                        # {'comment_id': 2, 'content': 'test', 'id': 4},   # private is false
+                    ],
+                    'id': 2,
+                    'task_id': 1}
+                ],
+            }
+        ]
+
+    assert result_1 == expected_1
+    assert result_2 == expected_2
+    
+    assert counter['load-comment'] == 2  # Resolver + LoaderDepend can fix cache issue
+    assert counter['load-feedback'] == 2
diff --git a/tests/pydantic_v2/resolver/test_12_loader_global_filter_exception.py b/tests/pydantic_v2/resolver/test_12_loader_global_filter_exception.py
new file mode 100644
index 0000000..d64f234
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_12_loader_global_filter_exception.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+from typing import List
+import pytest
+from pydantic import BaseModel
+from pydantic_resolve import Resolver, LoaderDepend, LoaderFieldNotProvidedError
+from aiodataloader import DataLoader
+
+@pytest.mark.asyncio
+async def test_loader_depends():
+    counter = {
+        "book": 0
+    }
+
+    BOOKS = {
+        1: [{'name': 'book1'}, {'name': 'book2'}],
+        2: [{'name': 'book3'}, {'name': 'book4'}],
+        3: [{'name': 'book1'}, {'name': 'book2'}],
+    }
+
+    class Book(BaseModel):
+        name: str
+
+    class BookLoader(DataLoader):
+        inventory: bool
+        async def batch_load_fn(self, keys) -> List[List[Book]]:
+            counter["book"] += 1
+            books = [[Book(**bb) for bb in BOOKS.get(k, [])] for k in keys]
+            return books
+
+    # for testing, loder instance need to initialized inside a thread with eventloop
+    # (which means it can't be put in global scope of this file)
+    # otherwise it will generate anthoer loop which will raise error of
+    # "task attached to another loop"
+
+    class Student(BaseModel):
+        id: int
+        name: str
+
+        books: List[Book] = [] 
+        def resolve_books(self, loader=LoaderDepend(BookLoader)):
+            return loader.load(self.id)
+
+    students = [Student(id=1, name="jack"), Student(id=2, name="mike"), Student(id=3, name="wiki")]
+    with pytest.raises(LoaderFieldNotProvidedError):
+        await Resolver().resolve(students)
\ No newline at end of file
diff --git a/tests/pydantic_v2/resolver/test_13_check_wrong_type.py b/tests/pydantic_v2/resolver/test_13_check_wrong_type.py
new file mode 100644
index 0000000..dc4c569
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_13_check_wrong_type.py
@@ -0,0 +1,16 @@
+from __future__ import annotations
+from pydantic import BaseModel
+from pydantic_resolve import Resolver, MissingAnnotationError
+import pytest
+
+class Student(BaseModel):
+    name: str
+    intro: str = ''
+    def resolve_intro(self):
+        return f'hello {self.name}'
+
+@pytest.mark.asyncio
+async def test_check_miss_anno():
+    stu = Student(name="martin")
+    with pytest.raises(MissingAnnotationError):
+        await Resolver(ensure_type=True).resolve(stu)
diff --git a/tests/pydantic_v2/resolver/test_14_check_loader_name.py b/tests/pydantic_v2/resolver/test_14_check_loader_name.py
new file mode 100644
index 0000000..ce89ef8
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_14_check_loader_name.py
@@ -0,0 +1,37 @@
+from __future__ import annotations
+from typing import List
+import pytest
+from pydantic import BaseModel
+from pydantic_resolve import Resolver, LoaderDepend
+import tests.pydantic_v2.resolver.test_14_deps.mod_a as a
+import tests.pydantic_v2.resolver.test_14_deps.mod_b as b
+
+@pytest.mark.asyncio
+async def test_loader_depends():
+    print(a.BookLoader.__module__)
+    class Student(BaseModel):
+        id: int
+        name: str
+
+        a_books: List[a.Book] = [] 
+        def resolve_a_books(self, loader=LoaderDepend(a.BookLoader)):
+            return loader.load(self.id)
+
+        b_books: List[b.Book] = [] 
+        def resolve_b_books(self, loader=LoaderDepend(b.BookLoader)):
+            return loader.load(self.id)
+
+    students = [Student(id=1, name="jack"), Student(id=2, name="mike")]
+    results = await Resolver().resolve(students)
+    source = [r.model_dump() for r in results]
+    expected = [
+        {'id': 1, 'name': 'jack', 
+         'a_books': [{ 'name': 'book1'}, {'name': 'book2'}],
+         'b_books': [{ 'name': 'book1', 'public': 'public'}, {'name': 'book2', 'public': 'public'}]
+        },
+        {'id': 2, 'name': 'mike', 
+         'a_books': [{ 'name': 'book3'}, {'name': 'book4'}],
+         'b_books': [{ 'name': 'book3', 'public': 'public'}, {'name': 'book4', 'public': 'public'}]
+        },
+    ]
+    assert source == expected
diff --git a/tests/pydantic_v2/resolver/test_14_deps/__init__.py b/tests/pydantic_v2/resolver/test_14_deps/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/pydantic_v2/resolver/test_14_deps/mod_a.py b/tests/pydantic_v2/resolver/test_14_deps/mod_a.py
new file mode 100644
index 0000000..f5ae150
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_14_deps/mod_a.py
@@ -0,0 +1,16 @@
+from aiodataloader import DataLoader
+from pydantic import BaseModel
+
+BOOKS = {
+    1: [{'name': 'book1'}, {'name': 'book2'}],
+    2: [{'name': 'book3'}, {'name': 'book4'}],
+    3: [{'name': 'book1'}, {'name': 'book2'}],
+}
+
+class Book(BaseModel):
+    name: str
+
+class BookLoader(DataLoader):
+    async def batch_load_fn(self, keys):
+        books = [[Book(**bb) for bb in BOOKS.get(k, [])] for k in keys]
+        return books
\ No newline at end of file
diff --git a/tests/pydantic_v2/resolver/test_14_deps/mod_b.py b/tests/pydantic_v2/resolver/test_14_deps/mod_b.py
new file mode 100644
index 0000000..f3dbcc8
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_14_deps/mod_b.py
@@ -0,0 +1,17 @@
+from aiodataloader import DataLoader
+from pydantic import BaseModel
+
+BOOKS = {
+    1: [{'name': 'book1'}, {'name': 'book2'}],
+    2: [{'name': 'book3'}, {'name': 'book4'}],
+    3: [{'name': 'book1'}, {'name': 'book2'}],
+}
+
+class Book(BaseModel):
+    name: str
+    public: str = 'public'
+
+class BookLoader(DataLoader):
+    async def batch_load_fn(self, keys):
+        books = [[Book(**bb) for bb in BOOKS.get(k, [])] for k in keys]
+        return books
\ No newline at end of file
diff --git a/tests/pydantic_v2/resolver/test_15_support_batch_load_fn.py b/tests/pydantic_v2/resolver/test_15_support_batch_load_fn.py
new file mode 100644
index 0000000..68bb9c6
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_15_support_batch_load_fn.py
@@ -0,0 +1,38 @@
+from typing import List
+import pytest
+from pydantic import BaseModel
+from pydantic_resolve import Resolver, LoaderDepend
+
+@pytest.mark.asyncio
+async def test_loader_depends():
+
+    BOOKS = {
+        1: [{'name': 'book1'}, {'name': 'book2'}],
+        2: [{'name': 'book3'}, {'name': 'book4'}],
+        3: [{'name': 'book1'}, {'name': 'book2'}],
+    }
+
+    class Book(BaseModel):
+        name: str
+
+    async def batch_load_fn(keys):
+        books = [[Book(**bb) for bb in BOOKS.get(k, [])] for k in keys]
+        return books
+
+    class Student(BaseModel):
+        id: int
+        name: str
+
+        books: List[Book] = [] 
+        def resolve_books(self, loader=LoaderDepend(batch_load_fn)):
+            return loader.load(self.id)
+
+    students = [Student(id=1, name="jack"), Student(id=2, name="mike"), Student(id=3, name="wiki")]
+    results = await Resolver().resolve(students)
+    source = [r.model_dump() for r in results]
+    expected = [
+        {'id': 1, 'name': 'jack', 'books': [{ 'name': 'book1'}, {'name': 'book2'}]},
+        {'id': 2, 'name': 'mike', 'books': [{ 'name': 'book3'}, {'name': 'book4'}]},
+        {'id': 3, 'name': 'wiki', 'books': [{ 'name': 'book1'}, {'name': 'book2'}]},
+    ]
+    assert source == expected
\ No newline at end of file
diff --git a/tests/pydantic_v2/resolver/test_16_mapper.py b/tests/pydantic_v2/resolver/test_16_mapper.py
new file mode 100644
index 0000000..d60980b
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_16_mapper.py
@@ -0,0 +1,229 @@
+from __future__ import annotations
+from dataclasses import dataclass
+from typing import List, Optional
+import pytest
+from pydantic import ConfigDict, BaseModel
+from pydantic_resolve import Resolver, LoaderDepend, mapper
+from aiodataloader import DataLoader
+
+@pytest.mark.asyncio
+async def test_mapper():
+    """
+    user provided mapper
+    """
+    class BookLoader(DataLoader):
+        async def batch_load_fn(self, keys):
+            return keys 
+
+    class Student(BaseModel):
+        id: int
+        name: str
+
+        books: str = ''
+        @mapper(lambda x: str(x))
+        def resolve_books(self, loader=LoaderDepend(BookLoader)):
+            return loader.load(self.id)
+
+    students = [
+        Student(id=1, name="jack"),
+        Student(id=2, name="jack")
+        ]
+    results = await Resolver().resolve(students)
+    source = [r.model_dump() for r in results]
+
+    expected = [
+        {'id': 1, 'name': 'jack', 'books': '1' },
+        {'id': 2, 'name': 'jack', 'books': '2' }
+        ]
+    assert source == expected
+
+
+@pytest.mark.asyncio
+async def test_mapper_2():
+    """
+    auto mapping: dict -> pydantic
+    """
+
+    class BookLoader(DataLoader):
+        async def batch_load_fn(self, keys):
+            return [[{'name': f'book-{k}'}] for k in keys ]  # return [[obj]]
+    
+    class Book(BaseModel):
+        name: str
+
+    class Student(BaseModel):
+        id: int
+        name: str
+
+        book: List[Book] = []
+        @mapper(Book)
+        def resolve_book(self, loader=LoaderDepend(BookLoader)):
+            return loader.load(self.id)
+
+    students = [ Student(id=1, name="jack") ]
+    results = await Resolver().resolve(students)
+    source = [r.model_dump() for r in results]
+
+    expected = [
+        {'id': 1, 'name': 'jack', 'book': [{'name': 'book-1'}] },
+        ]
+    assert source == expected
+
+
+@pytest.mark.asyncio
+async def test_mapper_3():
+    """
+    auto mapping: obj -> pydantic
+    """
+    class Bo:
+        def __init__(self, name):
+            self.name = name
+
+    async def batch_load_fn(keys):
+        return [Bo(name=f'book-{k}') for k in keys ]  # return [obj]
+    
+    class Book(BaseModel):
+        name: str
+        model_config = ConfigDict(from_attributes=True)
+
+    class Student(BaseModel):
+        id: int
+        name: str
+
+        book: Optional[Book] = None
+        @mapper(Book)
+        def resolve_book(self, loader=LoaderDepend(batch_load_fn)):
+            return loader.load(self.id)
+
+    students = [ Student(id=1, name="jack") ]
+    results = await Resolver().resolve(students)
+    source = [r.model_dump() for r in results]
+
+    expected = [
+        {'id': 1, 'name': 'jack', 'book': {'name': 'book-1'}},
+        ]
+    assert source == expected
+
+
+@pytest.mark.asyncio
+async def test_mapper_4():
+    """
+    auto mapping: dict -> dataclass
+    """
+
+    async def batch_load_fn(keys):
+        return [[{'name': f'book-{k}'}] for k in keys ]
+    
+    @dataclass
+    class Book:
+        name: str
+
+    class Student(BaseModel):
+        id: int
+        name: str
+
+        book: List[Book] = []
+        @mapper(Book)
+        def resolve_book(self, loader=LoaderDepend(batch_load_fn)):
+            return loader.load(self.id)
+
+    students = [ Student(id=1, name="jack") ]
+    results = await Resolver().resolve(students)
+    source = [r.model_dump() for r in results]
+
+    expected = [
+        {'id': 1, 'name': 'jack', 'book': [{'name':'book-1'}]},
+        ]
+    assert source == expected
+
+
+@pytest.mark.asyncio
+async def test_mapper_5():
+    """
+    auto mapping fail
+    """
+    class Bo:
+        def __init__(self, name):
+            self.name = name
+
+    async def batch_load_fn(keys):
+        return [[Bo(name=f'book-{k}')] for k in keys ]
+    
+    @dataclass
+    class Book:
+        name: str
+
+    class Student(BaseModel):
+        id: int
+        name: str
+
+        book: List[Book] = []
+        @mapper(Book)
+        def resolve_book(self, loader=LoaderDepend(batch_load_fn)):
+            return loader.load(self.id)
+
+    students = [ Student(id=1, name="jack") ]
+    with pytest.raises(NotImplementedError):
+        await Resolver().resolve(students)
+
+
+@pytest.mark.asyncio
+async def test_mapper_6():
+    """
+    pydantic to pydantic
+    """
+    class Bo(BaseModel):
+        name: str
+
+    class Book(BaseModel):
+        name: str
+        published: bool = False
+
+    async def batch_load_fn(keys):
+        return [[Bo(name=f'book-{k}')] for k in keys ]
+    
+
+    class Student(BaseModel):
+        id: int
+        name: str
+
+        books: List[Book] = []
+        @mapper(Book)
+        def resolve_books(self, loader=LoaderDepend(batch_load_fn)):
+            return loader.load(self.id)
+
+    students = [ Student(id=1, name="jack") ]
+    result = await Resolver().resolve(students)
+    assert result[0].model_dump() == {'id':1, 'name':"jack", 'books':[{'name': "book-1", 'published':False}]}
+
+
+@pytest.mark.asyncio
+async def test_mapper_7():
+    """
+    pydantic to pydantic
+    """
+    class Bo(BaseModel):
+        name: str
+
+    class Book(BaseModel):
+        name: str
+        published: bool = False
+
+        model_config = ConfigDict(from_attributes=True)
+
+    async def batch_load_fn(keys):
+        return [[Bo(name=f'book-{k}')] for k in keys ]
+    
+
+    class Student(BaseModel):
+        id: int
+        name: str
+
+        books: List[Book] = []
+        @mapper(Book)
+        def resolve_books(self, loader=LoaderDepend(batch_load_fn)):
+            return loader.load(self.id)
+
+    students = [ Student(id=1, name="jack") ]
+    result = await Resolver().resolve(students)
+    assert result[0].model_dump() == {'id':1, 'name':"jack", 'books':[{'name': "book-1", 'published':False}]}
diff --git a/tests/pydantic_v2/resolver/test_17_mapper_deep.py b/tests/pydantic_v2/resolver/test_17_mapper_deep.py
new file mode 100644
index 0000000..1497c43
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_17_mapper_deep.py
@@ -0,0 +1,155 @@
+from __future__ import annotations
+from typing import List
+import pytest
+from collections import Counter, defaultdict
+from aiodataloader import DataLoader
+from pydantic import ConfigDict, BaseModel
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import async_sessionmaker
+from sqlalchemy.ext.asyncio import create_async_engine
+from sqlalchemy.orm import DeclarativeBase
+from sqlalchemy.orm import Mapped
+from sqlalchemy.orm import mapped_column
+from pydantic_resolve import Resolver, LoaderDepend, mapper, build_list
+
+class Base(DeclarativeBase):
+    pass
+
+class Task(Base):
+    __tablename__ = "task"
+
+    id: Mapped[int] = mapped_column(primary_key=True)
+    name: Mapped[str]
+
+class Comment(Base):
+    __tablename__ = "comment"
+    id: Mapped[int] = mapped_column(primary_key=True)
+    task_id: Mapped[int] = mapped_column()
+    content: Mapped[str]
+
+class Feedback(Base):
+    __tablename__ = "feedback"
+    id: Mapped[int] = mapped_column(primary_key=True)
+    comment_id: Mapped[int] = mapped_column()
+    content: Mapped[str]
+
+@pytest.mark.asyncio
+async def test_sqlite_and_dataloader():
+    counter = Counter()
+    engine = create_async_engine(
+        "sqlite+aiosqlite://",
+        echo=False,
+    )
+    async_session = async_sessionmaker(engine, expire_on_commit=False)
+
+    async def insert_objects() -> None:
+        async with async_session() as session:
+            async with session.begin():
+                session.add_all(
+                    [
+                        Task(id=1, name="task-1"),
+                        Task(id=2, name="task-2"),
+                        Task(id=3, name="task-3"),
+
+                        Comment(id=1, task_id=1, content="comment-1 for task 1"),
+                        Comment(id=2, task_id=1, content="comment-2 for task 1"),
+                        Comment(id=3, task_id=2, content="comment-1 for task 2"),
+
+                        Feedback(id=1, comment_id=1, content="feedback-1 for comment-1"),
+                        Feedback(id=2, comment_id=1, content="feedback-1 for comment-1"),
+                        Feedback(id=3, comment_id=1, content="feedback-1 for comment-1"),
+                    ]
+                )
+
+    class FeedbackLoader(DataLoader):
+        async def batch_load_fn(self, comment_ids):
+            counter['load-feedback'] += 1
+            async with async_session() as session:
+                res = await session.execute(select(Feedback).where(Feedback.comment_id.in_(comment_ids)))
+                rows = res.scalars().all()
+                return build_list(rows, comment_ids, lambda x: x.comment_id)
+
+    class CommentLoader(DataLoader):
+        async def batch_load_fn(self, task_ids):
+            counter['load-comment'] += 1
+            async with async_session() as session:
+                res = await session.execute(select(Comment).where(Comment.task_id.in_(task_ids)))
+                rows = res.scalars().all()
+                return build_list(rows, task_ids, lambda x: x.task_id)
+
+    class FeedbackSchema(BaseModel):
+        id: int
+        comment_id: int
+        content: str
+        model_config = ConfigDict(from_attributes=True)
+
+    class CommentSchema(BaseModel):
+        id: int
+        task_id: int
+        content: str
+        feedbacks: List[FeedbackSchema]  = []
+        @mapper(FeedbackSchema)
+        def resolve_feedbacks(self, loader=LoaderDepend(FeedbackLoader)):
+            return loader.load(self.id)
+        model_config = ConfigDict(from_attributes=True)
+
+    class TaskSchema(BaseModel):
+        id: int
+        name: str
+        comments: List[CommentSchema]  = []
+        @mapper(CommentSchema)
+        def resolve_comments(self, loader=LoaderDepend(CommentLoader)):
+            return loader.load(self.id)
+        model_config = ConfigDict(from_attributes=True)
+
+    async def init():
+        async with engine.begin() as conn:
+            await conn.run_sync(Base.metadata.create_all)
+
+    async def query():
+        async with async_session() as session:
+            tasks = (await session.execute(select(Task))).scalars().all()
+            task_objs = [TaskSchema.model_validate(t) for t in tasks]
+            resolved_results = await Resolver().resolve(task_objs)
+            to_dict_arr = [r.model_dump() for r in resolved_results]
+            return to_dict_arr
+
+    await init()
+    await insert_objects()
+    result = await query()
+    expected = [
+        {
+            'comments': [{'content': 'comment-1 for task 1',
+            'feedbacks': [{'comment_id': 1,
+                            'content': 'feedback-1 for comment-1',
+                            'id': 1},
+                            {'comment_id': 1,
+                            'content': 'feedback-1 for comment-1',
+                            'id': 2},
+                            {'comment_id': 1,
+                            'content': 'feedback-1 for comment-1',
+                            'id': 3}],
+            'id': 1,
+            'task_id': 1},
+                        {'content': 'comment-2 for task 1',
+                            'feedbacks': [],
+                            'id': 2,
+                            'task_id': 1}],
+            'id': 1,
+            'name': 'task-1'},
+            {'comments': [{'content': 'comment-1 for task 2',
+                            'feedbacks': [],
+                            'id': 3,
+                            'task_id': 2}],
+            'id': 2,
+            'name': 'task-2'},
+            {'comments': [], 'id': 3, 'name': 'task-3'}]
+
+    assert result == expected
+    assert counter['load-comment'] == 1  # batch_load_fn only called once
+    assert counter['load-feedback'] == 1
+    
+    await query()
+
+    assert counter['load-comment'] == 2  # Resolver + LoaderDepend can fix cache issue
+    assert counter['load-feedback'] == 2
\ No newline at end of file
diff --git a/tests/pydantic_v2/resolver/test_18_post_methods.py b/tests/pydantic_v2/resolver/test_18_post_methods.py
new file mode 100644
index 0000000..e8ae49e
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_18_post_methods.py
@@ -0,0 +1,84 @@
+from typing import List, Optional
+from pydantic import BaseModel
+from pydantic_resolve import Resolver, mapper, LoaderDepend
+import pytest
+
+# define loader functions
+async def friends_batch_load_fn(names):
+    mock_db = {
+        'tangkikodo': ['tom', 'jerry'],
+        'john': ['mike', 'wallace'],
+        'trump': ['sam', 'jim'],
+        'sally': ['sindy', 'lydia'],
+    }
+    return [mock_db.get(name, []) for name in names]
+
+async def cash_batch_load_fn(names):
+    mock_db = {
+        'jerry':200, 'mike': 3000, 'wallace': 400, 'sam': 500,
+        'jim': 600, 'sindy': 700, 'lydia': 800, 'tangkikodo': 900, 'john': 1000,
+        'trump': 1200
+    }
+    result = []
+    for name in names:
+        n = mock_db.get(name, None)
+        result.append({'number': n} if n else None)  # conver to auto mapping compatible style
+    return result
+
+# define schemas
+class Cash(BaseModel):
+    number: Optional[int] = None
+
+class Friend(BaseModel):
+    name: str
+
+    cash: Optional[Cash] = None
+    @mapper(Cash)  # auto mapping
+    def resolve_cash(self, contact_loader=LoaderDepend(cash_batch_load_fn)):
+        return contact_loader.load(self.name)
+    
+    has_cash: bool = False
+    def post_has_cash(self):
+        # self.has_cash = self.cash is not None
+        return self.cash is not None
+
+    
+
+class User(BaseModel):
+    name: str
+    age: int
+
+    friends: List[Friend] = []
+    @mapper(lambda names: [Friend(name=name) for name in names])
+    def resolve_friends(self, friend_loader=LoaderDepend(friends_batch_load_fn)):
+        return friend_loader.load(self.name)
+    
+    has_cash: bool = False
+    def post_has_cash(self):
+        return any([f.has_cash for f in self.friends])
+
+class Root(BaseModel):
+    users: List[User] = []
+    @mapper(lambda items: [User(**item) for item in items])
+    def resolve_users(self):
+        return [
+            {"name": "tangkikodo", "age": 19}, 
+            {"name": "noone", "age": 19}, 
+        ]
+    hello: str = ''
+    def post_default_handler(self, context):
+        self.hello = f'hello, {context["world"]}'
+
+@pytest.mark.asyncio
+async def test_post_methods():
+    root = Root()
+    root = await Resolver(context={"world": "new world"}).resolve(root)
+    dct = root.model_dump()
+    assert dct == {'users': [{'age': 19,
+                     'has_cash': True,
+                     'friends': [{'has_cash': False,'cash': None, 'name': 'tom'},
+                                 {'has_cash': True, 'cash': {'number': 200}, 'name': 'jerry'}],
+                     'name': 'tangkikodo'},
+                     {'name': 'noone', 'age': 19, 'friends': [], 'has_cash':False }
+                     ],
+                    'hello': 'hello, new world'}
diff --git a/tests/pydantic_v2/resolver/test_19_post_methods_exception.py b/tests/pydantic_v2/resolver/test_19_post_methods_exception.py
new file mode 100644
index 0000000..25f3770
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_19_post_methods_exception.py
@@ -0,0 +1,71 @@
+from typing import List, Optional
+from pydantic import BaseModel
+from pydantic_resolve import Resolver, mapper, LoaderDepend, ResolverTargetAttrNotFound
+import pytest
+
+# define loader functions
+async def friends_batch_load_fn(names):
+    mock_db = {
+        'tangkikodo': ['tom', 'jerry'],
+        'john': ['mike', 'wallace'],
+        'trump': ['sam', 'jim'],
+        'sally': ['sindy', 'lydia'],
+    }
+    return [mock_db.get(name, []) for name in names]
+
+async def cash_batch_load_fn(names):
+    mock_db = {
+        'tom': 100, 'jerry':200, 'mike': 3000, 'wallace': 400, 'sam': 500,
+        'jim': 600, 'sindy': 700, 'lydia': 800, 'tangkikodo': 900, 'john': 1000,
+        'trump': 1200, 'sally': 1300,
+    }
+    result = []
+    for name in names:
+        n = mock_db.get(name, None)
+        result.append({'number': n} if n else None)  # conver to auto mapping compatible style
+    return result
+
+# define schemas
+class Cash(BaseModel):
+    number: Optional[int] = None
+
+class Friend(BaseModel):
+    name: str
+
+    cash: Optional[Cash] = None
+    @mapper(Cash)  # auto mapping
+    def resolve_cash(self, contact_loader=LoaderDepend(cash_batch_load_fn)):
+        return contact_loader.load(self.name)
+    
+    has_cash: bool = False
+    def post_has_cashx(self):
+        self.has_cash = self.cash is not None
+    
+
+class User(BaseModel):
+    name: str
+    age: int
+
+    friends: List[Friend] = []
+    @mapper(lambda names: [Friend(name=name) for name in names])
+    def resolve_friends(self, friend_loader=LoaderDepend(friends_batch_load_fn)):
+        return friend_loader.load(self.name)
+    
+    has_cash: bool = False
+    def post_has_cash(self):
+        self.has_cash = any([f.has_cash for f in self.friends])
+
+class Root(BaseModel):
+    users: List[User] = []
+    @mapper(lambda items: [User(**item) for item in items])
+    def resolve_users(self):
+        return [
+            {"name": "tangkikodo", "age": 19}, 
+            {"name": "noone", "age": 19}, 
+        ]
+
+@pytest.mark.asyncio
+async def test_post_methods():
+    root = Root()
+    with pytest.raises(ResolverTargetAttrNotFound):
+        await Resolver().resolve(root)
\ No newline at end of file
diff --git a/tests/pydantic_v2/resolver/test_1_pydantic_resolve.py b/tests/pydantic_v2/resolver/test_1_pydantic_resolve.py
new file mode 100644
index 0000000..3117ced
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_1_pydantic_resolve.py
@@ -0,0 +1,41 @@
+from __future__ import annotations
+from typing import List
+import asyncio
+from pydantic import BaseModel
+from pydantic_resolve import Resolver
+import pytest
+
+class Student(BaseModel):
+    name: str
+    intro: str = ''
+    def resolve_intro(self) -> str:
+        return f'hello {self.name}'
+
+    books: List[str] = []    
+    async def resolve_books(self) -> List[str]:
+        await asyncio.sleep(1)
+        return ['book1', 'book2']
+
+
+@pytest.mark.asyncio
+async def test_resolve_object():
+    stu = Student(name="martin")
+    result = await Resolver().resolve(stu)
+    expected = {
+        'name': 'martin',
+        'intro': 'hello martin',
+        'books': ['book1', 'book2']
+    }
+    assert result.model_dump() == expected
+
+@pytest.mark.asyncio
+async def test_resolve_array():
+    stu = [Student(name="martin")]
+    results = await Resolver().resolve(stu)
+    results = [r.model_dump() for r in results]
+    expected = [{
+        'name': 'martin',
+        'intro': 'hello martin',
+        'books': ['book1', 'book2']
+    }]
+    assert results == expected
diff --git a/tests/pydantic_v2/resolver/test_20_loader_instance.py b/tests/pydantic_v2/resolver/test_20_loader_instance.py
new file mode 100644
index 0000000..ae972a1
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_20_loader_instance.py
@@ -0,0 +1,105 @@
+from typing import List
+from pydantic import BaseModel
+from pydantic_resolve import Resolver, mapper, LoaderDepend
+from aiodataloader import DataLoader
+import pytest
+
+counter = {
+    "n": 0
+}
+
+# define loader functions
+class FriendLoader(DataLoader):
+    async def batch_load_fn(self, names):
+        print(names)
+        counter["n"] += 1
+        mock_db = {
+            'tangkikodo': ['tom', 'jerry'],
+            'john': ['mike', 'wallace'],
+            'trump': ['sam', 'jim'],
+            'sally': ['sindy', 'lydia'],
+        }
+        return [mock_db.get(name, []) for name in names]
+
+class FriendLoaderCopy(DataLoader):
+    async def batch_load_fn(self, names):
+        print(names)
+        counter["n"] += 1
+        mock_db = {
+            'tangkikodo': ['tom', 'jerry'],
+            'john': ['mike', 'wallace'],
+            'trump': ['sam', 'jim'],
+            'sally': ['sindy', 'lydia'],
+        }
+        return [mock_db.get(name, []) for name in names]
+
+class Friend(BaseModel):
+    name: str
+
+class User(BaseModel):
+    name: str
+    age: int
+
+    friends: List[Friend] = []
+    @mapper(lambda names: [Friend(name=name) for name in names])
+    def resolve_friends(self, friend_loader=LoaderDepend(FriendLoader)):
+        return friend_loader.load(self.name)
+
+class Root(BaseModel):
+    users: List[User] = []
+    @mapper(lambda items: [User(**item) for item in items])
+    def resolve_users(self):
+        return [
+            {"name": "tangkikodo", "age": 19}, 
+            {"name": "john", "age": 19}, 
+        ]
+
+@pytest.mark.asyncio
+async def test_loader_instance_0():
+    counter["n"] = 0
+    root = Root()
+    loader = FriendLoader()
+    loader.prime('tangkikodo', ['tom', 'jerry'])
+    loader.prime('john', ['mike', 'wallace'])
+    result = await Resolver(loader_instances={FriendLoader: loader}).resolve(root)
+    assert len(result.users[0].friends) == 2
+    assert counter["n"] == 0
+
+@pytest.mark.asyncio
+async def test_loader_instance_1():
+    counter["n"] = 0
+    root = Root()
+    loader = FriendLoader()
+    await loader.load_many(['tangkikodo', 'john'])
+    result = await Resolver(loader_instances={FriendLoader: loader}).resolve(root)
+    assert len(result.users[0].friends) == 2
+    assert counter["n"] == 1
+
+@pytest.mark.asyncio
+async def test_loader_instance_2():
+    counter["n"] = 0
+    root = Root()
+    loader = FriendLoader()
+    await loader.load_many(['tangkikodo'])
+    result = await Resolver(loader_instances={FriendLoader: loader}).resolve(root)
+    assert len(result.users[0].friends) == 2
+    assert counter["n"] == 2
+
+@pytest.mark.asyncio
+async def test_loader_instance_3():
+    root = Root()
+    loader = FriendLoader()
+    await loader.load_many(['tangkikodo'])
+    with pytest.raises(AttributeError):
+        await Resolver(loader_instances={FriendLoaderCopy: loader}).resolve(root)
+
+@pytest.mark.asyncio
+async def test_loader_instance_4():
+    root = Root()
+    loader = FriendLoader()
+    await loader.load_many(['tangkikodo'])
+    class A:
+        name= 'a'
+
+    with pytest.raises(AttributeError):
+        await Resolver(loader_instances={A: loader}).resolve(root)
\ No newline at end of file
diff --git a/tests/pydantic_v2/resolver/test_21_not_stop_by_idle_level.py b/tests/pydantic_v2/resolver/test_21_not_stop_by_idle_level.py
new file mode 100644
index 0000000..4c24100
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_21_not_stop_by_idle_level.py
@@ -0,0 +1,52 @@
+import pytest
+from pydantic import BaseModel
+from typing import Optional
+from pydantic_resolve import Resolver
+import asyncio
+
+class D(BaseModel):
+    name: str = 'kikodo'
+    age: int = 0
+    def resolve_age(self):
+        return 11
+
+class C(BaseModel):
+    name: str = ''
+    d: D = D()
+
+
+class B(BaseModel):
+    name: str
+    c: Optional[C] = None
+    async def resolve_c(self) -> Optional[C]:
+        await asyncio.sleep(1)
+        return C(name='hello world')
+
+class A(BaseModel):
+    b: B
+
+class Z(BaseModel):
+    a: A
+    resolve_age: int
+
+@pytest.mark.asyncio
+async def test_resolve_object():
+    s = Z(a=A(b=B(name="kikodo")), resolve_age=21)
+
+    result = await Resolver().resolve(s)
+    expected = {
+        "a": {
+            "b": {
+                "name":"kikodo",
+                "c": {
+                    "name": "hello world",
+                    "d": {
+                        "name": "kikodo",
+                        "age": 11
+                    }
+                }
+            }
+        },
+        "resolve_age": 21
+    }
+    assert result.model_dump() == expected
diff --git a/tests/pydantic_v2/resolver/test_22_not_stop_by_idle_level_complex.py b/tests/pydantic_v2/resolver/test_22_not_stop_by_idle_level_complex.py
new file mode 100644
index 0000000..4f461e9
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_22_not_stop_by_idle_level_complex.py
@@ -0,0 +1,85 @@
+from dataclasses import asdict, dataclass, field
+from typing import List
+import pytest
+from pydantic import BaseModel
+from pydantic_resolve import Resolver, LoaderDepend
+
+@pytest.mark.asyncio
+async def test_loader_depends():
+    BOOKS = {
+        1: [{'name': 'book1'}, {'name': 'book2'}],
+        2: [{'name': 'book3'}, {'name': 'book4'}],
+        3: [{'name': 'book1'}, {'name': 'book2'}],
+    }
+
+    class Book(BaseModel):
+        name: str
+
+    async def batch_load_fn(keys):
+        print('oader')
+        books = [[Book(**bb) for bb in BOOKS.get(k, [])] for k in keys]
+        return books
+
+    class Student(BaseModel):
+        id: int
+        name: str
+
+        books: List[Book] = [] 
+        def resolve_books(self, loader=LoaderDepend(batch_load_fn)):
+            return loader.load(self.id)
+    
+    class ClassRoom(BaseModel):
+        students: List[Student]
+
+
+    students = [Student(id=1, name="jack"), Student(id=2, name="mike"), Student(id=3, name="wiki")]
+    classroom = ClassRoom(students=students)
+    res = await Resolver().resolve(classroom)
+    source = res.model_dump()
+    expected = {
+        "students": [
+            {'id': 1, 'name': 'jack', 'books': [{ 'name': 'book1'}, {'name': 'book2'}]},
+            {'id': 2, 'name': 'mike', 'books': [{ 'name': 'book3'}, {'name': 'book4'}]},
+            {'id': 3, 'name': 'wiki', 'books': [{ 'name': 'book1'}, {'name': 'book2'}]}]}
+    assert source == expected
+
+
+@pytest.mark.asyncio
+async def test_loader_depends_2():
+    BOOKS = {
+        1: [{'name': 'book1'}, {'name': 'book2'}],
+        2: [{'name': 'book3'}, {'name': 'book4'}],
+        3: [{'name': 'book1'}, {'name': 'book2'}],
+    }
+
+    @dataclass
+    class Book():
+        name: str
+
+    async def batch_load_fn(keys):
+        books = [[Book(name=bb['name']) for bb in BOOKS.get(k, [])] for k in keys]
+        return books
+
+    @dataclass
+    class Student():
+        id: int
+        name: str
+
+        books: List[Book] = field(default_factory=list)
+        def resolve_books(self, loader=LoaderDepend(batch_load_fn)):
+            return loader.load(self.id)
+    
+    @dataclass
+    class ClassRoom():
+        students: List[Student]
+
+    students = [Student(id=1, name="jack"), Student(id=2, name="mike"), Student(id=3, name="wiki")]
+    classroom = ClassRoom(students=students)
+    res = await Resolver().resolve(classroom)
+    source = asdict(res)
+    expected = {
+        "students": [
+            {'id': 1, 'name': 'jack', 'books': [{ 'name': 'book1'}, {'name': 'book2'}]},
+            {'id': 2, 'name': 'mike', 'books': [{ 'name': 'book3'}, {'name': 'book4'}]},
+            {'id': 3, 'name': 'wiki', 'books': [{ 'name': 'book1'}, {'name': 'book2'}]}]}
+    assert source == expected
\ No newline at end of file
diff --git a/tests/pydantic_v2/resolver/test_23_parse_to_obj_for_pydantic.py b/tests/pydantic_v2/resolver/test_23_parse_to_obj_for_pydantic.py
new file mode 100644
index 0000000..a8a6ae5
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_23_parse_to_obj_for_pydantic.py
@@ -0,0 +1,179 @@
+from collections import namedtuple
+from typing import List
+import pytest
+from pydantic import ConfigDict, BaseModel, ValidationError
+from pydantic_resolve import Resolver, LoaderDepend, mapper
+
+@pytest.mark.asyncio
+async def test_1():
+    BOOKS = {
+        1: [{'name': 'book1'}, {'name': 'book2'}],
+        2: [{'name': 'book3'}, {'name': 'book4'}],
+        3: [{'name': 'book1'}, {'name': 'book2'}],
+    }
+
+    class Book(BaseModel):
+        name: str
+
+    async def batch_load_fn(keys):
+        print('oader')
+        books = [BOOKS.get(k, []) for k in keys]
+        return books
+
+    class Student(BaseModel):
+        id: int
+        name: str
+
+        books: List[Book] = [] 
+        def resolve_books(self, loader=LoaderDepend(batch_load_fn)):
+            return loader.load(self.id)
+    
+    class ClassRoom(BaseModel):
+        students: List[Student]
+
+
+    students = [Student(id=1, name="jack"), Student(id=2, name="mike"), Student(id=3, name="wiki")]
+    classroom = ClassRoom(students=students)
+    classroom = await Resolver().resolve(classroom)
+    assert isinstance(classroom.students[0].books[0], Book)
+
+
+@pytest.mark.asyncio
+async def test_2():
+    BOOKS = {
+        1: [{'name': 'book1'}, {'name': 'book2'}],
+        2: [{'name': 'book3'}, {'name': 'book4'}],
+        3: [{'name': 'book1'}, {'name': 'book2'}],
+    }
+
+    class Book(BaseModel):
+        name: str
+        num: int
+
+    async def batch_load_fn(keys):
+        print('oader')
+        books = [[bb for bb in BOOKS.get(k, [])] for k in keys]
+        return books
+
+    class Student(BaseModel):
+        id: int
+        name: str
+
+        books: List[Book] = [] 
+        @mapper(lambda xx: [Book(name=x['name'], num=11) for x in xx])
+        def resolve_books(self, loader=LoaderDepend(batch_load_fn)):
+            return loader.load(self.id)
+    
+    class ClassRoom(BaseModel):
+        students: List[Student]
+
+
+    students = [Student(id=1, name="jack"), Student(id=2, name="mike"), Student(id=3, name="wiki")]
+    classroom = ClassRoom(students=students)
+    classroom = await Resolver().resolve(classroom)
+    assert isinstance(classroom.students[0].books[0], Book)
+
+@pytest.mark.asyncio
+async def test_3():
+    BOOKS = {
+        1: [{'name': 'book1'}, {'name': 'book2'}],
+        2: [{'name': 'book3'}, {'name': 'book4'}],
+        3: [{'name': 'book1'}, {'name': 'book2'}],
+    }
+
+    class Book(BaseModel):
+        name: str
+        num: int  # missing fields
+
+    async def batch_load_fn(keys):
+        books = [[bb for bb in BOOKS.get(k, [])] for k in keys]
+        return books
+
+    class Student(BaseModel):
+        id: int
+        name: str
+
+        books: List[Book] = [] 
+        def resolve_books(self, loader=LoaderDepend(batch_load_fn)):
+            return loader.load(self.id)
+    
+    class ClassRoom(BaseModel):
+        students: List[Student]
+
+
+    students = [Student(id=1, name="jack"), Student(id=2, name="mike"), Student(id=3, name="wiki")]
+    classroom = ClassRoom(students=students)
+    with pytest.raises(ValidationError):
+        await Resolver().resolve(classroom)
+
+
+@pytest.mark.asyncio
+async def test_4():
+    BB = namedtuple('BB', 'name')
+
+    BOOKS = {
+        1: [BB(name='book1'),BB(name='book2')],
+        2: [BB(name='book3'),BB(name='book4')],
+        3: [BB(name='book1'),BB(name='book2')],
+    }
+
+    class Book(BaseModel):
+        name: str
+        model_config = ConfigDict(from_attributes=True)
+
+    async def batch_load_fn(keys):
+        books = [BOOKS.get(k, []) for k in keys]
+        return books
+
+    class Student(BaseModel):
+        id: int
+        name: str
+
+        books: List[Book] = [] 
+        def resolve_books(self, loader=LoaderDepend(batch_load_fn)):
+            return loader.load(self.id)
+    
+    class ClassRoom(BaseModel):
+        students: List[Student]
+
+
+    students = [Student(id=1, name="jack"), Student(id=2, name="mike"), Student(id=3, name="wiki")]
+    classroom = ClassRoom(students=students)
+    classroom = await Resolver().resolve(classroom)
+    assert isinstance(classroom.students[0].books[0], Book)
+
+@pytest.mark.asyncio
+async def test_5():
+    BOOKS = {
+        1: [{'name': 'book1'}, {'name': 'book2'}],
+        2: [{'name': 'book3'}, {'name': 'book4'}],
+        3: [{'name': 'book1'}, {'name': 'book2'}],
+    }
+
+    class Book(BaseModel):
+        name: str
+        num: int  # missing fields
+
+    async def batch_load_fn(keys):
+        books = [[bb for bb in BOOKS.get(k, [])] for k in keys]
+        return books
+
+    class StudentBase(BaseModel):
+        id: int
+        name: str
+
+    class Student(StudentBase):
+
+        books: List[Book] = [] 
+        def resolve_books(self, loader=LoaderDepend(batch_load_fn)):
+            return loader.load(self.id)
+    
+    class ClassRoom(BaseModel):
+        students: List[Student]
+
+
+    students = [Student(id=1, name="jack"), Student(id=2, name="mike"), Student(id=3, name="wiki")]
+    classroom = ClassRoom(students=students)
+    with pytest.raises(ValidationError):
+        await Resolver().resolve(classroom)
+
diff --git a/tests/pydantic_v2/resolver/test_24_parse_to_obj_for_dataclass.py b/tests/pydantic_v2/resolver/test_24_parse_to_obj_for_dataclass.py
new file mode 100644
index 0000000..4e2fde0
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_24_parse_to_obj_for_dataclass.py
@@ -0,0 +1,38 @@
+from typing import List
+import pytest
+from dataclasses import dataclass, field
+from pydantic_resolve import Resolver, LoaderDepend
+
+@pytest.mark.asyncio
+async def test_loader_depends_1():
+    BOOKS = {
+        1: [{'name': 'book1'}, {'name': 'book2'}],
+        2: [{'name': 'book3'}, {'name': 'book4'}],
+        3: [{'name': 'book1'}, {'name': 'book2'}],
+    }
+
+    @dataclass
+    class Book():
+        name: str
+
+    async def batch_load_fn(keys):
+        books = [[dict(name=bb['name']) for bb in BOOKS.get(k, [])] for k in keys]
+        return books
+
+    @dataclass
+    class Student():
+        id: int
+        name: str
+
+        books: List[Book] = field(default_factory=list)
+        def resolve_books(self, loader=LoaderDepend(batch_load_fn)):
+            return loader.load(self.id)
+    
+    @dataclass
+    class ClassRoom():
+        students: List[Student]
+
+    students = [Student(id=1, name="jack"), Student(id=2, name="mike"), Student(id=3, name="wiki")]
+    classroom = ClassRoom(students=students)
+    res = await Resolver().resolve(classroom)
+    assert isinstance(res.students[0].books[0], Book)
diff --git a/tests/pydantic_v2/resolver/test_25_parse_to_obj_for_pydantic_with_annotation.py b/tests/pydantic_v2/resolver/test_25_parse_to_obj_for_pydantic_with_annotation.py
new file mode 100644
index 0000000..f8da875
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_25_parse_to_obj_for_pydantic_with_annotation.py
@@ -0,0 +1,41 @@
+from __future__ import annotations
+from typing import List
+import pytest
+from pydantic import BaseModel
+from pydantic_resolve import Resolver, LoaderDepend
+
+BOOKS = {
+    1: [{'name': 'book1'}, {'name': 'book2'}],
+    2: [{'name': 'book3'}, {'name': 'book4'}],
+    3: [{'name': 'book1'}, {'name': 'book2'}],
+}
+async def batch_load_fn(keys):
+    books = [BOOKS.get(k, []) for k in keys]
+    return books
+
+# in reverse order
+
+class ClassRoom(BaseModel):
+    students: List[Student] = []
+    def resolve_students(self):
+        students = [dict(id=1, name="jack"), dict(id=2, name="mike"), dict(id=3, name="wiki")]
+        return students
+
+class Student(BaseModel):
+    id: int
+    name: str
+
+    books: List[Book] = [] 
+    def resolve_books(self, loader=LoaderDepend(batch_load_fn)):
+        return loader.load(self.id)
+
+class Book(BaseModel):
+    name: str
+
+
+@pytest.mark.asyncio
+async def test_1():
+    classroom = ClassRoom()
+    classroom = await Resolver().resolve(classroom)  # annotation is not required any more.
+    assert isinstance(classroom.students[0].books[0], Book)
+
diff --git a/tests/pydantic_v2/resolver/test_26_tree.py b/tests/pydantic_v2/resolver/test_26_tree.py
new file mode 100644
index 0000000..504de50
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_26_tree.py
@@ -0,0 +1,54 @@
+from __future__ import annotations
+from collections import defaultdict
+from dataclasses import dataclass, field, asdict
+from typing import List
+from pydantic import BaseModel
+from pydantic_resolve import Resolver, LoaderDepend, mapper
+from aiodataloader import DataLoader
+import pytest
+
+class DummyLoader(DataLoader):
+    async def batch_load_fn(self, keys):
+        d = dict()
+        return [d.get(k, []) for k in keys]
+
+class Tree(BaseModel):
+    id: int
+    content: str
+    children: List[Tree] = []
+    def resolve_children(self, loader=LoaderDepend(DummyLoader)):
+        return loader.load(self.id)
+    
+
+@pytest.mark.asyncio
+async def test_1():
+
+    loader = DummyLoader()
+
+    records = [
+        {'id': 2, 'parent': 1, 'content': '2'},
+        {'id': 3, 'parent': 1, 'content': '3'},
+        {'id': 4, 'parent': 2, 'content': '4'},
+    ]
+
+    d = defaultdict(list)
+    for r in records:
+        d[r['parent']].append(r)
+
+    for k, v in d.items():
+        loader.prime(k, v)
+
+    tree = Tree(id=1, content='1')
+    tree = await Resolver(loader_instances={DummyLoader: loader}).resolve(tree)
+    expected = {
+        'id': 1, 'content': '1', 'children': [
+            {
+                'id': 2, 'content': '2', 'children': [
+                    { 'id': 4, 'content': '4', 'children': [] }
+                ]
+            },
+            {'id': 3, 'content': '3', 'children': [] },
+        ],
+    }
+    assert tree.model_dump() == expected
+
diff --git a/tests/pydantic_v2/resolver/test_27_context.py b/tests/pydantic_v2/resolver/test_27_context.py
new file mode 100644
index 0000000..9045e4c
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_27_context.py
@@ -0,0 +1,72 @@
+import asyncio
+from random import random
+from pydantic import BaseModel
+from pydantic_resolve import Resolver
+from typing import List
+import pytest
+
+class Human(BaseModel):
+    name: str
+    lucky: bool = True
+    async def resolve_lucky(self):
+        print('calculating...')
+        await asyncio.sleep(1)  # mock i/o
+        return random() > 0.5
+    
+class Earth(BaseModel):
+    humans: List[Human] = []
+    def resolve_humans(self, context):
+        return [dict(name=f'man-{i}') for i in range(context['count'])]
+    
+    count: int = 0
+    def post_count(self, context):
+        return context['count']
+
+class EarthBad(BaseModel):
+    humans: List[Human] = []
+    def resolve_humans(self, context):
+        context['name'] = 'earth'  # mappingproxytype will raise TypeError
+        return [dict(name=f'man-{i}') for i in range(context['count'])]
+
+class EarthBad2(BaseModel):
+    humans: List[Human] = []
+    def resolve_humans(self, context):
+        return [dict(name=f'man-{i}') for i in range(context['count'])]
+    
+    count: int = 0
+    def post_count(self, context):
+        context['count'] = 111
+        return context['count']
+
+
+@pytest.mark.asyncio
+async def test_1():
+    earth = Earth()
+    earth = await Resolver(context={'count': 10}).resolve(earth)
+    assert len(earth.humans) == 10
+    assert earth.count == 10
+
+
+@pytest.mark.asyncio
+async def test_2():
+    earth = Earth()
+    with pytest.raises(AttributeError):
+        await Resolver().resolve(earth)
+
+@pytest.mark.asyncio
+async def test_3():
+    earth = Earth()
+    with pytest.raises(KeyError):
+        await Resolver(context={'age': 10}).resolve(earth)
+
+@pytest.mark.asyncio
+async def test_4():
+    earth = EarthBad()
+    with pytest.raises(TypeError):
+        await Resolver(context={'count': 10}).resolve(earth)
+
+@pytest.mark.asyncio
+async def test_5():
+    earth = EarthBad2()
+    with pytest.raises(TypeError):
+        await Resolver(context={'count': 10}).resolve(earth)
\ No newline at end of file
diff --git a/tests/pydantic_v2/resolver/test_28_parse_to_obj_for_dataclass_with_annotation.py b/tests/pydantic_v2/resolver/test_28_parse_to_obj_for_dataclass_with_annotation.py
new file mode 100644
index 0000000..e5974f6
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_28_parse_to_obj_for_dataclass_with_annotation.py
@@ -0,0 +1,49 @@
+from __future__ import annotations
+
+from typing import List, Optional
+import pytest
+from dataclasses import dataclass, field
+from pydantic_resolve import Resolver, LoaderDepend
+
+BOOKS = {
+    1: [{'name': 'book1'}, {'name': 'book2'}],
+    2: [{'name': 'book3'}, {'name': 'book4'}],
+    3: [{'name': 'book1'}, {'name': 'book2'}],
+}
+
+async def batch_load_fn(keys):
+    books = [[dict(name=bb['name']) for bb in BOOKS.get(k, [])] for k in keys]
+    return books
+
+@dataclass
+class Student():
+    id: int
+    name: str
+
+    books: List[Book] = field(default_factory=list)
+    def resolve_books(self, loader=LoaderDepend(batch_load_fn)):
+        return loader.load(self.id)
+
+@dataclass
+class ClassRoom():
+    students: List[Student]
+    info: Optional[Info] = None
+    def resolve_info(self):
+        return dict(content='kikodo')
+
+@dataclass
+class Book():
+    name: str
+
+@dataclass
+class Info():
+    content: str
+
+@pytest.mark.asyncio
+async def test_loader_depends_1():
+
+    students = [Student(id=1, name="jack"), Student(id=2, name="mike"), Student(id=3, name="wiki")]
+    classroom = ClassRoom(students=students)
+    res = await Resolver().resolve(classroom)  # auto resolve
+    assert isinstance(res.students[0].books[0], Book)
+    assert isinstance(res.info, Info)
diff --git a/tests/pydantic_v2/resolver/test_29_better_warning_of_list.py b/tests/pydantic_v2/resolver/test_29_better_warning_of_list.py
new file mode 100644
index 0000000..9e6cee6
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_29_better_warning_of_list.py
@@ -0,0 +1,116 @@
+from __future__ import annotations
+from pydantic import BaseModel, ValidationError
+from pydantic_resolve import build_object, LoaderDepend, Resolver, build_list
+from typing import List, Optional
+import pytest
+
+
+departments = [
+    dict(id=1, name='INFRA'),
+    dict(id=2, name='DevOps'),
+    dict(id=3, name='Sales'),
+]
+
+teams = [
+    dict(id=1, department_id=1, name="K8S"),
+    dict(id=2, department_id=1, name="MONITORING"),
+    dict(id=3, department_id=1, name="Jenkins"), 
+    dict(id=5, department_id=2, name="Frontend"),
+    dict(id=6, department_id=2, name="Bff"),
+    dict(id=7, department_id=2, name="Backend"), 
+    dict(id=8, department_id=3, name="CAT"),
+    dict(id=9, department_id=3, name="Account"),
+    dict(id=10, department_id=3, name="Operation"),
+]
+
+department_lead = [
+    dict(id=1, department_id=1, name='mike'),
+    dict(id=2, department_id=2, name='john'),
+    dict(id=3, department_id=3, name='ralf'),
+]
+
+members = [
+  dict(id=1, team_id=1, name="Sophia", gender='female'),
+  dict(id=2, team_id=1, name="Jackson", gender='male'),
+  dict(id=3, team_id=2, name="Olivia", gender='female'),
+  dict(id=4, team_id=2, name="Liam", gender='male'),
+  dict(id=5, team_id=3, name="Emma", gender='female'),
+  dict(id=6, team_id=4, name="Noah", gender='male'),
+  dict(id=7, team_id=5, name="Ava", gender='female'),
+  dict(id=8, team_id=6, name="Lucas", gender='male'),
+  dict(id=9, team_id=6, name="Isabella", gender='female'),
+  dict(id=10, team_id=6, name="Mason", gender='male'),
+  dict(id=11, team_id=7, name="Mia", gender='female'),
+  dict(id=12, team_id=8, name="Ethan", gender='male'),
+  dict(id=13, team_id=8, name="Amelia", gender='female'),
+  dict(id=14, team_id=9, name="Oliver", gender='male'),
+  dict(id=15, team_id=9, name="Charlotte", gender='female'),
+  dict(id=16, team_id=10, name="Jacob", gender='male'),
+  dict(id=17, team_id=10, name="Abigail", gender='female'),
+  dict(id=18, team_id=10, name="Daniel", gender='male'),
+  dict(id=19, team_id=10, name="Emily", gender='female'),
+  dict(id=20, team_id=10, name="Ella", gender='female')
+]
+
+class DepartmentBase(BaseModel):
+    id: int
+    name: str
+
+class TeamBase(BaseModel):
+    id: int
+    name: str
+
+
+class MemberBase(BaseModel):
+    id: int
+    name: str
+    gender: str
+
+
+async def lead_batch_load_fn(department_ids):
+    return build_list(department_lead, department_ids, lambda t: t['department_id'])
+
+async def teams_batch_load_fn(department_ids):
+    return build_list(teams, department_ids, lambda t: t['department_id'])
+
+async def members_batch_load_fn(team_ids):
+    return build_list(members, team_ids, lambda t: t['team_id'])
+
+
+class Result(BaseModel):
+    departments: List[Department]
+
+class Department(DepartmentBase):
+    teams: List[Team] = []
+    def resolve_teams(self, loader=LoaderDepend(teams_batch_load_fn)):
+        return loader.load(self.id)
+
+    lead: Optional[Lead] = None
+    def resolve_lead(self, loader=LoaderDepend(lead_batch_load_fn)):
+        return loader.load(self.id)
+
+class Team(TeamBase):
+    members: List[Member] = []
+    def resolve_members(self, loader=LoaderDepend(members_batch_load_fn)):
+        return loader.load(self.id)
+
+class Member(MemberBase):
+    ...
+
+class Lead(BaseModel):
+    id: int
+    name: str
+
+
+@pytest.mark.asyncio
+async def test_error():
+    """
+    1. generate data from departments to members  (top to bottom)
+    """
+    Result.model_rebuild()
+    department_ids = {2,3}
+    _departments = [Department(**d) for d in departments if d['id'] in department_ids] 
+    result = Result(departments=_departments)
+    with pytest.raises(ValidationError):
+        await Resolver().resolve(result)  # auto resolve
+
diff --git a/tests/pydantic_v2/resolver/test_2_resolve_object.py b/tests/pydantic_v2/resolver/test_2_resolve_object.py
new file mode 100644
index 0000000..aa80204
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_2_resolve_object.py
@@ -0,0 +1,75 @@
+import pytest
+from pydantic import BaseModel
+from typing import Optional
+from pydantic_resolve import Resolver
+import asyncio
+import time
+
+class DetailA(BaseModel):
+    name: str = ''
+
+class ServiceDetail1(BaseModel):
+    detail_a: Optional[DetailA] = None
+
+    async def resolve_detail_a(self) -> Optional[DetailA]:
+        await asyncio.sleep(1)
+        return DetailA(name='hello world')
+
+    detail_b: str = ''
+
+    async def resolve_detail_b(self) -> str:
+        await asyncio.sleep(1)
+        return 'good'
+
+class Service(BaseModel):
+    service_detail_1: Optional[ServiceDetail1] = None
+    async def resolve_service_detail_1(self) -> Optional[ServiceDetail1]:
+        await asyncio.sleep(1)
+        return ServiceDetail1()
+
+    service_detail_1b: Optional[ServiceDetail1] = None
+    async def resolve_service_detail_1b(self) -> Optional[ServiceDetail1]:
+        await asyncio.sleep(1)
+        return ServiceDetail1()
+
+    service_detail_2: str = ''
+    async def resolve_service_detail_2(self) -> str:
+        await asyncio.sleep(1)
+        return "detail_2"
+
+    service_detail_3: str = ''
+    async def resolve_service_detail_3(self) -> str:
+        await asyncio.sleep(1)
+        return "detail_3"
+
+    service_detail_4: str = ''
+    async def resolve_service_detail_4(self) -> str:
+        await asyncio.sleep(1)
+        return "detail_4"
+
+@pytest.mark.asyncio
+async def test_resolve_object():
+    t = time.time()
+    s = Service()
+    result = await Resolver().resolve(s)
+    expected = {
+        "service_detail_1": {
+            "detail_a": {
+                "name": "hello world"
+            },
+            "detail_b": "good"
+        },
+        "service_detail_1b": {
+            "detail_a": {
+                "name": "hello world"
+            },
+            "detail_b": "good"
+        },
+        "service_detail_2": "detail_2",
+        "service_detail_3": "detail_3",
+        "service_detail_4": "detail_4",
+    }
+    assert result.model_dump() == expected
+    delta = time.time() - t
+
+    assert delta < 2.1
\ No newline at end of file
diff --git a/tests/pydantic_v2/resolver/test_30_loader_in_object.py b/tests/pydantic_v2/resolver/test_30_loader_in_object.py
new file mode 100644
index 0000000..81f207c
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_30_loader_in_object.py
@@ -0,0 +1,59 @@
+from typing import List
+import pytest
+from pydantic import BaseModel
+from pydantic_resolve import Resolver, LoaderDepend
+from aiodataloader import DataLoader
+
+# for testing, loder instance need to initialized inside a thread with eventloop
+# (which means it can't be put in global scope of this file)
+# otherwise it will generate anthoer loop which will raise error of
+# "task attached to another loop"
+
+@pytest.mark.asyncio
+async def test_loader_depends():
+    counter = {
+        "book": 0
+    }
+
+    BOOKS = {
+        1: [{'name': 'book1'}, {'name': 'book2'}],
+        2: [{'name': 'book3'}, {'name': 'book4'}],
+        3: [{'name': 'book1'}, {'name': 'book2'}],
+    }
+
+    class Book(BaseModel):
+        name: str
+
+    class BookLoader(DataLoader):
+        async def batch_load_fn(self, keys):
+            counter["book"] += 1
+            books = [[Book(**bb) for bb in BOOKS.get(k, [])] for k in keys]
+            return books
+
+    class Student(BaseModel):
+        id: int
+        name: str
+
+        books: List[Book] = [] 
+        def resolve_books(self, loader=LoaderDepend(BookLoader)):
+            return loader.load(self.id)
+
+        books_2: List[Book] = [] 
+        def resolve_books_2(self, loader=LoaderDepend(BookLoader)):
+            return loader.load(self.id)
+
+        books_3: List[Book] = [] 
+        def resolve_books_3(self, loader=LoaderDepend(BookLoader)):
+            return loader.load(self.id)
+
+    students = [Student(id=1, name="jack"), Student(id=2, name="mike"), Student(id=3, name="wiki")]
+    await Resolver().resolve(students)
+    assert counter["book"] == 1
+
+    students = [Student(id=1, name="jack"), Student(id=2, name="mike"), Student(id=3, name="wiki")]
+    await Resolver().resolve(students)
+    assert counter["book"] == 2  
+
+    students = [Student(id=1, name="jack"), Student(id=2, name="mike"), Student(id=3, name="wiki")]
+    await Resolver().resolve(students)
+    assert counter["book"] == 3  
\ No newline at end of file
diff --git a/tests/pydantic_v2/resolver/test_31_dynamic_variable_for_descdant_loader.py b/tests/pydantic_v2/resolver/test_31_dynamic_variable_for_descdant_loader.py
new file mode 100644
index 0000000..bec12c2
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_31_dynamic_variable_for_descdant_loader.py
@@ -0,0 +1,69 @@
+import pytest
+from typing import List, Optional
+from pydantic import BaseModel
+from pydantic_resolve.resolver import Resolver
+
+
+class Kar(BaseModel):
+    name: str
+
+    desc: str = ''
+    def resolve_desc(self, ancestor_context):
+        return f"{self.name} - {ancestor_context['bar_num']} - {ancestor_context['foo_a']} - {ancestor_context['foo_b']}"
+    
+    output: str = ''
+    def post_output(self, ancestor_context):
+        return f"{self.name} - {ancestor_context['bar_num']} - {ancestor_context['foo_a']} - {ancestor_context['foo_b']}"
+    
+    kar_c: Optional[str] = None
+    def resolve_kar_c(self, ancestor_context):
+        return ancestor_context['foo_c']
+
+
+class Bar(BaseModel):
+    __pydantic_resolve_expose__ = {'num': 'bar_num'}
+
+    num: int
+
+    kars: List[Kar] = []
+    def resolve_kars(self):
+        return [{'name': n} for n in ['a', 'b', 'c']]
+
+
+class Foo(BaseModel):
+    __pydantic_resolve_expose__ = {'a': 'foo_a', 'b': 'foo_b', 'c': 'foo_c'}
+
+    a: str
+    b: str
+    c: Optional[str] = None
+    nums:List[int]
+    bars: List[Bar] = []
+
+    def resolve_bars(self):
+        return [{'num': n} for n in self.nums]
+
+
+@pytest.mark.asyncio
+async def test_case():
+    foo = Foo(nums=[1,2,3], a='a', b='b', c=None)
+    await Resolver().resolve(foo)
+    assert foo.model_dump() == {
+        'a': 'a',
+        'b': 'b',
+        'c': None,
+        'nums': [1,2,3],
+        'bars': [
+            {'num': 1, 'kars': [ 
+                {'name': 'a', 'desc': 'a - 1 - a - b', 'output': 'a - 1 - a - b', 'kar_c': None},
+                {'name': 'b', 'desc': 'b - 1 - a - b', 'output': 'b - 1 - a - b', 'kar_c': None},
+                {'name': 'c', 'desc': 'c - 1 - a - b', 'output': 'c - 1 - a - b', 'kar_c': None} ]},
+            {'num': 2, 'kars': [ 
+                {'name': 'a', 'desc': 'a - 2 - a - b', 'output': 'a - 2 - a - b', 'kar_c': None},
+                {'name': 'b', 'desc': 'b - 2 - a - b', 'output': 'b - 2 - a - b', 'kar_c': None},
+                {'name': 'c', 'desc': 'c - 2 - a - b', 'output': 'c - 2 - a - b', 'kar_c': None} ]},
+            {'num': 3, 'kars': [ 
+                {'name': 'a', 'desc': 'a - 3 - a - b', 'output': 'a - 3 - a - b', 'kar_c': None},
+                {'name': 'b', 'desc': 'b - 3 - a - b', 'output': 'b - 3 - a - b', 'kar_c': None},
+                {'name': 'c', 'desc': 'c - 3 - a - b', 'output': 'c - 3 - a - b', 'kar_c': None} ]}
+        ]
+    }
\ No newline at end of file
diff --git a/tests/pydantic_v2/resolver/test_32_dynamic_variable_for_descdant_loader_exception.py b/tests/pydantic_v2/resolver/test_32_dynamic_variable_for_descdant_loader_exception.py
new file mode 100644
index 0000000..3669e36
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_32_dynamic_variable_for_descdant_loader_exception.py
@@ -0,0 +1,108 @@
+import pytest
+from typing import List
+from pydantic import BaseModel
+from pydantic_resolve.resolver import Resolver
+
+
+@pytest.mark.asyncio
+async def test_case_0():
+    class Kar(BaseModel):
+        name: str
+
+        desc: str = ''
+        def resolve_desc(self, ancestor_context):
+            return f"{self.name} - {ancestor_context['bar_num']}"
+
+
+    class Bar(BaseModel):
+        __pydantic_resolve_expose__ = {'num': 'bar_num'}
+
+        num: int
+
+        kars: List[Kar] = []
+        def resolve_kars(self):
+            return [{'name': n} for n in ['a', 'b', 'c']]
+
+
+    class Foo(BaseModel):
+        __pydantic_resolve_expose__ = {'nums': 'bar_num'}
+
+        nums:List[int]
+        bars: List[Bar] = []
+
+        def resolve_bars(self):
+            return [{'num': n} for n in self.nums]
+
+
+    foo = Foo(nums=[1,2,3])
+    with pytest.raises(AttributeError) as e:
+        await Resolver().resolve(foo)
+    assert 'alias name conflicts, please check' in str(e.value)
+
+
+@pytest.mark.asyncio
+async def test_case_1():
+    class Kar(BaseModel):
+        name: str
+
+        desc: str = ''
+        def resolve_desc(self, ancestor_context):
+            return f"{self.name} - {ancestor_context['bar_num']}"
+
+
+    class Bar(BaseModel):
+        __pydantic_resolve_expose__ = ['num', 'bar_num']
+
+        num: int
+
+        kars: List[Kar] = []
+        def resolve_kars(self):
+            return [{'name': n} for n in ['a', 'b', 'c']]
+
+
+    class Foo(BaseModel):
+        nums:List[int]
+        bars: List[Bar] = []
+
+        def resolve_bars(self):
+            return [{'num': n} for n in self.nums]
+
+
+    foo = Foo(nums=[1,2,3])
+    with pytest.raises(AttributeError) as e:
+        await Resolver().resolve(foo)
+    assert 'is not dict' in str(e.value)
+
+
+@pytest.mark.asyncio
+async def test_case_2():
+    class Kar(BaseModel):
+        name: str
+
+        desc: str = ''
+        def resolve_desc(self, ancestor_context):
+            return f"{self.name} - {ancestor_context['bar_num']}"
+
+
+    class Bar(BaseModel):
+        __pydantic_resolve_expose__ = {'xnum': 'bar_num'}
+
+        num: int
+
+        kars: List[Kar] = []
+        def resolve_kars(self):
+            return [{'name': n} for n in ['a', 'b', 'c']]
+
+
+    class Foo(BaseModel):
+        nums:List[int]
+        bars: List[Bar] = []
+
+        def resolve_bars(self):
+            return [{'num': n} for n in self.nums]
+
+
+    foo = Foo(nums=[1,2,3])
+    with pytest.raises(AttributeError) as e:
+        await Resolver().resolve(foo)
+    assert 'does not existed' in str(e.value)
\ No newline at end of file
diff --git a/tests/pydantic_v2/resolver/test_33_global_loader_filter.py b/tests/pydantic_v2/resolver/test_33_global_loader_filter.py
new file mode 100644
index 0000000..3d67ad5
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_33_global_loader_filter.py
@@ -0,0 +1,70 @@
+from pydantic import BaseModel
+from typing import List
+from pydantic_resolve import Resolver, LoaderDepend
+from aiodataloader import DataLoader
+import pytest
+from pydantic_resolve.exceptions import GlobalLoaderFieldOverlappedError
+
+class LoaderA(DataLoader):
+    power: int
+    async def batch_load_fn(self, keys: List[int]):
+        return [ k** self.power for k in keys ]
+
+class LoaderB(DataLoader):
+    power: int
+    async def batch_load_fn(self, keys: List[int]):
+        return [ k** self.power for k in keys ]
+
+class LoaderC(DataLoader):
+    power: int
+    add: int 
+    async def batch_load_fn(self, keys: List[int]):
+        return [ k** self.power + self.add for k in keys ]
+
+
+async def loader_fn_a(keys):
+    return [ k**2 for k in keys ]
+
+class A(BaseModel):
+    val: int
+
+    a: int = 0
+    def resolve_a(self, loader=LoaderDepend(LoaderA)):
+        return loader.load(self.val)
+
+    b: int = 0
+    def resolve_b(self, loader=LoaderDepend(LoaderB)):
+        return loader.load(self.val)
+
+    c: int = 0
+    def resolve_c(self, loader=LoaderDepend(LoaderC)):
+        return loader.load(self.val)
+
+
+@pytest.mark.asyncio
+async def test_case_0():
+    data = [A(val=n) for n in range(3)]
+    data = await Resolver(global_loader_param={'power': 2}, 
+                          loader_params={LoaderC:{'add': 1}}).resolve(data)
+    assert data[2].a == 4
+    assert data[2].b == 4
+    assert data[2].c == 5
+
+
+@pytest.mark.asyncio
+async def test_case_1():
+    data = [A(val=n) for n in range(3)]
+    data = await Resolver(loader_params={LoaderA:{'power': 2}, 
+                                          LoaderB:{'power': 3},
+                                          LoaderC:{'power': 3, 'add': 1}}).resolve(data)
+    assert data[2].a == 4
+    assert data[2].b == 8
+    assert data[2].c == 9
+
+
+@pytest.mark.asyncio
+async def test_case_3():
+    data = [A(val=n) for n in range(3)]
+    with pytest.raises(GlobalLoaderFieldOverlappedError):
+        data = await Resolver(global_loader_param={'power': 2}, 
+                            loader_params={LoaderC:{'add': 1, 'power': 3}}).resolve(data)
\ No newline at end of file
diff --git a/tests/pydantic_v2/resolver/test_34_filter_rename_deprecation.py b/tests/pydantic_v2/resolver/test_34_filter_rename_deprecation.py
new file mode 100644
index 0000000..86e6772
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_34_filter_rename_deprecation.py
@@ -0,0 +1,59 @@
+from pydantic import BaseModel
+from typing import List
+from pydantic_resolve import Resolver, LoaderDepend
+from aiodataloader import DataLoader
+import pytest
+from pydantic_resolve.exceptions import GlobalLoaderFieldOverlappedError
+
+class LoaderA(DataLoader):
+    power: int
+    async def batch_load_fn(self, keys: List[int]):
+        return [ k** self.power for k in keys ]
+
+class LoaderB(DataLoader):
+    power: int
+    async def batch_load_fn(self, keys: List[int]):
+        return [ k** self.power for k in keys ]
+
+class LoaderC(DataLoader):
+    power: int
+    add: int 
+    async def batch_load_fn(self, keys: List[int]):
+        return [ k** self.power + self.add for k in keys ]
+
+
+async def loader_fn_a(keys):
+    return [ k**2 for k in keys ]
+
+class A(BaseModel):
+    val: int
+
+    a: int = 0
+    def resolve_a(self, loader=LoaderDepend(LoaderA)):
+        return loader.load(self.val)
+
+    b: int = 0
+    def resolve_b(self, loader=LoaderDepend(LoaderB)):
+        return loader.load(self.val)
+
+    c: int = 0
+    def resolve_c(self, loader=LoaderDepend(LoaderC)):
+        return loader.load(self.val)
+
+
+@pytest.mark.asyncio
+async def test_case_0():
+    data = [A(val=n) for n in range(3)]
+    with pytest.warns(DeprecationWarning):
+        data = await Resolver(global_loader_filter={'power': 2}, 
+                            loader_filters={LoaderC:{'add': 1}}).resolve(data)
+
+
+@pytest.mark.asyncio
+async def test_case_1():
+    data = [A(val=n) for n in range(3)]
+    with pytest.warns(DeprecationWarning):
+        data = await Resolver(loader_filters={LoaderA:{'power': 2}, 
+                                            LoaderB:{'power': 3},
+                                            LoaderC:{'power': 3, 'add': 1}}).resolve(data)
+
diff --git a/tests/pydantic_v2/resolver/test_35_collector.py b/tests/pydantic_v2/resolver/test_35_collector.py
new file mode 100644
index 0000000..4fbec79
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_35_collector.py
@@ -0,0 +1,85 @@
+from __future__ import annotations
+from pydantic import BaseModel
+from typing import List
+from pydantic_resolve import Resolver, Collector
+import pytest
+
+from pydantic_resolve.core import LoaderDepend
+
+class SubCollector(Collector):
+    def add(self, val):  # replace with your implementation
+        print('add')
+        self.val.append(val)
+
+async def c_loader_fn(keys):
+    return [[C(detail=f'{k}-1'), C(detail=f'{k}-2')] for k in keys]
+
+class A(BaseModel):
+    b_list: List[B] = []
+    async def resolve_b_list(self):
+        return [dict(name='b1'), dict(name='b2')]
+
+    names: List[str] = []
+    def post_names(self, collector=SubCollector('b_name')):
+        return collector.values()
+
+    items: List[str] = []
+    def post_items(self, collector=Collector('b_items', flat=True)):
+        return collector.values()
+
+    details: List[str] = []
+    def post_details(self, collector=Collector('c_details', flat=True)):
+        return collector.values()
+
+    details_nest: List[List[str]] = []
+    def post_details_nest(self, 
+                          collector=Collector('c_details')):
+        return collector.values()
+
+    details_compare: bool = False
+    def post_details_compare(self, 
+                          collector=Collector('c_details'),
+                          collector2=Collector('c_details'),
+                          ):
+        return collector.values() == collector2.values()
+
+class B(BaseModel):
+    __pydantic_resolve_collect__ = {
+        'name': 'b_name',
+        'items': 'b_items'
+    }
+    name: str
+    items: List[str] = ['x', 'y']
+
+    details: List[str] = []
+    def post_details(self, collector=Collector('c_details', flat=True)):
+        return collector.values()
+
+    c_list: List[C] = []
+    async def resolve_c_list(self, loader=LoaderDepend(c_loader_fn)):
+        return loader.load(self.name)
+
+class C(BaseModel):
+    __pydantic_resolve_collect__ = {
+        'details': 'c_details'
+    }
+    detail: str
+
+    details: List[str] = []
+    def resolve_details(self):
+        return [f'{self.detail}-detail-1', f'{self.detail}-detail-2']
+
+
+@pytest.mark.asyncio
+async def test_collector_1():
+    a = A()
+    a = await Resolver().resolve(a)
+    assert a.names == ['b1', 'b2']
+    assert a.items == ['x', 'y', 'x', 'y']
+    assert a.details == ['b1-1-detail-1', 'b1-1-detail-2', 'b1-2-detail-1', 'b1-2-detail-2', 'b2-1-detail-1', 'b2-1-detail-2', 'b2-2-detail-1', 'b2-2-detail-2']
+    assert a.details_nest == [['b1-1-detail-1', 'b1-1-detail-2'],
+                              ['b1-2-detail-1', 'b1-2-detail-2'], 
+                              ['b2-1-detail-1', 'b2-1-detail-2'], 
+                              ['b2-2-detail-1', 'b2-2-detail-2']]
+
+    assert a.details_compare is True
\ No newline at end of file
diff --git a/tests/pydantic_v2/resolver/test_36_collector_level_by_level.py b/tests/pydantic_v2/resolver/test_36_collector_level_by_level.py
new file mode 100644
index 0000000..9878529
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_36_collector_level_by_level.py
@@ -0,0 +1,84 @@
+from __future__ import annotations
+from pydantic import BaseModel
+from typing import List
+from pydantic_resolve import Resolver, Collector
+from pydantic_resolve.core import LoaderDepend
+import pytest
+
+
+class Root(BaseModel):
+    list_a: List[A] = []
+    def resolve_list_a(self):
+        return data
+    
+    names: List[str] = []
+    def post_names(self, collector=Collector('b_name')):
+        return collector.values()
+
+class A(BaseModel):
+    list_b: List[B]
+
+    names: List[str] = []
+    def post_names(self, collector=Collector('b_name')):
+        return collector.values()
+
+class B(BaseModel):
+    __pydantic_resolve_collect__ = {'name': 'b_name'}
+    name: str
+
+
+data = [
+        {'list_b': [
+            {'name': 'b1'},
+            {'name': 'b2'},
+        ]},
+        {'list_b': [
+            {'name': 'b3'},
+            {'name': 'b4'},
+        ]},
+    ]
+
+@pytest.mark.asyncio
+async def test_level():
+    r = Root()
+    resolver = Resolver()
+    r = await resolver.resolve(r)
+    # print(resolver.object_collect_alias_map_store)
+    assert r.model_dump() == {
+        "list_a": [
+            {
+            "list_b": [
+                {
+                "name": "b1"
+                },
+                {
+                "name": "b2"
+                }
+            ],
+            "names": [
+                "b1",
+                "b2"
+            ]
+            },
+            {
+            "list_b": [
+                {
+                "name": "b3"
+                },
+                {
+                "name": "b4"
+                }
+            ],
+            "names": [
+                "b3",
+                "b4"
+            ]
+            }
+        ],
+        "names": [
+            "b1",
+            "b2",
+            "b3",
+            "b4"
+        ]
+    }
\ No newline at end of file
diff --git a/tests/pydantic_v2/resolver/test_37_specific_types.py b/tests/pydantic_v2/resolver/test_37_specific_types.py
new file mode 100644
index 0000000..b4f0639
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_37_specific_types.py
@@ -0,0 +1,17 @@
+import pytest
+from typing import Tuple
+from pydantic import BaseModel
+from pydantic_resolve import Resolver
+
+class Case(BaseModel):
+    value: Tuple[int, int]=(0,0)
+
+    def resolve_value(self):
+        return (1, 1)
+
+
+@pytest.mark.asyncio
+async def test_works():
+    # https://github.com/allmonday/pydantic2-resolve/issues/7
+    result = await Resolver().resolve(Case())
+    assert result.value == (1, 1)
\ No newline at end of file
diff --git a/tests/pydantic_v2/resolver/test_39_parent.py b/tests/pydantic_v2/resolver/test_39_parent.py
new file mode 100644
index 0000000..a135cf0
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_39_parent.py
@@ -0,0 +1,76 @@
+from __future__ import annotations
+import pytest
+from typing import Optional, List
+from pydantic import BaseModel
+from pydantic_resolve import Resolver
+
+
+class Base(BaseModel):
+    name: str
+
+    child: Optional[Child] = None
+    def resolve_child(self):
+        return Child()
+
+    children: List[Child] = []
+    def resolve_children(self):
+        return [Child()]
+    
+    parent: Optional[str] = '123'
+    def resolve_parent(self, parent):
+        return parent
+
+class Child(BaseModel):
+    pname: str = ''
+    def resolve_pname(self, parent: Base):
+        return parent.name
+    
+    pname2: str = ''
+    def resolve_pname2(self, parent: Base):
+        return parent.name
+
+
+@pytest.mark.asyncio
+async def test_parent():
+    b = Base(name='kikodo')
+    b = await Resolver().resolve(b)
+    assert b.parent is None  # parent of root is none
+
+    assert b.name == 'kikodo'
+    assert b.child.pname == 'kikodo'
+    assert b.child.pname2 == 'kikodo'  # work with obj
+
+    assert b.children[0].pname == 'kikodo'
+    assert b.children[0].pname2 == 'kikodo'  # work with list
+
+
+class Tree(BaseModel):
+    name: str
+
+    path: str = ''
+    def resolve_path(self, parent):
+        if parent is not None:
+            return f'{parent.path}/{self.name}'
+        return self.name
+    children: List[Tree] = []
+
+@pytest.mark.asyncio
+async def test_tree():
+    data = dict(name="a", children=[
+        dict(name="b", children=[
+            dict(name="c")
+        ]),
+        dict(name="d", children=[
+            dict(name="c")
+        ])
+    ]) 
+    data = await Resolver().resolve(Tree(**data))
+    assert data.model_dump() ==  dict(name="a", path="a", children=[
+        dict(name="b", path="a/b", children=[
+            dict(name="c", path="a/b/c", children=[])
+        ]),
+        dict(name="d", path="a/d", children=[
+            dict(name="c", path="a/d/c", children=[])
+        ])
+    ]) 
+    print(data.model_dump_json(indent=2))
diff --git a/tests/pydantic_v2/resolver/test_3_tuple_list.py b/tests/pydantic_v2/resolver/test_3_tuple_list.py
new file mode 100644
index 0000000..97dd371
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_3_tuple_list.py
@@ -0,0 +1,28 @@
+from __future__ import annotations
+from typing import List, Tuple
+import asyncio
+from pydantic import BaseModel
+from pydantic_resolve import Resolver
+import pytest
+
+class Student(BaseModel):
+    new_books: List[str] = []    
+    async def resolve_new_books(self) -> List[str]:
+        await asyncio.sleep(1)
+        return ['book1', 'book2']
+
+    old_books: List[str] = []    
+    async def resolve_old_books(self) -> List[str]:
+        await asyncio.sleep(1)
+        return ['book1', 'book2']
+
+
+@pytest.mark.asyncio
+async def test_type_definition():
+    stu = Student()
+    result = await Resolver().resolve(stu)
+    expected = {
+        'new_books': ['book1', 'book2'],
+        'old_books': ['book1', 'book2']
+    }
+    assert result.model_dump() == expected
diff --git a/tests/pydantic_v2/resolver/test_40_post_async.py b/tests/pydantic_v2/resolver/test_40_post_async.py
new file mode 100644
index 0000000..dce45b4
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_40_post_async.py
@@ -0,0 +1,18 @@
+import asyncio
+from pydantic import BaseModel
+from pydantic_resolve import Resolver
+import pytest
+
+
+class A(BaseModel):
+    name: str
+
+    greet: str = ''
+    async def post_greet(self):
+        await asyncio.sleep(1)
+        return 'hello ' + self.name
+
+@pytest.mark.asyncio
+async def test_post_async():
+    data = await Resolver().resolve(A(name='kikodo'))
+    assert data.greet == 'hello kikodo'
diff --git a/tests/pydantic_v2/resolver/test_41_multiple_collect_source.py b/tests/pydantic_v2/resolver/test_41_multiple_collect_source.py
new file mode 100644
index 0000000..35b213d
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_41_multiple_collect_source.py
@@ -0,0 +1,50 @@
+from __future__ import annotations
+from pydantic import BaseModel
+from typing import List
+from pydantic_resolve import Resolver, Collector
+import pytest
+
+class Root(BaseModel):
+    list_a: List[A] = []
+    def resolve_list_a(self):
+        return data
+    
+    names: List[str] = []
+    def post_names(self, collector=Collector('field_name')):
+        return collector.values()
+
+class A(BaseModel):
+    list_b: List[B]
+
+class B(BaseModel):
+    __pydantic_resolve_collect__ = {'name': 'field_name'}
+    name: str
+    list_c: List[C]
+
+class C(BaseModel):
+    __pydantic_resolve_collect__ = {'name': 'field_name'}
+    name: str
+    async def post_name(self):
+        return f'{self.name}!'
+
+
+data = [
+        {'list_b': [
+            {'name': 'b1', 'list_c': [
+                {'name': 'c1'}
+            ]},
+            {'name': 'b2', 'list_c': []},
+        ]},
+        {'list_b': [
+            {'name': 'b3', 'list_c': []},
+            {'name': 'b4', 'list_c': [
+                {'name': 'c4'}
+            ]},
+        ]},
+    ]
+
+@pytest.mark.asyncio
+async def test_collector_1():
+    root = Root()
+    root = await Resolver().resolve(root)
+    assert set(root.names) == {'b1', 'b2', 'b3', 'b4', 'c1!', 'c4!'}
\ No newline at end of file
diff --git a/tests/pydantic_v2/resolver/test_42_validate_collect_relationship.py b/tests/pydantic_v2/resolver/test_42_validate_collect_relationship.py
new file mode 100644
index 0000000..95b35c5
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_42_validate_collect_relationship.py
@@ -0,0 +1,44 @@
+from __future__ import annotations
+from pydantic import BaseModel
+from typing import List
+from pydantic_resolve import Resolver
+from pydantic_resolve.exceptions import MissingCollector
+import pytest
+
+class Root(BaseModel):
+    list_a: List[A] = []
+    def resolve_list_a(self):
+        return data
+
+class A(BaseModel):
+    list_b: List[B]
+
+class B(BaseModel):
+    __pydantic_resolve_collect__ = {'name': 'field_name'}  # missing collector
+    name: str
+    list_c: List[C]
+
+class C(BaseModel):
+    __pydantic_resolve_collect__ = {'name': 'field_name'}
+    name: str
+
+data = [
+        {'list_b': [
+            {'name': 'b1', 'list_c': [
+                {'name': 'c1'}
+            ]},
+            {'name': 'b2', 'list_c': []},
+        ]},
+        {'list_b': [
+            {'name': 'b3', 'list_c': []},
+            {'name': 'b4', 'list_c': [
+                {'name': 'c4'}
+            ]},
+        ]},
+    ]
+
+@pytest.mark.asyncio
+async def test_collector_1():
+    root = Root()
+    with pytest.raises(MissingCollector):
+        await Resolver().resolve(root)
\ No newline at end of file
diff --git a/tests/pydantic_v2/resolver/test_4_resolve_return_types.py b/tests/pydantic_v2/resolver/test_4_resolve_return_types.py
new file mode 100644
index 0000000..d56a76f
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_4_resolve_return_types.py
@@ -0,0 +1,42 @@
+from __future__ import annotations
+from typing import List, Optional
+import asyncio
+from pydantic import BaseModel
+from pydantic_resolve import Resolver
+import pytest
+
+async def set_after(fut, value):
+    await asyncio.sleep(1)
+    fut.set_result(value)
+
+class Student(BaseModel):
+    scores: List[int] = []
+    async def resolve_scores(self) -> List[int]:
+        return [1,2,3]
+
+    age: Optional[int] = None
+    def resolve_age(self) -> Optional[int]:
+        return 12
+
+    name: Optional[str] = None
+    def resolve_name(self) -> Optional[str]:
+        return 'name'
+    
+    future: Optional[str] = None
+    def resolve_future(self) -> Optional[str]:
+        loop = asyncio.get_running_loop()
+        fut = loop.create_future()
+        loop.create_task(set_after(fut, 'hello'))
+        return fut
+
+@pytest.mark.asyncio
+async def test_resolve_future():
+    stu = Student()
+    result = await Resolver().resolve(stu)
+    expected = {
+        'name': 'name',
+        'scores': [1,2,3],
+        'age': 12,
+        'future': 'hello'
+    }
+    assert result.model_dump() == expected
diff --git a/tests/pydantic_v2/resolver/test_5_exception.py b/tests/pydantic_v2/resolver/test_5_exception.py
new file mode 100644
index 0000000..41c9a2f
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_5_exception.py
@@ -0,0 +1,49 @@
+from __future__ import annotations
+from pydantic_resolve import ResolverTargetAttrNotFound, Resolver
+from pydantic import ConfigDict, BaseModel, ValidationError
+import pytest
+
+class Service(BaseModel):
+    service_detail: str = ''
+    def resolve_service_details(self) -> str:
+        return "detail"
+
+@pytest.mark.asyncio
+async def test_exception_1():
+    with pytest.raises(ResolverTargetAttrNotFound, match="attribute service_details not found"):
+        s = Service()
+        await Resolver().resolve(s)
+
+class CustomException(Exception):
+    pass
+
+class CustomException2(Exception):
+    pass
+
+class Service2(BaseModel):
+    service_detail: str = ''
+    async def resolve_service_detail(self) -> str:
+        raise CustomException2('oops')
+
+    service_detail_2: str = ''
+    async def resolve_service_detail_2(self) -> str:
+        raise CustomException('oops')
+
+@pytest.mark.asyncio
+async def test_custom_exception():
+    with pytest.raises((CustomException, CustomException2), match="oops"):
+        s = Service2()
+        await Resolver().resolve(s)
+
+
+class Service3(BaseModel):
+    service_detail: int = 0
+    def resolve_service_detail(self) -> int: 
+        return 'abc'  # type:ignore
+    model_config = ConfigDict(validate_assignment=True)
+
+@pytest.mark.asyncio
+async def test_pydantic_validate_exception():
+    with pytest.raises(ValidationError):
+        s = Service3()
+        await Resolver().resolve(s)
diff --git a/tests/pydantic_v2/resolver/test_6_resolve_dataclass.py b/tests/pydantic_v2/resolver/test_6_resolve_dataclass.py
new file mode 100644
index 0000000..260a61f
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_6_resolve_dataclass.py
@@ -0,0 +1,38 @@
+from dataclasses import dataclass, asdict, field
+import asyncio
+from pydantic_resolve import Resolver
+from typing import List
+import pytest
+
+@dataclass
+class Wheel:
+    is_ok: bool 
+
+@dataclass
+class Car:
+    name: str
+    wheels: List[Wheel] = field(default_factory=list)
+
+    async def resolve_wheels(self) -> List[Wheel]:
+        await asyncio.sleep(1)
+        return [Wheel(is_ok=True)]
+
+@pytest.mark.asyncio
+async def test_resolve_dataclass_1():
+    car = Car(name="byd")
+    result = await Resolver().resolve(car)
+    expected = {
+        'name': 'byd',
+        'wheels': [{'is_ok': True}]
+    }
+    assert asdict(result) == expected
+
+@pytest.mark.asyncio
+async def test_resolver_dataclass_2():
+    car = [Car(name="byd")]
+    result = await Resolver().resolve(car)
+    expected = {
+        'name': 'byd',
+        'wheels': [{'is_ok': True}]
+    }
+    assert asdict(result[0]) == expected
\ No newline at end of file
diff --git a/tests/pydantic_v2/resolver/test_7_sqlalchemy_query.py b/tests/pydantic_v2/resolver/test_7_sqlalchemy_query.py
new file mode 100644
index 0000000..dde9542
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_7_sqlalchemy_query.py
@@ -0,0 +1,164 @@
+from typing import List
+import pytest
+from collections import Counter, defaultdict
+from aiodataloader import DataLoader
+from pydantic import ConfigDict, BaseModel
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import async_sessionmaker
+from sqlalchemy.ext.asyncio import create_async_engine
+from sqlalchemy.orm import DeclarativeBase
+from sqlalchemy.orm import Mapped
+from sqlalchemy.orm import mapped_column
+from pydantic_resolve import Resolver
+
+class Base(DeclarativeBase):
+    pass
+
+class Task(Base):
+    __tablename__ = "task"
+
+    id: Mapped[int] = mapped_column(primary_key=True)
+    name: Mapped[str]
+
+class Comment(Base):
+    __tablename__ = "comment"
+    id: Mapped[int] = mapped_column(primary_key=True)
+    task_id: Mapped[int] = mapped_column()
+    content: Mapped[str]
+
+class Feedback(Base):
+    __tablename__ = "feedback"
+    id: Mapped[int] = mapped_column(primary_key=True)
+    comment_id: Mapped[int] = mapped_column()
+    content: Mapped[str]
+
+@pytest.mark.asyncio
+async def test_sqlite_and_dataloader():
+    counter = Counter()
+    engine = create_async_engine(
+        "sqlite+aiosqlite://",
+        echo=False,
+    )
+    async_session = async_sessionmaker(engine, expire_on_commit=False)
+
+    async def insert_objects() -> None:
+        async with async_session() as session:
+            async with session.begin():
+                session.add_all(
+                    [
+                        Task(id=1, name="task-1"),
+                        Task(id=2, name="task-2"),
+                        Task(id=3, name="task-3"),
+
+                        Comment(id=1, task_id=1, content="comment-1 for task 1"),
+                        Comment(id=2, task_id=1, content="comment-2 for task 1"),
+                        Comment(id=3, task_id=2, content="comment-1 for task 2"),
+
+                        Feedback(id=1, comment_id=1, content="feedback-1 for comment-1"),
+                        Feedback(id=2, comment_id=1, content="feedback-1 for comment-1"),
+                        Feedback(id=3, comment_id=1, content="feedback-1 for comment-1"),
+                    ]
+                )
+
+    class FeedbackLoader(DataLoader):
+        async def batch_load_fn(self, comment_ids):
+            counter['load-feedback'] += 1
+            async with async_session() as session:
+                res = await session.execute(select(Feedback).where(Feedback.comment_id.in_(comment_ids)))
+                rows = res.scalars().all()
+                dct = defaultdict(list)
+                for row in rows:
+                    dct[row.comment_id].append(FeedbackSchema.model_validate(row))
+                return [dct.get(k, []) for k in comment_ids]
+
+    class CommentLoader(DataLoader):
+        async def batch_load_fn(self, task_ids):
+            counter['load-comment'] += 1
+            async with async_session() as session:
+                res = await session.execute(select(Comment).where(Comment.task_id.in_(task_ids)))
+                rows = res.scalars().all()
+
+                dct = defaultdict(list)
+                for row in rows:
+                    dct[row.task_id].append(CommentSchema.model_validate(row))
+                return [dct.get(k, []) for k in task_ids]
+
+    feedback_loader = FeedbackLoader()
+    comment_loader = CommentLoader()
+
+    class FeedbackSchema(BaseModel):
+        id: int
+        comment_id: int
+        content: str
+        model_config = ConfigDict(from_attributes=True)
+
+    class CommentSchema(BaseModel):
+        id: int
+        task_id: int
+        content: str
+        feedbacks: List[FeedbackSchema]  = []
+
+        def resolve_feedbacks(self):
+            return feedback_loader.load(self.id)
+        model_config = ConfigDict(from_attributes=True)
+
+    class TaskSchema(BaseModel):
+        id: int
+        name: str
+        comments: List[CommentSchema] = [] 
+        
+        def resolve_comments(self):
+            return comment_loader.load(self.id)
+        model_config = ConfigDict(from_attributes=True)
+
+    async def init():
+        async with engine.begin() as conn:
+            await conn.run_sync(Base.metadata.create_all)
+
+    async def query():
+        async with async_session() as session:
+            tasks = (await session.execute(select(Task))).scalars().all()
+            task_objs = [TaskSchema.model_validate(t) for t in tasks]
+            resolved_results = await Resolver().resolve(task_objs)
+            to_dict_arr = [r.model_dump() for r in resolved_results]
+            return to_dict_arr
+
+    await init()
+    await insert_objects()
+    result = await query()
+    expected = [
+        {
+            'comments': [{'content': 'comment-1 for task 1',
+            'feedbacks': [{'comment_id': 1,
+                            'content': 'feedback-1 for comment-1',
+                            'id': 1},
+                            {'comment_id': 1,
+                            'content': 'feedback-1 for comment-1',
+                            'id': 2},
+                            {'comment_id': 1,
+                            'content': 'feedback-1 for comment-1',
+                            'id': 3}],
+            'id': 1,
+            'task_id': 1},
+                        {'content': 'comment-2 for task 1',
+                            'feedbacks': [],
+                            'id': 2,
+                            'task_id': 1}],
+            'id': 1,
+            'name': 'task-1'},
+            {'comments': [{'content': 'comment-1 for task 2',
+                            'feedbacks': [],
+                            'id': 3,
+                            'task_id': 2}],
+            'id': 2,
+            'name': 'task-2'},
+            {'comments': [], 'id': 3, 'name': 'task-3'}]
+
+    assert result == expected
+    assert counter['load-comment'] == 1  # batch_load_fn only called once
+    assert counter['load-feedback'] == 1
+    
+    await query()
+
+    assert counter['load-comment'] == 1  # Resolver alone can't fix cache issue
+    assert counter['load-feedback'] == 1
\ No newline at end of file
diff --git a/tests/pydantic_v2/resolver/test_8_loader_depend.py b/tests/pydantic_v2/resolver/test_8_loader_depend.py
new file mode 100644
index 0000000..31889ee
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_8_loader_depend.py
@@ -0,0 +1,54 @@
+from typing import List
+import pytest
+from pydantic import BaseModel
+from pydantic_resolve import Resolver, LoaderDepend
+from aiodataloader import DataLoader
+
+@pytest.mark.asyncio
+async def test_loader_depends():
+    counter = {
+        "book": 0
+    }
+
+    BOOKS = {
+        1: [{'name': 'book1'}, {'name': 'book2'}],
+        2: [{'name': 'book3'}, {'name': 'book4'}],
+        3: [{'name': 'book1'}, {'name': 'book2'}],
+    }
+
+    class Book(BaseModel):
+        name: str
+
+    class BookLoader(DataLoader):
+        async def batch_load_fn(self, keys):
+            counter["book"] += 1
+            books = [[Book(**bb) for bb in BOOKS.get(k, [])] for k in keys]
+            return books
+
+    # for testing, loder instance need to initialized inside a thread with eventloop
+    # (which means it can't be put in global scope of this file)
+    # otherwise it will generate anthoer loop which will raise error of
+    # "task attached to another loop"
+
+    class Student(BaseModel):
+        id: int
+        name: str
+
+        books: List[Book] = [] 
+        def resolve_books(self, loader=LoaderDepend(BookLoader)):
+            return loader.load(self.id)
+
+    students = [Student(id=1, name="jack"), Student(id=2, name="mike"), Student(id=3, name="wiki")]
+    results = await Resolver().resolve(students)
+    source = [r.model_dump() for r in results]
+    expected = [
+        {'id': 1, 'name': 'jack', 'books': [{ 'name': 'book1'}, {'name': 'book2'}]},
+        {'id': 2, 'name': 'mike', 'books': [{ 'name': 'book3'}, {'name': 'book4'}]},
+        {'id': 3, 'name': 'wiki', 'books': [{ 'name': 'book1'}, {'name': 'book2'}]},
+    ]
+    assert source == expected
+    assert counter["book"] == 1
+
+    students2 = [Student(id=1, name="jack"), Student(id=2, name="mike"), Student(id=3, name="wiki")]
+    await Resolver().resolve(students2)
+    assert counter["book"] == 2  # called twice (means no cache)
\ No newline at end of file
diff --git a/tests/pydantic_v2/resolver/test_9_sqlalchemy_query_fix_cache.py b/tests/pydantic_v2/resolver/test_9_sqlalchemy_query_fix_cache.py
new file mode 100644
index 0000000..592c46d
--- /dev/null
+++ b/tests/pydantic_v2/resolver/test_9_sqlalchemy_query_fix_cache.py
@@ -0,0 +1,162 @@
+from typing import List
+import pytest
+from collections import Counter, defaultdict
+from typing import Tuple
+from aiodataloader import DataLoader
+from pydantic import ConfigDict, BaseModel
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import async_sessionmaker
+from sqlalchemy.ext.asyncio import create_async_engine
+from sqlalchemy.orm import DeclarativeBase
+from sqlalchemy.orm import Mapped
+from sqlalchemy.orm import mapped_column
+from pydantic_resolve import Resolver, LoaderDepend
+
+class Base(DeclarativeBase):
+    pass
+
+class Task(Base):
+    __tablename__ = "task"
+
+    id: Mapped[int] = mapped_column(primary_key=True)
+    name: Mapped[str]
+
+class Comment(Base):
+    __tablename__ = "comment"
+    id: Mapped[int] = mapped_column(primary_key=True)
+    task_id: Mapped[int] = mapped_column()
+    content: Mapped[str]
+
+class Feedback(Base):
+    __tablename__ = "feedback"
+    id: Mapped[int] = mapped_column(primary_key=True)
+    comment_id: Mapped[int] = mapped_column()
+    content: Mapped[str]
+
+@pytest.mark.asyncio
+async def test_sqlite_and_dataloader():
+    counter = Counter()
+    engine = create_async_engine(
+        "sqlite+aiosqlite://",
+        echo=False,
+    )
+    async_session = async_sessionmaker(engine, expire_on_commit=False)
+
+    async def insert_objects() -> None:
+        async with async_session() as session:
+            async with session.begin():
+                session.add_all(
+                    [
+                        Task(id=1, name="task-1"),
+                        Task(id=2, name="task-2"),
+                        Task(id=3, name="task-3"),
+
+                        Comment(id=1, task_id=1, content="comment-1 for task 1"),
+                        Comment(id=2, task_id=1, content="comment-2 for task 1"),
+                        Comment(id=3, task_id=2, content="comment-1 for task 2"),
+
+                        Feedback(id=1, comment_id=1, content="feedback-1 for comment-1"),
+                        Feedback(id=2, comment_id=1, content="feedback-1 for comment-1"),
+                        Feedback(id=3, comment_id=1, content="feedback-1 for comment-1"),
+                    ]
+                )
+
+    class FeedbackLoader(DataLoader):
+        async def batch_load_fn(self, comment_ids):
+            counter['load-feedback'] += 1
+            async with async_session() as session:
+                res = await session.execute(select(Feedback).where(Feedback.comment_id.in_(comment_ids)))
+                rows = res.scalars().all()
+                dct = defaultdict(list)
+                for row in rows:
+                    dct[row.comment_id].append(FeedbackSchema.model_validate(row))
+                return [dct.get(k, []) for k in comment_ids]
+
+    class CommentLoader(DataLoader):
+        async def batch_load_fn(self, task_ids):
+            counter['load-comment'] += 1
+            async with async_session() as session:
+                res = await session.execute(select(Comment).where(Comment.task_id.in_(task_ids)))
+                rows = res.scalars().all()
+
+                dct = defaultdict(list)
+                for row in rows:
+                    dct[row.task_id].append(CommentSchema.model_validate(row))
+                return [dct.get(k, []) for k in task_ids]
+
+    class FeedbackSchema(BaseModel):
+        id: int
+        comment_id: int
+        content: str
+        model_config = ConfigDict(from_attributes=True)
+
+    class CommentSchema(BaseModel):
+        id: int
+        task_id: int
+        content: str
+        feedbacks: List[FeedbackSchema]  = []
+
+        def resolve_feedbacks(self, loader=LoaderDepend(FeedbackLoader)):
+            return loader.load(self.id)
+        model_config = ConfigDict(from_attributes=True)
+
+    class TaskSchema(BaseModel):
+        id: int
+        name: str
+        comments: List[CommentSchema]  = []
+        
+        def resolve_comments(self, loader=LoaderDepend(CommentLoader)):
+            return loader.load(self.id)
+        model_config = ConfigDict(from_attributes=True)
+
+    async def init():
+        async with engine.begin() as conn:
+            await conn.run_sync(Base.metadata.create_all)
+
+    async def query():
+        async with async_session() as session:
+            tasks = (await session.execute(select(Task))).scalars().all()
+            task_objs = [TaskSchema.model_validate(t) for t in tasks]
+            resolved_results = await Resolver().resolve(task_objs)
+            to_dict_arr = [r.model_dump() for r in resolved_results]
+            return to_dict_arr
+
+    await init()
+    await insert_objects()
+    result = await query()
+    expected = [
+        {
+            'comments': [{'content': 'comment-1 for task 1',
+            'feedbacks': [{'comment_id': 1,
+                            'content': 'feedback-1 for comment-1',
+                            'id': 1},
+                            {'comment_id': 1,
+                            'content': 'feedback-1 for comment-1',
+                            'id': 2},
+                            {'comment_id': 1,
+                            'content': 'feedback-1 for comment-1',
+                            'id': 3}],
+            'id': 1,
+            'task_id': 1},
+                        {'content': 'comment-2 for task 1',
+                            'feedbacks': [],
+                            'id': 2,
+                            'task_id': 1}],
+            'id': 1,
+            'name': 'task-1'},
+            {'comments': [{'content': 'comment-1 for task 2',
+                            'feedbacks': [],
+                            'id': 3,
+                            'task_id': 2}],
+            'id': 2,
+            'name': 'task-2'},
+            {'comments': [], 'id': 3, 'name': 'task-3'}]
+
+    assert result == expected
+    assert counter['load-comment'] == 1  # batch_load_fn only called once
+    assert counter['load-feedback'] == 1
+    
+    await query()
+
+    assert counter['load-comment'] == 2  # Resolver + LoaderDepend can fix cache issue
+    assert counter['load-feedback'] == 2
\ No newline at end of file
diff --git a/tests/pydantic_v2/utils/test_2_ensure_subset.py b/tests/pydantic_v2/utils/test_2_ensure_subset.py
new file mode 100644
index 0000000..4f82b0f
--- /dev/null
+++ b/tests/pydantic_v2/utils/test_2_ensure_subset.py
@@ -0,0 +1,54 @@
+from dataclasses import dataclass
+from pydantic import BaseModel
+from pydantic_resolve import util
+import pytest
+
+def test_ensure_subset():
+    class Base(BaseModel):
+        a: str
+        b: int
+
+    @dataclass
+    class BaseX():
+        a: str
+        b: int
+    
+    @util.ensure_subset(Base)
+    class ChildA(BaseModel):
+        a: str
+
+    @util.ensure_subset(Base)
+    class ChildB(BaseModel):
+        a: str
+        c: int = 0
+        def resolve_c(self):
+            return 21
+
+    with pytest.raises(AttributeError):
+        @util.ensure_subset(Base)
+        class ChildC(BaseModel):
+            a: str
+            b: str
+
+    with pytest.raises(AttributeError):
+        @util.ensure_subset(Base)
+        class ChildD(BaseModel):
+            a: str
+            b: int
+            c: int
+
+    with pytest.raises(AssertionError):
+        @util.ensure_subset(Base)
+        @dataclass
+        class ChildE():
+            a: str
+            b: int
+            c: int
+
+    with pytest.raises(AssertionError):
+        @util.ensure_subset(BaseX)
+        @dataclass
+        class ChildF():
+            a: str
+            b: int
+            c: int
\ No newline at end of file
diff --git a/tests/pydantic_v2/utils/test_model_config.py b/tests/pydantic_v2/utils/test_model_config.py
new file mode 100644
index 0000000..8c7f51e
--- /dev/null
+++ b/tests/pydantic_v2/utils/test_model_config.py
@@ -0,0 +1,47 @@
+from pydantic_resolve.util import model_config
+from pydantic import BaseModel, Field
+from pydantic_resolve import Resolver
+import pytest
+
+@pytest.mark.asyncio
+async def test_schema_config():
+
+    @model_config()
+    class Y(BaseModel):
+        id: int = Field(default=0, exclude=True)
+        def resolve_id(self):
+            return 1
+
+        name: str
+
+        password: str = Field(default='', exclude=True)
+        def resolve_password(self):
+            return 'confidential'
+
+    schema = Y.model_json_schema()
+    assert list((schema['properties']).keys()) == ['name']
+
+    y = Y(name='kikodo')
+
+    y = await Resolver().resolve(y) 
+    assert y.model_dump() == {'name': 'kikodo'}
+
+
+
+@pytest.mark.asyncio
+async def test_schema_config_required():
+    @model_config(default_required=False)
+    class Y(BaseModel):
+        id: int = 0
+        def resolve_id(self):
+            return 1
+
+        name: str
+
+        password: str = ''
+        def resolve_password(self):
+            return 'confidential'
+
+    schema = Y.model_json_schema()
+    assert set(schema['required']) == {'name'}
+
diff --git a/tests/pydantic_v2/utils/test_output.py b/tests/pydantic_v2/utils/test_output.py
new file mode 100644
index 0000000..7277ed3
--- /dev/null
+++ b/tests/pydantic_v2/utils/test_output.py
@@ -0,0 +1,66 @@
+from dataclasses import dataclass
+from pydantic_resolve.util import output
+from pydantic import BaseModel
+from typing import List, Optional
+import pytest
+
+def test_output():
+    @output
+    class A(BaseModel):
+        id: int
+        opt: Optional[str] = None
+
+        name: str = ''
+        def resolve_name(self):
+            return 'hi'
+
+        age: int = 0
+        def resolve_age(self):
+            return 1
+
+        greet: str = ''
+        def post_greet(self):
+            return 'hi'
+    
+    json_schema = A.model_json_schema()
+    assert set(json_schema['required']) == {'id', 'name', 'age', 'greet'}
+
+    a = A(id=1)
+    assert a.id == 1  # can assign
+
+
+def test_output2():
+
+    with pytest.raises(AttributeError):
+        @output
+        @dataclass
+        class A():
+            name: str
+            age: int
+
+
+def test_output_3():
+    
+    class B(BaseModel):
+        id: int 
+
+    class ABase(BaseModel):
+        id: int
+
+    @output
+    class A(ABase):
+        name: List[str] = [] 
+        def resolve_name(self):
+            return ['hi']
+
+        age: int = 0
+        def resolve_age(self):
+            return 1
+        
+        bs: List[B] = []
+        def resolve_bs(self):
+            return []
+    
+    json_schema = A.model_json_schema()
+    assert set(json_schema['required']) == {'id', 'name', 'age', 'bs'}
+
diff --git a/tests/pydantic_v2/utils/test_output_2.py b/tests/pydantic_v2/utils/test_output_2.py
new file mode 100644
index 0000000..c480a9c
--- /dev/null
+++ b/tests/pydantic_v2/utils/test_output_2.py
@@ -0,0 +1,35 @@
+from dataclasses import dataclass
+from pydantic_resolve.util import output
+from pydantic import BaseModel
+from typing import List, Optional
+import pytest
+
+def test_output_will_not_affect_other():
+
+    @output
+    class X(BaseModel):
+        id: int
+        name: str
+
+    class A(BaseModel):
+        id: int
+        opt: Optional[str] = None
+
+        name: str = ''
+        def resolve_name(self):
+            return 'hi'
+
+        age: int = 0
+        def resolve_age(self):
+            return 1
+
+        greet: str = ''
+        def post_greet(self):
+            return 'hi'
+    
+    json_schema = A.model_json_schema()
+    assert set(json_schema['required']) == {'id'}
+
+    a = A(id=1)
+    assert a.id == 1  # can assign
+
diff --git a/tests/pydantic_v2/utils/test_parse.py b/tests/pydantic_v2/utils/test_parse.py
new file mode 100644
index 0000000..3750472
--- /dev/null
+++ b/tests/pydantic_v2/utils/test_parse.py
@@ -0,0 +1,81 @@
+from pydantic_resolve import util
+from typing import Optional
+from pydantic import ConfigDict, BaseModel, ValidationError
+import pytest
+from dataclasses import dataclass
+
+def test_pydantic():
+    class A(BaseModel):
+        name: Optional[str] = None
+    
+    a = A(name=None)
+    value = util.try_parse_data_to_target_field_type(a, 'name', '123')
+    assert value == '123'
+
+    value = util.try_parse_data_to_target_field_type(a, 'name', None)
+    assert value is None
+
+    with pytest.raises(ValidationError):
+        util.try_parse_data_to_target_field_type(a, 'name', [1,2,3])
+
+
+def test_dataclass():
+    @dataclass
+    class B:
+        age: int
+
+    @dataclass
+    class A:
+        b: Optional[B] 
+    
+    a = A(b=None)
+    value = util.try_parse_data_to_target_field_type(a, 'b', None)
+    assert value is None 
+
+    value = util.try_parse_data_to_target_field_type(a, 'b', {'age': 21})
+    assert value == B(age=21)
+
+    with pytest.raises(ValidationError):
+        util.try_parse_data_to_target_field_type(a, 'b', [1,2,3])
+
+def test_mix():
+    class B(BaseModel):
+        age: int
+
+    @dataclass
+    class A:
+        b: Optional[B] 
+    
+    a = A(b=None)
+    value = util.try_parse_data_to_target_field_type(a, 'b', None)
+    assert value is None 
+
+    value = util.try_parse_data_to_target_field_type(a, 'b', {'age': 21})
+    assert value == B(age=21)
+
+    with pytest.raises(ValidationError):
+        util.try_parse_data_to_target_field_type(a, 'b', [1,2,3])
+
+def test_orm():
+    class B(BaseModel):
+        age: int
+        model_config = ConfigDict(from_attributes=True)
+
+    @dataclass
+    class A:
+        b: Optional[B] 
+
+    class BB():
+        def __init__(self, age: int):
+            self.age = age
+
+    
+    a = A(b=None)
+    value = util.try_parse_data_to_target_field_type(a, 'b', None)
+    assert value is None 
+
+    value = util.try_parse_data_to_target_field_type(a, 'b', BB(age=21))
+    assert value == B(age=21)
+
+    with pytest.raises(ValidationError):
+        util.try_parse_data_to_target_field_type(a, 'b', [1,2,3])
diff --git a/tests/pydantic_v2/utils/test_parse_forward_ref.py b/tests/pydantic_v2/utils/test_parse_forward_ref.py
new file mode 100644
index 0000000..fe1a1be
--- /dev/null
+++ b/tests/pydantic_v2/utils/test_parse_forward_ref.py
@@ -0,0 +1,50 @@
+from __future__ import annotations  # which will cause config error
+from dataclasses import dataclass
+from typing import Optional
+from pydantic import ConfigDict, BaseModel, ValidationError
+import pytest
+
+def test_dc():
+    @dataclass
+    class A:
+        name: str
+
+    @dataclass
+    class B:
+        a: A
+
+    b = B(a=A(name='kikodo'))
+    assert isinstance(b, B)
+
+
+def test_parse():
+    class B(BaseModel):
+        age: int
+
+    class A(BaseModel):
+        b: Optional[B] = None 
+
+    a = A.model_validate({'b': {'age': 21}})
+    assert isinstance(a, A)
+
+def test_orm():
+    class B(BaseModel):
+        age: int
+        model_config = ConfigDict(from_attributes=True)
+
+
+    class A(BaseModel):
+        b: Optional[B] = None 
+        model_config = ConfigDict(from_attributes=True)
+    
+    class AA:
+        def __init__(self, b):
+            self.b = b
+    class BB:
+        def __init__(self, age):
+            self.age = age
+    
+    aa = AA(b=BB(age=21))
+
+    a = A.model_validate(aa)
+    assert isinstance(a, A)
diff --git a/tests/pydantic_v2/utils/test_utils.py b/tests/pydantic_v2/utils/test_utils.py
new file mode 100644
index 0000000..67c2f24
--- /dev/null
+++ b/tests/pydantic_v2/utils/test_utils.py
@@ -0,0 +1,175 @@
+import asyncio
+from dataclasses import dataclass
+from typing import List
+from pydantic_resolve import util
+from pydantic import ConfigDict, BaseModel
+import pytest
+
+def test_get_class_field_annotations():
+    class C:
+        hello: str
+
+        def __init__(self, c: str):
+            self.c = c
+        
+    class D(C):
+        pass
+
+    class E(C):
+        world: str
+    
+    assert list(util.get_class_field_annotations(C)) == ['hello']
+    assert list(util.get_class_field_annotations(D)) == []
+    assert list(util.get_class_field_annotations(E)) == ['world']
+
+
+class User(BaseModel):
+    id: int
+    name: str
+    age: int
+
+
+def test_build_object():
+    raw = [(1, 'peter', 10), (2, 'mike', 21), (3, 'john', 12)]
+    users = [User(id=i[0], name=i[1], age=i[2]) for i in raw]
+    a, b, c = users
+    ids = [2, 3, 1, 4]
+    output = util.build_object(users, ids, lambda x: x.id)
+    assert list(output) == [b, c, a, None]
+    
+
+def test_build_list():
+    raw = [(1, 'peter', 10), (2, 'mike', 21), (3, 'john', 12)]
+    users = [User(id=i[0], name=i[1], age=i[2]) for i in raw]
+    a, b, c = users
+    ids = [2, 3, 1, 4]
+    output = util.build_list(users, ids, lambda x: x.id)
+    assert list(output) == [[b], [c], [a], []]
+
+@pytest.mark.asyncio
+async def test_replace_method():
+    class A():
+        def __init__(self, name: str):
+            self.name = name
+
+        async def say(self, arr: List[str]):
+            return f'{self.name}, {len(arr)}'
+
+    a = A('kikodo')
+    r1 = await a.say(['1'])
+    assert r1 == 'kikodo, 1'
+
+    async def kls_method(self, *args):
+        v = await A.say(self, *args)
+        return f'hello, {v}'
+
+    AA = util.replace_method(A, 'AA', 'say', kls_method)
+    k = AA('kimi')
+    r2 = await k.say(['1', '2', '3'])
+
+    assert r2 == 'hello, kimi, 3'
+    assert AA.__name__ == 'AA'
+
+
+def test_super_logic():
+    class A():
+        def say(self):
+            return 'A'
+    
+    class B(A):
+        def say(self):
+            val = A().say()
+            return f'B.{val}'
+    
+
+    b = B()
+    assert b.say() == 'B.A'
+
+
+
+@pytest.mark.asyncio
+async def test_mapper_1():
+    class A(BaseModel):
+        a: int
+
+    @util.mapper(lambda x: A(**x))
+    async def foo():
+        return {'a': 1}
+
+    async def call_later(f):
+        await asyncio.sleep(1)
+        f.set_result({'a': 1})
+
+    @util.mapper(lambda x: A(**x))
+    async def bar():
+        lp = asyncio.get_event_loop()
+        f = lp.create_future()
+        asyncio.create_task(call_later(f))
+        return f
+    
+    ret = await foo()
+    ret2 = await bar()
+    assert ret == A(a=1)
+    assert ret2 == A(a=1)
+
+
+def test_auto_mapper_1():
+    class A(BaseModel):
+        a: int
+    
+    params = (A, {'a': 1})
+    ret = util._get_mapping_rule(*params)(*params)  # type:ignore
+    assert ret == A(a=1)
+    
+    @dataclass
+    class B:
+        a: int
+
+    params2 = (B, {'a': 1})
+    ret2 = util._get_mapping_rule(*params2)(*params2) # type:ignore
+    assert ret2 == B(a=1)
+
+
+def test_auto_mapper_2():
+    class A(BaseModel):
+        a: int
+        model_config = ConfigDict(from_attributes=True)
+    
+    class AA:
+        def __init__(self, a):
+            self.a = a
+    
+    p1 = (A, AA(1))
+    ret = util._get_mapping_rule(*p1)(*p1)  # type: ignore
+    assert ret == A(a=1)
+
+    p2 = (A, {'a': 1})
+    with pytest.raises(AttributeError):
+        util._get_mapping_rule(*p2)  # type: ignore
+    
+
+def test_auto_mapper_3():
+    class A(BaseModel):
+        a: int
+        model_config = ConfigDict(from_attributes=True)
+    
+    p1 = (A, A(a=1))
+    rule = util._get_mapping_rule(*p1)  # type: ignore
+
+    assert rule is None
+    output = util._apply_rule(rule, *p1, is_list=False)
+    assert output == A(a=1)
+
+
+def test_auto_mapper_4():
+    class A(BaseModel):
+        a: int
+
+    class AA:
+        def __init__(self, a):
+            self.a = a
+    
+    p1 = (A, AA(a=1))
+    with pytest.raises(AttributeError):
+        util._get_mapping_rule(*p1)(*p1)  # type: ignore
+