hypern 0.3.11__tar.gz → 0.3.13__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.
- {hypern-0.3.11 → hypern-0.3.13}/.github/workflows/security-scan.yml +1 -0
- {hypern-0.3.11 → hypern-0.3.13}/Cargo.lock +1 -1
- {hypern-0.3.11 → hypern-0.3.13}/Cargo.toml +1 -1
- {hypern-0.3.11 → hypern-0.3.13}/PKG-INFO +1 -1
- {hypern-0.3.11 → hypern-0.3.13}/hypern/__init__.py +6 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/application.py +5 -4
- {hypern-0.3.11 → hypern-0.3.13}/hypern/datastructures.py +0 -13
- {hypern-0.3.11 → hypern-0.3.13}/hypern/enum.py +12 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/processpool.py +0 -2
- {hypern-0.3.11 → hypern-0.3.13}/hypern/response/response.py +1 -3
- {hypern-0.3.11 → hypern-0.3.13}/hypern/routing/parser.py +1 -1
- {hypern-0.3.11 → hypern-0.3.13}/hypern/routing/route.py +22 -12
- {hypern-0.3.11 → hypern-0.3.13}/pyproject.toml +2 -2
- {hypern-0.3.11 → hypern-0.3.13}/src/lib.rs +0 -2
- hypern-0.3.13/src/router/mod.rs +3 -0
- hypern-0.3.13/src/router/radix.rs +226 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/router/route.rs +25 -5
- hypern-0.3.13/src/router/router.rs +143 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/server.rs +7 -6
- {hypern-0.3.11 → hypern-0.3.13}/src/types/mod.rs +0 -2
- {hypern-0.3.11 → hypern-0.3.13}/src/types/request.rs +2 -2
- hypern-0.3.11/hypern/background.py +0 -4
- hypern-0.3.11/hypern/scheduler.py +0 -5
- hypern-0.3.11/src/mem_pool.rs +0 -155
- hypern-0.3.11/src/router/mod.rs +0 -2
- hypern-0.3.11/src/router/router.rs +0 -237
- hypern-0.3.11/src/types/http.rs +0 -16
- hypern-0.3.11/src/types/url.rs +0 -24
- {hypern-0.3.11 → hypern-0.3.13}/.github/dependabot.yml +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/.github/workflows/codeql.yml +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/.github/workflows/codspeed.yml +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/.github/workflows/preview-deployments.yml +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/.github/workflows/release-CI.yml +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/.gitignore +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/.pre-commit-config.yaml +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/CODE_OF_CONDUCT.md +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/LICENSE +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/README.md +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/SECURITY.md +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/benchmark.sh +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/docs/background.md +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/docs/cache.md +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/docs/database.md +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/docs/index.md +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/docs/response.md +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/docs/schedule.md +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/docs/websocket.md +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/args_parser.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/auth/__init__.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/auth/authorization.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/caching/__init__.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/caching/backend.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/caching/redis_backend.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/caching/strategies.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/cli/__init__.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/cli/commands.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/config.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/database/__init__.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/database/sqlalchemy/__init__.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/database/sqlalchemy/config.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/database/sqlalchemy/repository.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/database/sqlx/__init__.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/database/sqlx/field.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/database/sqlx/migrate.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/database/sqlx/model.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/database/sqlx/query.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/exceptions/__init__.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/exceptions/base.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/exceptions/common.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/exceptions/errors.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/exceptions/formatters.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/exceptions/http.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/gateway/__init__.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/gateway/aggregator.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/gateway/gateway.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/gateway/proxy.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/gateway/service.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/hypern.pyi +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/i18n/__init__.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/logging/__init__.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/logging/logger.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/middleware/__init__.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/middleware/base.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/middleware/cache.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/middleware/compress.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/middleware/cors.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/middleware/i18n.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/middleware/limit.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/middleware/security.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/openapi/__init__.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/openapi/schemas.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/openapi/swagger.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/py.typed +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/reload.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/response/__init__.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/routing/__init__.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/routing/dispatcher.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/routing/endpoint.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/routing/queue.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/worker.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/ws/__init__.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/ws/channel.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/ws/heartbeat.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/ws/room.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/hypern/ws/route.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/poetry.lock +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/scripts/format.sh +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/sonar-project.properties +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/background/background_task.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/background/background_tasks.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/background/mod.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/database/context.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/database/mod.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/database/sql/config.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/database/sql/connection.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/database/sql/db_trait.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/database/sql/mod.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/database/sql/mysql.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/database/sql/postgresql.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/database/sql/sqlite.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/database/sql/transaction.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/di.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/executor.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/instants.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/middlewares/base.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/middlewares/mod.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/openapi/mod.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/openapi/schemas.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/openapi/swagger.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/scheduler/job.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/scheduler/mod.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/scheduler/retry.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/scheduler/scheduler.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/types/body.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/types/function_info.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/types/header.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/types/middleware.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/types/query.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/types/response.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/ws/mod.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/ws/route.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/ws/router.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/ws/socket.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/src/ws/websocket.rs +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/tests/__init__.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/tests/conftest.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/tests/server.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/tests/test_functional_handler.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/tests/test_request_file.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/tests/test_response_type.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/tests/test_sync_async.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/tests/test_validate_field.py +0 -0
- {hypern-0.3.11 → hypern-0.3.13}/tests/utils.py +0 -0
@@ -5,6 +5,9 @@ from hypern.ws import WebsocketRoute, WebSocketSession
|
|
5
5
|
from .application import Hypern
|
6
6
|
from .hypern import Request, Response
|
7
7
|
from .response import FileResponse, HTMLResponse, JSONResponse, PlainTextResponse, RedirectResponse
|
8
|
+
from .hypern import BackgroundTask
|
9
|
+
from .hypern import BackgroundTasks
|
10
|
+
from .hypern import Scheduler
|
8
11
|
|
9
12
|
__all__ = [
|
10
13
|
"Hypern",
|
@@ -21,4 +24,7 @@ __all__ = [
|
|
21
24
|
"PlainTextResponse",
|
22
25
|
"RedirectResponse",
|
23
26
|
"logger",
|
27
|
+
"BackgroundTask",
|
28
|
+
"BackgroundTasks",
|
29
|
+
"Scheduler",
|
24
30
|
]
|
@@ -10,8 +10,9 @@ import psutil
|
|
10
10
|
from typing_extensions import Annotated, Doc
|
11
11
|
|
12
12
|
from hypern.args_parser import ArgsConfig
|
13
|
-
from hypern.datastructures import Contact,
|
14
|
-
from hypern.
|
13
|
+
from hypern.datastructures import Contact, Info, License
|
14
|
+
from hypern.enum import HTTPMethod
|
15
|
+
from hypern.hypern import DatabaseConfig, FunctionInfo, MiddlewareConfig, Router, Server, WebsocketRouter, Scheduler
|
15
16
|
from hypern.hypern import Route as InternalRoute
|
16
17
|
from hypern.logging import logger
|
17
18
|
from hypern.middleware import Middleware
|
@@ -19,7 +20,6 @@ from hypern.openapi import SchemaGenerator, SwaggerUI
|
|
19
20
|
from hypern.processpool import run_processes
|
20
21
|
from hypern.response import HTMLResponse, JSONResponse
|
21
22
|
from hypern.routing import Route
|
22
|
-
from hypern.scheduler import Scheduler
|
23
23
|
from hypern.ws import WebsocketRoute
|
24
24
|
|
25
25
|
AppType = TypeVar("AppType", bound="Hypern")
|
@@ -243,7 +243,7 @@ class Hypern:
|
|
243
243
|
self.thread_config = ThreadConfigurator().get_config()
|
244
244
|
|
245
245
|
for route in routes or []:
|
246
|
-
self.router.extend_route(route(
|
246
|
+
self.router.extend_route(route())
|
247
247
|
|
248
248
|
for websocket_route in websockets or []:
|
249
249
|
for route in websocket_route.routes:
|
@@ -438,6 +438,7 @@ class Hypern:
|
|
438
438
|
self.args.max_blocking_threads = self.thread_config.max_blocking_threads
|
439
439
|
|
440
440
|
if self.args.http2:
|
441
|
+
logger.info("HTTP/2 enabled")
|
441
442
|
server.enable_http2()
|
442
443
|
|
443
444
|
run_processes(
|
@@ -1,5 +1,4 @@
|
|
1
1
|
from typing import Optional
|
2
|
-
from enum import Enum
|
3
2
|
from pydantic import BaseModel, AnyUrl
|
4
3
|
|
5
4
|
|
@@ -26,15 +25,3 @@ class Info(BaseModelWithConfig):
|
|
26
25
|
contact: Optional[Contact] = None
|
27
26
|
license: Optional[License] = None
|
28
27
|
version: str
|
29
|
-
|
30
|
-
|
31
|
-
class HTTPMethod(Enum):
|
32
|
-
GET = "GET"
|
33
|
-
POST = "POST"
|
34
|
-
PUT = "PUT"
|
35
|
-
DELETE = "DELETE"
|
36
|
-
PATCH = "PATCH"
|
37
|
-
OPTIONS = "OPTIONS"
|
38
|
-
HEAD = "HEAD"
|
39
|
-
TRACE = "TRACE"
|
40
|
-
CONNECT = "CONNECT"
|
@@ -11,3 +11,15 @@ class ErrorCode(Enum):
|
|
11
11
|
METHOD_NOT_ALLOW = "METHOD_NOT_ALLOW"
|
12
12
|
UNAUTHORIZED = "UNAUTHORIZED"
|
13
13
|
VALIDATION_ERROR = "VALIDATION_ERROR"
|
14
|
+
|
15
|
+
|
16
|
+
class HTTPMethod(Enum):
|
17
|
+
GET = "GET"
|
18
|
+
POST = "POST"
|
19
|
+
PUT = "PUT"
|
20
|
+
DELETE = "DELETE"
|
21
|
+
PATCH = "PATCH"
|
22
|
+
OPTIONS = "OPTIONS"
|
23
|
+
HEAD = "HEAD"
|
24
|
+
TRACE = "TRACE"
|
25
|
+
CONNECT = "CONNECT"
|
@@ -120,8 +120,6 @@ def initialize_event_loop(max_blocking_threads: int = 100) -> asyncio.AbstractEv
|
|
120
120
|
loop = uvloop.new_event_loop()
|
121
121
|
asyncio.set_event_loop(loop)
|
122
122
|
|
123
|
-
loop.slow_callback_duration = 0.1 # Log warnings for slow callbacks
|
124
|
-
loop.set_debug(False) # Disable debug mode
|
125
123
|
return loop
|
126
124
|
|
127
125
|
|
@@ -2,12 +2,10 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import typing
|
4
4
|
from urllib.parse import quote
|
5
|
-
from hypern.hypern import Response as InternalResponse, Header
|
5
|
+
from hypern.hypern import Response as InternalResponse, Header, BackgroundTask, BackgroundTasks
|
6
6
|
import orjson
|
7
7
|
import msgpack
|
8
8
|
|
9
|
-
from hypern.background import BackgroundTask, BackgroundTasks
|
10
|
-
|
11
9
|
|
12
10
|
class BaseResponse:
|
13
11
|
media_type = None
|
@@ -36,7 +36,7 @@ class ParamParser:
|
|
36
36
|
return {k: v[0] for k, v in query_params.items()}
|
37
37
|
|
38
38
|
def _parse_path_params(self) -> dict:
|
39
|
-
return
|
39
|
+
return dict(self.request.path_params.items())
|
40
40
|
|
41
41
|
def _parse_form_data(self) -> dict:
|
42
42
|
return self.request.json()
|
@@ -9,8 +9,10 @@ from pydantic import BaseModel
|
|
9
9
|
from pydantic.fields import FieldInfo
|
10
10
|
|
11
11
|
from hypern.auth.authorization import Authorization
|
12
|
-
|
13
|
-
from hypern.
|
12
|
+
|
13
|
+
from hypern.enum import HTTPMethod
|
14
|
+
from hypern.hypern import FunctionInfo, Request
|
15
|
+
|
14
16
|
from hypern.hypern import Route as InternalRoute
|
15
17
|
|
16
18
|
from .dispatcher import dispatch
|
@@ -20,6 +22,16 @@ def get_field_type(field):
|
|
20
22
|
return field.outer_type_
|
21
23
|
|
22
24
|
|
25
|
+
def join_url_paths(*parts):
|
26
|
+
first = parts[0]
|
27
|
+
parts = [part.strip("/") for part in parts]
|
28
|
+
starts_with_slash = first.startswith("/") if first else False
|
29
|
+
joined = "/".join(part for part in parts if part)
|
30
|
+
if starts_with_slash:
|
31
|
+
joined = "/" + joined
|
32
|
+
return joined
|
33
|
+
|
34
|
+
|
23
35
|
def pydantic_to_swagger(model: type[BaseModel] | dict):
|
24
36
|
if isinstance(model, dict):
|
25
37
|
# Handle the case when a dict is passed instead of a Pydantic model
|
@@ -216,18 +228,16 @@ class Route:
|
|
216
228
|
func_info = FunctionInfo(handler=handler, is_async=is_async)
|
217
229
|
return InternalRoute(path=path, function=func_info, method=method)
|
218
230
|
|
219
|
-
def __call__(self,
|
220
|
-
|
231
|
+
def __call__(self, *args: Any, **kwds: Any) -> Any:
|
232
|
+
routes = []
|
221
233
|
|
222
234
|
# Validate handlers
|
223
235
|
if not self.endpoint and not self.functional_handlers:
|
224
236
|
raise ValueError(f"No handler found for route: {self.path}")
|
225
237
|
|
226
238
|
# Handle functional routes
|
227
|
-
for route in self.functional_handlers:
|
228
|
-
router.add_route(route=route)
|
229
239
|
if not self.endpoint:
|
230
|
-
return
|
240
|
+
return self.functional_handlers
|
231
241
|
|
232
242
|
# Handle class-based routes
|
233
243
|
for name, func in self.endpoint.__dict__.items():
|
@@ -235,15 +245,15 @@ class Route:
|
|
235
245
|
sig = inspect.signature(func)
|
236
246
|
doc = self.swagger_generate(sig, func.__doc__)
|
237
247
|
endpoint_obj = self.endpoint()
|
238
|
-
route = self.make_internal_route(path=
|
248
|
+
route = self.make_internal_route(path=self.path, handler=endpoint_obj.dispatch, method=name.upper())
|
239
249
|
route.doc = doc
|
240
|
-
|
250
|
+
routes.append(route)
|
241
251
|
del endpoint_obj # free up memory
|
242
|
-
return
|
252
|
+
return routes
|
243
253
|
|
244
254
|
def add_route(
|
245
255
|
self,
|
246
|
-
|
256
|
+
func_path: str,
|
247
257
|
method: str,
|
248
258
|
) -> Callable:
|
249
259
|
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
@@ -251,7 +261,7 @@ class Route:
|
|
251
261
|
return await dispatch(func, request, inject)
|
252
262
|
|
253
263
|
sig = inspect.signature(func)
|
254
|
-
route = self.make_internal_route(path=path, handler=functional_wrapper, method=method.upper())
|
264
|
+
route = self.make_internal_route(path=join_url_paths(self.path, func_path), handler=functional_wrapper, method=method.upper())
|
255
265
|
route.doc = self.swagger_generate(sig, func.__doc__)
|
256
266
|
|
257
267
|
self.functional_handlers.append(route)
|
@@ -5,7 +5,7 @@ build-backend = "maturin"
|
|
5
5
|
|
6
6
|
[project]
|
7
7
|
name = "hypern"
|
8
|
-
version = "0.3.
|
8
|
+
version = "0.3.13"
|
9
9
|
description = "A Fast Async Python backend with a Rust runtime."
|
10
10
|
authors = [{ name = "Martin Dang", email = "vannghiem848@gmail.com" }]
|
11
11
|
requires-python = ">=3.10"
|
@@ -37,7 +37,7 @@ module-name = "hypern"
|
|
37
37
|
|
38
38
|
[tool.poetry]
|
39
39
|
name = "hypern"
|
40
|
-
version = "0.3.
|
40
|
+
version = "0.3.13"
|
41
41
|
description = "A Fast Async Python backend with a Rust runtime."
|
42
42
|
authors = ["Martin Dang <vannghiem848@gmail.com>"]
|
43
43
|
|
@@ -11,7 +11,6 @@ mod ws;
|
|
11
11
|
mod executor;
|
12
12
|
mod middlewares;
|
13
13
|
mod database;
|
14
|
-
mod mem_pool;
|
15
14
|
mod di;
|
16
15
|
|
17
16
|
#[pymodule]
|
@@ -28,7 +27,6 @@ fn hypern(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
|
|
28
27
|
m.add_class::<server::Server>()?;
|
29
28
|
m.add_class::<router::route::Route>()?;
|
30
29
|
m.add_class::<router::router::Router>()?;
|
31
|
-
m.add_class::<types::http::HttpMethod>()?;
|
32
30
|
m.add_class::<types::function_info::FunctionInfo>()?;
|
33
31
|
m.add_class::<types::response::PyResponse>()?;
|
34
32
|
m.add_class::<types::header::Header>()?;
|
@@ -0,0 +1,226 @@
|
|
1
|
+
use crate::router::route::Route;
|
2
|
+
use pyo3::prelude::*;
|
3
|
+
use std::collections::HashMap;
|
4
|
+
|
5
|
+
#[derive(Debug)]
|
6
|
+
#[pyclass]
|
7
|
+
#[derive(Clone)]
|
8
|
+
pub struct RadixNode {
|
9
|
+
pub path: String,
|
10
|
+
pub children: HashMap<char, RadixNode>,
|
11
|
+
pub is_endpoint: bool,
|
12
|
+
pub routes: HashMap<String, Route>,
|
13
|
+
pub param_name: Option<String>,
|
14
|
+
}
|
15
|
+
|
16
|
+
impl Default for RadixNode {
|
17
|
+
fn default() -> Self {
|
18
|
+
Self::new()
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
impl RadixNode {
|
23
|
+
pub fn new() -> Self {
|
24
|
+
Self {
|
25
|
+
path: String::new(),
|
26
|
+
children: HashMap::new(),
|
27
|
+
is_endpoint: false,
|
28
|
+
routes: HashMap::new(),
|
29
|
+
param_name: None,
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
fn find_common_prefix(a: &str, b: &str) -> String {
|
34
|
+
a.chars()
|
35
|
+
.zip(b.chars())
|
36
|
+
.take_while(|(ac, bc)| ac == bc)
|
37
|
+
.map(|(c, _)| c)
|
38
|
+
.collect()
|
39
|
+
}
|
40
|
+
|
41
|
+
pub fn insert(&mut self, path: &str, route: Route) {
|
42
|
+
let normalized_path = if path == "/" {
|
43
|
+
String::new()
|
44
|
+
} else {
|
45
|
+
path.trim_end_matches('/').to_string()
|
46
|
+
};
|
47
|
+
|
48
|
+
if normalized_path.is_empty() {
|
49
|
+
self.is_endpoint = true;
|
50
|
+
self.routes.insert(route.method.to_uppercase(), route);
|
51
|
+
return;
|
52
|
+
}
|
53
|
+
|
54
|
+
let segments: Vec<&str> = normalized_path.split('/')
|
55
|
+
.filter(|s| !s.is_empty())
|
56
|
+
.collect();
|
57
|
+
|
58
|
+
self._insert_segments(&segments, 0, route);
|
59
|
+
}
|
60
|
+
|
61
|
+
fn _insert_segments(&mut self, segments: &[&str], index: usize, route: Route) {
|
62
|
+
if index >= segments.len() {
|
63
|
+
self.is_endpoint = true;
|
64
|
+
self.routes.insert(route.method.to_uppercase(), route);
|
65
|
+
return;
|
66
|
+
}
|
67
|
+
|
68
|
+
let segment = segments[index];
|
69
|
+
|
70
|
+
if segment.starts_with(':') {
|
71
|
+
let param_name = segment[1..].to_string();
|
72
|
+
let param_node = self.children
|
73
|
+
.entry(':')
|
74
|
+
.or_insert_with(|| {
|
75
|
+
let mut node = RadixNode::new();
|
76
|
+
node.param_name = Some(param_name.clone());
|
77
|
+
node
|
78
|
+
});
|
79
|
+
param_node._insert_segments(segments, index + 1, route);
|
80
|
+
return;
|
81
|
+
}
|
82
|
+
|
83
|
+
let first_char = match segment.chars().next() {
|
84
|
+
Some(c) => c,
|
85
|
+
None => return, // Empty segment, shouldn't happen due to normalization
|
86
|
+
};
|
87
|
+
|
88
|
+
if let Some(existing_node) = self.children.get_mut(&first_char) {
|
89
|
+
let common_prefix = Self::find_common_prefix(&existing_node.path, segment);
|
90
|
+
|
91
|
+
if common_prefix == existing_node.path {
|
92
|
+
// Full match of existing node's path, check remaining segment
|
93
|
+
let remaining = &segment[common_prefix.len()..];
|
94
|
+
if remaining.is_empty() {
|
95
|
+
// Exact match, proceed to next segment
|
96
|
+
existing_node._insert_segments(segments, index + 1, route);
|
97
|
+
} else {
|
98
|
+
// Split remaining part and insert
|
99
|
+
let mut new_node = RadixNode::new();
|
100
|
+
new_node.path = remaining.to_string();
|
101
|
+
new_node._insert_segments(segments, index + 1, route);
|
102
|
+
existing_node.children
|
103
|
+
.entry(remaining.chars().next().unwrap())
|
104
|
+
.or_insert(new_node);
|
105
|
+
}
|
106
|
+
} else if !common_prefix.is_empty() {
|
107
|
+
// Split existing node and insert new path
|
108
|
+
let existing_remaining = existing_node.path[common_prefix.len()..].to_string();
|
109
|
+
let new_remaining = segment[common_prefix.len()..].to_string();
|
110
|
+
|
111
|
+
// Create new parent node with common prefix
|
112
|
+
let mut new_parent = RadixNode::new();
|
113
|
+
new_parent.path = common_prefix;
|
114
|
+
|
115
|
+
// Modify existing node to hold remaining path
|
116
|
+
let mut existing_child = RadixNode::new();
|
117
|
+
existing_child.path = existing_remaining;
|
118
|
+
existing_child.children = existing_node.children.clone();
|
119
|
+
existing_child.is_endpoint = existing_node.is_endpoint;
|
120
|
+
existing_child.routes = existing_node.routes.clone();
|
121
|
+
existing_child.param_name = existing_node.param_name.clone();
|
122
|
+
|
123
|
+
// Create new node for the new remaining path
|
124
|
+
let mut new_child = RadixNode::new();
|
125
|
+
new_child.path = new_remaining;
|
126
|
+
new_child._insert_segments(segments, index + 1, route);
|
127
|
+
|
128
|
+
// Attach children to new parent
|
129
|
+
if let Some(c) = existing_child.path.chars().next() {
|
130
|
+
new_parent.children.insert(c, existing_child);
|
131
|
+
}
|
132
|
+
if let Some(c) = new_child.path.chars().next() {
|
133
|
+
new_parent.children.insert(c, new_child);
|
134
|
+
}
|
135
|
+
|
136
|
+
// Replace existing node with new parent
|
137
|
+
*existing_node = new_parent;
|
138
|
+
} else {
|
139
|
+
// No common prefix, create new sibling node
|
140
|
+
let mut new_node = RadixNode::new();
|
141
|
+
new_node.path = segment.to_string();
|
142
|
+
new_node._insert_segments(segments, index + 1, route);
|
143
|
+
self.children.insert(first_char, new_node);
|
144
|
+
}
|
145
|
+
} else {
|
146
|
+
// No existing node, create new
|
147
|
+
let mut new_node = RadixNode::new();
|
148
|
+
new_node.path = segment.to_string();
|
149
|
+
new_node._insert_segments(segments, index + 1, route);
|
150
|
+
self.children.insert(first_char, new_node);
|
151
|
+
}
|
152
|
+
}
|
153
|
+
|
154
|
+
pub fn find(&self, path: &str, method: &str) -> Option<(&Route, HashMap<String, String>)> {
|
155
|
+
let normalized_path = if path == "/" {
|
156
|
+
String::new()
|
157
|
+
} else {
|
158
|
+
path.trim_end_matches('/').to_string()
|
159
|
+
};
|
160
|
+
|
161
|
+
let mut params = HashMap::new();
|
162
|
+
if normalized_path.is_empty() {
|
163
|
+
return if self.is_endpoint {
|
164
|
+
self.routes.get(&method.to_uppercase()).map(|r| (r, params))
|
165
|
+
} else {
|
166
|
+
None
|
167
|
+
};
|
168
|
+
}
|
169
|
+
|
170
|
+
let segments: Vec<&str> = normalized_path.split('/')
|
171
|
+
.filter(|s| !s.is_empty())
|
172
|
+
.collect();
|
173
|
+
|
174
|
+
self._find_segments(&segments, 0, method, &mut params)
|
175
|
+
}
|
176
|
+
|
177
|
+
fn _find_segments<'a>(
|
178
|
+
&'a self,
|
179
|
+
segments: &[&str],
|
180
|
+
index: usize,
|
181
|
+
method: &str,
|
182
|
+
params: &mut HashMap<String, String>,
|
183
|
+
) -> Option<(&'a Route, HashMap<String, String>)> {
|
184
|
+
if index >= segments.len() {
|
185
|
+
return if self.is_endpoint {
|
186
|
+
self.routes.get(&method.to_uppercase()).map(|r| (r, params.clone()))
|
187
|
+
} else {
|
188
|
+
None
|
189
|
+
};
|
190
|
+
}
|
191
|
+
|
192
|
+
let segment = segments[index];
|
193
|
+
|
194
|
+
// Check static nodes
|
195
|
+
if let Some(first_char) = segment.chars().next() {
|
196
|
+
if let Some(child) = self.children.get(&first_char) {
|
197
|
+
if let Some(remaining) = segment.strip_prefix(&child.path) {
|
198
|
+
if remaining.is_empty() {
|
199
|
+
// Full match, proceed to next segment
|
200
|
+
if let Some(result) = child._find_segments(segments, index + 1, method, params) {
|
201
|
+
return Some(result);
|
202
|
+
}
|
203
|
+
} else {
|
204
|
+
// Check if remaining part matches any child
|
205
|
+
if let Some(result) = child._find_segments(&[remaining], 0, method, params) {
|
206
|
+
return Some(result);
|
207
|
+
}
|
208
|
+
}
|
209
|
+
}
|
210
|
+
}
|
211
|
+
}
|
212
|
+
|
213
|
+
// Check parameter node
|
214
|
+
if let Some(param_node) = self.children.get(&':') {
|
215
|
+
if let Some(param_name) = ¶m_node.param_name {
|
216
|
+
params.insert(param_name.clone(), segment.to_string());
|
217
|
+
if let Some(result) = param_node._find_segments(segments, index + 1, method, params) {
|
218
|
+
return Some(result);
|
219
|
+
}
|
220
|
+
params.remove(param_name);
|
221
|
+
}
|
222
|
+
}
|
223
|
+
|
224
|
+
None
|
225
|
+
}
|
226
|
+
}
|
@@ -41,11 +41,6 @@ impl Route {
|
|
41
41
|
self.path, self.method))
|
42
42
|
}
|
43
43
|
|
44
|
-
// Check if route matches given path and method
|
45
|
-
pub fn matches(&self, path: &str, method: &str) -> bool {
|
46
|
-
self.path == path && self.method.to_uppercase() == method.to_uppercase()
|
47
|
-
}
|
48
|
-
|
49
44
|
// Update the route path
|
50
45
|
pub fn update_path(&mut self, new_path: &str) {
|
51
46
|
self.path = new_path.to_string();
|
@@ -109,4 +104,29 @@ impl Route {
|
|
109
104
|
_ => 99
|
110
105
|
}
|
111
106
|
}
|
107
|
+
|
108
|
+
pub fn matches(&self, path: &str, method: &str) -> bool {
|
109
|
+
if self.method.to_uppercase() != method.to_uppercase() {
|
110
|
+
return false;
|
111
|
+
}
|
112
|
+
|
113
|
+
let route_parts: Vec<&str> = self.path.split('/')
|
114
|
+
.filter(|s| !s.is_empty())
|
115
|
+
.collect();
|
116
|
+
let path_parts: Vec<&str> = path.split('/')
|
117
|
+
.filter(|s| !s.is_empty())
|
118
|
+
.collect();
|
119
|
+
|
120
|
+
if route_parts.len() != path_parts.len() {
|
121
|
+
return false;
|
122
|
+
}
|
123
|
+
|
124
|
+
for (route_part, path_part) in route_parts.iter().zip(path_parts.iter()) {
|
125
|
+
if !route_part.starts_with(':') && route_part != path_part {
|
126
|
+
return false;
|
127
|
+
}
|
128
|
+
}
|
129
|
+
|
130
|
+
true
|
131
|
+
}
|
112
132
|
}
|