hypern 0.2.0__cp312-none-win32.whl
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.
- hypern/__init__.py +4 -0
- hypern/application.py +412 -0
- hypern/auth/__init__.py +0 -0
- hypern/auth/authorization.py +2 -0
- hypern/background.py +4 -0
- hypern/caching/__init__.py +0 -0
- hypern/caching/base/__init__.py +8 -0
- hypern/caching/base/backend.py +3 -0
- hypern/caching/base/key_maker.py +8 -0
- hypern/caching/cache_manager.py +56 -0
- hypern/caching/cache_tag.py +10 -0
- hypern/caching/custom_key_maker.py +11 -0
- hypern/caching/redis_backend.py +3 -0
- hypern/cli/__init__.py +0 -0
- hypern/cli/commands.py +0 -0
- hypern/config.py +149 -0
- hypern/datastructures.py +40 -0
- hypern/db/__init__.py +0 -0
- hypern/db/nosql/__init__.py +25 -0
- hypern/db/nosql/addons/__init__.py +4 -0
- hypern/db/nosql/addons/color.py +16 -0
- hypern/db/nosql/addons/daterange.py +30 -0
- hypern/db/nosql/addons/encrypted.py +53 -0
- hypern/db/nosql/addons/password.py +134 -0
- hypern/db/nosql/addons/unicode.py +10 -0
- hypern/db/sql/__init__.py +179 -0
- hypern/db/sql/addons/__init__.py +14 -0
- hypern/db/sql/addons/color.py +16 -0
- hypern/db/sql/addons/daterange.py +23 -0
- hypern/db/sql/addons/datetime.py +22 -0
- hypern/db/sql/addons/encrypted.py +58 -0
- hypern/db/sql/addons/password.py +171 -0
- hypern/db/sql/addons/ts_vector.py +46 -0
- hypern/db/sql/addons/unicode.py +15 -0
- hypern/db/sql/repository.py +290 -0
- hypern/enum.py +13 -0
- hypern/exceptions.py +97 -0
- hypern/hypern.cp312-win32.pyd +0 -0
- hypern/hypern.pyi +266 -0
- hypern/i18n/__init__.py +0 -0
- hypern/logging/__init__.py +3 -0
- hypern/logging/logger.py +82 -0
- hypern/middleware/__init__.py +5 -0
- hypern/middleware/base.py +18 -0
- hypern/middleware/cors.py +38 -0
- hypern/middleware/i18n.py +1 -0
- hypern/middleware/limit.py +176 -0
- hypern/openapi/__init__.py +5 -0
- hypern/openapi/schemas.py +53 -0
- hypern/openapi/swagger.py +3 -0
- hypern/processpool.py +106 -0
- hypern/py.typed +0 -0
- hypern/response/__init__.py +3 -0
- hypern/response/response.py +134 -0
- hypern/routing/__init__.py +4 -0
- hypern/routing/dispatcher.py +67 -0
- hypern/routing/endpoint.py +30 -0
- hypern/routing/parser.py +100 -0
- hypern/routing/route.py +284 -0
- hypern/scheduler.py +5 -0
- hypern/security.py +44 -0
- hypern/worker.py +30 -0
- hypern-0.2.0.dist-info/METADATA +127 -0
- hypern-0.2.0.dist-info/RECORD +66 -0
- hypern-0.2.0.dist-info/WHEEL +4 -0
- hypern-0.2.0.dist-info/licenses/LICENSE +24 -0
hypern/routing/route.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import asyncio
|
|
3
|
+
import inspect
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, Callable, Dict, List, Type, Union, get_args, get_origin
|
|
6
|
+
|
|
7
|
+
import yaml # type: ignore
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from pydantic.fields import FieldInfo
|
|
10
|
+
|
|
11
|
+
from hypern.auth.authorization import Authorization
|
|
12
|
+
from hypern.datastructures import HTTPMethod
|
|
13
|
+
from hypern.hypern import FunctionInfo, Request, Router
|
|
14
|
+
from hypern.hypern import Route as InternalRoute
|
|
15
|
+
|
|
16
|
+
from .dispatcher import dispatch
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_field_type(field):
|
|
20
|
+
return field.outer_type_
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def pydantic_to_swagger(model: type[BaseModel] | dict):
|
|
24
|
+
if isinstance(model, dict):
|
|
25
|
+
# Handle the case when a dict is passed instead of a Pydantic model
|
|
26
|
+
schema = {}
|
|
27
|
+
for name, field_type in model.items():
|
|
28
|
+
schema[name] = _process_field(name, field_type)
|
|
29
|
+
return schema
|
|
30
|
+
|
|
31
|
+
schema = {
|
|
32
|
+
model.__name__: {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"properties": {},
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for name, field in model.model_fields.items():
|
|
39
|
+
schema[model.__name__]["properties"][name] = _process_field(name, field)
|
|
40
|
+
|
|
41
|
+
return schema
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SchemaProcessor:
|
|
45
|
+
@staticmethod
|
|
46
|
+
def process_union(args: tuple) -> Dict[str, Any]:
|
|
47
|
+
"""Process Union types"""
|
|
48
|
+
if type(None) in args:
|
|
49
|
+
inner_type = next(arg for arg in args if arg is not type(None))
|
|
50
|
+
schema = SchemaProcessor._process_field("", inner_type)
|
|
51
|
+
schema["nullable"] = True
|
|
52
|
+
return schema
|
|
53
|
+
return {"oneOf": [SchemaProcessor._process_field("", arg) for arg in args]}
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def process_enum(annotation: Type[Enum]) -> Dict[str, Any]:
|
|
57
|
+
"""Process Enum types"""
|
|
58
|
+
return {"type": "string", "enum": [e.value for e in annotation.__members__.values()]}
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def process_primitive(annotation: type) -> Dict[str, str]:
|
|
62
|
+
"""Process primitive types"""
|
|
63
|
+
type_mapping = {int: "integer", float: "number", str: "string", bool: "boolean"}
|
|
64
|
+
return {"type": type_mapping.get(annotation, "object")}
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def process_list(annotation: type) -> Dict[str, Any]:
|
|
68
|
+
"""Process list types"""
|
|
69
|
+
schema = {"type": "array"}
|
|
70
|
+
|
|
71
|
+
args = get_args(annotation)
|
|
72
|
+
if args:
|
|
73
|
+
item_type = args[0]
|
|
74
|
+
schema["items"] = SchemaProcessor._process_field("item", item_type)
|
|
75
|
+
else:
|
|
76
|
+
schema["items"] = {}
|
|
77
|
+
return schema
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def process_dict(annotation: type) -> Dict[str, Any]:
|
|
81
|
+
"""Process dict types"""
|
|
82
|
+
schema = {"type": "object"}
|
|
83
|
+
|
|
84
|
+
args = get_args(annotation)
|
|
85
|
+
if args:
|
|
86
|
+
key_type, value_type = args
|
|
87
|
+
if key_type == str: # noqa: E721
|
|
88
|
+
schema["additionalProperties"] = SchemaProcessor._process_field("value", value_type)
|
|
89
|
+
return schema
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def _process_field(cls, name: str, field: Any) -> Dict[str, Any]:
|
|
93
|
+
"""Process a single field"""
|
|
94
|
+
if isinstance(field, FieldInfo):
|
|
95
|
+
annotation = field.annotation
|
|
96
|
+
else:
|
|
97
|
+
annotation = field
|
|
98
|
+
|
|
99
|
+
# Process Union types
|
|
100
|
+
origin = get_origin(annotation)
|
|
101
|
+
if origin is Union:
|
|
102
|
+
return cls.process_union(get_args(annotation))
|
|
103
|
+
|
|
104
|
+
# Process Enum types
|
|
105
|
+
if isinstance(annotation, type) and issubclass(annotation, Enum):
|
|
106
|
+
return cls.process_enum(annotation)
|
|
107
|
+
|
|
108
|
+
# Process primitive types
|
|
109
|
+
if annotation in {int, float, str, bool}:
|
|
110
|
+
return cls.process_primitive(annotation)
|
|
111
|
+
|
|
112
|
+
# Process list types
|
|
113
|
+
if annotation == list or origin is list: # noqa: E721
|
|
114
|
+
return cls.process_list(annotation)
|
|
115
|
+
|
|
116
|
+
# Process dict types
|
|
117
|
+
if annotation == dict or origin is dict: # noqa: E721
|
|
118
|
+
return cls.process_dict(annotation)
|
|
119
|
+
|
|
120
|
+
# Process Pydantic models
|
|
121
|
+
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
|
122
|
+
return pydantic_to_swagger(annotation)
|
|
123
|
+
|
|
124
|
+
# Fallback for complex types
|
|
125
|
+
return {"type": "object"}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _process_field(name: str, field: Any) -> Dict[str, Any]:
|
|
129
|
+
"""
|
|
130
|
+
Process a field and return its schema representation
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
name: Field name
|
|
134
|
+
field: Field type or FieldInfo object
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Dictionary representing the JSON schema for the field
|
|
138
|
+
"""
|
|
139
|
+
return SchemaProcessor._process_field(name, field)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class Route:
|
|
143
|
+
def __init__(
|
|
144
|
+
self,
|
|
145
|
+
path: str,
|
|
146
|
+
endpoint: Callable[..., Any] | None = None,
|
|
147
|
+
*,
|
|
148
|
+
name: str | None = None,
|
|
149
|
+
tags: List[str] | None = None,
|
|
150
|
+
) -> None:
|
|
151
|
+
self.path = path
|
|
152
|
+
self.endpoint = endpoint
|
|
153
|
+
self.tags = tags or ["Default"]
|
|
154
|
+
self.name = name
|
|
155
|
+
|
|
156
|
+
self.http_methods = {
|
|
157
|
+
"GET": HTTPMethod.GET,
|
|
158
|
+
"POST": HTTPMethod.POST,
|
|
159
|
+
"PUT": HTTPMethod.PUT,
|
|
160
|
+
"DELETE": HTTPMethod.DELETE,
|
|
161
|
+
"PATCH": HTTPMethod.PATCH,
|
|
162
|
+
"HEAD": HTTPMethod.HEAD,
|
|
163
|
+
"OPTIONS": HTTPMethod.OPTIONS,
|
|
164
|
+
}
|
|
165
|
+
self.functional_handlers = []
|
|
166
|
+
|
|
167
|
+
def _process_authorization(self, item: type, docs: Dict) -> None:
|
|
168
|
+
if isinstance(item, type) and issubclass(item, Authorization):
|
|
169
|
+
auth_obj = item()
|
|
170
|
+
docs["security"] = [{auth_obj.name: []}]
|
|
171
|
+
|
|
172
|
+
def _process_model_params(self, key: str, item: type, docs: Dict) -> None:
|
|
173
|
+
if not (isinstance(item, type) and issubclass(item, BaseModel)):
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
if key == "form_data":
|
|
177
|
+
docs["requestBody"] = {"content": {"application/json": {"schema": pydantic_to_swagger(item).get(item.__name__)}}}
|
|
178
|
+
elif key == "query_params":
|
|
179
|
+
docs["parameters"] = [{"name": param, "in": "query", "schema": _process_field(param, field)} for param, field in item.model_fields.items()]
|
|
180
|
+
elif key == "path_params":
|
|
181
|
+
path_params = [
|
|
182
|
+
{"name": param, "in": "path", "required": True, "schema": _process_field(param, field)} for param, field in item.model_fields.items()
|
|
183
|
+
]
|
|
184
|
+
docs.setdefault("parameters", []).extend(path_params)
|
|
185
|
+
|
|
186
|
+
def _process_response(self, response_type: type, docs: Dict) -> None:
|
|
187
|
+
if isinstance(response_type, type) and issubclass(response_type, BaseModel):
|
|
188
|
+
docs["responses"] = {
|
|
189
|
+
"200": {
|
|
190
|
+
"description": "Successful response",
|
|
191
|
+
"content": {"application/json": {"schema": response_type.model_json_schema()}},
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
def swagger_generate(self, signature: inspect.Signature, summary: str = "Document API") -> str:
|
|
196
|
+
_inputs = signature.parameters.values()
|
|
197
|
+
_inputs_dict = {_input.name: _input.annotation for _input in _inputs}
|
|
198
|
+
_docs: Dict = {"summary": summary, "tags": self.tags, "responses": [], "name": self.name}
|
|
199
|
+
|
|
200
|
+
for key, item in _inputs_dict.items():
|
|
201
|
+
self._process_authorization(item, _docs)
|
|
202
|
+
self._process_model_params(key, item, _docs)
|
|
203
|
+
|
|
204
|
+
self._process_response(signature.return_annotation, _docs)
|
|
205
|
+
return yaml.dump(_docs)
|
|
206
|
+
|
|
207
|
+
def _combine_path(self, path1: str, path2: str) -> str:
|
|
208
|
+
if path1.endswith("/") and path2.startswith("/"):
|
|
209
|
+
return path1 + path2[1:]
|
|
210
|
+
if not path1.endswith("/") and not path2.startswith("/"):
|
|
211
|
+
return path1 + "/" + path2
|
|
212
|
+
return path1 + path2
|
|
213
|
+
|
|
214
|
+
def make_internal_route(self, path, handler, method) -> InternalRoute:
|
|
215
|
+
is_async = asyncio.iscoroutinefunction(handler)
|
|
216
|
+
func_info = FunctionInfo(handler=handler, is_async=is_async)
|
|
217
|
+
return InternalRoute(path=path, function=func_info, method=method)
|
|
218
|
+
|
|
219
|
+
def __call__(self, app, *args: Any, **kwds: Any) -> Any:
|
|
220
|
+
router = Router(self.path)
|
|
221
|
+
|
|
222
|
+
# Validate handlers
|
|
223
|
+
if not self.endpoint and not self.functional_handlers:
|
|
224
|
+
raise ValueError(f"No handler found for route: {self.path}")
|
|
225
|
+
|
|
226
|
+
# Handle functional routes
|
|
227
|
+
for h in self.functional_handlers:
|
|
228
|
+
router.add_route(route=self.make_internal_route(path=h["path"], handler=h["func"], method=h["method"].upper()))
|
|
229
|
+
if not self.endpoint:
|
|
230
|
+
return router
|
|
231
|
+
|
|
232
|
+
# Handle class-based routes
|
|
233
|
+
for name, func in self.endpoint.__dict__.items():
|
|
234
|
+
if name.upper() in self.http_methods:
|
|
235
|
+
sig = inspect.signature(func)
|
|
236
|
+
doc = self.swagger_generate(sig, func.__doc__)
|
|
237
|
+
self.endpoint.dispatch.__doc__ = doc
|
|
238
|
+
endpoint_obj = self.endpoint()
|
|
239
|
+
router.add_route(route=self.make_internal_route(path="/", handler=endpoint_obj.dispatch, method=name.upper()))
|
|
240
|
+
del endpoint_obj # free up memory
|
|
241
|
+
return router
|
|
242
|
+
|
|
243
|
+
def add_route(
|
|
244
|
+
self,
|
|
245
|
+
path: str,
|
|
246
|
+
method: str,
|
|
247
|
+
) -> Callable:
|
|
248
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
249
|
+
async def functional_wrapper(request: Request, inject: Dict[str, Any]) -> Any:
|
|
250
|
+
return await dispatch(func, request, inject)
|
|
251
|
+
|
|
252
|
+
sig = inspect.signature(func)
|
|
253
|
+
functional_wrapper.__doc__ = self.swagger_generate(sig, func.__doc__)
|
|
254
|
+
|
|
255
|
+
self.functional_handlers.append(
|
|
256
|
+
{
|
|
257
|
+
"path": path,
|
|
258
|
+
"method": method,
|
|
259
|
+
"func": functional_wrapper,
|
|
260
|
+
}
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
return decorator
|
|
264
|
+
|
|
265
|
+
def get(self, path: str) -> Callable:
|
|
266
|
+
return self.add_route(path, "GET")
|
|
267
|
+
|
|
268
|
+
def post(self, path: str) -> Callable:
|
|
269
|
+
return self.add_route(path, "POST")
|
|
270
|
+
|
|
271
|
+
def put(self, path: str) -> Callable:
|
|
272
|
+
return self.add_route(path, "PUT")
|
|
273
|
+
|
|
274
|
+
def delete(self, path: str) -> Callable:
|
|
275
|
+
return self.add_route(path, "DELETE")
|
|
276
|
+
|
|
277
|
+
def patch(self, path: str) -> Callable:
|
|
278
|
+
return self.add_route(path, "PATCH")
|
|
279
|
+
|
|
280
|
+
def head(self, path: str) -> Callable:
|
|
281
|
+
return self.add_route(path, "HEAD")
|
|
282
|
+
|
|
283
|
+
def options(self, path: str) -> Callable:
|
|
284
|
+
return self.add_route(path, "OPTIONS")
|
hypern/scheduler.py
ADDED
hypern/security.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
4
|
+
from cryptography.hazmat.backends import default_backend
|
|
5
|
+
from base64 import b64encode, b64decode
|
|
6
|
+
|
|
7
|
+
import typing
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EDEngine(ABC):
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def encrypt(self, data: str) -> str:
|
|
13
|
+
raise NotImplementedError("Method not implemented")
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def decrypt(self, data: str) -> str:
|
|
17
|
+
raise NotImplementedError("Method not implemented")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AESEngine(EDEngine):
|
|
21
|
+
def __init__(self, secret_key: bytes, iv: bytes, padding_class: typing.Type) -> None:
|
|
22
|
+
super().__init__()
|
|
23
|
+
self.secret_key = secret_key
|
|
24
|
+
self.iv = iv
|
|
25
|
+
self.padding = padding_class(128)
|
|
26
|
+
|
|
27
|
+
def encrypt(self, data: str) -> bytes:
|
|
28
|
+
bytes_data = data.encode("utf-8")
|
|
29
|
+
encryptor = Cipher(algorithms.AES(self.secret_key), modes.GCM(self.iv), backend=default_backend()).encryptor()
|
|
30
|
+
padder = self.padding.padder()
|
|
31
|
+
padded_data = padder.update(bytes_data) + padder.finalize()
|
|
32
|
+
enctyped_data = encryptor.update(padded_data) + encryptor.finalize()
|
|
33
|
+
tag = encryptor.tag
|
|
34
|
+
return b64encode(tag + enctyped_data)
|
|
35
|
+
|
|
36
|
+
def decrypt(self, data: bytes) -> str:
|
|
37
|
+
data = b64decode(data)
|
|
38
|
+
tag = data[:16]
|
|
39
|
+
encrypted_data = data[16:]
|
|
40
|
+
decryptor = Cipher(algorithms.AES(self.secret_key), modes.GCM(self.iv, tag), backend=default_backend()).decryptor()
|
|
41
|
+
unpadder = self.padding.unpadder()
|
|
42
|
+
decrypted_data = decryptor.update(encrypted_data) + decryptor.finalize()
|
|
43
|
+
unpadded_data = unpadder.update(decrypted_data) + unpadder.finalize()
|
|
44
|
+
return unpadded_data.decode("utf-8")
|
hypern/worker.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from typing import Any
|
|
3
|
+
from celery import Celery
|
|
4
|
+
from asgiref.sync import async_to_sync
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AsyncCelery(Celery):
|
|
8
|
+
def __new__(cls, *args, **kwargs) -> Any:
|
|
9
|
+
if not hasattr(cls, "instance") or not cls.instance: # type: ignore
|
|
10
|
+
cls.instance = super().__new__(cls)
|
|
11
|
+
return cls.instance
|
|
12
|
+
|
|
13
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
14
|
+
super().__init__(*args, **kwargs)
|
|
15
|
+
self.patch_task()
|
|
16
|
+
|
|
17
|
+
def patch_task(self) -> None:
|
|
18
|
+
TaskBase = self.Task
|
|
19
|
+
|
|
20
|
+
class ContextTask(TaskBase): # type: ignore
|
|
21
|
+
abstract = True
|
|
22
|
+
|
|
23
|
+
def _run(self, *args, **kwargs):
|
|
24
|
+
result = async_to_sync(TaskBase.__call__)(self, *args, **kwargs)
|
|
25
|
+
return result
|
|
26
|
+
|
|
27
|
+
def __call__(self, *args, **kwargs):
|
|
28
|
+
return self._run(*args, **kwargs)
|
|
29
|
+
|
|
30
|
+
self.Task = ContextTask
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: hypern
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Classifier: Programming Language :: Rust
|
|
5
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
7
|
+
Requires-Dist: sqlalchemy[asyncio] ==2.0.31
|
|
8
|
+
Requires-Dist: pydantic[email] ==2.8.2
|
|
9
|
+
Requires-Dist: passlib ==1.7.4
|
|
10
|
+
Requires-Dist: pyjwt ==2.8.0
|
|
11
|
+
Requires-Dist: pydash ==8.0.3
|
|
12
|
+
Requires-Dist: sentry-sdk ==2.11.0
|
|
13
|
+
Requires-Dist: pydantic-settings ==2.3.4
|
|
14
|
+
Requires-Dist: celery ==5.4.0
|
|
15
|
+
Requires-Dist: asgiref ==3.8.1
|
|
16
|
+
Requires-Dist: psycopg ==3.2.3
|
|
17
|
+
Requires-Dist: pyyaml ==6.0.2
|
|
18
|
+
Requires-Dist: mongoengine ==0.29.1
|
|
19
|
+
Requires-Dist: argon2-cffi ==23.1.0
|
|
20
|
+
Requires-Dist: bcrypt ==4.2.0
|
|
21
|
+
Requires-Dist: orjson ==3.10.11
|
|
22
|
+
Requires-Dist: multiprocess ==0.70.17
|
|
23
|
+
Requires-Dist: uvloop ==0.21.0 ; sys_platform != 'win32' and platform_python_implementation == 'CPython' and platform_machine != 'armv7l'
|
|
24
|
+
Requires-Dist: cryptography ==43.0.3
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Summary: A Fast Async Python backend with a Rust runtime.
|
|
27
|
+
Author-email: Martin Dang <vannghiem848@gmail.com>
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Hypern
|
|
33
|
+
|
|
34
|
+
Hypern: A Versatile Python and Rust Framework
|
|
35
|
+
|
|
36
|
+
Hypern is a flexible, open-source framework built on the [Axum](https://github.com/tokio-rs/axum), designed to jumpstart your high-performance web development endeavors. By providing a pre-configured structure and essential components, Hypern empowers you to rapidly develop custom web applications that leverage the combined power of Python and Rust.
|
|
37
|
+
|
|
38
|
+
With Hypern, you can seamlessly integrate asynchronous features and build scalable solutions for RESTful APIs and dynamic web applications. Its intuitive design and robust tooling allow developers to focus on creating high-quality code while maximizing performance. Embrace the synergy of Python and Rust to elevate your web development experience.
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
### 🏁 Get started
|
|
42
|
+
|
|
43
|
+
### ⚙️ To Develop Locally
|
|
44
|
+
|
|
45
|
+
- Setup a virtual environment:
|
|
46
|
+
```
|
|
47
|
+
python3 -m venv venv
|
|
48
|
+
source venv/bin/activate
|
|
49
|
+
```
|
|
50
|
+
- Install required packages
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
pip install pre-commit poetry maturin
|
|
54
|
+
```
|
|
55
|
+
- Install development dependencies
|
|
56
|
+
```
|
|
57
|
+
poetry install --with dev --with test
|
|
58
|
+
```
|
|
59
|
+
- Install pre-commit git hooks
|
|
60
|
+
```
|
|
61
|
+
pre-commit install
|
|
62
|
+
```
|
|
63
|
+
- Build & install Rust package
|
|
64
|
+
```
|
|
65
|
+
maturin develop
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 🤔 Usage
|
|
69
|
+
|
|
70
|
+
### 🏃 Run your code
|
|
71
|
+
|
|
72
|
+
You will then have access to a server on the `localhost:5005`,
|
|
73
|
+
```python
|
|
74
|
+
# main.py
|
|
75
|
+
from hypern import Hypern
|
|
76
|
+
from hypern.routing import Route, HTTPEndpoint
|
|
77
|
+
|
|
78
|
+
class MyEndpoint(HTTPEndpoint):
|
|
79
|
+
|
|
80
|
+
async def get(self, request):
|
|
81
|
+
return {"data": "Hello World"}
|
|
82
|
+
|
|
83
|
+
routing = [
|
|
84
|
+
Route("/hello", MyEndpoint)
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
app = Hypern(routing)
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
app.start(host='localhost', port=5005)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
$ python3 main.py
|
|
95
|
+
```
|
|
96
|
+
You can open swagger UI at path `/docs`
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
## 💡 Features
|
|
100
|
+
|
|
101
|
+
### ⚡ High Performance
|
|
102
|
+
- Rust-powered core with Python flexibility
|
|
103
|
+
- Multi-process architecture for optimal CPU utilization
|
|
104
|
+
- Async/await support for non-blocking operations
|
|
105
|
+
- Built on top of production-ready Axum web framework
|
|
106
|
+
|
|
107
|
+
### 🛠 Development Experience
|
|
108
|
+
- Type hints and IDE support
|
|
109
|
+
- Built-in Swagger/OpenAPI documentation
|
|
110
|
+
- Hot reload during development
|
|
111
|
+
- Comprehensive error handling and logging
|
|
112
|
+
|
|
113
|
+
### 🔌 Integration & Extensions
|
|
114
|
+
- Easy dependency injection
|
|
115
|
+
- Middleware support (before/after request hooks)
|
|
116
|
+
- WebSocket support (Comming soon)
|
|
117
|
+
- Background task scheduling
|
|
118
|
+
- File upload handling
|
|
119
|
+
|
|
120
|
+
### 🔒 Security
|
|
121
|
+
- Built-in authentication/authorization (Comming soon)
|
|
122
|
+
- CORS configuration
|
|
123
|
+
- Rate limiting
|
|
124
|
+
- Request validation
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
hypern-0.2.0.dist-info/METADATA,sha256=0dwMDftVhFSbT6b6f41rTkO-2VTmxvLyUNBgblWBXc0,3722
|
|
2
|
+
hypern-0.2.0.dist-info/WHEEL,sha256=UEVASIeUpdjAl1B_6jhV8WLVdRfPuElhhLy1VDNI-c0,91
|
|
3
|
+
hypern-0.2.0.dist-info/licenses/LICENSE,sha256=qbYKAIJLS6jYg5hYncKE7OtWmqOtpVTvKNkwOa0Iwwg,1328
|
|
4
|
+
hypern/application.py,sha256=W_eltIwV2WcTaAQonxstMebGGgvMkIujowNvLYTX0nc,15183
|
|
5
|
+
hypern/auth/authorization.py,sha256=-NprZsI0np889ZN1fp-MiVFrPoMNzUtatBJaCMtkllM,32
|
|
6
|
+
hypern/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
hypern/background.py,sha256=xy38nQZSJsYFRXr3-uFJeNW9E1GiXXOC7lSe5pC0eyE,124
|
|
8
|
+
hypern/caching/base/backend.py,sha256=nHl5_nH95LSYWBS9RmOvOzsps_p19HcnQsb4h4W7cP8,68
|
|
9
|
+
hypern/caching/base/key_maker.py,sha256=-W1r3ywfQ-K6saniiK3aTMaW3iy3aXai2pvQqM8f74I,232
|
|
10
|
+
hypern/caching/base/__init__.py,sha256=M-B56YGTkSwCkKW_I6GcV1LSIFfxZ6KuXowB1_aJQpQ,155
|
|
11
|
+
hypern/caching/cache_manager.py,sha256=TF6UosJ54950JgsrPVZUS3MH2R8zafAu5PTryNJ0sRs,2053
|
|
12
|
+
hypern/caching/cache_tag.py,sha256=bZcjivMNETAzAHAIobuLN0S2wHgPgLLL8Gg4uso_qbk,267
|
|
13
|
+
hypern/caching/custom_key_maker.py,sha256=88RIIJjpQYFnv857wOlCKgWWBbK_S23zNHsIrJz_4PY,394
|
|
14
|
+
hypern/caching/redis_backend.py,sha256=IgQToCnHYGpKEErq2CNZkR5woo01z456Ef3C-XRPRV8,70
|
|
15
|
+
hypern/caching/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
hypern/cli/commands.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
hypern/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
hypern/config.py,sha256=frZSdXBI8GaM0tkw1Rs-XydZ9-XjGLRPj6DL4d51-y4,4930
|
|
19
|
+
hypern/datastructures.py,sha256=zZGGSP07kPc9KJDf11hX5uYhAyRE-Ck5wezW5QtOVXw,897
|
|
20
|
+
hypern/db/nosql/addons/color.py,sha256=bAGRuARCAYwZ1nO4jK0lzGYKmavTDtS34BxvrsetF74,446
|
|
21
|
+
hypern/db/nosql/addons/daterange.py,sha256=hGUSoVFqatNY-TB5wjZTq62iZpHpdsyRJIsHxsj1uDs,1192
|
|
22
|
+
hypern/db/nosql/addons/encrypted.py,sha256=B0M-uDqvZHVmIZcFdwcuC2MGsv0pGJFQ1lrOg8klR9U,1741
|
|
23
|
+
hypern/db/nosql/addons/password.py,sha256=jfZxvWFm6nV9EWpXq5Mj-jpqnl9QbokZj9WT14n7dKE,5035
|
|
24
|
+
hypern/db/nosql/addons/unicode.py,sha256=LaDpLfdoTcJuASPE-8fqOVD05H_uOx8gOdnyDn5Iu0c,268
|
|
25
|
+
hypern/db/nosql/addons/__init__.py,sha256=WEtPM8sPHilvga7zxwqvINeTkF0hdcfgPcAnHc4MASE,125
|
|
26
|
+
hypern/db/nosql/__init__.py,sha256=MH9YvlbRlbBCrQVNOdfTaK-hINwJxbJLmxwY9Mei7I8,644
|
|
27
|
+
hypern/db/sql/addons/color.py,sha256=Jj8q4lkT0ukKyjVyZjBx7fokGrX7AIJpKOGzwsBpfnU,480
|
|
28
|
+
hypern/db/sql/addons/daterange.py,sha256=qEfQN9c4jQdzeXNYKgQ4VIJ3Qc0HXHWRouzNF1se-RA,853
|
|
29
|
+
hypern/db/sql/addons/datetime.py,sha256=Bp2jMja2lb_b2WnzRnfbjXXTHBgBTyph1ECsItIwvvg,643
|
|
30
|
+
hypern/db/sql/addons/encrypted.py,sha256=pXsg4ImPpK-VkLDruKrv2gUtdp2kajQX7xDbRDxa-SI,1930
|
|
31
|
+
hypern/db/sql/addons/password.py,sha256=9pypORygWaINj3oiAOBRIOGgpuA0lcjPq4rh1pqJxq0,5789
|
|
32
|
+
hypern/db/sql/addons/ts_vector.py,sha256=bQyXYvQ1bAfFpYcS-sFwM7fU6L1lg9_7nGDRGp0CoUo,1361
|
|
33
|
+
hypern/db/sql/addons/unicode.py,sha256=rbqyHlsPUqRDWIjYQSRFF3zkHncnwxo0sdlzqUlcrUw,411
|
|
34
|
+
hypern/db/sql/addons/__init__.py,sha256=mLN_AvwgpSAbrWZvVHZuO7ff0gk1T_JbVwd5wug5nlw,359
|
|
35
|
+
hypern/db/sql/repository.py,sha256=ue6vWOTrnEPyDevlyh3v-7PU6GSfrZHYKrbXVuoS8UA,9516
|
|
36
|
+
hypern/db/sql/__init__.py,sha256=1UoWQi2CIcUAbQj3FadR-8V0o_b286nI2wYvOsvtbFc,6478
|
|
37
|
+
hypern/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
38
|
+
hypern/enum.py,sha256=KcVziJj7vWvyie0r2rtxhrLzdtkZAsf0DY58oJ4tQl4,360
|
|
39
|
+
hypern/exceptions.py,sha256=nHTkF0YdNBMKfSiNtjRMHMNKoY3RMUm68YYluuW15us,2428
|
|
40
|
+
hypern/hypern.pyi,sha256=PsoQ-UV3q_xJs0BsW3In8gVWJjmqrffCY4xTC0hSDfk,6915
|
|
41
|
+
hypern/i18n/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
42
|
+
hypern/logging/logger.py,sha256=ebLT-DX5OOt1UqP2OajFTm4I9tL6RSBN_GbJEKe4UI4,3256
|
|
43
|
+
hypern/logging/__init__.py,sha256=6eVriyncsJ4J73fGYhoejv9MX7aGTkRezTpPxO4DX1I,52
|
|
44
|
+
hypern/middleware/base.py,sha256=mJoz-i-7lqw1eDxZ8Bb0t8sz60lx5TE-OjZT4UR75e4,541
|
|
45
|
+
hypern/middleware/cors.py,sha256=x90DnCOJSfp4ojm1krttn_EdtlqeDazyUzVg66NES4A,1681
|
|
46
|
+
hypern/middleware/i18n.py,sha256=jHzVzjTx1nnjbraZtIVOprrnSaeKMxZB8RuSqRp2I4s,16
|
|
47
|
+
hypern/middleware/limit.py,sha256=S2abCkQb3yGEQlXZ8azGIlpzTmE8eS6gt-c546IC9_c,8148
|
|
48
|
+
hypern/middleware/__init__.py,sha256=lXwR3fdmpVK4Z7QWaLsgf3Sazy5NPPFXIOxIEv1xDC8,273
|
|
49
|
+
hypern/openapi/schemas.py,sha256=YHfMlPUeP5DzDX5ao3YH8p_25Vvyaf616dh6XDCUZRc,1677
|
|
50
|
+
hypern/openapi/swagger.py,sha256=naqUY3rFAEYA1ZLIlmDsMYaol0yIm6TVebdkFa5cMTc,64
|
|
51
|
+
hypern/openapi/__init__.py,sha256=4rEVD8pa0kdSpsy7ZkJ5JY0Z2XF0NGSKDMwYAd7YZpE,141
|
|
52
|
+
hypern/processpool.py,sha256=_vtC9mmEisfDWuSH9prvgaipbHdP5tszPWEwqsxQGo4,3031
|
|
53
|
+
hypern/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
54
|
+
hypern/response/response.py,sha256=-dnboAraPic8asf503PxwmDuxhNllUO5h97_DGmbER4,4582
|
|
55
|
+
hypern/response/__init__.py,sha256=_w3u3TDNuYx5ejnnN1unqnTY8NlBgUATQi6wepEB_FQ,226
|
|
56
|
+
hypern/routing/dispatcher.py,sha256=i2wLAAW1ZXgpi5K2heGXhTODnP1WdQzaR5WlUjs1o9c,2368
|
|
57
|
+
hypern/routing/endpoint.py,sha256=RKVhvqOEGL9IKBXQ3KJgPi9bgJj9gfWC5BdZc5U_atc,1026
|
|
58
|
+
hypern/routing/parser.py,sha256=R-4lcN9Ha1iMeAjlqDe8HwkjjMVG-c-ubQLZyWKXj6M,3554
|
|
59
|
+
hypern/routing/route.py,sha256=tW6orvfWz9O1XDWD295IuovALa4rBd4zwTsRwWaMZJk,10374
|
|
60
|
+
hypern/routing/__init__.py,sha256=7rw7EAxougCXtmkgJjrmLP3N5RXctIpI_3JmG9FcKVU,101
|
|
61
|
+
hypern/scheduler.py,sha256=-k3tW2AGCnHYSthKXk-FOs_SCtWp3yIxQzwzUJMJsbo,67
|
|
62
|
+
hypern/security.py,sha256=3E86Yp_eOSVa1emUvBrDgoF0Sn6eNX0CfLnt87w5CPI,1773
|
|
63
|
+
hypern/worker.py,sha256=WQrhY_awR6zjMwY4Q7izXi4E4fFrDqt7jIblUW8Bzcg,924
|
|
64
|
+
hypern/__init__.py,sha256=9Ww_aUQ0vJls0tOq7Yw1_TVOCRsa5bHJ-RtnSeComwk,119
|
|
65
|
+
hypern/hypern.cp312-win32.pyd,sha256=UvRTM7wqjq0Nu1LOtf_Dv_akLU_9gSOo2rjOd3beV8s,5274112
|
|
66
|
+
hypern-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, Dang Van Nghiem
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
16
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
17
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
18
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
19
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
20
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
21
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
23
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
24
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|