local2public 0.1.0__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.
@@ -0,0 +1,11 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+ uv.lock
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: local2public
3
+ Version: 0.1.0
4
+ Summary: Local to public tunnel
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: httpx
@@ -0,0 +1,5 @@
1
+ # l2p - Local to Public
2
+
3
+ ```
4
+ python cli.py --port [port_number] --name [your_website_name]
5
+ ```
@@ -0,0 +1,54 @@
1
+ from fastapi import FastAPI, Request, Response
2
+ from fastapi.responses import HTMLResponse, JSONResponse
3
+ import asyncio
4
+ import uuid
5
+ from fastapi import WebSocket, WebSocketDisconnect
6
+
7
+
8
+ app = FastAPI(
9
+ title="Local To Public",
10
+ docs_url='/',
11
+ redoc_url='/docs',
12
+ )
13
+
14
+ import asyncio
15
+ import uuid
16
+
17
+ pending_requests = {} # request_id -> {"request": ..., "future": ...}
18
+
19
+ @app.get("/poll/{name}")
20
+ async def poll(name: str):
21
+ requests = {
22
+ rid: {"method": r["method"], "path": r["path"], "body": r["body"]}
23
+ for rid, r in pending_requests.items()
24
+ }
25
+ return JSONResponse(requests)
26
+
27
+ @app.post("/respond/{request_id}")
28
+ async def respond(request_id: str, request: Request):
29
+ data = await request.json()
30
+ if request_id in pending_requests:
31
+ pending_requests[request_id]["future"].set_result(data)
32
+ return JSONResponse({"ok": True})
33
+
34
+ @app.api_route("/join/{name}/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
35
+ async def proxy(name: str, path: str, request: Request):
36
+ request_id = str(uuid.uuid4())
37
+ loop = asyncio.get_event_loop()
38
+ future = loop.create_future()
39
+
40
+ pending_requests[request_id] = {
41
+ "method": request.method,
42
+ "path": f"/{path}",
43
+ "body": (await request.body()).decode(),
44
+ "future": future,
45
+ }
46
+
47
+ # wait for CLI to respond
48
+ try:
49
+ response = await asyncio.wait_for(future, timeout=30)
50
+ return Response(content=response["body"], status_code=response["status"])
51
+ except asyncio.TimeoutError:
52
+ return JSONResponse({"error": "timeout"}, status_code=504)
53
+ finally:
54
+ pending_requests.pop(request_id, None)
File without changes
@@ -0,0 +1,61 @@
1
+ from argparse import ArgumentParser
2
+ import asyncio
3
+ import httpx
4
+
5
+ SERVER_URL = "https://l2p-beige.vercel.app" # change to your deployed server
6
+
7
+ def parse_args():
8
+ parser = ArgumentParser(description="ltp - local to public")
9
+ parser.add_argument("-p", "--port", type=int, required=True)
10
+ parser.add_argument("-n", "--name", type=str, required=True)
11
+ parser.add_argument("-v", "--verbose", action="store_true")
12
+ return parser.parse_args()
13
+
14
+ async def tunnel(local_port: int, name: str, verbose: bool):
15
+ print(f"Tunnel open: {SERVER_URL}/join/{name}/")
16
+ print("Ctrl+C to stop")
17
+
18
+ async with httpx.AsyncClient() as client:
19
+ while True:
20
+ try:
21
+ # poll for pending requests
22
+ resp = await client.get(f"{SERVER_URL}/poll/{name}", timeout=5)
23
+ requests = resp.json()
24
+
25
+ for request_id, req in requests.items():
26
+ if verbose:
27
+ print(f"→ {req['method']} {req['path']}")
28
+
29
+ try:
30
+ # forward to local server
31
+ local_resp = await client.request(
32
+ method=req["method"],
33
+ url=f"http://localhost:{local_port}{req['path']}",
34
+ content=req["body"],
35
+ timeout=10,
36
+ )
37
+ # send response back to server
38
+ await client.post(f"{SERVER_URL}/respond/{request_id}", json={
39
+ "status": local_resp.status_code,
40
+ "body": local_resp.text,
41
+ })
42
+ except Exception as e:
43
+ await client.post(f"{SERVER_URL}/respond/{request_id}", json={
44
+ "status": 502,
45
+ "body": str(e),
46
+ })
47
+
48
+ except Exception as e:
49
+ print(f"Poll error: {e}")
50
+
51
+ await asyncio.sleep(0.5)
52
+
53
+ def main():
54
+ args = parse_args()
55
+ try:
56
+ asyncio.run(tunnel(args.port, args.name, args.verbose))
57
+ except KeyboardInterrupt:
58
+ print("\nTunnel closed")
59
+
60
+ if __name__ == "__main__":
61
+ main()
@@ -0,0 +1,15 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "local2public"
7
+ version = "0.1.0"
8
+ description = "Local to public tunnel"
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "httpx",
12
+ ]
13
+
14
+ [project.scripts]
15
+ ltp = "ltp.cli:main"
@@ -0,0 +1,3 @@
1
+ fastapi
2
+ websockets
3
+ uvicorn
@@ -0,0 +1,10 @@
1
+ {
2
+ "builds": [{
3
+ "src": "app.py",
4
+ "use": "@vercel/python"
5
+ }],
6
+ "routes": [{
7
+ "src": "/(.*)",
8
+ "dest": "app.py"
9
+ }]
10
+ }