jvserve 2.0.10__tar.gz → 2.0.12__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.
- {jvserve-2.0.10 → jvserve-2.0.12}/PKG-INFO +1 -1
- {jvserve-2.0.10 → jvserve-2.0.12}/jvserve/__init__.py +1 -1
- {jvserve-2.0.10 → jvserve-2.0.12}/jvserve/cli.py +117 -16
- {jvserve-2.0.10 → jvserve-2.0.12}/jvserve/lib/file_interface.py +4 -0
- {jvserve-2.0.10 → jvserve-2.0.12}/jvserve.egg-info/PKG-INFO +1 -1
- {jvserve-2.0.10 → jvserve-2.0.12}/tests/test_jvserve.py +20 -0
- {jvserve-2.0.10 → jvserve-2.0.12}/LICENSE +0 -0
- {jvserve-2.0.10 → jvserve-2.0.12}/README.md +0 -0
- {jvserve-2.0.10 → jvserve-2.0.12}/jvserve/lib/__init__.py +0 -0
- {jvserve-2.0.10 → jvserve-2.0.12}/jvserve/lib/agent_interface.py +0 -0
- {jvserve-2.0.10 → jvserve-2.0.12}/jvserve/lib/agent_pulse.py +0 -0
- {jvserve-2.0.10 → jvserve-2.0.12}/jvserve/lib/jvlogger.py +0 -0
- {jvserve-2.0.10 → jvserve-2.0.12}/jvserve.egg-info/SOURCES.txt +0 -0
- {jvserve-2.0.10 → jvserve-2.0.12}/jvserve.egg-info/dependency_links.txt +0 -0
- {jvserve-2.0.10 → jvserve-2.0.12}/jvserve.egg-info/entry_points.txt +0 -0
- {jvserve-2.0.10 → jvserve-2.0.12}/jvserve.egg-info/requires.txt +0 -0
- {jvserve-2.0.10 → jvserve-2.0.12}/jvserve.egg-info/top_level.txt +0 -0
- {jvserve-2.0.10 → jvserve-2.0.12}/setup.cfg +0 -0
- {jvserve-2.0.10 → jvserve-2.0.12}/setup.py +0 -0
- {jvserve-2.0.10 → jvserve-2.0.12}/tests/test_file_interface.py +0 -0
- {jvserve-2.0.10 → jvserve-2.0.12}/tests/test_jvlogger.py +0 -0
|
@@ -7,20 +7,80 @@ from contextlib import asynccontextmanager
|
|
|
7
7
|
from typing import AsyncIterator, Optional
|
|
8
8
|
|
|
9
9
|
from dotenv import load_dotenv
|
|
10
|
+
from fastapi.responses import FileResponse, StreamingResponse
|
|
10
11
|
from jac_cloud.jaseci.security import authenticator
|
|
12
|
+
from jac_cloud.plugin.jaseci import NodeAnchor
|
|
11
13
|
from jaclang.cli.cmdreg import cmd_registry
|
|
12
14
|
from jaclang.plugin.default import hookimpl
|
|
13
15
|
from jaclang.runtimelib.context import ExecutionContext
|
|
14
16
|
from jaclang.runtimelib.machine import JacMachine
|
|
17
|
+
from requests import Response
|
|
15
18
|
from uvicorn import run as _run
|
|
16
19
|
|
|
17
20
|
from jvserve.lib.agent_interface import AgentInterface
|
|
18
21
|
from jvserve.lib.agent_pulse import AgentPulse
|
|
22
|
+
from jvserve.lib.file_interface import (
|
|
23
|
+
DEFAULT_FILES_ROOT,
|
|
24
|
+
FILE_INTERFACE,
|
|
25
|
+
file_interface,
|
|
26
|
+
)
|
|
19
27
|
from jvserve.lib.jvlogger import JVLogger
|
|
20
28
|
|
|
21
29
|
load_dotenv(".env")
|
|
22
30
|
|
|
23
31
|
|
|
32
|
+
def serve_proxied_file(file_path: str) -> FileResponse | StreamingResponse:
|
|
33
|
+
"""Serve a proxied file from a remote or local URL."""
|
|
34
|
+
import mimetypes
|
|
35
|
+
|
|
36
|
+
import requests
|
|
37
|
+
from fastapi import HTTPException
|
|
38
|
+
|
|
39
|
+
if FILE_INTERFACE == "local":
|
|
40
|
+
return FileResponse(path=os.path.join(DEFAULT_FILES_ROOT, file_path))
|
|
41
|
+
|
|
42
|
+
file_url = file_interface.get_file_url(file_path)
|
|
43
|
+
|
|
44
|
+
if file_url and ("localhost" in file_url or "127.0.0.1" in file_url):
|
|
45
|
+
# prevent recusive calls when env vars are not detected
|
|
46
|
+
raise HTTPException(status_code=500, detail="Environment not set up correctly")
|
|
47
|
+
|
|
48
|
+
if not file_url:
|
|
49
|
+
raise HTTPException(status_code=404, detail="File not found")
|
|
50
|
+
|
|
51
|
+
file_extension = os.path.splitext(file_path)[1].lower()
|
|
52
|
+
|
|
53
|
+
# List of extensions to serve directly
|
|
54
|
+
direct_serve_extensions = [
|
|
55
|
+
".pdf",
|
|
56
|
+
".html",
|
|
57
|
+
".txt",
|
|
58
|
+
".js",
|
|
59
|
+
".css",
|
|
60
|
+
".json",
|
|
61
|
+
".xml",
|
|
62
|
+
".svg",
|
|
63
|
+
".csv",
|
|
64
|
+
".ico",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
if file_extension in direct_serve_extensions:
|
|
68
|
+
file_response = requests.get(file_url)
|
|
69
|
+
file_response.raise_for_status() # Raise HTTPError for bad responses (4XX or 5XX)
|
|
70
|
+
|
|
71
|
+
mime_type = mimetypes.guess_type(file_path)[0] or "application/octet-stream"
|
|
72
|
+
|
|
73
|
+
return StreamingResponse(iter([file_response.content]), media_type=mime_type)
|
|
74
|
+
|
|
75
|
+
file_response = requests.get(file_url, stream=True)
|
|
76
|
+
file_response.raise_for_status()
|
|
77
|
+
|
|
78
|
+
return StreamingResponse(
|
|
79
|
+
file_response.iter_content(chunk_size=1024),
|
|
80
|
+
media_type="application/octet-stream",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
24
84
|
class JacCmd:
|
|
25
85
|
"""Jac CLI."""
|
|
26
86
|
|
|
@@ -116,18 +176,12 @@ class JacCmd:
|
|
|
116
176
|
def jvfileserve(
|
|
117
177
|
directory: str, host: str = "0.0.0.0", port: int = 9000
|
|
118
178
|
) -> None:
|
|
119
|
-
"""Launch the file server."""
|
|
179
|
+
"""Launch the file server for local files."""
|
|
120
180
|
# load FastAPI
|
|
121
181
|
from fastapi import FastAPI
|
|
122
182
|
from fastapi.middleware.cors import CORSMiddleware
|
|
123
183
|
from fastapi.staticfiles import StaticFiles
|
|
124
184
|
|
|
125
|
-
if directory:
|
|
126
|
-
os.environ["JIVAS_FILES_ROOT_PATH"] = directory
|
|
127
|
-
|
|
128
|
-
if not os.path.exists(directory):
|
|
129
|
-
os.makedirs(directory)
|
|
130
|
-
|
|
131
185
|
# Setup custom routes
|
|
132
186
|
app = FastAPI()
|
|
133
187
|
|
|
@@ -140,21 +194,68 @@ class JacCmd:
|
|
|
140
194
|
allow_headers=["*"],
|
|
141
195
|
)
|
|
142
196
|
|
|
197
|
+
if not os.path.exists(directory):
|
|
198
|
+
os.makedirs(directory)
|
|
199
|
+
|
|
200
|
+
# Set the environment variable for the file root path
|
|
201
|
+
os.environ["JIVAS_FILES_ROOT_PATH"] = directory
|
|
202
|
+
|
|
203
|
+
# Mount the static files directory
|
|
143
204
|
app.mount(
|
|
144
205
|
"/files",
|
|
145
|
-
StaticFiles(
|
|
146
|
-
directory=os.environ.get("JIVAS_FILES_ROOT_PATH", ".files")
|
|
147
|
-
),
|
|
206
|
+
StaticFiles(directory=directory),
|
|
148
207
|
name="files",
|
|
149
208
|
)
|
|
150
209
|
|
|
151
|
-
app
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
210
|
+
# run the app
|
|
211
|
+
_run(app, host=host, port=port)
|
|
212
|
+
|
|
213
|
+
@cmd_registry.register
|
|
214
|
+
def jvproxyserve(
|
|
215
|
+
directory: str, host: str = "0.0.0.0", port: int = 9000
|
|
216
|
+
) -> None:
|
|
217
|
+
"""Launch the file proxy server for remote files."""
|
|
218
|
+
# load FastAPI
|
|
219
|
+
from bson import ObjectId
|
|
220
|
+
from fastapi import FastAPI, HTTPException
|
|
221
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
222
|
+
|
|
223
|
+
# Setup custom routes
|
|
224
|
+
app = FastAPI()
|
|
225
|
+
|
|
226
|
+
# Add CORS middleware
|
|
227
|
+
app.add_middleware(
|
|
228
|
+
CORSMiddleware,
|
|
229
|
+
allow_origins=["*"],
|
|
230
|
+
allow_credentials=True,
|
|
231
|
+
allow_methods=["*"],
|
|
232
|
+
allow_headers=["*"],
|
|
157
233
|
)
|
|
158
234
|
|
|
235
|
+
# Add proxy routes only if using S3
|
|
236
|
+
if FILE_INTERFACE == "s3":
|
|
237
|
+
|
|
238
|
+
@app.get("/files/{file_path:path}", response_model=None)
|
|
239
|
+
async def serve_file(
|
|
240
|
+
file_path: str,
|
|
241
|
+
) -> Response:
|
|
242
|
+
return serve_proxied_file(file_path)
|
|
243
|
+
|
|
244
|
+
@app.get("/f/{file_id:path}", response_model=None)
|
|
245
|
+
async def get_proxied_file(
|
|
246
|
+
file_id: str,
|
|
247
|
+
) -> Response:
|
|
248
|
+
params = file_id.split("/")
|
|
249
|
+
object_id = params[0]
|
|
250
|
+
|
|
251
|
+
# mongo db collection
|
|
252
|
+
collection = NodeAnchor.Collection.get_collection("url_proxies")
|
|
253
|
+
file_details = collection.find_one({"_id": ObjectId(object_id)})
|
|
254
|
+
|
|
255
|
+
if file_details:
|
|
256
|
+
return serve_proxied_file(file_details["path"])
|
|
257
|
+
|
|
258
|
+
raise HTTPException(status_code=404, detail="File not found")
|
|
259
|
+
|
|
159
260
|
# run the app
|
|
160
261
|
_run(app, host=host, port=port)
|
|
@@ -7,6 +7,10 @@ import logging
|
|
|
7
7
|
import os
|
|
8
8
|
from abc import ABC, abstractmethod
|
|
9
9
|
|
|
10
|
+
from dotenv import load_dotenv
|
|
11
|
+
|
|
12
|
+
load_dotenv(".env")
|
|
13
|
+
|
|
10
14
|
# Interface type determined by environment variable, defaults to local
|
|
11
15
|
FILE_INTERFACE = os.environ.get("JIVAS_FILE_INTERFACE", "local")
|
|
12
16
|
DEFAULT_FILES_ROOT = os.environ.get("JIVAS_FILES_ROOT_PATH", ".files")
|
|
@@ -118,6 +118,26 @@ class JVServeCliTest(unittest.TestCase):
|
|
|
118
118
|
os.remove(f"{directory}/test.txt")
|
|
119
119
|
os.rmdir(directory)
|
|
120
120
|
|
|
121
|
+
def test_jvproxyserve_runs(self) -> None:
|
|
122
|
+
"""Ensure `jac jvproxyserve` runs successfully."""
|
|
123
|
+
try:
|
|
124
|
+
directory = "test_files"
|
|
125
|
+
server_process = subprocess.Popen(
|
|
126
|
+
["jac", "jvproxyserve", directory, "--port", "9100"],
|
|
127
|
+
stdout=subprocess.PIPE,
|
|
128
|
+
stderr=subprocess.PIPE,
|
|
129
|
+
text=True,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Wait for the proxy server to be ready
|
|
133
|
+
self.wait_for_server("http://127.0.0.1:9100/docs")
|
|
134
|
+
|
|
135
|
+
res = httpx.get("http://127.0.0.1:9100/docs")
|
|
136
|
+
self.assertEqual(res.status_code, 200)
|
|
137
|
+
|
|
138
|
+
finally:
|
|
139
|
+
server_process.kill()
|
|
140
|
+
|
|
121
141
|
def tearDown(self) -> None:
|
|
122
142
|
"""Cleanup after each test."""
|
|
123
143
|
self.stop_server()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|