hypern 0.3.0__cp312-cp312-win32.whl → 0.3.2__cp312-cp312-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/ws/heartbeat.py ADDED
@@ -0,0 +1,74 @@
1
+ import asyncio
2
+ from dataclasses import dataclass
3
+ from time import time
4
+ from typing import Dict
5
+
6
+ from hypern.hypern import WebSocketSession
7
+
8
+
9
+ @dataclass
10
+ class HeartbeatConfig:
11
+ ping_interval: float = 30.0 # Send ping every 30 seconds
12
+ ping_timeout: float = 10.0 # Wait 10 seconds for pong response
13
+ max_missed_pings: int = 2 # Disconnect after 2 missed pings
14
+
15
+
16
+ class HeartbeatManager:
17
+ def __init__(self, config: HeartbeatConfig = None):
18
+ self.config = config or HeartbeatConfig()
19
+ self.active_sessions: Dict[WebSocketSession, float] = {}
20
+ self.ping_tasks: Dict[WebSocketSession, asyncio.Task] = {}
21
+ self.missed_pings: Dict[WebSocketSession, int] = {}
22
+
23
+ async def start_heartbeat(self, session: WebSocketSession):
24
+ """Start heartbeat monitoring for a session"""
25
+ self.active_sessions[session] = time()
26
+ self.missed_pings[session] = 0
27
+ self.ping_tasks[session] = asyncio.create_task(self._heartbeat_loop(session))
28
+
29
+ async def stop_heartbeat(self, session: WebSocketSession):
30
+ """Stop heartbeat monitoring for a session"""
31
+ if session in self.ping_tasks:
32
+ self.ping_tasks[session].cancel()
33
+ del self.ping_tasks[session]
34
+ self.active_sessions.pop(session, None)
35
+ self.missed_pings.pop(session, None)
36
+
37
+ async def handle_pong(self, session: WebSocketSession):
38
+ """Handle pong response from client"""
39
+ if session in self.active_sessions:
40
+ self.active_sessions[session] = time()
41
+ self.missed_pings[session] = 0
42
+
43
+ async def _heartbeat_loop(self, session: WebSocketSession):
44
+ """Main heartbeat loop for a session"""
45
+ try:
46
+ while True:
47
+ await asyncio.sleep(self.config.ping_interval)
48
+
49
+ if session not in self.active_sessions:
50
+ break
51
+
52
+ # Send ping frame
53
+ try:
54
+ await session.ping()
55
+ last_pong = self.active_sessions[session]
56
+
57
+ # Wait for pong timeout
58
+ await asyncio.sleep(self.config.ping_timeout)
59
+
60
+ # Check if we received a pong
61
+ if self.active_sessions[session] == last_pong:
62
+ self.missed_pings[session] += 1
63
+
64
+ # Check if we exceeded max missed pings
65
+ if self.missed_pings[session] >= self.config.max_missed_pings:
66
+ await session.close(1001, "Connection timeout")
67
+ break
68
+
69
+ except Exception as e:
70
+ await session.close(1001, f"Heartbeat failed: {str(e)}")
71
+ break
72
+
73
+ finally:
74
+ await self.stop_heartbeat(session)
hypern/ws/room.py ADDED
@@ -0,0 +1,76 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Dict, Set
3
+
4
+ from hypern.hypern import WebSocketSession
5
+
6
+
7
+ @dataclass
8
+ class Room:
9
+ name: str
10
+ clients: Set[WebSocketSession] = field(default_factory=set)
11
+
12
+ def broadcast(self, message: str, exclude: WebSocketSession = None):
13
+ """Broadcast message to all clients in the room except excluded one"""
14
+ for client in self.clients:
15
+ if client != exclude:
16
+ client.send(message)
17
+
18
+ def add_client(self, client: WebSocketSession):
19
+ """Add a client to the room"""
20
+ self.clients.add(client)
21
+
22
+ def remove_client(self, client: WebSocketSession):
23
+ """Remove a client from the room"""
24
+ self.clients.discard(client)
25
+
26
+ @property
27
+ def client_count(self) -> int:
28
+ return len(self.clients)
29
+
30
+
31
+ class RoomManager:
32
+ def __init__(self):
33
+ self.rooms: Dict[str, Room] = {}
34
+ self.client_rooms: Dict[WebSocketSession, Set[str]] = {}
35
+
36
+ def create_room(self, room_name: str) -> Room:
37
+ """Create a new room if it doesn't exist"""
38
+ if room_name not in self.rooms:
39
+ self.rooms[room_name] = Room(room_name)
40
+ return self.rooms[room_name]
41
+
42
+ def get_room(self, room_name: str) -> Room:
43
+ """Get a room by name"""
44
+ return self.rooms.get(room_name)
45
+
46
+ def join_room(self, client: WebSocketSession, room_name: str):
47
+ """Add a client to a room"""
48
+ room = self.create_room(room_name)
49
+ room.add_client(client)
50
+
51
+ if client not in self.client_rooms:
52
+ self.client_rooms[client] = set()
53
+ self.client_rooms[client].add(room_name)
54
+
55
+ room.broadcast(f"Client joined room: {room_name}", exclude=client)
56
+
57
+ def leave_room(self, client: WebSocketSession, room_name: str):
58
+ """Remove a client from a room"""
59
+ room = self.get_room(room_name)
60
+ if room:
61
+ room.remove_client(client)
62
+ if client in self.client_rooms:
63
+ self.client_rooms[client].discard(room_name)
64
+
65
+ room.broadcast(f"Client left room: {room_name}", exclude=client)
66
+
67
+ if room.client_count == 0:
68
+ del self.rooms[room_name]
69
+
70
+ def leave_all_rooms(self, client: WebSocketSession):
71
+ """Remove a client from all rooms"""
72
+ if client in self.client_rooms:
73
+ rooms = self.client_rooms[client].copy()
74
+ for room_name in rooms:
75
+ self.leave_room(client, room_name)
76
+ del self.client_rooms[client]
hypern/ws/route.py ADDED
@@ -0,0 +1,26 @@
1
+ from typing import Callable, Optional
2
+
3
+ from hypern.hypern import WebsocketRoute as WebsocketRouteInternal, WebSocketSession
4
+
5
+
6
+ class WebsocketRoute:
7
+ def __init__(self) -> None:
8
+ self.routes = []
9
+ self._disconnect_handler: Optional[Callable] = None
10
+
11
+ def on(self, path):
12
+ def wrapper(func):
13
+ self.routes.append(WebsocketRouteInternal(path, func))
14
+ return func
15
+
16
+ return wrapper
17
+
18
+ def on_disconnect(self, func):
19
+ """Register a disconnect handler"""
20
+ self._disconnect_handler = func
21
+ return func
22
+
23
+ def handle_disconnect(self, session: WebSocketSession):
24
+ """Internal method to handle disconnection"""
25
+ if self._disconnect_handler:
26
+ return self._disconnect_handler(session)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypern
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -1,19 +1,15 @@
1
- hypern-0.3.0.dist-info/METADATA,sha256=aG7HYARcYTZed6140LAyzlcSeZ189_4OOZIT0bqiOxE,3754
2
- hypern-0.3.0.dist-info/WHEEL,sha256=llwl3fX8kOi_BiOUBLq5Qrnznqct4O7tJSEJsqg9n1c,92
3
- hypern-0.3.0.dist-info/licenses/LICENSE,sha256=qbYKAIJLS6jYg5hYncKE7OtWmqOtpVTvKNkwOa0Iwwg,1328
4
- hypern/application.py,sha256=D8tye5rpOJzHGQHUelaDpjUL3B9xO40B7xAL9wC3Uno,14328
5
- hypern/args_parser.py,sha256=Crxzr8_uhiIk_AWJvuwJTEfRqEBqU_GfTbg6chg_YiY,1790
1
+ hypern-0.3.2.dist-info/METADATA,sha256=xbMB8V67yK1aLvRGbYrsjqoScFdSOZXs97zXg0yVVQ4,3754
2
+ hypern-0.3.2.dist-info/WHEEL,sha256=SK_cql1gpDHx6aBV-LOSvGbTt4TUC8AJJOzjOP2tdpI,92
3
+ hypern-0.3.2.dist-info/licenses/LICENSE,sha256=qbYKAIJLS6jYg5hYncKE7OtWmqOtpVTvKNkwOa0Iwwg,1328
4
+ hypern/application.py,sha256=yNlohWdSG0lpKfuTVX7N9gI3cmXl3IoEvP8WLH-yAiE,16251
5
+ hypern/args_parser.py,sha256=diz3Oq1PDNMBlG7ElqK01iokY5w_X3U1Ky1jikPVXRg,2020
6
6
  hypern/auth/authorization.py,sha256=-NprZsI0np889ZN1fp-MiVFrPoMNzUtatBJaCMtkllM,32
