hypern 0.2.0__cp312-none-win_amd64.whl → 0.2.1__cp312-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 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-%d %H:%M:%S.%fZ"), fg=(101, 111, 104))
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
- logger.info("Press Ctrl + C to stop \n")
37
- for process in process_pool:
38
- process.join()
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.0
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.0.dist-info/METADATA,sha256=0dwMDftVhFSbT6b6f41rTkO-2VTmxvLyUNBgblWBXc0,3722
2
- hypern-0.2.0.dist-info/WHEEL,sha256=VGMDu5jnp_XpyXOq-L1-cbr3x9_PzCpsKi1udyLAeSI,95
3
- hypern-0.2.0.dist-info/licenses/LICENSE,sha256=qbYKAIJLS6jYg5hYncKE7OtWmqOtpVTvKNkwOa0Iwwg,1328
4
- hypern/application.py,sha256=W_eltIwV2WcTaAQonxstMebGGgvMkIujowNvLYTX0nc,15183
1
+ hypern-0.2.1.dist-info/METADATA,sha256=PczP9NPw85PZdaLvHplRZCSGQ5bs0FRQlaWXUp9bfYU,3754
2
+ hypern-0.2.1.dist-info/WHEEL,sha256=VGMDu5jnp_XpyXOq-L1-cbr3x9_PzCpsKi1udyLAeSI,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=ebLT-DX5OOt1UqP2OajFTm4I9tL6RSBN_GbJEKe4UI4,3256
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=_vtC9mmEisfDWuSH9prvgaipbHdP5tszPWEwqsxQGo4,3031
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.cp312-win_amd64.pyd,sha256=tW0O9hPiYkkNB-ev14wiefs0CIvcZHVnmUbkOEUywYo,6056960
66
- hypern-0.2.0.dist-info/RECORD,,
67
+ hypern/hypern.cp312-win_amd64.pyd,sha256=kF5uvmgCkYzxEPDH6w8UU53SMlOU6TeQbZIashEysK4,6056960
68
+ hypern-0.2.1.dist-info/RECORD,,
File without changes