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.
Files changed (66) hide show
  1. hypern/__init__.py +4 -0
  2. hypern/application.py +412 -0
  3. hypern/auth/__init__.py +0 -0
  4. hypern/auth/authorization.py +2 -0
  5. hypern/background.py +4 -0
  6. hypern/caching/__init__.py +0 -0
  7. hypern/caching/base/__init__.py +8 -0
  8. hypern/caching/base/backend.py +3 -0
  9. hypern/caching/base/key_maker.py +8 -0
  10. hypern/caching/cache_manager.py +56 -0
  11. hypern/caching/cache_tag.py +10 -0
  12. hypern/caching/custom_key_maker.py +11 -0
  13. hypern/caching/redis_backend.py +3 -0
  14. hypern/cli/__init__.py +0 -0
  15. hypern/cli/commands.py +0 -0
  16. hypern/config.py +149 -0
  17. hypern/datastructures.py +40 -0
  18. hypern/db/__init__.py +0 -0
  19. hypern/db/nosql/__init__.py +25 -0
  20. hypern/db/nosql/addons/__init__.py +4 -0
  21. hypern/db/nosql/addons/color.py +16 -0
  22. hypern/db/nosql/addons/daterange.py +30 -0
  23. hypern/db/nosql/addons/encrypted.py +53 -0
  24. hypern/db/nosql/addons/password.py +134 -0
  25. hypern/db/nosql/addons/unicode.py +10 -0
  26. hypern/db/sql/__init__.py +179 -0
  27. hypern/db/sql/addons/__init__.py +14 -0
  28. hypern/db/sql/addons/color.py +16 -0
  29. hypern/db/sql/addons/daterange.py +23 -0
  30. hypern/db/sql/addons/datetime.py +22 -0
  31. hypern/db/sql/addons/encrypted.py +58 -0
  32. hypern/db/sql/addons/password.py +171 -0
  33. hypern/db/sql/addons/ts_vector.py +46 -0
  34. hypern/db/sql/addons/unicode.py +15 -0
  35. hypern/db/sql/repository.py +290 -0
  36. hypern/enum.py +13 -0
  37. hypern/exceptions.py +97 -0
  38. hypern/hypern.cp312-win32.pyd +0 -0
  39. hypern/hypern.pyi +266 -0
  40. hypern/i18n/__init__.py +0 -0
  41. hypern/logging/__init__.py +3 -0
  42. hypern/logging/logger.py +82 -0
  43. hypern/middleware/__init__.py +5 -0
  44. hypern/middleware/base.py +18 -0
  45. hypern/middleware/cors.py +38 -0
  46. hypern/middleware/i18n.py +1 -0
  47. hypern/middleware/limit.py +176 -0
  48. hypern/openapi/__init__.py +5 -0
  49. hypern/openapi/schemas.py +53 -0
  50. hypern/openapi/swagger.py +3 -0
  51. hypern/processpool.py +106 -0
  52. hypern/py.typed +0 -0
  53. hypern/response/__init__.py +3 -0
  54. hypern/response/response.py +134 -0
  55. hypern/routing/__init__.py +4 -0
  56. hypern/routing/dispatcher.py +67 -0
  57. hypern/routing/endpoint.py +30 -0
  58. hypern/routing/parser.py +100 -0
  59. hypern/routing/route.py +284 -0
  60. hypern/scheduler.py +5 -0
  61. hypern/security.py +44 -0
  62. hypern/worker.py +30 -0
  63. hypern-0.2.0.dist-info/METADATA +127 -0
  64. hypern-0.2.0.dist-info/RECORD +66 -0
  65. hypern-0.2.0.dist-info/WHEEL +4 -0
  66. hypern-0.2.0.dist-info/licenses/LICENSE +24 -0