7
7
  hypern/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  hypern/background.py,sha256=xy38nQZSJsYFRXr3-uFJeNW9E1GiXXOC7lSe5pC0eyE,124
9
- hypern/caching/base/backend.py,sha256=nHl5_nH95LSYWBS9RmOvOzsps_p19HcnQsb4h4W7cP8,68
10
- hypern/caching/base/key_maker.py,sha256=-W1r3ywfQ-K6saniiK3aTMaW3iy3aXai2pvQqM8f74I,232
11
- hypern/caching/base/__init__.py,sha256=M-B56YGTkSwCkKW_I6GcV1LSIFfxZ6KuXowB1_aJQpQ,155
12
- hypern/caching/cache_manager.py,sha256=TF6UosJ54950JgsrPVZUS3MH2R8zafAu5PTryNJ0sRs,2053
13
- hypern/caching/cache_tag.py,sha256=bZcjivMNETAzAHAIobuLN0S2wHgPgLLL8Gg4uso_qbk,267
14
- hypern/caching/custom_key_maker.py,sha256=88RIIJjpQYFnv857wOlCKgWWBbK_S23zNHsIrJz_4PY,394
15
- hypern/caching/redis_backend.py,sha256=IgQToCnHYGpKEErq2CNZkR5woo01z456Ef3C-XRPRV8,70
16
- hypern/caching/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ hypern/caching/backend.py,sha256=bfSEgJQxS3FDdj08CY4V9b9U3rwf4uy9gS46QmwA3Zc,829
10
+ hypern/caching/redis_backend.py,sha256=SWYlJvr8qi2kS_grhxhqQBLEEATpFremT2XyXkio8h0,5968
11
+ hypern/caching/strategies.py,sha256=qQjqgZLUX7KZjhwPD4SYUaMRewgCgsp6qa1FunK3Y0I,7288
12
+ hypern/caching/__init__.py,sha256=ODO7zMm4iFG8wcvrhKmukryG5wOTW0DnzFvNMfF57Cc,352
17
13
  hypern/cli/commands.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
