type-less 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ on:
2
+ release:
3
+ types: [published]
4
+
5
+
6
+ jobs:
7
+ pypi-publish:
8
+ name: Upload release to PyPI
9
+ runs-on: ubuntu-latest
10
+ environment:
11
+ name: pypi
12
+ url: https://github.com
13
+ permissions:
14
+ contents: read
15
+ id-token: write
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: pdm-project/setup-pdm@v4
19
+ - name: Publish package distributions to PyPI
20
+ run: pdm publish
@@ -0,0 +1,28 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: Set up Python 3.12
17
+ uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.12"
20
+
21
+ - name: Install dependencies
22
+ run: |
23
+ python -m pip install uv
24
+ uv sync --dev
25
+
26
+ - name: Run tests
27
+ run: |
28
+ uv run pytest
@@ -0,0 +1,8 @@
1
+ .env
2
+ node_modules
3
+ dist
4
+ .venv
5
+ .pytest_cache
6
+ .python-version
7
+ .vite
8
+ __pycache__
@@ -0,0 +1 @@
1
+ /home/runner/work/type-less/type-less/.venv/bin/python
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: type-less
3
+ Version: 0.1.1
4
+ Summary: Type less types with inferred return types
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+
8
+ # Type-less
9
+
10
+ Type less with automatic type inference for Python! Inject function return types at runtime for code generation.
11
+
12
+ ## FastAPI Example
13
+
14
+ ```python
15
+ @app.get("/user/me")
16
+ def user_me():
17
+ return {
18
+ "id": 1,
19
+ "balance": 13.37,
20
+ "name": {
21
+ "first": "Testy",
22
+ "last": "McTestFace",
23
+ },
24
+ }
25
+ ```
26
+
27
+ 🚀 Generates Return Types:
28
+
29
+ ```python
30
+ class UserMeReturnName(TypedDict):
31
+ first: str
32
+ last: str
33
+
34
+ class UserMeReturn(TypedDict):
35
+ id: str
36
+ balance: str
37
+ name: UserMeReturnName
38
+ ```
39
+
40
+ 📋 OpenAPI Spec:
41
+
42
+ <img src="docs/example.png" alt="OpenAPI Example" height="170">
43
+
44
+ ## Using in FastAPI Project
45
+
46
+ Inject types before by hooking before setting up routes. Types will be automatically generated when new routes are added.
47
+
48
+ ```python
49
+ from type_less.inject import inject_fastapi_route_types
50
+ from fastapi import FastAPI
51
+
52
+ app = FastAPI()
53
+ app = inject_fastapi_route_types(app)
54
+
55
+ @app.get("/test")
56
+ def test(request):
57
+ ...
58
+ ```
59
+
60
+ ## TODO:
61
+ ### Add Support:
62
+ * Nested class inference
63
+ * Deep function call / return inference
64
+ ### Better Way?:
65
+ * Possibly use pyre, mypy, or anything else to infer the type?
@@ -0,0 +1,58 @@
1
+ # Type-less
2
+
3
+ Type less with automatic type inference for Python! Inject function return types at runtime for code generation.
4
+
5
+ ## FastAPI Example
6
+
7
+ ```python
8
+ @app.get("/user/me")
9
+ def user_me():
10
+ return {
11
+ "id": 1,
12
+ "balance": 13.37,
13
+ "name": {
14
+ "first": "Testy",
15
+ "last": "McTestFace",
16
+ },
17
+ }
18
+ ```
19
+
20
+ 🚀 Generates Return Types:
21
+
22
+ ```python
23
+ class UserMeReturnName(TypedDict):
24
+ first: str
25
+ last: str
26
+
27
+ class UserMeReturn(TypedDict):
28
+ id: str
29
+ balance: str
30
+ name: UserMeReturnName
31
+ ```
32
+
33
+ 📋 OpenAPI Spec:
34
+
35
+ <img src="docs/example.png" alt="OpenAPI Example" height="170">
36
+
37
+ ## Using in FastAPI Project
38
+
39
+ Inject types before by hooking before setting up routes. Types will be automatically generated when new routes are added.
40
+
41
+ ```python
42
+ from type_less.inject import inject_fastapi_route_types
43
+ from fastapi import FastAPI
44
+
45
+ app = FastAPI()
46
+ app = inject_fastapi_route_types(app)
47
+
48
+ @app.get("/test")
49
+ def test(request):
50
+ ...
51
+ ```
52
+
53
+ ## TODO:
54
+ ### Add Support:
55
+ * Nested class inference
56
+ * Deep function call / return inference
57
+ ### Better Way?:
58
+ * Possibly use pyre, mypy, or anything else to infer the type?
Binary file
@@ -0,0 +1,22 @@
1
+ [project]
2
+ name = "type-less"
3
+ version = "0.1.1"
4
+ description = "Type less types with inferred return types"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = []
8
+
9
+ [build-system]
10
+ requires = ["hatchling"]
11
+ build-backend = "hatchling.build"
12
+
13
+ [tool.pytest.ini_options]
14
+ addopts = [
15
+ "--import-mode=importlib",
16
+ ]
17
+
18
+ [dependency-groups]
19
+ dev = [
20
+ "fastapi>=0.115.11",
21
+ "pytest>=8.3.5",
22
+ ]
@@ -0,0 +1,4 @@
1
+ from .inference import guess_return_type
2
+ from .inject import fill_type_hints
3
+
4
+ __all__ = ["fill_type_hints", "guess_return_type"]
@@ -0,0 +1,379 @@
1
+ from __future__ import annotations
2
+ import ast
3
+ import inspect
4
+ from typing import (
5
+ Any,
6
+ Callable,
7
+ Dict,
8
+ Literal,
9
+ Optional,
10
+ Tuple,
11
+ TypedDict,
12
+ Type,
13
+ Union,
14
+ )
15
+ import textwrap
16
+
17
+
18
+ def _snake_case_to_capital_case(name: str) -> str:
19
+ return "".join(word.capitalize() for word in name.split("_"))
20
+
21
+
22
+ def _sanitize_name(name: str) -> str:
23
+ return "".join(c if c.isalnum() else "" for c in name)
24
+
25
+
26
+ def _get_module_type(func: Callable, name: str) -> Type:
27
+ import sys
28
+
29
+ # TODO: support types not at the root
30
+ module = sys.modules.get(func.__module__, None)
31
+ if hasattr(module, name):
32
+ result = getattr(module, name)
33
+ if isinstance(result, type):
34
+ return result
35
+ elif isinstance(result, Callable):
36
+ return guess_return_type(result)
37
+
38
+ return Any
39
+
40
+
41
+ def guess_return_type(func: Callable, use_literals=True) -> Type:
42
+ """
43
+ Infer the return type of a Python function by analyzing its AST.
44
+ For dictionary returns, creates a TypedDict representation.
45
+
46
+ Args:
47
+ func: The function to analyze
48
+
49
+ Returns:
50
+ The inferred return type
51
+ """
52
+
53
+ # If annotations exist, return the return type
54
+ if hasattr(func, "__annotations__") and "return" in func.__annotations__:
55
+ return func.__annotations__["return"]
56
+
57
+ # Get function source code and create AST
58
+ try:
59
+ source = inspect.getsource(func)
60
+ source = textwrap.dedent(source)
61
+ except Exception:
62
+ return Any
63
+
64
+ module = ast.parse(source)
65
+
66
+ # Extract the function definition node
67
+ func_def = module.body[0]
68
+ if not isinstance(func_def, (ast.FunctionDef, ast.AsyncFunctionDef)):
69
+ raise ValueError("Input is not a function definition")
70
+
71
+ # Create a symbol table for type analysis
72
+ symbol_table = {}
73
+
74
+ # Populate the symbol table with type hints from function annotations
75
+ if func_def.returns:
76
+ # If function has a return type annotation, use it directly
77
+ return _resolve_annotation(func_def.returns, {}, func)
78
+
79
+ # Gather type information from annotations and assignments
80
+ _analyze_function_body(func_def, symbol_table, func, use_literals)
81
+
82
+ # Find all return statements
83
+ return_types = []
84
+ for node in ast.walk(func_def):
85
+ if isinstance(node, ast.Return) and node.value:
86
+ return_type = _infer_expr_type(node.value, symbol_table, func, [], use_literals)
87
+ return_types.append(return_type)
88
+
89
+ # If we found return statements
90
+ if return_types:
91
+ if len(return_types) == 1:
92
+ return return_types[0]
93
+ else:
94
+ # Multiple return types - use Union
95
+ return Union[tuple(set(return_types))]
96
+
97
+ # Default to Any if no return statements or couldn't infer
98
+ return Any
99
+
100
+
101
+ def _analyze_function_body(
102
+ func_def: ast.FunctionDef, symbol_table: dict[str, Type], func: Callable, use_literals: bool
103
+ ) -> None:
104
+ """Analyze function body to populate symbol table with type information"""
105
+ # First gather parameter types
106
+ for arg in func_def.args.args:
107
+ if arg.annotation:
108
+ symbol_table[arg.arg] = _resolve_annotation(arg.annotation, {}, func)
109
+
110
+ # Analyze assignments to track variable types
111
+ for node in ast.walk(func_def):
112
+ if isinstance(node, ast.Assign):
113
+ assigned_type = _infer_expr_type(node.value, symbol_table, func, [], use_literals)
114
+ for target in node.targets:
115
+ if isinstance(target, ast.Name):
116
+ symbol_table[target.id] = assigned_type
117
+ elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
118
+ # Handle annotated assignments
119
+ symbol_table[node.target.id] = _resolve_annotation(
120
+ node.annotation, {}, func
121
+ )
122
+
123
+
124
+ def _resolve_annotation(
125
+ annotation: ast.AST, type_context: dict[str, Any], func: Callable
126
+ ) -> Type:
127
+ """Resolve a type annotation AST node to a real type"""
128
+ if isinstance(annotation, ast.Name):
129
+ # Simple types like int, str, etc.
130
+ type_name = annotation.id
131
+ # Check Python's built-in types first
132
+ if type_name in __builtins__:
133
+ return __builtins__[type_name]
134
+
135
+ print(annotation.id)
136
+
137
+ # Otherwise check if it's imported
138
+ return _get_module_type(func, type_name)
139
+
140
+ elif isinstance(annotation, ast.Subscript):
141
+ # Handle generic types like list[int], dict[str, int], etc.
142
+ if isinstance(annotation.value, ast.Name):
143
+ base_type = annotation.value.id
144
+ if base_type == "List" or base_type == "list":
145
+ elem_type = _resolve_annotation(annotation.slice, type_context, func)
146
+ return list[elem_type]
147
+ elif base_type == "Dict" or base_type == "dict":
148
+ if isinstance(annotation.slice, ast.Tuple):
149
+ key_type = _resolve_annotation(
150
+ annotation.slice.elts[0], type_context, func
151
+ )
152
+ val_type = _resolve_annotation(
153
+ annotation.slice.elts[1], type_context, func
154
+ )
155
+ return dict[key_type, val_type]
156
+ return Dict
157
+ elif base_type == "Set" or base_type == "set":
158
+ elem_type = _resolve_annotation(annotation.slice, type_context, func)
159
+ return set[elem_type]
160
+ elif base_type == "Tuple" or base_type == "tuple":
161
+ if isinstance(annotation.slice, ast.Tuple):
162
+ elem_types = [
163
+ _resolve_annotation(e, type_context, func)
164
+ for e in annotation.slice.elts
165
+ ]
166
+ return tuple[tuple(elem_types)]
167
+ return Tuple
168
+ elif base_type == "Optional":
169
+ elem_type = _resolve_annotation(annotation.slice, type_context, func)
170
+ return Optional[elem_type]
171
+ elif base_type == "Union":
172
+ if isinstance(annotation.slice, ast.Tuple):
173
+ elem_types = [
174
+ _resolve_annotation(e, type_context, func)
175
+ for e in annotation.slice.elts
176
+ ]
177
+ return Union[tuple(elem_types)]
178
+ return Union
179
+
180
+ # Fallback for unresolved or complex annotations
181
+ print("FAIL3")
182
+ return Any
183
+
184
+
185
+ def _infer_expr_type(
186
+ node: ast.AST, symbol_table: dict[str, Type], func: Callable, nested_path: list[str], use_literals: bool
187
+ ) -> Type:
188
+ """Infer the type of an expression"""
189
+ if isinstance(node, ast.Dict):
190
+ # For dictionary literals, create a TypedDict
191
+ return _create_typed_dict_from_dict(node, symbol_table, func, nested_path, use_literals)
192
+
193
+ elif isinstance(node, ast.List):
194
+ # Handle list literals
195
+ if not node.elts:
196
+ return list[Any]
197
+ element_types = [
198
+ _infer_expr_type(elt, symbol_table, func, nested_path, use_literals) for elt in node.elts
199
+ ]
200
+ if len(set(element_types)) == 1:
201
+ return list[element_types[0]]
202
+ return list[Union[tuple(set(element_types))]]
203
+
204
+ elif isinstance(node, ast.Tuple):
205
+ # Handle tuple literals
206
+ if not node.elts:
207
+ return tuple[()]
208
+ element_types = [
209
+ _infer_expr_type(elt, symbol_table, func, nested_path, use_literals) for elt in node.elts
210
+ ]
211
+ return tuple[tuple(element_types)]
212
+
213
+ elif isinstance(node, ast.Set):
214
+ # Handle set literals
215
+ if not node.elts:
216
+ return set[Any]
217
+ element_types = [
218
+ _infer_expr_type(elt, symbol_table, func, nested_path, use_literals) for elt in node.elts
219
+ ]
220
+ if len(set(element_types)) == 1:
221
+ return set[element_types[0]]
222
+ return set[Union[tuple(set(element_types))]]
223
+
224
+ elif isinstance(node, ast.Constant):
225
+ # Handle literals
226
+ if use_literals:
227
+ return Literal[node.value]
228
+ else:
229
+ return type(node.value)
230
+
231
+ elif isinstance(node, ast.Name):
232
+ # Look up variable types in the symbol table
233
+ if node.id in symbol_table:
234
+ return symbol_table[node.id]
235
+
236
+ # Handle built-in types referenced by name
237
+ if node.id in __builtins__ and isinstance(__builtins__[node.id], type):
238
+ return __builtins__[node.id]
239
+
240
+ return _get_module_type(func, node.id)
241
+
242
+ elif isinstance(node, ast.Call):
243
+ # Handle function calls - this is complex, so we'll use a simplified approach
244
+ if isinstance(node.func, ast.Name):
245
+ func_name = node.func.id
246
+ # Handle some common built-in functions
247
+ if func_name == "int":
248
+ return int
249
+ elif func_name == "str":
250
+ return str
251
+ elif func_name == "float":
252
+ return float
253
+ elif func_name == "list":
254
+ return list[Any]
255
+ elif func_name == "dict":
256
+ return dict[Any, Any]
257
+ elif func_name == "set":
258
+ return set[Any]
259
+ elif func_name == "tuple":
260
+ return Tuple
261
+
262
+ return _get_module_type(func, func_name)
263
+
264
+ # For other function calls, we default to Any
265
+ return Any
266
+
267
+ elif isinstance(node, ast.BinOp):
268
+ # Handle binary operations
269
+ left_type = _infer_expr_type(node.left, symbol_table, func, nested_path, use_literals)
270
+ right_type = _infer_expr_type(node.right, symbol_table, func, nested_path, use_literals)
271
+
272
+ # String concatenation
273
+ if isinstance(node.op, ast.Add) and (left_type == str or right_type == str):
274
+ return str
275
+
276
+ # Numeric operations typically return numeric types
277
+ if isinstance(node.op, (ast.Add, ast.Sub, ast.Mult, ast.Div)):
278
+ if left_type == float or right_type == float:
279
+ return float
280
+ return int
281
+
282
+ return Any
283
+
284
+ elif isinstance(node, ast.Compare):
285
+ return bool
286
+
287
+ elif isinstance(node, ast.IfExp):
288
+ body_type = _infer_expr_type(node.body, symbol_table, func, nested_path, use_literals)
289
+ orelse_type = _infer_expr_type(node.orelse, symbol_table, func, nested_path, use_literals)
290
+ return body_type | orelse_type
291
+
292
+ elif isinstance(node, ast.Attribute):
293
+ # Handle attribute access (e.g., obj.attr)
294
+ if isinstance(node.value, ast.Name) and node.value.id in symbol_table:
295
+ print(f"TRYYYY {node.value.id}")
296
+ # Get the type of the object
297
+ obj_type = symbol_table[node.value.id]
298
+ print(obj_type)
299
+ print(type(obj_type))
300
+
301
+ # If the object has type annotations, try to get the attribute type
302
+ if hasattr(obj_type, "__annotations__") and node.attr in obj_type.__annotations__:
303
+ return obj_type.__annotations__[node.attr]
304
+
305
+ # If the object is a class with class variables
306
+ if isinstance(obj_type, type):
307
+ if hasattr(obj_type, node.attr):
308
+ attr_value = getattr(obj_type, node.attr)
309
+ # If it's a Literal type or other type annotation
310
+ if hasattr(attr_value, "__origin__") and attr_value.__origin__ is Literal:
311
+ return attr_value
312
+ # For regular attributes, infer their type
313
+ return type(attr_value)
314
+
315
+ # For other attribute access, default to Any
316
+ print("FAIL3,5")
317
+ return Any
318
+
319
+ # Default for complex or unknown expressions
320
+ print("FAIL4")
321
+ print(node)
322
+ print(type(node))
323
+ return Any
324
+
325
+
326
+ def _create_typed_dict_from_dict(
327
+ dict_node: ast.Dict,
328
+ symbol_table: dict[str, Type],
329
+ func: Callable,
330
+ nested_path: list[str],
331
+ use_literals: bool,
332
+ ) -> Type:
333
+ """Create a TypedDict from a dictionary literal"""
334
+ # Check if all keys are string literals
335
+ field_types = {}
336
+ is_valid_typeddict = True
337
+
338
+ for i, key in enumerate(dict_node.keys):
339
+ if isinstance(key, ast.Constant) and isinstance(key.value, str):
340
+ value_type = _infer_expr_type(
341
+ dict_node.values[i], symbol_table, func, nested_path + [key.value], use_literals
342
+ )
343
+ field_types[key.value] = value_type
344
+ else:
345
+ is_valid_typeddict = False
346
+ break
347
+
348
+ if is_valid_typeddict and field_types:
349
+ # Create a dynamic TypedDict class
350
+ # Capitalize function name and remove underscores
351
+ class_name = f"{_snake_case_to_capital_case(func.__name__)}Return"
352
+
353
+ # Add nested path components
354
+ for component in nested_path:
355
+ class_name += _snake_case_to_capital_case(_sanitize_name(component))
356
+ return TypedDict(class_name, field_types)
357
+
358
+ # If not a valid TypedDict, return a regular Dict with inferred types
359
+ if dict_node.keys:
360
+ key_types = [
361
+ _infer_expr_type(key, symbol_table, func, nested_path + [key], use_literals)
362
+ for key in dict_node.keys
363
+ ]
364
+ value_types = [
365
+ _infer_expr_type(value, symbol_table, func, nested_path, use_literals)
366
+ for value in dict_node.values
367
+ ]
368
+
369
+ # Determine common types
370
+ if len(set(key_types)) == 1 and len(set(value_types)) == 1:
371
+ return dict[key_types[0], value_types[0]]
372
+ elif len(set(key_types)) == 1:
373
+ return dict[key_types[0], Union[tuple(set(value_types))]]
374
+ elif len(set(value_types)) == 1:
375
+ return dict[Union[tuple(set(key_types))], value_types[0]]
376
+ else:
377
+ return dict[Union[tuple(set(key_types))], Union[tuple(set(value_types))]]
378
+
379
+ return dict[Any, Any]
@@ -0,0 +1,40 @@
1
+ from .inference import guess_return_type
2
+ from functools import wraps
3
+ from typing import Callable, TypeVar
4
+
5
+
6
+ def fill_type_hints(func: Callable, use_literals=False):
7
+ if not getattr(func, "__annotations__", None):
8
+ func.__annotations__ = {}
9
+
10
+ if not "return" in func.__annotations__:
11
+ func.__annotations__["return"] = guess_return_type(func, use_literals=use_literals)
12
+
13
+
14
+ T = TypeVar("T")
15
+ def fastapi_app_inject_types(app: T, use_literals=False) -> T:
16
+ """
17
+ Auto-injects return types into FastAPI untyped routes.
18
+
19
+ Can be run before or after route initialization
20
+ Arguments:
21
+ app: FastAPI Application
22
+ """
23
+ routes = getattr(app, 'routes', None)
24
+ if not type(routes) is list:
25
+ raise ValueError("Invalid app provided, no routes found. Please be sure this is a FastAPI app.")
26
+ for route in routes:
27
+ endpoint = getattr(route, "endpoint", None)
28
+ if not endpoint:
29
+ raise ValueError(f"Route {route} is missing an endpoint function")
30
+ fill_type_hints(endpoint, use_literals=use_literals)
31
+
32
+ # Auto-fill type hings
33
+ app_add_route = app.router.add_api_route
34
+ @wraps(app_add_route)
35
+ def add_route_injected(path: str, func, *args, **kwargs):
36
+ fill_type_hints(func, use_literals=use_literals)
37
+ app_add_route(path=path, endpoint=func, *args, **kwargs)
38
+ app.router.add_api_route = add_route_injected
39
+
40
+ return app
@@ -0,0 +1,185 @@
1
+ from typing import get_type_hints, get_origin, get_args, Literal, TypeVar, Union
2
+ import types
3
+
4
+ def is_equivalent_type(type1, type2):
5
+ """
6
+ Determine if two Python types are equivalent, handling complex nested types.
7
+
8
+ This function compares two types for equivalence, including:
9
+ - Basic types (int, str, etc.)
10
+ - Generic types (List, Dict, etc.)
11
+ - Union types (Union[int, str], Optional[int], etc.)
12
+ - TypedDict types with nested structure
13
+ - Literal types
14
+ - TypeVar with constraints
15
+ - ForwardRef types
16
+ - Callable types
17
+
18
+ Args:
19
+ type1: First type to compare
20
+ type2: Second type to compare
21
+
22
+ Returns:
23
+ bool: True if types are equivalent, False otherwise
24
+ """
25
+ # Handle None type
26
+ if type1 is None and type2 is None:
27
+ return True
28
+
29
+ # Handle direct equality (same type object)
30
+ if type1 is type2:
31
+ return True
32
+
33
+ # Get origin types (for generics)
34
+ origin1 = get_origin(type1)
35
+ origin2 = get_origin(type2)
36
+
37
+ # If one has origin and other doesn't, they're not equivalent
38
+ if (origin1 is None) != (origin2 is None):
39
+ return False
40
+
41
+ # Special case for Optional and Union
42
+ if {origin1, origin2} <= {Union, types.UnionType}:
43
+ # Handle Optional[T] == Union[T, None]
44
+ args1 = get_args(type1)
45
+ args2 = get_args(type2)
46
+
47
+ # Check if one is Optional (Union with None)
48
+ has_none1 = type(None) in args1
49
+ has_none2 = type(None) in args2
50
+
51
+ if has_none1 != has_none2:
52
+ return False
53
+
54
+ # Compare non-None args
55
+ non_none_args1 = [arg for arg in args1 if arg is not type(None)]
56
+ non_none_args2 = [arg for arg in args2 if arg is not type(None)]
57
+
58
+ if len(non_none_args1) != len(non_none_args2):
59
+ return False
60
+
61
+ # For Union, order doesn't matter
62
+ args2_remaining = set(non_none_args2)
63
+ for arg1 in non_none_args1:
64
+ for arg2 in args2_remaining:
65
+ if is_equivalent_type(arg1, arg2):
66
+ args2_remaining.remove(arg2)
67
+ break
68
+ else:
69
+ return False
70
+ return True
71
+
72
+ # Get arguments of generic types
73
+ args1 = get_args(type1)
74
+ args2 = get_args(type2)
75
+
76
+ # If number of args differs, types are not equivalent
77
+ if len(args1) != len(args2):
78
+ return False
79
+
80
+ # Special handling for TypedDict
81
+ if hasattr(type1, "__annotations__") and hasattr(type2, "__annotations__"):
82
+ # Check if both are TypedDict
83
+ if hasattr(type1, "__total__") and hasattr(type2, "__total__"):
84
+ # Check if totality is the same
85
+ if type1.__total__ != type2.__total__:
86
+ return False
87
+
88
+ # Get annotations
89
+ annotations1 = get_type_hints(type1)
90
+ annotations2 = get_type_hints(type2)
91
+
92
+ # Check if keys match
93
+ if set(annotations1.keys()) != set(annotations2.keys()):
94
+ return False
95
+
96
+ # Check if field types match
97
+ return all(is_equivalent_type(annotations1[key], annotations2[key]) for key in annotations1)
98
+
99
+ # Handle Literal
100
+ if origin1 is Literal and origin2 is Literal:
101
+ # For Literal, order doesn't matter but values must be identical
102
+ return set(args1) == set(args2)
103
+
104
+ # Handle Callable
105
+ if origin1 in {types.FunctionType, callable} and origin2 in {types.FunctionType, callable}:
106
+ if not args1 or not args2:
107
+ return True # Callable without specified signature
108
+
109
+ if len(args1) != 2 or len(args2) != 2:
110
+ return False
111
+
112
+ # Compare parameter types
113
+ params1, return1 = args1
114
+ params2, return2 = args2
115
+
116
+ # Handle Ellipsis in parameters
117
+ if params1 is Ellipsis or params2 is Ellipsis:
118
+ return is_equivalent_type(return1, return2)
119
+
120
+ # If parameter counts differ, not equivalent
121
+ if len(params1) != len(params2):
122
+ return False
123
+
124
+ # Check parameters and return type
125
+ return all(is_equivalent_type(p1, p2) for p1, p2 in zip(params1, params2)) and \
126
+ is_equivalent_type(return1, return2)
127
+
128
+ # Handle basic types
129
+ if origin1 is None and origin2 is None:
130
+ if isinstance(type1, type) and isinstance(type2, type):
131
+ return type1 is type2 or type1 == type2
132
+
133
+ # Handle TypeVar
134
+ if isinstance(type1, TypeVar) and isinstance(type2, TypeVar):
135
+ return (type1.__name__ == type2.__name__ and
136
+ type1.__constraints__ == type2.__constraints__ and
137
+ type1.__bound__ == type2.__bound__ and
138
+ type1.__covariant__ == type2.__covariant__ and
139
+ type1.__contravariant__ == type2.__contravariant__)
140
+
141
+ # For other generic types, check if all arguments are equivalent
142
+ # For tuples, order matters
143
+ return all(is_equivalent_type(arg1, arg2) for arg1, arg2 in zip(args1, args2))
144
+
145
+
146
+
147
+ def validate_openapi_has_return_schema(openapi_spec: dict, path: str, method: Literal["get", "post", "put", "delete"]) -> bool:
148
+ """
149
+ Validates that the OpenAPI specification for a given endpoint matches the expected return type.
150
+
151
+ Args:
152
+ openapi_spec: The OpenAPI specification dictionary
153
+ path: The API endpoint path
154
+ method: The HTTP method (get, post, put)
155
+ expected_type: The expected return type to validate against
156
+
157
+ Returns:
158
+ bool: True if the OpenAPI schema matches the expected type, False otherwise
159
+ """
160
+ # Check if the OpenAPI spec exists
161
+ if not openapi_spec or "paths" not in openapi_spec:
162
+ return False
163
+
164
+ # Check if the path exists in the spec
165
+ if path not in openapi_spec["paths"]:
166
+ return False
167
+
168
+ # Check if the method exists for the path
169
+ path_spec = openapi_spec["paths"][path]
170
+ if method not in path_spec:
171
+ return False
172
+
173
+ # Get the response schema
174
+ method_spec = path_spec[method]
175
+ if "responses" not in method_spec or "200" not in method_spec["responses"]:
176
+ return False
177
+
178
+ response_spec = method_spec["responses"]["200"]
179
+ if "content" not in response_spec or "application/json" not in response_spec["content"]:
180
+ return False
181
+
182
+ schema = response_spec["content"]["application/json"].get("schema") or {}
183
+
184
+ # For now, just check if schema exists
185
+ return schema.get("$ref") is not None
@@ -0,0 +1,25 @@
1
+ from fastapi import FastAPI
2
+ from type_less.inject import fastapi_app_inject_types
3
+ from .matching import validate_openapi_has_return_schema
4
+
5
+ def test_fastapi_basic():
6
+ app = FastAPI()
7
+
8
+ @app.get("/root/before")
9
+ async def root_before():
10
+ return {"message": "Hello World"}
11
+
12
+ injected_app = fastapi_app_inject_types(app)
13
+
14
+ @app.get("/root/after")
15
+ async def root_after():
16
+ return {"message": "Hello World"}
17
+
18
+ assert type(app) == type(injected_app)
19
+
20
+ # Validate generated OpenAPI
21
+ openapi_spec = app.openapi()
22
+
23
+ assert openapi_spec is not None
24
+ assert not validate_openapi_has_return_schema(openapi_spec, "/root/before", "get")
25
+ assert validate_openapi_has_return_schema(openapi_spec, "/root/after", "get")
@@ -0,0 +1,138 @@
1
+ from type_less.inference import guess_return_type
2
+ from typing import TypedDict, Literal, Union
3
+ from .matching import is_equivalent_type
4
+
5
+
6
+ def test_guess_return_type_dict():
7
+ def func():
8
+ return {"key": "value"}
9
+
10
+ assert guess_return_type(func) == dict
11
+
12
+
13
+ def test_guess_return_type_list():
14
+ def func():
15
+ return [1, 2, 3]
16
+
17
+ assert guess_return_type(func, use_literals=False) == list[int]
18
+
19
+
20
+ def test_guess_return_type_list_literals():
21
+ def func():
22
+ return [1, 2, 3]
23
+
24
+ assert guess_return_type(func, use_literals=True) == list[Union[Literal[1], Literal[2], Literal[3]]]
25
+
26
+
27
+ def test_guess_return_type_string():
28
+ def func():
29
+ return "hello world"
30
+
31
+ assert guess_return_type(func, use_literals=False) == str
32
+
33
+
34
+ def test_guess_return_type_int():
35
+ def func():
36
+ return 42
37
+
38
+ assert guess_return_type(func, use_literals=False) == int
39
+
40
+
41
+ def test_guess_return_type_float():
42
+ def func():
43
+ return 3.14
44
+
45
+ assert guess_return_type(func, use_literals=False) == float
46
+
47
+
48
+ def test_guess_return_type_bool():
49
+ def func():
50
+ return True
51
+
52
+ assert guess_return_type(func, use_literals=False) == bool
53
+
54
+
55
+ def test_guess_return_type_none():
56
+ def func():
57
+ return None
58
+
59
+ assert guess_return_type(func, use_literals=False) == type(None)
60
+
61
+
62
+ def test_guess_return_type_multiple_returns():
63
+ def func(x):
64
+ if x > 0:
65
+ return "positive"
66
+ else:
67
+ return "negative"
68
+
69
+ assert guess_return_type(func) == Literal["positive"] | Literal["negative"]
70
+
71
+ def test_guess_return_type_dict():
72
+ def func(x):
73
+ return {
74
+ "name": "tester",
75
+ "age": 123,
76
+ }
77
+
78
+ class FuncReturn(TypedDict):
79
+ name: str
80
+ age: int
81
+
82
+ assert is_equivalent_type(guess_return_type(func, use_literals=False), FuncReturn)
83
+
84
+
85
+ def test_guess_return_type_complex_fuzzy():
86
+ def func(x):
87
+ if x > 10:
88
+ return {"result": "large"}
89
+ elif x > 0:
90
+ return {"result": "small"}
91
+ else:
92
+ return {"result": "negative"}
93
+
94
+ class FuncReturn1(TypedDict):
95
+ result: str
96
+ class FuncReturn2(TypedDict):
97
+ result: str
98
+ class FuncReturn3(TypedDict):
99
+ result: str
100
+
101
+ assert is_equivalent_type(guess_return_type(func, use_literals=False), Union[FuncReturn1, FuncReturn2, FuncReturn3])
102
+
103
+
104
+ def test_guess_return_type_complex_literals():
105
+ def func(x):
106
+ if x > 10:
107
+ return {"result": "large"}
108
+ elif x > 0:
109
+ return {"result": "small"}
110
+ else:
111
+ return {"result": "negative"}
112
+
113
+ class FuncReturn1(TypedDict):
114
+ result: Literal["large"]
115
+ class FuncReturn2(TypedDict):
116
+ result: Literal["small"]
117
+ class FuncReturn3(TypedDict):
118
+ result: Literal["negative"]
119
+
120
+ assert is_equivalent_type(guess_return_type(func, use_literals=True), Union[FuncReturn1, FuncReturn2, FuncReturn3])
121
+
122
+
123
+ class TestCat:
124
+ color: Literal["black", "orange"]
125
+ has_ears: bool
126
+
127
+ def test_guess_return_type_follow_class_members():
128
+ class TheCatReturns(TypedDict):
129
+ color: Literal["black", "orange"]
130
+ has_ears: bool
131
+
132
+ def func(cat: TestCat):
133
+ return {
134
+ "color": cat.color,
135
+ "has_ears": cat.has_ears,
136
+ }
137
+
138
+ assert is_equivalent_type(guess_return_type(func, use_literals=True), TheCatReturns)
@@ -0,0 +1,202 @@
1
+ version = 1
2
+ revision = 1
3
+ requires-python = ">=3.12"
4
+
5
+ [[package]]
6
+ name = "annotated-types"
7
+ version = "0.7.0"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "anyio"
16
+ version = "4.8.0"
17
+ source = { registry = "https://pypi.org/simple" }
18
+ dependencies = [
19
+ { name = "idna" },
20
+ { name = "sniffio" },
21
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
22
+ ]
23
+ sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 }
24
+ wheels = [
25
+ { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 },
26
+ ]
27
+
28
+ [[package]]
29
+ name = "colorama"
30
+ version = "0.4.6"
31
+ source = { registry = "https://pypi.org/simple" }
32
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
33
+ wheels = [
34
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
35
+ ]
36
+
37
+ [[package]]
38
+ name = "fastapi"
39
+ version = "0.115.11"
40
+ source = { registry = "https://pypi.org/simple" }
41
+ dependencies = [
42
+ { name = "pydantic" },
43
+ { name = "starlette" },
44
+ { name = "typing-extensions" },
45
+ ]
46
+ sdist = { url = "https://files.pythonhosted.org/packages/b5/28/c5d26e5860df807241909a961a37d45e10533acef95fc368066c7dd186cd/fastapi-0.115.11.tar.gz", hash = "sha256:cc81f03f688678b92600a65a5e618b93592c65005db37157147204d8924bf94f", size = 294441 }
47
+ wheels = [
48
+ { url = "https://files.pythonhosted.org/packages/b3/5d/4d8bbb94f0dbc22732350c06965e40740f4a92ca560e90bb566f4f73af41/fastapi-0.115.11-py3-none-any.whl", hash = "sha256:32e1541b7b74602e4ef4a0260ecaf3aadf9d4f19590bba3e1bf2ac4666aa2c64", size = 94926 },
49
+ ]
50
+
51
+ [[package]]
52
+ name = "idna"
53
+ version = "3.10"
54
+ source = { registry = "https://pypi.org/simple" }
55
+ sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
56
+ wheels = [
57
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
58
+ ]
59
+
60
+ [[package]]
61
+ name = "iniconfig"
62
+ version = "2.0.0"
63
+ source = { registry = "https://pypi.org/simple" }
64
+ sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
65
+ wheels = [
66
+ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
67
+ ]
68
+
69
+ [[package]]
70
+ name = "packaging"
71
+ version = "24.2"
72
+ source = { registry = "https://pypi.org/simple" }
73
+ sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
74
+ wheels = [
75
+ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
76
+ ]
77
+
78
+ [[package]]
79
+ name = "pluggy"
80
+ version = "1.5.0"
81
+ source = { registry = "https://pypi.org/simple" }
82
+ sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
83
+ wheels = [
84
+ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
85
+ ]
86
+
87
+ [[package]]
88
+ name = "pydantic"
89
+ version = "2.10.6"
90
+ source = { registry = "https://pypi.org/simple" }
91
+ dependencies = [
92
+ { name = "annotated-types" },
93
+ { name = "pydantic-core" },
94
+ { name = "typing-extensions" },
95
+ ]
96
+ sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 }
97
+ wheels = [
98
+ { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 },
99
+ ]
100
+
101
+ [[package]]
102
+ name = "pydantic-core"
103
+ version = "2.27.2"
104
+ source = { registry = "https://pypi.org/simple" }
105
+ dependencies = [
106
+ { name = "typing-extensions" },
107
+ ]
108
+ sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 }
109
+ wheels = [
110
+ { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 },
111
+ { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 },
112
+ { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 },
113
+ { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 },
114
+ { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 },
115
+ { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 },
116
+ { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 },
117
+ { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 },
118
+ { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 },
119
+ { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 },
120
+ { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 },
121
+ { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 },
122
+ { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 },
123
+ { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 },
124
+ { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 },
125
+ { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 },
126
+ { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 },
127
+ { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 },
128
+ { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 },
129
+ { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 },
130
+ { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 },
131
+ { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 },
132
+ { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 },
133
+ { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 },
134
+ { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 },
135
+ { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 },
136
+ { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 },
137
+ { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 },
138
+ ]
139
+
140
+ [[package]]
141
+ name = "pytest"
142
+ version = "8.3.5"
143
+ source = { registry = "https://pypi.org/simple" }
144
+ dependencies = [
145
+ { name = "colorama", marker = "sys_platform == 'win32'" },
146
+ { name = "iniconfig" },
147
+ { name = "packaging" },
148
+ { name = "pluggy" },
149
+ ]
150
+ sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 }
151
+ wheels = [
152
+ { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 },
153
+ ]
154
+
155
+ [[package]]
156
+ name = "sniffio"
157
+ version = "1.3.1"
158
+ source = { registry = "https://pypi.org/simple" }
159
+ sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
160
+ wheels = [
161
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
162
+ ]
163
+
164
+ [[package]]
165
+ name = "starlette"
166
+ version = "0.46.1"
167
+ source = { registry = "https://pypi.org/simple" }
168
+ dependencies = [
169
+ { name = "anyio" },
170
+ ]
171
+ sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 }
172
+ wheels = [
173
+ { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 },
174
+ ]
175
+
176
+ [[package]]
177
+ name = "type-less"
178
+ version = "0.1.0"
179
+ source = { editable = "." }
180
+
181
+ [package.dev-dependencies]
182
+ dev = [
183
+ { name = "fastapi" },
184
+ { name = "pytest" },
185
+ ]
186
+
187
+ [package.metadata]
188
+
189
+ [package.metadata.requires-dev]
190
+ dev = [
191
+ { name = "fastapi", specifier = ">=0.115.11" },
192
+ { name = "pytest", specifier = ">=8.3.5" },
193
+ ]
194
+
195
+ [[package]]
196
+ name = "typing-extensions"
197
+ version = "4.12.2"
198
+ source = { registry = "https://pypi.org/simple" }
199
+ sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
200
+ wheels = [
201
+ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
202
+ ]