flask_dependency 0.2.0__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.
- flask_dependency-0.2.0/PKG-INFO +81 -0
- flask_dependency-0.2.0/README.md +64 -0
- flask_dependency-0.2.0/flask_dependency/__init__.py +7 -0
- flask_dependency-0.2.0/flask_dependency/background_task.py +11 -0
- flask_dependency-0.2.0/flask_dependency/depends.py +31 -0
- flask_dependency-0.2.0/flask_dependency/exceptions/unprocessable_content.py +22 -0
- flask_dependency-0.2.0/flask_dependency/form.py +17 -0
- flask_dependency-0.2.0/flask_dependency/inject.py +32 -0
- flask_dependency-0.2.0/flask_dependency/query.py +17 -0
- flask_dependency-0.2.0/flask_dependency/route.py +37 -0
- flask_dependency-0.2.0/flask_dependency/validate_schema.py +11 -0
- flask_dependency-0.2.0/pyproject.toml +19 -0
@@ -0,0 +1,81 @@
|
|
1
|
+
Metadata-Version: 2.3
|
2
|
+
Name: flask_dependency
|
3
|
+
Version: 0.2.0
|
4
|
+
Summary:
|
5
|
+
Author: ventura94
|
6
|
+
Author-email: arianventura94@gmail.com
|
7
|
+
Requires-Python: >=3.10,<4.0
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
13
|
+
Requires-Dist: flask (>=3.1.1,<4.0.0)
|
14
|
+
Requires-Dist: pydantic[dotenv] (>=1.10.19,<2.0.0)
|
15
|
+
Description-Content-Type: text/markdown
|
16
|
+
|
17
|
+
## Flask Dependency
|
18
|
+
|
19
|
+
Dependency injection is a powerful concept that allows you to manage and inject dependencies into your application
|
20
|
+
components. In Flask, we can create a simple dependency injection system inspired by FastAPI's `Depends`.
|
21
|
+
|
22
|
+
### Approach
|
23
|
+
|
24
|
+
1. **Create a Dependency Class**: We'll define a class called `Depends` that will handle our dependencies. This class
|
25
|
+
will allow us to declare dependencies for specific route handlers.
|
26
|
+
|
27
|
+
2. **Decorator for Dependency Resolution**: We'll create a decorator that inspects the function and resolves any
|
28
|
+
dependencies when called. This decorator will be applied to our route handlers.
|
29
|
+
|
30
|
+
3. **Dependency Functions**: We'll define individual functions (similar to FastAPI's dependencies) that represent our
|
31
|
+
dependencies. These functions will be called automatically when needed.
|
32
|
+
|
33
|
+
### Sample Code
|
34
|
+
|
35
|
+
```python
|
36
|
+
# app.py
|
37
|
+
from flask import Flask, Blueprint
|
38
|
+
from pydantic import BaseModel
|
39
|
+
from flask_dependency import BackgroundTask, Depends, route, FormModel
|
40
|
+
|
41
|
+
app = Flask(__name__)
|
42
|
+
blueprint = Blueprint("example_blueprint", __name__)
|
43
|
+
app.debug = True
|
44
|
+
|
45
|
+
|
46
|
+
def sample_task(duration):
|
47
|
+
import time
|
48
|
+
time.sleep(duration)
|
49
|
+
return f"Slept for {duration} seconds"
|
50
|
+
|
51
|
+
|
52
|
+
# Example Backgraund Task
|
53
|
+
@route(blueprint, "/backgraund_task", methods=["POST"], endpoint="route_test")
|
54
|
+
def route_test(background_task: BackgroundTask = Depends()):
|
55
|
+
background_task.run(sample_task, 1)
|
56
|
+
return {"message": "OK"}
|
57
|
+
|
58
|
+
|
59
|
+
class InputModelForm(FormModel):
|
60
|
+
id: int
|
61
|
+
name: str
|
62
|
+
|
63
|
+
|
64
|
+
class ResponseInputModelForm(BaseModel):
|
65
|
+
id: int
|
66
|
+
name: str
|
67
|
+
|
68
|
+
class Config:
|
69
|
+
orm_mode = True
|
70
|
+
|
71
|
+
|
72
|
+
# Example Form
|
73
|
+
@route(blueprint, "/example_form", methods=["POST"], endpoint="route_test", response_schema=ResponseInputModelForm)
|
74
|
+
def route_test(input_data: InputModelForm = Depends()):
|
75
|
+
return {"message": "success"}
|
76
|
+
|
77
|
+
|
78
|
+
app.register_blueprint(blueprint)
|
79
|
+
```
|
80
|
+
|
81
|
+
|
@@ -0,0 +1,64 @@
|
|
1
|
+
## Flask Dependency
|
2
|
+
|
3
|
+
Dependency injection is a powerful concept that allows you to manage and inject dependencies into your application
|
4
|
+
components. In Flask, we can create a simple dependency injection system inspired by FastAPI's `Depends`.
|
5
|
+
|
6
|
+
### Approach
|
7
|
+
|
8
|
+
1. **Create a Dependency Class**: We'll define a class called `Depends` that will handle our dependencies. This class
|
9
|
+
will allow us to declare dependencies for specific route handlers.
|
10
|
+
|
11
|
+
2. **Decorator for Dependency Resolution**: We'll create a decorator that inspects the function and resolves any
|
12
|
+
dependencies when called. This decorator will be applied to our route handlers.
|
13
|
+
|
14
|
+
3. **Dependency Functions**: We'll define individual functions (similar to FastAPI's dependencies) that represent our
|
15
|
+
dependencies. These functions will be called automatically when needed.
|
16
|
+
|
17
|
+
### Sample Code
|
18
|
+
|
19
|
+
```python
|
20
|
+
# app.py
|
21
|
+
from flask import Flask, Blueprint
|
22
|
+
from pydantic import BaseModel
|
23
|
+
from flask_dependency import BackgroundTask, Depends, route, FormModel
|
24
|
+
|
25
|
+
app = Flask(__name__)
|
26
|
+
blueprint = Blueprint("example_blueprint", __name__)
|
27
|
+
app.debug = True
|
28
|
+
|
29
|
+
|
30
|
+
def sample_task(duration):
|
31
|
+
import time
|
32
|
+
time.sleep(duration)
|
33
|
+
return f"Slept for {duration} seconds"
|
34
|
+
|
35
|
+
|
36
|
+
# Example Backgraund Task
|
37
|
+
@route(blueprint, "/backgraund_task", methods=["POST"], endpoint="route_test")
|
38
|
+
def route_test(background_task: BackgroundTask = Depends()):
|
39
|
+
background_task.run(sample_task, 1)
|
40
|
+
return {"message": "OK"}
|
41
|
+
|
42
|
+
|
43
|
+
class InputModelForm(FormModel):
|
44
|
+
id: int
|
45
|
+
name: str
|
46
|
+
|
47
|
+
|
48
|
+
class ResponseInputModelForm(BaseModel):
|
49
|
+
id: int
|
50
|
+
name: str
|
51
|
+
|
52
|
+
class Config:
|
53
|
+
orm_mode = True
|
54
|
+
|
55
|
+
|
56
|
+
# Example Form
|
57
|
+
@route(blueprint, "/example_form", methods=["POST"], endpoint="route_test", response_schema=ResponseInputModelForm)
|
58
|
+
def route_test(input_data: InputModelForm = Depends()):
|
59
|
+
return {"message": "success"}
|
60
|
+
|
61
|
+
|
62
|
+
app.register_blueprint(blueprint)
|
63
|
+
```
|
64
|
+
|
@@ -0,0 +1,11 @@
|
|
1
|
+
from concurrent.futures import ThreadPoolExecutor, Future
|
2
|
+
from typing import Callable, Any
|
3
|
+
|
4
|
+
|
5
|
+
class BackgroundTask:
|
6
|
+
def __init__(self, max_workers: int = 1):
|
7
|
+
self.executor = ThreadPoolExecutor(max_workers=max_workers)
|
8
|
+
|
9
|
+
def run(self, func: Callable, *args: Any, **kwargs: Any) -> Future:
|
10
|
+
future = self.executor.submit(func, *args, **kwargs)
|
11
|
+
return future
|
@@ -0,0 +1,31 @@
|
|
1
|
+
from typing import Any, Callable, Optional
|
2
|
+
|
3
|
+
from flask import g
|
4
|
+
|
5
|
+
|
6
|
+
class Depends:
|
7
|
+
def __init__(self, dependency: Optional[Callable[..., Any]] = None, cache: bool = True):
|
8
|
+
self.dependency = dependency
|
9
|
+
self.cache_key = f"_depends_cache_{id(dependency)}"
|
10
|
+
self.cache = cache
|
11
|
+
|
12
|
+
def __call__(self):
|
13
|
+
if not hasattr(g, self.cache_key) or not self.cache:
|
14
|
+
result = self.dependency()
|
15
|
+
if hasattr(result, "__enter__") and hasattr(result, "__exit__"):
|
16
|
+
with result as ctx_result:
|
17
|
+
setattr(g, self.cache_key, ctx_result)
|
18
|
+
return ctx_result
|
19
|
+
setattr(g, self.cache_key, result)
|
20
|
+
return result
|
21
|
+
return getattr(g, self.cache_key)
|
22
|
+
|
23
|
+
def exists(self) -> bool:
|
24
|
+
print(hasattr(g, self.cache_key))
|
25
|
+
return bool(hasattr(g, self.cache_key))
|
26
|
+
|
27
|
+
@classmethod
|
28
|
+
def attach_dependency(cls, dependency_instance: Any) -> Any:
|
29
|
+
instance = cls(type(dependency_instance))
|
30
|
+
setattr(g, instance.cache_key, dependency_instance)
|
31
|
+
return dependency_instance
|
@@ -0,0 +1,22 @@
|
|
1
|
+
from pydantic import ValidationError
|
2
|
+
|
3
|
+
|
4
|
+
class UnprocessableContent(Exception):
|
5
|
+
def __init__(
|
6
|
+
self,
|
7
|
+
error: ValidationError,
|
8
|
+
message: str = "Unprocessable Content",
|
9
|
+
status_code: int = 422,
|
10
|
+
):
|
11
|
+
super().__init__(error)
|
12
|
+
self.error = error
|
13
|
+
self.message = message
|
14
|
+
self.status_code = status_code
|
15
|
+
self.error_details = self.get_error_details()
|
16
|
+
|
17
|
+
def get_error_details(self):
|
18
|
+
error_messages = []
|
19
|
+
for error in self.error.errors():
|
20
|
+
field = ".".join(str(loc) for loc in error["loc"])
|
21
|
+
error_messages.append(f"{field} {error['msg']}")
|
22
|
+
return error_messages
|
@@ -0,0 +1,17 @@
|
|
1
|
+
import json
|
2
|
+
|
3
|
+
from .exceptions.unprocessable_content import UnprocessableContent
|
4
|
+
from flask import request
|
5
|
+
from pydantic import BaseModel, ValidationError
|
6
|
+
|
7
|
+
|
8
|
+
class FormModel(BaseModel):
|
9
|
+
def __init__(self):
|
10
|
+
data = request.get_json()
|
11
|
+
try:
|
12
|
+
if isinstance(data, (str, bytes, bytearray)):
|
13
|
+
super().__init__(**json.loads(data))
|
14
|
+
if isinstance(data, dict):
|
15
|
+
super().__init__(**data)
|
16
|
+
except ValidationError as exc:
|
17
|
+
raise UnprocessableContent(message="Invalid Input Data", error=exc)
|
@@ -0,0 +1,32 @@
|
|
1
|
+
from inspect import signature
|
2
|
+
from types import UnionType
|
3
|
+
from typing import get_origin, get_args
|
4
|
+
|
5
|
+
from .depends import Depends
|
6
|
+
|
7
|
+
|
8
|
+
def inject(func):
|
9
|
+
sig = signature(func)
|
10
|
+
params = sig.parameters
|
11
|
+
|
12
|
+
def wrapper(*args, **kwargs):
|
13
|
+
for name, param in params.items():
|
14
|
+
if isinstance(param.default, Depends):
|
15
|
+
if param.default.dependency:
|
16
|
+
kwargs[name] = param.default()
|
17
|
+
else:
|
18
|
+
if get_origin(param.annotation) is UnionType:
|
19
|
+
possible_types = get_args(param.annotation)
|
20
|
+
else:
|
21
|
+
possible_types = (param.annotation,)
|
22
|
+
if len(possible_types) == 1:
|
23
|
+
kwargs[name] = Depends(possible_types[0])()
|
24
|
+
else:
|
25
|
+
for dep_type in possible_types:
|
26
|
+
depends = Depends(dep_type)
|
27
|
+
if depends.exists():
|
28
|
+
kwargs[name] = depends()
|
29
|
+
break
|
30
|
+
return func(*args, **kwargs)
|
31
|
+
|
32
|
+
return wrapper
|
@@ -0,0 +1,17 @@
|
|
1
|
+
import json
|
2
|
+
|
3
|
+
from .exceptions.unprocessable_content import UnprocessableContent
|
4
|
+
from flask import request
|
5
|
+
from pydantic import BaseModel, ValidationError
|
6
|
+
|
7
|
+
|
8
|
+
class QueryModel(BaseModel):
|
9
|
+
def __init__(self):
|
10
|
+
data = request.args
|
11
|
+
try:
|
12
|
+
if isinstance(data, (str, bytes, bytearray)):
|
13
|
+
super().__init__(**json.loads(data))
|
14
|
+
if isinstance(data, dict):
|
15
|
+
super().__init__(**data)
|
16
|
+
except ValidationError as e:
|
17
|
+
raise UnprocessableContent(message="Invalid Query Data", error=e)
|
@@ -0,0 +1,37 @@
|
|
1
|
+
from functools import wraps
|
2
|
+
from typing import List, Literal, Type
|
3
|
+
|
4
|
+
from flask import Blueprint
|
5
|
+
from pydantic import BaseModel
|
6
|
+
|
7
|
+
from .depends import Depends
|
8
|
+
from .inject import inject
|
9
|
+
from .validate_schema import validate_schema
|
10
|
+
|
11
|
+
|
12
|
+
def route(
|
13
|
+
blueprint: Blueprint,
|
14
|
+
rule: str,
|
15
|
+
endpoint: str,
|
16
|
+
methods: List[Literal["GET", "POST", "PUT", "PATCH", "DELETE"]],
|
17
|
+
response_schema: Type[BaseModel] = None,
|
18
|
+
status_code_response: int = 200,
|
19
|
+
dependencies: List[Depends] = None,
|
20
|
+
):
|
21
|
+
def decorator(func):
|
22
|
+
func = inject(func)
|
23
|
+
|
24
|
+
@wraps(func)
|
25
|
+
@blueprint.route(rule, methods=methods, endpoint=endpoint)
|
26
|
+
def wrapper(*args, **kwargs):
|
27
|
+
|
28
|
+
if dependencies:
|
29
|
+
[_() for _ in dependencies]
|
30
|
+
data_response = func(*args, **kwargs)
|
31
|
+
if response_schema:
|
32
|
+
data_response = validate_schema(response_schema, data_response).dict()
|
33
|
+
return data_response, status_code_response
|
34
|
+
|
35
|
+
return wrapper
|
36
|
+
|
37
|
+
return decorator
|
@@ -0,0 +1,11 @@
|
|
1
|
+
from typing import Type, Any, Dict
|
2
|
+
|
3
|
+
from pydantic import BaseModel
|
4
|
+
|
5
|
+
|
6
|
+
def validate_schema(schema: Type[BaseModel], data: Any) -> BaseModel:
|
7
|
+
if isinstance(data, str):
|
8
|
+
return schema.parse_raw(data)
|
9
|
+
if isinstance(data, Dict):
|
10
|
+
return schema(**data)
|
11
|
+
return schema.from_orm(data)
|
@@ -0,0 +1,19 @@
|
|
1
|
+
[tool.poetry]
|
2
|
+
name = "flask_dependency"
|
3
|
+
version = "0.2.0"
|
4
|
+
description = ""
|
5
|
+
authors = ["ventura94 <arianventura94@gmail.com>"]
|
6
|
+
readme = "README.md"
|
7
|
+
|
8
|
+
[tool.poetry.dependencies]
|
9
|
+
python = "^3.10"
|
10
|
+
flask = "^3.1.1"
|
11
|
+
pydantic = { version = "^1.10.19", extras = ["dotenv"] }
|
12
|
+
|
13
|
+
[tool.poetry.group.tests.dependencies]
|
14
|
+
pytest = "^8.2.2"
|
15
|
+
httpx = "^0.27.0"
|
16
|
+
|
17
|
+
[build-system]
|
18
|
+
requires = ["poetry-core"]
|
19
|
+
build-backend = "poetry.core.masonry.api"
|