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.
- type_less-0.1.1/.github/workflows/release.yml +20 -0
- type_less-0.1.1/.github/workflows/test.yml +28 -0
- type_less-0.1.1/.gitignore +8 -0
- type_less-0.1.1/.pdm-python +1 -0
- type_less-0.1.1/PKG-INFO +65 -0
- type_less-0.1.1/README.md +58 -0
- type_less-0.1.1/docs/example.png +0 -0
- type_less-0.1.1/pyproject.toml +22 -0
- type_less-0.1.1/src/type_less/__init__.py +4 -0
- type_less-0.1.1/src/type_less/inference.py +379 -0
- type_less-0.1.1/src/type_less/inject.py +40 -0
- type_less-0.1.1/tests/matching.py +185 -0
- type_less-0.1.1/tests/test_fastapi.py +25 -0
- type_less-0.1.1/tests/test_return_types.py +138 -0
- type_less-0.1.1/uv.lock +202 -0
|
@@ -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 @@
|
|
|
1
|
+
/home/runner/work/type-less/type-less/.venv/bin/python
|
type_less-0.1.1/PKG-INFO
ADDED
|
@@ -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,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)
|
type_less-0.1.1/uv.lock
ADDED
|
@@ -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
|
+
]
|