14
  hypern/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
15
  hypern/config.py,sha256=frZSdXBI8GaM0tkw1Rs-XydZ9-XjGLRPj6DL4d51-y4,4930
@@ -38,32 +34,46 @@ hypern/db/sql/__init__.py,sha256=1UoWQi2CIcUAbQj3FadR-8V0o_b286nI2wYvOsvtbFc,647
38
34
  hypern/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
35
  hypern/enum.py,sha256=KcVziJj7vWvyie0r2rtxhrLzdtkZAsf0DY58oJ4tQl4,360
40
36
  hypern/exceptions.py,sha256=nHTkF0YdNBMKfSiNtjRMHMNKoY3RMUm68YYluuW15us,2428
41
- hypern/hypern.pyi,sha256=b5Zjr9ipm43rI8eSUV4WYmKpIJWUsv6O2TrpvvbeTlE,7700
37
+ hypern/gateway/aggregator.py,sha256=N1onAp9gdzpCR-E5VubkVoUjjEmVNxG8gDZx9rhnbXc,1132
38
+ hypern/gateway/gateway.py,sha256=26K2qvJUR-0JnN4IlhwvSSt7EYcpYrBVDuzZ1ivQQ34,1475
39
+ hypern/gateway/proxy.py,sha256=w1wcTplDnVrfjn7hb0M0yBVth5TGl88irF-MUYHysQQ,2463
40
+ hypern/gateway/service.py,sha256=PkRaM08olqM_j_4wRjEJCR8X8ZysAF2WOcfhWjaX2eo,1701
41
+ hypern/gateway/__init__.py,sha256=TpFWtqnJerW1-jCWq5fjypJcw9Y6ytyrkvkzby1Eg0E,235
42
+ hypern/hypern.pyi,sha256=GiUjtY6tbV8d6KI1PDTTZK5zWyOD4hooahALnFd7Fl4,8197
42
43
  hypern/i18n/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
