jvserve 2.0.2__py3-none-any.whl → 2.0.4__py3-none-any.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.

Potentially problematic release.


This version of jvserve might be problematic. Click here for more details.

jvserve/__init__.py CHANGED
@@ -4,5 +4,5 @@ jvserve package initialization.
4
4
  This package provides the webserver for loading and interacting with JIVAS agents.
5
5
  """
6
6
 
7
- __version__ = "2.0.2"
7
+ __version__ = "2.0.4"
8
8
  __supported__jivas__versions__ = ["2.0.0"]
jvserve/cli.py CHANGED
@@ -116,6 +116,7 @@ class JacCmd:
116
116
  """Launch the file server."""
117
117
  # load FastAPI
118
118
  from fastapi import FastAPI
119
+ from fastapi.middleware.cors import CORSMiddleware
119
120
  from fastapi.staticfiles import StaticFiles
120
121
 
121
122
  if directory:
@@ -126,6 +127,24 @@ class JacCmd:
126
127
 
127
128
  # Setup custom routes
128
129
  app = FastAPI()
130
+
131
+ # Add CORS middleware
132
+ app.add_middleware(
133
+ CORSMiddleware,
134
+ allow_origins=["*"],
135
+ allow_credentials=True,
136
+ allow_methods=["*"],
137
+ allow_headers=["*"],
138
+ )
139
+
140
+ app.mount(
141
+ "/files",
142
+ StaticFiles(
143
+ directory=os.environ.get("JIVAS_FILES_ROOT_PATH", ".files")
144
+ ),
145
+ name="files",
146
+ )
147
+
129
148
  app.mount(
130
149
  "/files",
131
150
  StaticFiles(
@@ -6,7 +6,7 @@ import os
6
6
  import string
7
7
  import time
8
8
  import traceback
9
- from typing import Any, Dict, Optional
9
+ from typing import Any, Dict, List, Optional
10
10
  from urllib.parse import quote, unquote
11
11
 
12
12
  import aiohttp
@@ -163,7 +163,7 @@ class AgentInterface:
163
163
  module_root: str = Form(...), # noqa: B008
164
164
  walker: str = Form(...), # noqa: B008
165
165
  args: Optional[str] = Form(None), # noqa: B008
166
- attachments: Optional[list[UploadFile]] = None,
166
+ attachments: List[UploadFile] = Form(default_factory=list), # noqa: B008
167
167
  ) -> JSONResponse:
168
168
  """Execute a named walker exposed by an action within context; capable of handling JSON or file data depending on request"""
169
169
 
@@ -564,11 +564,7 @@ class AgentInterface:
564
564
 
565
565
  # if user context still active, return it
566
566
  now = int(time.time())
567
- if (
568
- AgentInterface.EXPIRATION
569
- and AgentInterface.EXPIRATION.isdigit()
570
- and int(AgentInterface.EXPIRATION) > now
571
- ):
567
+ if AgentInterface.EXPIRATION and AgentInterface.EXPIRATION > now:
572
568
  return {
573
569
  "root_id": AgentInterface.ROOT_ID,
574
570
  "token": AgentInterface.TOKEN,
@@ -0,0 +1,170 @@
1
+ """
2
+ File interface module with base class for making file handling configurable
3
+ for different storage backends.
4
+ """
5
+
6
+ import logging
7
+ import os
8
+ from abc import ABC, abstractmethod
9
+
10
+ # Interface type determined by environment variable, defaults to local
11
+ FILE_INTERFACE = os.environ.get("JIVAS_FILE_INTERFACE", "local")
12
+
13
+
14
+ class FileInterface(ABC):
15
+ """Abstract base class defining the interface for file storage operations."""
16
+
17
+ __root_dir: str = ""
18
+ LOGGER: logging.Logger = logging.getLogger(__name__)
19
+
20
+ @abstractmethod
21
+ def get_file(self, filename: str) -> bytes | None:
22
+ """Retrieve a file from storage and return its contents as bytes."""
23
+ pass
24
+
25
+ @abstractmethod
26
+ def save_file(self, filename: str, content: bytes) -> bool:
27
+ """Save content to a file in storage."""
28
+ pass
29
+
30
+ @abstractmethod
31
+ def delete_file(self, filename: str) -> bool:
32
+ """Delete a file from storage."""
33
+ pass
34
+
35
+ @abstractmethod
36
+ def get_file_url(self, filename: str) -> str | None:
37
+ """Get a URL to access the file."""
38
+ pass
39
+
40
+
41
+ class LocalFileInterface(FileInterface):
42
+ """Implementation of FileInterface for local filesystem storage."""
43
+
44
+ def __init__(self, files_root: str = "") -> None:
45
+ """Initialize local file interface with root directory."""
46
+ self.__root_dir = files_root
47
+
48
+ def get_file(self, filename: str) -> bytes | None:
49
+ """Read and return contents of local file."""
50
+ file_path = os.path.join(self.__root_dir, filename)
51
+ if os.path.exists(file_path):
52
+ with open(file_path, "rb") as f:
53
+ return f.read()
54
+ return None
55
+
56
+ def save_file(self, filename: str, content: bytes) -> bool:
57
+ """Write content to a local file."""
58
+ file_path = os.path.join(self.__root_dir, filename)
59
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
60
+ with open(file_path, "wb") as f:
61
+ f.write(content)
62
+ return True
63
+
64
+ def delete_file(self, filename: str) -> bool:
65
+ """Delete local file."""
66
+ file_path = os.path.join(self.__root_dir, filename)
67
+ if os.path.exists(file_path):
68
+ os.remove(file_path)
69
+ return True
70
+ return False
71
+
72
+ def get_file_url(self, filename: str) -> str | None:
73
+ """Get URL for accessing local file via HTTP."""
74
+ file_path = os.path.join(self.__root_dir, filename)
75
+ if os.path.exists(file_path):
76
+ return f"{os.environ.get('JIVAS_FILES_URL', 'http://localhost:9000/files')}/{filename}"
77
+ return None
78
+
79
+
80
+ class S3FileInterface(FileInterface):
81
+ """Implementation of FileInterface for AWS S3 storage."""
82
+
83
+ def __init__(
84
+ self,
85
+ bucket_name: str,
86
+ aws_access_key_id: str,
87
+ aws_secret_access_key: str,
88
+ region_name: str,
89
+ endpoint_url: str | None = None,
90
+ files_root: str = ".files",
91
+ ) -> None:
92
+ """Initialize S3 file interface."""
93
+ import boto3
94
+ from botocore.config import Config
95
+
96
+ self.s3_client = boto3.client(
97
+ "s3",
98
+ aws_access_key_id=aws_access_key_id,
99
+ aws_secret_access_key=aws_secret_access_key,
100
+ region_name=region_name,
101
+ endpoint_url=endpoint_url,
102
+ config=Config(signature_version="v4"),
103
+ )
104
+ self.bucket_name = bucket_name
105
+ self.__root_dir = files_root
106
+
107
+ # Check for missing AWS credentials
108
+ if not aws_access_key_id or not aws_secret_access_key or not region_name:
109
+ FileInterface.LOGGER.warn(
110
+ "Missing AWS credentials - S3 operations may fail"
111
+ )
112
+
113
+ def get_file(self, filename: str) -> bytes | None:
114
+ """Get file contents from S3."""
115
+ try:
116
+ file_key = os.path.join(self.__root_dir, filename)
117
+ response = self.s3_client.get_object(Bucket=self.bucket_name, Key=file_key)
118
+ return response["Body"].read()
119
+ except Exception:
120
+ return None
121
+
122
+ def save_file(self, filename: str, content: bytes) -> bool:
123
+ """Save file to S3 bucket."""
124
+ try:
125
+ file_key = os.path.join(self.__root_dir, filename)
126
+ self.s3_client.put_object(
127
+ Bucket=self.bucket_name, Key=file_key, Body=content
128
+ )
129
+ return True
130
+ except Exception:
131
+ return False
132
+
133
+ def delete_file(self, filename: str) -> bool:
134
+ """Delete file from S3 bucket."""
135
+ try:
136
+ file_key = os.path.join(self.__root_dir, filename)
137
+ self.s3_client.delete_object(Bucket=self.bucket_name, Key=file_key)
138
+ return True
139
+ except Exception:
140
+ return False
141
+
142
+ def get_file_url(self, filename: str) -> str | None:
143
+ """Get pre-signed URL for S3 file access."""
144
+ try:
145
+ file_key = os.path.join(self.__root_dir, filename)
146
+ url = self.s3_client.generate_presigned_url(
147
+ "get_object",
148
+ Params={"Bucket": self.bucket_name, "Key": file_key},
149
+ ExpiresIn=3600,
150
+ )
151
+ return url
152
+ except Exception:
153
+ return None
154
+
155
+
156
+ file_interface: FileInterface
157
+
158
+ if FILE_INTERFACE == "s3":
159
+ file_interface = S3FileInterface(
160
+ bucket_name=os.environ.get("JIVAS_S3_BUCKET_NAME", ""),
161
+ region_name=os.environ.get("JIVAS_S3_REGION_NAME", "us-east-1"),
162
+ aws_access_key_id=os.environ.get("JIVAS_S3_ACCESS_KEY_ID", ""),
163
+ aws_secret_access_key=os.environ.get("JIVAS_S3_SECRET_ACCESS_KEY", ""),
164
+ endpoint_url=os.environ.get("JIVAS_S3_ENDPOINT_URL"),
165
+ files_root=os.environ.get("JIVAS_FILES_ROOT_PATH", ".files"),
166
+ )
167
+ else:
168
+ file_interface = LocalFileInterface(
169
+ files_root=os.environ.get("JIVAS_FILES_ROOT_PATH", ".files")
170
+ )
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: jvserve
3
- Version: 2.0.2
3
+ Version: 2.0.4
4
4
  Summary: FastAPI webserver for loading and interaction with JIVAS agents.
5
5
  Home-page: https://github.com/TrueSelph/jvserve
6
6
  Author: TrueSelph Inc.
@@ -14,6 +14,7 @@ Requires-Dist: pyaml>=25.1.0
14
14
  Requires-Dist: requests>=2.32.3
15
15
  Requires-Dist: aiohttp>=3.10.10
16
16
  Requires-Dist: schedule>=1.2.2
17
+ Requires-Dist: boto3>=1.37.10
17
18
  Provides-Extra: dev
18
19
  Requires-Dist: pre-commit; extra == "dev"
19
20
  Requires-Dist: pytest; extra == "dev"
@@ -26,6 +27,7 @@ Dynamic: description
26
27
  Dynamic: description-content-type
27
28
  Dynamic: home-page
28
29
  Dynamic: keywords
30
+ Dynamic: license-file
29
31
  Dynamic: provides-extra
30
32
  Dynamic: requires-dist
31
33
  Dynamic: requires-python
@@ -0,0 +1,13 @@
1
+ jvserve/__init__.py,sha256=7UFcPqavIGuuk32gYeDVWCgMuafw020dneTSbvQ4JQI,190
2
+ jvserve/cli.py,sha256=oeWCqA9MAqMYST506f8Nv22qs91OSy555V-NoP-pNOM,4942
3
+ jvserve/lib/__init__.py,sha256=cnzfSHLoTWG9Ygut2nOpDys5aPlQz-m0BSkB-nd7OMs,31
4
+ jvserve/lib/agent_interface.py,sha256=WFHAxCNbkRhqrOsycRnMtPNOQxHvKbQ4oOifz30WhxE,25801
5
+ jvserve/lib/agent_pulse.py,sha256=6hBF6KQYr6Z9Mi_yoWKGfdnW7gg84kK20Slu-bLR_m8,2067
6
+ jvserve/lib/file_interface.py,sha256=lnNOhV9GEmet3-uDQw2hBbvdpnB3-U7amFuR80nv04g,5819
7
+ jvserve/lib/jvlogger.py,sha256=RNiB9PHuBzTvNIQWhxoDgrDlNYA0PYm1SVpvzlqu8mE,4180
8
+ jvserve-2.0.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
9
+ jvserve-2.0.4.dist-info/METADATA,sha256=3ADQS-pw8H6v2WCuKWWAb1jz3r3FlAWiY1lWzVwi9Ks,4790
10
+ jvserve-2.0.4.dist-info/WHEEL,sha256=tTnHoFhvKQHCh4jz3yCn0WPTYIy7wXx3CJtJ7SJGV7c,91
11
+ jvserve-2.0.4.dist-info/entry_points.txt,sha256=HYyg1QXoLs0JRb004L300VeLOZyDLY27ynD1tnTnEN4,35
12
+ jvserve-2.0.4.dist-info/top_level.txt,sha256=afoCXZv-zXNBuhVIvfJGjafXKEiJl_ooy4BtgQwAG4Q,8
13
+ jvserve-2.0.4.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.2)
2
+ Generator: setuptools (77.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,12 +0,0 @@
1
- jvserve/__init__.py,sha256=zlHBOIzb_NwAkzEGHhTKYSrlQdnlIxyyLFcAj5nEJ6Q,190
2
- jvserve/cli.py,sha256=KVWuGPlAt6Vzti-J8CbB4YsgTCc6gS3HaC2OElGqMD0,4392
3
- jvserve/lib/__init__.py,sha256=cnzfSHLoTWG9Ygut2nOpDys5aPlQz-m0BSkB-nd7OMs,31
4
- jvserve/lib/agent_interface.py,sha256=Ob8E2z8kJYIoJ5X1ijInC00yBmv6ri7zLJKQJ5oyp_8,25862
5
- jvserve/lib/agent_pulse.py,sha256=6hBF6KQYr6Z9Mi_yoWKGfdnW7gg84kK20Slu-bLR_m8,2067
6
- jvserve/lib/jvlogger.py,sha256=RNiB9PHuBzTvNIQWhxoDgrDlNYA0PYm1SVpvzlqu8mE,4180
7
- jvserve-2.0.2.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
8
- jvserve-2.0.2.dist-info/METADATA,sha256=_8FBj6wu6ym2ByxcMVsNM8iHAFsGLW5Fo29xWlKtQpI,4738
9
- jvserve-2.0.2.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
10
- jvserve-2.0.2.dist-info/entry_points.txt,sha256=HYyg1QXoLs0JRb004L300VeLOZyDLY27ynD1tnTnEN4,35
11
- jvserve-2.0.2.dist-info/top_level.txt,sha256=afoCXZv-zXNBuhVIvfJGjafXKEiJl_ooy4BtgQwAG4Q,8
12
- jvserve-2.0.2.dist-info/RECORD,,