hypern/processpool.py ADDED
@@ -0,0 +1,106 @@
1
+ import asyncio
2
+ import signal
3
+ import sys
4
+ from typing import Any, Dict, List
5
+
6
+ from multiprocess import Process
7
+
8
+ from .hypern import FunctionInfo, Router, Server, SocketHeld
9
+ from .logging import logger
10
+
11
+
12
+ def run_processes(
13
+ host: str,
14
+ port: int,
15
+ workers: int,
16
+ processes: int,
17
+ max_blocking_threads: int,
18
+ router: Router,
19
+ injectables: Dict[str, Any],
20
+ before_request: List[FunctionInfo],
21
+ after_request: List[FunctionInfo],
22
+ response_headers: Dict[str, str],
23
+ ) -> List[Process]:
24
+ socket = SocketHeld(host, port)
25
+
26
+ process_pool = init_processpool(router, socket, workers, processes, max_blocking_threads, injectables, before_request, after_request, response_headers)
27
+
28
+ def terminating_signal_handler(_sig, _frame):
29
+ logger.info("Terminating server!!")
30
+ for process in process_pool:
31
+ process.kill()
32
+
33
+ signal.signal(signal.SIGINT, terminating_signal_handler)
34
+ signal.signal(signal.SIGTERM, terminating_signal_handler)
35
+
36
+ logger.info("Press Ctrl + C to stop \n")
37
+ for process in process_pool:
38
+ process.join()
39
+
40
+ return process_pool
41
+
42
+
43
+ def init_processpool(
44
+ router: Router,
45
+ socket: SocketHeld,
46
+ workers: int,
47
+ processes: int,
48
+ max_blocking_threads: int,
49
+ injectables: Dict[str, Any],
50
+ before_request: List[FunctionInfo],
51
+ after_request: List[FunctionInfo],
52
+ response_headers: Dict[str, str],
53
+ ) -> List[Process]:
54
+ process_pool = []
55
+
56
+ for _ in range(processes):
57
+ copied_socket = socket.try_clone()
58
+ process = Process(
59
+ target=spawn_process,
60
+ args=(router, copied_socket, workers, max_blocking_threads, injectables, before_request, after_request, response_headers),
61
+ )
62
+ process.start()
63
+ process_pool.append(process)
64
+
65
+ return process_pool
66
+
67
+
68
+ def initialize_event_loop():
69
+ if sys.platform.startswith("win32") or sys.platform.startswith("linux-cross"):
70
+ loop = asyncio.new_event_loop()
71
+ asyncio.set_event_loop(loop)
72
+ return loop
73
+ else:
74
+ import uvloop
75
+
76
+ uvloop.install()
77
+ loop = uvloop.new_event_loop()
78
+ asyncio.set_event_loop(loop)
79
+ return loop
80
+
81
+
82
+ def spawn_process(
83
+ router: Router,
84
+ socket: SocketHeld,
85
+ workers: int,
86
+ max_blocking_threads: int,
87
+ injectables: Dict[str, Any],
88
+ before_request: List[FunctionInfo],
89
+ after_request: List[FunctionInfo],
90
+ response_headers: Dict[str, str],
91
+ ):
92
+ loop = initialize_event_loop()
93
+
94
+ server = Server()
95
+ server.set_router(router=router)
96
+ server.set_injected(injected=injectables)
97
+ server.set_before_hooks(hooks=before_request)
98
+ server.set_after_hooks(hooks=after_request)
99
+ server.set_response_headers(headers=response_headers)
100
+
101
+ try:
102
+ server.start(socket, workers, max_blocking_threads)
103
+ loop = asyncio.get_event_loop()
104
+ loop.run_forever()
105
+ except KeyboardInterrupt:
106
+ loop.close()
hypern/py.typed ADDED
File without changes
@@ -0,0 +1,3 @@
1
+ from .response import Response, JSONResponse, HTMLResponse, PlainTextResponse, RedirectResponse, FileResponse
2
+
3
+ __all__ = ["Response", "JSONResponse", "HTMLResponse", "PlainTextResponse", "RedirectResponse", "FileResponse"]
@@ -0,0 +1,134 @@
1
+ from __future__ import annotations
2
+
3
+ import typing
4
+ from urllib.parse import quote
5
+ from hypern.hypern import Response as InternalResponse, Header
6
+ import orjson
7
+
8
+ from hypern.background import BackgroundTask, BackgroundTasks
9
+
10
+
11
+ class BaseResponse:
12
+ media_type = None
13
+ charset = "utf-8"
14
+
15
+ def __init__(
16
+ self,
17
+ content: typing.Any = None,
18
+ status_code: int = 200,
19
+ headers: typing.Mapping[str, str] | None = None,
20
+ media_type: str | None = None,
21
+ backgrounds: typing.List[BackgroundTask] | None = None,
22
+ ) -> None:
23
+ self.status_code = status_code
24
+ if media_type is not None:
25
+ self.media_type = media_type
26
+ self.body = self.render(content)
27
+ self.init_headers(headers)
28
+ self.backgrounds = backgrounds
29
+
30
+ def render(self, content: typing.Any) -> bytes | memoryview:
31
+ if content is None:
32
+ return b""
33
+ if isinstance(content, (bytes, memoryview)):
34
+ return content
35
+ if isinstance(content, str):
36
+ return content.encode(self.charset)
37
+ return orjson.dumps(content) # type: ignore
38
+
39
+ def init_headers(self, headers: typing.Mapping[str, str] | None = None) -> None:
40
+ if headers is None:
41
+ raw_headers: dict = {}
42
+ populate_content_length = True
43
+ populate_content_type = True
44
+ else:
45
+ raw_headers = {k.lower(): v for k, v in headers.items()}
46
+ keys = raw_headers.keys()
47
+ populate_content_length = "content-length" not in keys
48
+ populate_content_type = "content-type" not in keys
49
+
50
+ body = getattr(self, "body", None)
51
+ if body is not None and populate_content_length and not (self.status_code < 200 or self.status_code in (204, 304)):
52
+ content_length = str(len(body))
53
+ raw_headers.setdefault("content-length", content_length)
54
+
55
+ content_type = self.media_type
56
+ if content_type is not None and populate_content_type:
57
+ if content_type.startswith("text/") and "charset=" not in content_type.lower():
58
+ content_type += "; charset=" + self.charset
59
+ raw_headers.setdefault("content-type", content_type)
60
+
61
+ self.raw_headers = raw_headers
62
+
63
+
64
+ def to_response(cls):
65
+ class ResponseWrapper(cls):
66
+ def __new__(cls, *args, **kwargs):
67
+ instance = super().__new__(cls)
68
+ instance.__init__(*args, **kwargs)
69
+ # Execute background tasks
70
+ task_manager = BackgroundTasks()
71
+ if instance.backgrounds:
72
+ for task in instance.backgrounds:
73
+ task_manager.add_task(task)
74
+ task_manager.execute_all()
75
+ del task_manager
76
+
77
+ headers = Header(instance.raw_headers)
78
+ return InternalResponse(
79
+ status_code=instance.status_code,
80
+ headers=headers,
81
+ description=instance.body,
82
+ )
83
+
84
+ return ResponseWrapper
85
+
86
+
87
+ @to_response
88
+ class Response(BaseResponse):
89
+ media_type = None
90
+ charset = "utf-8"
91
+
92
+
93
+ @to_response
94
+ class JSONResponse(BaseResponse):
95
+ media_type = "application/json"
96
+
97
+
98
+ @to_response
99
+ class HTMLResponse(BaseResponse):
100
+ media_type = "text/html"
101
+
102
+
103
+ @to_response
104
+ class PlainTextResponse(BaseResponse):
105
+ media_type = "text/plain"
106
+
107
+
108
+ @to_response
109
+ class RedirectResponse(BaseResponse):
110
+ def __init__(
111
+ self,
112
+ url: str,
113
+ status_code: int = 307,
114
+ headers: typing.Mapping[str, str] | None = None,
115
+ backgrounds: typing.List[BackgroundTask] | None = None,
116
+ ) -> None:
117
+ super().__init__(content=b"", status_code=status_code, headers=headers, backgrounds=backgrounds)
118
+ self.raw_headers["location"] = quote(str(url), safe=":/%#?=@[]!$&'()*+,;")
119
+
120
+
121
+ @to_response
122
+ class FileResponse(BaseResponse):
123
+ def __init__(
124
+ self,
125
+ content: bytes | memoryview,
126
+ filename: str,
127
+ status_code: int = 200,
128
+ headers: typing.Mapping[str, str] | None = None,
129
+ backgrounds: typing.List[BackgroundTask] | None = None,
130
+ ) -> None:
131
+ super().__init__(content=content, status_code=status_code, headers=headers, backgrounds=backgrounds)
132
+ self.raw_headers["content-disposition"] = f'attachment; filename="{filename}"'
133
+ self.raw_headers.setdefault("content-type", "application/octet-stream")
134
+ self.raw_headers.setdefault("content-length", str(len(content)))
@@ -0,0 +1,4 @@
1
+ from .route import Route
2
+ from .endpoint import HTTPEndpoint
3
+
4
+ __all__ = ["Route", "HTTPEndpoint"]
@@ -0,0 +1,67 @@
1
+ # -*- coding: utf-8 -*-
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import functools
6
+ import inspect
7
+ import traceback
8
+ import typing
9
+
10
+ import orjson
11
+ from pydantic import BaseModel
12
+
13
+ from hypern.exceptions import BaseException
14
+ from hypern.hypern import Request, Response
15
+ from hypern.response import JSONResponse
16
+
17
+ from .parser import InputHandler
18
+
19
+
20
+ def is_async_callable(obj: typing.Any) -> bool:
21
+ while isinstance(obj, functools.partial):
22
+ obj = obj.funcz
23
+ return asyncio.iscoroutinefunction(obj) or (callable(obj) and asyncio.iscoroutinefunction(obj.__call__))
24
+
25
+
26
+ async def run_in_threadpool(func: typing.Callable, *args, **kwargs):
27
+ if kwargs: # pragma: no cover
28
+ # run_sync doesn't accept 'kwargs', so bind them in here
29
+ func = functools.partial(func, **kwargs)
30
+ return await asyncio.to_thread(func, *args)
31
+
32
+
33
+ async def dispatch(handler, request: Request, inject: typing.Dict[str, typing.Any]) -> Response:
34
+ try:
35
+ is_async = is_async_callable(handler)
36
+ signature = inspect.signature(handler)
37
+ input_handler = InputHandler(request)
38
+ _response_type = signature.return_annotation
39
+ _kwargs = await input_handler.get_input_handler(signature, inject)
40
+
41
+ if is_async:
42
+ response = await handler(**_kwargs) # type: ignore
43
+ else:
44
+ response = await run_in_threadpool(handler, **_kwargs)
45
+ if not isinstance(response, Response):
46
+ if isinstance(_response_type, type) and issubclass(_response_type, BaseModel):
47
+ response = _response_type.model_validate(response).model_dump(mode="json") # type: ignore
48
+ response = JSONResponse(
49
+ content=orjson.dumps({"message": response, "error_code": None}),
50
+ status_code=200,
51
+ )
52
+
53
+ except Exception as e:
54
+ _res: typing.Dict = {"message": "", "error_code": "UNKNOWN_ERROR"}
55
+ if isinstance(e, BaseException):
56
+ _res["error_code"] = e.error_code
57
+ _res["message"] = e.msg
58
+ _status = e.status
59
+ else:
60
+ traceback.print_exc()
61
+ _res["message"] = str(e)
62
+ _status = 400
63
+ response = JSONResponse(
64
+ content=orjson.dumps(_res),
65
+ status_code=_status,
66
+ )
67
+ return response
@@ -0,0 +1,30 @@
1
+ # -*- coding: utf-8 -*-
2
+ from __future__ import annotations
3
+
4
+ import typing
5
+ from typing import Any, Dict
6
+
7
+ import orjson
8
+
9
+ from hypern.hypern import Request, Response
10
+ from hypern.response import JSONResponse
11
+
12
+ from .dispatcher import dispatch
13
+
14
+
15
+ class HTTPEndpoint:
16
+ def __init__(self, *args, **kwargs) -> None:
17
+ super().__init__(*args, **kwargs)
18
+
19
+ def method_not_allowed(self, request: Request) -> Response:
20
+ return JSONResponse(
21
+ description=orjson.dumps({"message": "Method Not Allowed", "error_code": "METHOD_NOT_ALLOW"}),
22
+ status_code=405,
23
+ )
24
+
25
+ async def dispatch(self, request: Request, inject: Dict[str, Any]) -> Response:
26
+ handler_name = "get" if request.method == "HEAD" and not hasattr(self, "head") else request.method.lower()
27
+ handler: typing.Callable[[Request], typing.Any] = getattr( # type: ignore
28
+ self, handler_name, self.method_not_allowed
29
+ )
30
+ return await dispatch(handler, request, inject)
@@ -0,0 +1,100 @@
1
+ # -*- coding: utf-8 -*-
2
+ from __future__ import annotations
3
+
4
+ import inspect
5
+ import typing
6
+
7
+ import orjson
8
+ from pydantic import BaseModel, ValidationError
9
+ from pydash import get
10
+
11
+ from hypern.auth.authorization import Authorization
12
+ from hypern.exceptions import BadRequest
13
+ from hypern.exceptions import ValidationError as HypernValidationError
14
+ from hypern.hypern import Request
15
+
16
+
17
+ class ParamParser:
18
+ def __init__(self, request: Request):
19
+ self.request = request
20
+
21
+ def parse_data_by_name(self, param_name: str) -> dict:
22
+ param_name = param_name.lower()
23
+ data_parsers = {
24
+ "query_params": self._parse_query_params,
25
+ "path_params": self._parse_path_params,
26
+ "form_data": self._parse_form_data,
27
+ }
28
+
29
+ parser = data_parsers.get(param_name)
30
+ if not parser:
31
+ raise BadRequest(msg="Backend Error: Invalid parameter type, must be query_params, path_params or form_data.")
32
+ return parser()
33
+
34
+ def _parse_query_params(self) -> dict:
35
+ query_params = self.request.query_params.to_dict()
36
+ return {k: v[0] for k, v in query_params.items()}
37
+
38
+ def _parse_path_params(self) -> dict:
39
+ return lambda: dict(self.request.path_params.items())
40
+
41
+ def _parse_form_data(self) -> dict:
42
+ return self.request.json()
43
+
44
+
45
+ class InputHandler:
46
+ def __init__(self, request):
47
+ self.request = request
48
+ self.param_parser = ParamParser(request)
49
+
50
+ async def parse_pydantic_model(self, param_name: str, model_class: typing.Type[BaseModel]) -> BaseModel:
51
+ try:
52
+ data = self.param_parser.parse_data_by_name(param_name)
53
+ return model_class(**data)
54
+ except ValidationError as e:
55
+ invalid_fields = orjson.loads(e.json())
56
+ raise HypernValidationError(
57
+ msg=orjson.dumps(
58
+ [
59
+ {
60
+ "field": get(item, "loc")[0],
61
+ "msg": get(item, "msg"),
62
+ }
63
+ for item in invalid_fields
64
+ ]
65
+ ).decode("utf-8"),
66
+ )
67
+
68
+ async def handle_special_params(self, param_name: str) -> typing.Any:
69
+ special_params = {
70
+ "request": lambda: self.request,
71
+ }
72
+ return special_params.get(param_name, lambda: None)()
73
+
74
+ async def get_input_handler(self, signature: inspect.Signature, inject: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:
75
+ """
76
+ Parse the request data and return the kwargs for the handler
77
+ """
78
+ kwargs = {}
79
+
80
+ for param in signature.parameters.values():
81
+ name = param.name
82
+ ptype = param.annotation
83
+
84
+ # Handle Pydantic models
85
+ if isinstance(ptype, type) and issubclass(ptype, BaseModel):
86
+ kwargs[name] = await self.parse_pydantic_model(name, ptype)
87
+ continue
88
+
89
+ # Handle Authorization
90
+ if isinstance(ptype, type) and issubclass(ptype, Authorization):
91
+ kwargs[name] = await ptype().validate(self.request)
92
+ continue
93
+
94
+ # Handle special parameters
95
+ special_value = await self.handle_special_params(name)
96
+ if special_value is not None:
97
+ kwargs[name] = special_value
98
+ if name in inject:
99
+ kwargs[name] = inject[name]
100
+ return kwargs