44
  hypern/logging/logger.py,sha256=WACam_IJiCMXX0hGVKMGSxUQpY4DgAXy7M1dD3q-Z9s,3256
44
45
  hypern/logging/__init__.py,sha256=6eVriyncsJ4J73fGYhoejv9MX7aGTkRezTpPxO4DX1I,52
45
- hypern/middleware/base.py,sha256=mJoz-i-7lqw1eDxZ8Bb0t8sz60lx5TE-OjZT4UR75e4,541
46
- hypern/middleware/cors.py,sha256=x90DnCOJSfp4ojm1krttn_EdtlqeDazyUzVg66NES4A,1681
46
+ hypern/middleware/base.py,sha256=3G7vxFXr0tDp28Bz3NeM76eo03qYGppm07N3yWoA_4U,369
47
+ hypern/middleware/cache.py,sha256=HFfghpnhS21DOsXLn6nt-TgRLCNNm6ZqTKPMgoXM0Mw,7583
48
+ hypern/middleware/compress.py,sha256=Zph3pQz15YrYB4dMUMbQnfWIFY8ovysgPMepbY_WV9k,3119
49
+ hypern/middleware/cors.py,sha256=pt5HyTd3J5L9Lvczo2xI8fxLmtntSbJq-CPU0vYXoAI,1800
47
50
  hypern/middleware/i18n.py,sha256=jHzVzjTx1nnjbraZtIVOprrnSaeKMxZB8RuSqRp2I4s,16
48
- hypern/middleware/limit.py,sha256=8hzUxu_mxra2QiDjAghgZtvwN6Dx07irPUiL12dbVhY,8152
49
- hypern/middleware/__init__.py,sha256=lXwR3fdmpVK4Z7QWaLsgf3Sazy5NPPFXIOxIEv1xDC8,273
51
+ hypern/middleware/limit.py,sha256=eAYARPjqxq8Ue0TCpnxlVRB5hv7hwBF0PxeD-bG6Sl0,8252
52
+ hypern/middleware/security.py,sha256=d9Qf2UNMN8wz-MLnG2wRb0Vgf55_IGZAje5hbc2T_HQ,7539
53
+ hypern/middleware/__init__.py,sha256=V-Gnv-Jf-14BVuA28z7PN7GBVQ9BBiBdab6-QnTPCfY,493
50
54
  hypern/openapi/schemas.py,sha256=YHfMlPUeP5DzDX5ao3YH8p_25Vvyaf616dh6XDCUZRc,1677
51
55
  hypern/openapi/swagger.py,sha256=naqUY3rFAEYA1ZLIlmDsMYaol0yIm6TVebdkFa5cMTc,64
52
56
  hypern/openapi/__init__.py,sha256=4rEVD8pa0kdSpsy7ZkJ5JY0Z2XF0NGSKDMwYAd7YZpE,141
