hypern 0.2.0__cp311-none-win_amd64.whl → 0.2.1__cp311-none-win_amd64.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/application.py +8 -39
- hypern/args_parser.py +59 -0
- hypern/hypern.cp311-win_amd64.pyd +0 -0
- hypern/logging/logger.py +1 -1
- hypern/processpool.py +28 -3
- hypern/reload.py +60 -0
- {hypern-0.2.0.dist-info → hypern-0.2.1.dist-info}/METADATA +2 -1
- {hypern-0.2.0.dist-info → hypern-0.2.1.dist-info}/RECORD +10 -8
- {hypern-0.2.0.dist-info → hypern-0.2.1.dist-info}/WHEEL +0 -0
- {hypern-0.2.0.dist-info → hypern-0.2.1.dist-info}/licenses/LICENSE +0 -0
hypern/application.py
CHANGED
|
@@ -2,23 +2,21 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import asyncio
|
|
5
|
-
import socket
|
|
6
5
|
from typing import Any, Callable, List, TypeVar
|
|
7
6
|
|
|
8
7
|
import orjson
|
|
9
8
|
from typing_extensions import Annotated, Doc
|
|
10
9
|
|
|
11
10
|
from hypern.datastructures import Contact, HTTPMethod, Info, License
|
|
12
|
-
from hypern.exceptions import InvalidPortNumber
|
|
13
11
|
from hypern.hypern import FunctionInfo, Router
|
|
14
12
|
from hypern.hypern import Route as InternalRoute
|
|
15
|
-
from hypern.logging import logger
|
|
16
13
|
from hypern.openapi import SchemaGenerator, SwaggerUI
|
|
17
14
|
from hypern.processpool import run_processes
|
|
18
15
|
from hypern.response import HTMLResponse, JSONResponse
|
|
19
16
|
from hypern.routing import Route
|
|
20
17
|
from hypern.scheduler import Scheduler
|
|
21
18
|
from hypern.middleware import Middleware
|
|
19
|
+
from hypern.args_parser import ArgsConfig
|
|
22
20
|
|
|
23
21
|
AppType = TypeVar("AppType", bound="Hypern")
|
|
24
22
|
|
|
@@ -193,6 +191,7 @@ class Hypern:
|
|
|
193
191
|
self.middleware_before_request = []
|
|
194
192
|
self.middleware_after_request = []
|
|
195
193
|
self.response_headers = {}
|
|
194
|
+
self.args = ArgsConfig()
|
|
196
195
|
|
|
197
196
|
for route in routes:
|
|
198
197
|
self.router.extend_route(route(app=self).routes)
|
|
@@ -340,60 +339,30 @@ class Hypern:
|
|
|
340
339
|
self.after_request()(after_request)
|
|
341
340
|
return self
|
|
342
341
|
|
|
343
|
-
def is_port_in_use(self, port: int) -> bool:
|
|
344
|
-
try:
|
|
345
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
346
|
-
return s.connect_ex(("localhost", port)) == 0
|
|
347
|
-
except Exception:
|
|
348
|
-
raise InvalidPortNumber(f"Invalid port number: {port}")
|
|
349
|
-
|
|
350
342
|
def start(
|
|
351
343
|
self,
|
|
352
|
-
host: Annotated[str, Doc("The host to run the server on. Defaults to `127.0.0.1`")] = "127.0.0.1",
|
|
353
|
-
port: Annotated[int, Doc("The port to run the server on. Defaults to `8080`")] = 8080,
|
|
354
|
-
workers: Annotated[int, Doc("The number of workers to run. Defaults to `1`")] = 1,
|
|
355
|
-
processes: Annotated[int, Doc("The number of processes to run. Defaults to `1`")] = 1,
|
|
356
|
-
max_blocking_threads: Annotated[int, Doc("The maximum number of blocking threads. Defaults to `100`")] = 1,
|
|
357
|
-
check_port: Annotated[bool, Doc("Check if the port is already in use. Defaults to `True`")] = False,
|
|
358
344
|
):
|
|
359
345
|
"""
|
|
360
346
|
Starts the server with the specified configuration.
|
|
361
|
-
|
|
362
|
-
Args:
|
|
363
|
-
host (str): The host to run the server on. Defaults to `127.0.0.1`.
|
|
364
|
-
port (int): The port to run the server on. Defaults to `8080`.
|
|
365
|
-
workers (int): The number of workers to run. Defaults to `1`.
|
|
366
|
-
processes (int): The number of processes to run. Defaults to `1`.
|
|
367
|
-
max_blocking_threads (int): The maximum number of blocking threads. Defaults to `100`.
|
|
368
|
-
check_port (bool): Check if the port is already in use. Defaults to `True`.
|
|
369
|
-
|
|
370
347
|
Raises:
|
|
371
348
|
ValueError: If an invalid port number is entered when prompted.
|
|
372
349
|
|
|
373
350
|
"""
|
|
374
|
-
if check_port:
|
|
375
|
-
while self.is_port_in_use(port):
|
|
376
|
-
logger.error("Port %s is already in use. Please use a different port.", port)
|
|
377
|
-
try:
|
|
378
|
-
port = int(input("Enter a different port: "))
|
|
379
|
-
except Exception:
|
|
380
|
-
logger.error("Invalid port number. Please enter a valid port number.")
|
|
381
|
-
continue
|
|
382
|
-
|
|
383
351
|
if self.scheduler:
|
|
384
352
|
self.scheduler.start()
|
|
385
353
|
|
|
386
354
|
run_processes(
|
|
387
|
-
host=host,
|
|
388
|
-
port=port,
|
|
389
|
-
workers=workers,
|
|
390
|
-
processes=processes,
|
|
391
|
-
max_blocking_threads=max_blocking_threads,
|
|
355
|
+
host=self.args.host,
|
|
356
|
+
port=self.args.port,
|
|
357
|
+
workers=self.args.workers,
|
|
358
|
+
processes=self.args.processes,
|
|
359
|
+
max_blocking_threads=self.args.max_blocking_threads,
|
|
392
360
|
router=self.router,
|
|
393
361
|
injectables=self.injectables,
|
|
394
362
|
before_request=self.middleware_before_request,
|
|
395
363
|
after_request=self.middleware_after_request,
|
|
396
364
|
response_headers=self.response_headers,
|
|
365
|
+
reload=self.args.reload,
|
|
397
366
|
)
|
|
398
367
|
|
|
399
368
|
def add_route(self, method: HTTPMethod, endpoint: str, handler: Callable[..., Any]):
|
hypern/args_parser.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ArgsConfig:
|
|
5
|
+
def __init__(self) -> None:
|
|
6
|
+
parser = argparse.ArgumentParser(description="Hypern: A Versatile Python and Rust Framework")
|
|
7
|
+
self.parser = parser
|
|
8
|
+
parser.add_argument(
|
|
9
|
+
"--host",
|
|
10
|
+
type=str,
|
|
11
|
+
default=None,
|
|
12
|
+
required=False,
|
|
13
|
+
help="Choose the host. [Defaults to `127.0.0.1`]",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"--port",
|
|
18
|
+
type=int,
|
|
19
|
+
default=None,
|
|
20
|
+
required=False,
|
|
21
|
+
help="Choose the port. [Defaults to `5000`]",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"--processes",
|
|
26
|
+
type=int,
|
|
27
|
+
default=None,
|
|
28
|
+
required=False,
|
|
29
|
+
help="Choose the number of processes. [Default: 1]",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--workers",
|
|
33
|
+
type=int,
|
|
34
|
+
default=None,
|
|
35
|
+
required=False,
|
|
36
|
+
help="Choose the number of workers. [Default: 1]",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--max-blocking-threads",
|
|
41
|
+
type=int,
|
|
42
|
+
default=None,
|
|
43
|
+
required=False,
|
|
44
|
+
help="Choose the maximum number of blocking threads. [Default: 100]",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--reload",
|
|
49
|
+
action="store_true",
|
|
50
|
+
help="It restarts the server based on file changes.",
|
|
51
|
+
)
|
|
52
|
+
args, _ = parser.parse_known_args()
|
|
53
|
+
|
|
54
|
+
self.host = args.host or "127.0.0.1"
|
|
55
|
+
self.port = args.port or 5000
|
|
56
|
+
self.max_blocking_threads = args.max_blocking_threads or 100
|
|
57
|
+
self.processes = args.processes or 1
|
|
58
|
+
self.workers = args.workers or 1
|
|
59
|
+
self.reload = args.reload or False
|
|
Binary file
|
hypern/logging/logger.py
CHANGED
|
@@ -59,7 +59,7 @@ class ColourizedFormatter(logging.Formatter):
|
|
|
59
59
|
recordcopy.__dict__["message"] = recordcopy.getMessage()
|
|
60
60
|
recordcopy.__dict__["levelprefix"] = levelname + separator
|
|
61
61
|
recordcopy.__dict__["process"] = click.style(str(process), fg="blue")
|
|
62
|
-
recordcopy.__dict__["asctime"] = click.style(datetime.fromtimestamp(created, tz=timezone.utc).strftime("%Y-%m-%
|
|
62
|
+
recordcopy.__dict__["asctime"] = click.style(datetime.fromtimestamp(created, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), fg=(101, 111, 104))
|
|
63
63
|
recordcopy.__dict__["filename"] = click.style(f"{module}/{filename}:{lineno}:", fg=(101, 111, 104))
|
|
64
64
|
return super().formatMessage(recordcopy)
|
|
65
65
|
|
hypern/processpool.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import os
|
|
2
3
|
import signal
|
|
3
4
|
import sys
|
|
4
5
|
from typing import Any, Dict, List
|
|
5
6
|
|
|
6
7
|
from multiprocess import Process
|
|
8
|
+
from watchdog.observers import Observer
|
|
7
9
|
|
|
8
10
|
from .hypern import FunctionInfo, Router, Server, SocketHeld
|
|
9
11
|
from .logging import logger
|
|
12
|
+
from .reload import EventHandler
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
def run_processes(
|
|
@@ -20,6 +23,7 @@ def run_processes(
|
|
|
20
23
|
before_request: List[FunctionInfo],
|
|
21
24
|
after_request: List[FunctionInfo],
|
|
22
25
|
response_headers: Dict[str, str],
|
|
26
|
+
reload: bool = True,
|
|
23
27
|
) -> List[Process]:
|
|
24
28
|
socket = SocketHeld(host, port)
|
|
25
29
|
|
|
@@ -33,9 +37,30 @@ def run_processes(
|
|
|
33
37
|
signal.signal(signal.SIGINT, terminating_signal_handler)
|
|
34
38
|
signal.signal(signal.SIGTERM, terminating_signal_handler)
|
|
35
39
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
if reload:
|
|
41
|
+
# Set up file system watcher for auto-reload
|
|
42
|
+
watch_dirs = [os.getcwd()]
|
|
43
|
+
observer = Observer()
|
|
44
|
+
reload_handler = EventHandler(file_path=sys.argv[0], directory_path=os.getcwd())
|
|
45
|
+
|
|
46
|
+
for directory in watch_dirs:
|
|
47
|
+
observer.schedule(reload_handler, directory, recursive=True)
|
|
48
|
+
|
|
49
|
+
observer.start()
|
|
50
|
+
|
|
51
|
+
logger.info(f"Server started at http://{host}:{port}")
|
|
52
|
+
logger.info("Press Ctrl + C to stop")
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
for process in process_pool:
|
|
56
|
+
logger.debug(f"Process {process.pid} started")
|
|
57
|
+
process.join()
|
|
58
|
+
except KeyboardInterrupt:
|
|
59
|
+
pass
|
|
60
|
+
finally:
|
|
61
|
+
if reload:
|
|
62
|
+
observer.stop()
|
|
63
|
+
observer.join()
|
|
39
64
|
|
|
40
65
|
return process_pool
|
|
41
66
|
|
hypern/reload.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import time
|
|
3
|
+
import subprocess
|
|
4
|
+
from watchdog.events import FileSystemEventHandler
|
|
5
|
+
|
|
6
|
+
from .logging import logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EventHandler(FileSystemEventHandler):
|
|
10
|
+
def __init__(self, file_path: str, directory_path: str) -> None:
|
|
11
|
+
self.file_path = file_path
|
|
12
|
+
self.directory_path = directory_path
|
|
13
|
+
self.process = None # Keep track of the subprocess
|
|
14
|
+
self.last_reload = time.time() # Keep track of the last reload. EventHandler is initialized with the process.
|
|
15
|
+
|
|
16
|
+
def stop_server(self):
|
|
17
|
+
if self.process:
|
|
18
|
+
try:
|
|
19
|
+
# Check if the process is still alive
|
|
20
|
+
if self.process.poll() is None: # None means the process is still running
|
|
21
|
+
self.process.terminate() # Gracefully terminate the process
|
|
22
|
+
self.process.wait(timeout=5) # Wait for the process to exit
|
|
23
|
+
else:
|
|
24
|
+
logger.error("Process is not running.")
|
|
25
|
+
except subprocess.TimeoutExpired:
|
|
26
|
+
logger.error("Process did not terminate in time. Forcing termination.")
|
|
27
|
+
self.process.kill() # Forcefully kill the process if it doesn't stop
|
|
28
|
+
except ProcessLookupError:
|
|
29
|
+
logger.error("Process does not exist.")
|
|
30
|
+
except Exception as e:
|
|
31
|
+
logger.error(f"An error occurred while stopping the server: {e}")
|
|
32
|
+
else:
|
|
33
|
+
logger.debug("No process to stop.")
|
|
34
|
+
|
|
35
|
+
def reload(self):
|
|
36
|
+
self.stop_server()
|
|
37
|
+
logger.debug("Reloading the server")
|
|
38
|
+
prev_process = self.process
|
|
39
|
+
if prev_process:
|
|
40
|
+
prev_process.kill()
|
|
41
|
+
|
|
42
|
+
self.process = subprocess.Popen(
|
|
43
|
+
[sys.executable, *sys.argv],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
self.last_reload = time.time()
|
|
47
|
+
|
|
48
|
+
def on_modified(self, event) -> None:
|
|
49
|
+
"""
|
|
50
|
+
This function is a callback that will start a new server on every even change
|
|
51
|
+
|
|
52
|
+
:param event FSEvent: a data structure with info about the events
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
# Avoid reloading multiple times when watchdog detects multiple events
|
|
56
|
+
if time.time() - self.last_reload < 0.5:
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
time.sleep(0.2) # Wait for the file to be fully written
|
|
60
|
+
self.reload()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: hypern
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
6
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
@@ -22,6 +22,7 @@ Requires-Dist: orjson ==3.10.11
|
|
|
22
22
|
Requires-Dist: multiprocess ==0.70.17
|
|
23
23
|
Requires-Dist: uvloop ==0.21.0 ; sys_platform != 'win32' and platform_python_implementation == 'CPython' and platform_machine != 'armv7l'
|
|
24
24
|
Requires-Dist: cryptography ==43.0.3
|
|
25
|
+
Requires-Dist: watchdog ==6.0.0
|
|
25
26
|
License-File: LICENSE
|
|
26
27
|
Summary: A Fast Async Python backend with a Rust runtime.
|
|
27
28
|
Author-email: Martin Dang <vannghiem848@gmail.com>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
hypern-0.2.
|
|
2
|
-
hypern-0.2.
|
|
3
|
-
hypern-0.2.
|
|
4
|
-
hypern/application.py,sha256=
|
|
1
|
+
hypern-0.2.1.dist-info/METADATA,sha256=PczP9NPw85PZdaLvHplRZCSGQ5bs0FRQlaWXUp9bfYU,3754
|
|
2
|
+
hypern-0.2.1.dist-info/WHEEL,sha256=bNtRK4IG-ZINiRwRhIvmsDSb0Vife_pt6vsyhyhjHi8,95
|
|
3
|
+
hypern-0.2.1.dist-info/licenses/LICENSE,sha256=qbYKAIJLS6jYg5hYncKE7OtWmqOtpVTvKNkwOa0Iwwg,1328
|
|
4
|
+
hypern/application.py,sha256=IUa1O9L3bU3VwEo6nbrKGHqy5Wx1-x1n1sSayt9P6Mg,13399
|
|
5
|
+
hypern/args_parser.py,sha256=Crxzr8_uhiIk_AWJvuwJTEfRqEBqU_GfTbg6chg_YiY,1790
|
|
5
6
|
hypern/auth/authorization.py,sha256=-NprZsI0np889ZN1fp-MiVFrPoMNzUtatBJaCMtkllM,32
|
|
6
7
|
hypern/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
8
|
hypern/background.py,sha256=xy38nQZSJsYFRXr3-uFJeNW9E1GiXXOC7lSe5pC0eyE,124
|
|
@@ -39,7 +40,7 @@ hypern/enum.py,sha256=KcVziJj7vWvyie0r2rtxhrLzdtkZAsf0DY58oJ4tQl4,360
|
|
|
39
40
|
hypern/exceptions.py,sha256=nHTkF0YdNBMKfSiNtjRMHMNKoY3RMUm68YYluuW15us,2428
|
|
40
41
|
hypern/hypern.pyi,sha256=PsoQ-UV3q_xJs0BsW3In8gVWJjmqrffCY4xTC0hSDfk,6915
|
|
41
42
|
hypern/i18n/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
42
|
-
hypern/logging/logger.py,sha256=
|
|
43
|
+
hypern/logging/logger.py,sha256=WACam_IJiCMXX0hGVKMGSxUQpY4DgAXy7M1dD3q-Z9s,3256
|
|
43
44
|
hypern/logging/__init__.py,sha256=6eVriyncsJ4J73fGYhoejv9MX7aGTkRezTpPxO4DX1I,52
|
|
44
45
|
hypern/middleware/base.py,sha256=mJoz-i-7lqw1eDxZ8Bb0t8sz60lx5TE-OjZT4UR75e4,541
|
|
45
46
|
hypern/middleware/cors.py,sha256=x90DnCOJSfp4ojm1krttn_EdtlqeDazyUzVg66NES4A,1681
|
|
@@ -49,8 +50,9 @@ hypern/middleware/__init__.py,sha256=lXwR3fdmpVK4Z7QWaLsgf3Sazy5NPPFXIOxIEv1xDC8
|
|
|
49
50
|
hypern/openapi/schemas.py,sha256=YHfMlPUeP5DzDX5ao3YH8p_25Vvyaf616dh6XDCUZRc,1677
|
|
50
51
|
hypern/openapi/swagger.py,sha256=naqUY3rFAEYA1ZLIlmDsMYaol0yIm6TVebdkFa5cMTc,64
|
|
51
52
|
hypern/openapi/__init__.py,sha256=4rEVD8pa0kdSpsy7ZkJ5JY0Z2XF0NGSKDMwYAd7YZpE,141
|
|
52
|
-
hypern/processpool.py,sha256=
|
|
53
|
+
hypern/processpool.py,sha256=1oLytripLMFu3RxCdvLx5G2NU_vaxEJmZyfq5FjfRtA,3788
|
|
53
54
|
hypern/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
55
|
+
hypern/reload.py,sha256=d28xP7MWiO2vL6RAAlIqSh8q7_ugsRBN66Mlbz3UWEI,2293
|
|
54
56
|
hypern/response/response.py,sha256=-dnboAraPic8asf503PxwmDuxhNllUO5h97_DGmbER4,4582
|
|
55
57
|
hypern/response/__init__.py,sha256=_w3u3TDNuYx5ejnnN1unqnTY8NlBgUATQi6wepEB_FQ,226
|
|
56
58
|
hypern/routing/dispatcher.py,sha256=i2wLAAW1ZXgpi5K2heGXhTODnP1WdQzaR5WlUjs1o9c,2368
|
|
@@ -62,5 +64,5 @@ hypern/scheduler.py,sha256=-k3tW2AGCnHYSthKXk-FOs_SCtWp3yIxQzwzUJMJsbo,67
|
|
|
62
64
|
hypern/security.py,sha256=3E86Yp_eOSVa1emUvBrDgoF0Sn6eNX0CfLnt87w5CPI,1773
|
|
63
65
|
hypern/worker.py,sha256=WQrhY_awR6zjMwY4Q7izXi4E4fFrDqt7jIblUW8Bzcg,924
|
|
64
66
|
hypern/__init__.py,sha256=9Ww_aUQ0vJls0tOq7Yw1_TVOCRsa5bHJ-RtnSeComwk,119
|
|
65
|
-
hypern/hypern.cp311-win_amd64.pyd,sha256=
|
|
66
|
-
hypern-0.2.
|
|
67
|
+
hypern/hypern.cp311-win_amd64.pyd,sha256=hXrPRs9yYW7A82aCUCJk4h-FnhFI7JtYN0pGzWW0XWg,6043648
|
|
68
|
+
hypern-0.2.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|