53
- hypern/processpool.py,sha256=YW7Dg33Hla9D33x3Mf8xsjaTprHaovkLPK-4XnQKiGU,4045
57
+ hypern/processpool.py,sha256=l6_PKRMFQ4GIMPLTMOEbIwWDsxER0NIj_wdxVWqA7zA,5042
54
58
  hypern/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
- hypern/reload.py,sha256=d28xP7MWiO2vL6RAAlIqSh8q7_ugsRBN66Mlbz3UWEI,2293
59
+ hypern/reload.py,sha256=nfaZCoChrQetHNtIqN4Xzi-a0v-irxSCMhwCK3bCEq0,1569
56
60
  hypern/response/response.py,sha256=-dnboAraPic8asf503PxwmDuxhNllUO5h97_DGmbER4,4582
57
61
  hypern/response/__init__.py,sha256=_w3u3TDNuYx5ejnnN1unqnTY8NlBgUATQi6wepEB_FQ,226
58
62
  hypern/routing/dispatcher.py,sha256=i2wLAAW1ZXgpi5K2heGXhTODnP1WdQzaR5WlUjs1o9c,2368
59
63
  hypern/routing/endpoint.py,sha256=RKVhvqOEGL9IKBXQ3KJgPi9bgJj9gfWC5BdZc5U_atc,1026
60
64
  hypern/routing/parser.py,sha256=R-4lcN9Ha1iMeAjlqDe8HwkjjMVG-c-ubQLZyWKXj6M,3554
65
+ hypern/routing/queue.py,sha256=NtFBbogU22ddyyX-CuQMip1XFDPZdMCVMIeUCQ-CR6Y,7176
61
66
  hypern/routing/route.py,sha256=IUnWU5ra-0R9rrRDpxJiwiw7vaEefn-We2dZ4EocJGw,10403
62
- hypern/routing/__init__.py,sha256=7rw7EAxougCXtmkgJjrmLP3N5RXctIpI_3JmG9FcKVU,101
67
+ hypern/routing/__init__.py,sha256=U4xW5fDRsn03z4cVLT4dJHHGGU6SVxyv2DL86LXodeE,162
63
68
  hypern/scheduler.py,sha256=-k3tW2AGCnHYSthKXk-FOs_SCtWp3yIxQzwzUJMJsbo,67
64
69
  hypern/security.py,sha256=3E86Yp_eOSVa1emUvBrDgoF0Sn6eNX0CfLnt87w5CPI,1773
65
70
  hypern/worker.py,sha256=WQrhY_awR6zjMwY4Q7izXi4E4fFrDqt7jIblUW8Bzcg,924
71
+ hypern/ws/channel.py,sha256=0ns2qmeoFJOpGLXS_hqldhywDQm_DxHwj6KloQx4Q3I,3183
72
+ hypern/ws/heartbeat.py,sha256=sWMXzQm6cbDHHA2NHc-gFjv7G_E56XtxswHQ93_BueM,2861
73
+ hypern/ws/room.py,sha256=0_L6Nun0n007F0rfNY8yX5x_A8EuXuI67JqpMkJ4RNI,2598
74
+ hypern/ws/route.py,sha256=fGQ2RC708MPOiiIHPUo8aZ-oK379TTAyQYm4htNA5jM,803
75
+ hypern/ws/__init__.py,sha256=dhRoRY683_rfPfSPM5qUczfTuyYDeuLOCFxY4hIdKt8,131
66
76
  hypern/ws.py,sha256=F6SA2Z1KVnqTEX8ssvOXqCtudUS4eo30JsiIsvfbHnE,394
67
77
  hypern/__init__.py,sha256=9Ww_aUQ0vJls0tOq7Yw1_TVOCRsa5bHJ-RtnSeComwk,119
68
- hypern/hypern.cp312-win32.pyd,sha256=Ufztl71auGJOE_0x63ZldXhd94YTxB_52KhlaaEODG4,5471744
69
- hypern-0.3.0.dist-info/RECORD,,
78
+ hypern/hypern.cp312-win32.pyd,sha256=JxNOpTLC3EaBfc6d3TLY05BbTHHx5ciyj3wSbOKbWIM,6539264
79
+ hypern-0.3.2.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: maturin (1.7.7)
2
+ Generator: maturin (1.7.8)
3
3
  Root-Is-Purelib: false
4
4
  Tag: cp312-cp312-win32
@@ -1,8 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- from .backend import BaseBackend
3
- from .key_maker import BaseKeyMaker
4
-
5
- __all__ = [
6
- "BaseKeyMaker",
7
- "BaseBackend",
8
- ]
@@ -1,3 +0,0 @@
1
- from hypern.hypern import BaseBackend
2
-
3
- __all__ = ["BaseBackend"]
@@ -1,8 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- from abc import ABC, abstractmethod
3
- from typing import Callable
4
-
5
-
6
- class BaseKeyMaker(ABC):
7
- @abstractmethod
8
- async def make(self, function: Callable, prefix: str, identify_key: str) -> str: ...
@@ -1,56 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- from functools import wraps
3
- from typing import Callable, Dict, Type
4
-
5
- from .base import BaseBackend, BaseKeyMaker
6
- from .cache_tag import CacheTag
7
- import orjson
8
-
9
-
10
- class CacheManager:
11
- def __init__(self):
12
- self.backend = None
13
- self.key_maker = None
14
-
15
- def init(self, backend: BaseBackend, key_maker: BaseKeyMaker) -> None:
16
- self.backend = backend
17
- self.key_maker = key_maker
18
-
19
- def cached(self, tag: CacheTag, ttl: int = 60, identify: Dict = {}) -> Type[Callable]:
20
- def _cached(function):
21
- @wraps(function)
22
- async def __cached(*args, **kwargs):
23
- if not self.backend or not self.key_maker:
24
- raise ValueError("Backend or KeyMaker not initialized")
25
-
26
- _identify_key = []
27
- for key, values in identify.items():
28
- _obj = kwargs.get(key, None)
29
- if not _obj:
30
- raise ValueError(f"Caching: Identify key {key} not found in kwargs")
31
- for attr in values:
32
- _identify_key.append(f"{attr}={getattr(_obj, attr)}")
33
- _identify_key = ":".join(_identify_key)
34
-
35
- key = await self.key_maker.make(function=function, prefix=tag.value, identify_key=_identify_key)
36
-
37
- cached_response = self.backend.get(key=key)
38
- if cached_response:
39
- return orjson.loads(cached_response)
40
-
41
- response = await function(*args, **kwargs)
42
- self.backend.set(response=orjson.dumps(response).decode("utf-8"), key=key, ttl=ttl)
43
- return response
44
-
45
- return __cached
46
-
47
- return _cached # type: ignore
48
-
49
- async def remove_by_tag(self, tag: CacheTag) -> None:
50
- await self.backend.delete_startswith(value=tag.value)
51
-
52
- async def remove_by_prefix(self, prefix: str) -> None:
53
- await self.backend.delete_startswith(value=prefix)
54
-
55
-
56
- Cache = CacheManager()
@@ -1,10 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- from enum import Enum
3
-
4
-
5
- class CacheTag(Enum):
6
- GET_HEALTH_CHECK = "get_health_check"
7
- GET_USER_INFO = "get_user_info"
8
- GET_CATEGORIES = "get_categories"
9
- GET_HISTORY = "get_chat_history"
10
- GET_QUESTION = "get_question"
@@ -1,11 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- from typing import Callable
3
- import inspect
4
-
5
- from hypern.caching.base import BaseKeyMaker
6
-
7
-
8
- class CustomKeyMaker(BaseKeyMaker):
9
- async def make(self, function: Callable, prefix: str, identify_key: str = "") -> str:
10
- path = f"{prefix}:{inspect.getmodule(function).__name__}.{function.__name__}:{identify_key}" # type: ignore
11
- return str(path)