inferencesh 0.2.23__py3-none-any.whl → 0.4.29__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.
- inferencesh/__init__.py +5 -0
- inferencesh/client.py +1081 -0
- inferencesh/models/base.py +81 -3
- inferencesh/models/file.py +120 -21
- inferencesh/models/llm.py +485 -136
- inferencesh/utils/download.py +15 -7
- inferencesh-0.4.29.dist-info/METADATA +196 -0
- inferencesh-0.4.29.dist-info/RECORD +15 -0
- inferencesh-0.2.23.dist-info/METADATA +0 -105
- inferencesh-0.2.23.dist-info/RECORD +0 -14
- {inferencesh-0.2.23.dist-info → inferencesh-0.4.29.dist-info}/WHEEL +0 -0
- {inferencesh-0.2.23.dist-info → inferencesh-0.4.29.dist-info}/entry_points.txt +0 -0
- {inferencesh-0.2.23.dist-info → inferencesh-0.4.29.dist-info}/licenses/LICENSE +0 -0
- {inferencesh-0.2.23.dist-info → inferencesh-0.4.29.dist-info}/top_level.txt +0 -0
inferencesh/models/base.py
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
from typing import Any, Dict, List
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
2
|
from pydantic import BaseModel, ConfigDict
|
|
3
3
|
import inspect
|
|
4
4
|
import ast
|
|
5
5
|
import textwrap
|
|
6
6
|
from collections import OrderedDict
|
|
7
|
-
|
|
7
|
+
from inferencesh.models.file import File
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
|
|
10
|
+
class Metadata(BaseModel):
|
|
11
|
+
app_id: Optional[str] = None
|
|
12
|
+
app_version_id: Optional[str] = None
|
|
13
|
+
app_variant: Optional[str] = None
|
|
14
|
+
worker_id: Optional[str] = None
|
|
15
|
+
def update(self, other: Dict[str, Any] | BaseModel) -> None:
|
|
16
|
+
update_dict = other.model_dump() if isinstance(other, BaseModel) else other
|
|
17
|
+
for key, value in update_dict.items():
|
|
18
|
+
setattr(self, key, value)
|
|
19
|
+
class Config:
|
|
20
|
+
extra = "allow"
|
|
8
21
|
|
|
9
22
|
class OrderedSchemaModel(BaseModel):
|
|
10
23
|
"""A base model that ensures the JSON schema properties and required fields are in the order of field definition."""
|
|
@@ -91,4 +104,69 @@ class BaseApp(BaseModel):
|
|
|
91
104
|
raise NotImplementedError("run method must be implemented")
|
|
92
105
|
|
|
93
106
|
async def unload(self):
|
|
94
|
-
pass
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# Mixins
|
|
111
|
+
|
|
112
|
+
class OptionalImageFieldMixin(BaseModel):
|
|
113
|
+
image: Optional[File] = Field(
|
|
114
|
+
description="the image to use for the model",
|
|
115
|
+
default=None,
|
|
116
|
+
contentMediaType="image/*",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
class RequiredImageFieldMixin(BaseModel):
|
|
120
|
+
image: File = Field(
|
|
121
|
+
description="the image to use for the model",
|
|
122
|
+
contentMediaType="image/*",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
class OptionalVideoFieldMixin(BaseModel):
|
|
126
|
+
video: Optional[File] = Field(
|
|
127
|
+
description="the video to use for the model",
|
|
128
|
+
default=None,
|
|
129
|
+
contentMediaType="video/*",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
class RequiredVideoFieldMixin(BaseModel):
|
|
133
|
+
video: File = Field(
|
|
134
|
+
description="the video to use for the model",
|
|
135
|
+
contentMediaType="video/*",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
class OptionalAudioFieldMixin(BaseModel):
|
|
139
|
+
audio: Optional[File] = Field(
|
|
140
|
+
description="the audio to use for the model",
|
|
141
|
+
default=None,
|
|
142
|
+
contentMediaType="audio/*",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
class RequiredAudioFieldMixin(BaseModel):
|
|
146
|
+
audio: File = Field(
|
|
147
|
+
description="the audio to use for the model",
|
|
148
|
+
contentMediaType="audio/*",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
class OptionalTextFieldMixin(BaseModel):
|
|
152
|
+
text: Optional[str] = Field(
|
|
153
|
+
description="the text to use for the model",
|
|
154
|
+
default=None,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
class RequiredTextFieldMixin(BaseModel):
|
|
158
|
+
text: str = Field(
|
|
159
|
+
description="the text to use for the model",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
class OptionalFileFieldMixin(BaseModel):
|
|
163
|
+
file: Optional[File] = Field(
|
|
164
|
+
description="the file to use for the model",
|
|
165
|
+
default=None,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
class RequiredFileFieldMixin(BaseModel):
|
|
169
|
+
file: Optional[File] = Field(
|
|
170
|
+
description="the file to use for the model",
|
|
171
|
+
default=None,
|
|
172
|
+
)
|
inferencesh/models/file.py
CHANGED
|
@@ -1,15 +1,48 @@
|
|
|
1
1
|
from typing import Optional, Union, Any
|
|
2
|
-
from pydantic import BaseModel, Field, PrivateAttr, model_validator
|
|
2
|
+
from pydantic import BaseModel, Field, PrivateAttr, model_validator, GetJsonSchemaHandler
|
|
3
|
+
from pydantic_core import CoreSchema
|
|
3
4
|
import mimetypes
|
|
4
5
|
import os
|
|
5
6
|
import urllib.request
|
|
6
7
|
import urllib.parse
|
|
7
|
-
import
|
|
8
|
+
import hashlib
|
|
9
|
+
from pathlib import Path
|
|
8
10
|
from tqdm import tqdm
|
|
9
11
|
|
|
10
|
-
|
|
11
12
|
class File(BaseModel):
|
|
12
13
|
"""A class representing a file in the inference.sh ecosystem."""
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def get_cache_dir(cls) -> Path:
|
|
17
|
+
"""Get the cache directory path based on environment variables or default location."""
|
|
18
|
+
if cache_dir := os.environ.get("FILE_CACHE_DIR"):
|
|
19
|
+
path = Path(cache_dir)
|
|
20
|
+
else:
|
|
21
|
+
path = Path.home() / ".cache" / "inferencesh" / "files"
|
|
22
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
return path
|
|
24
|
+
|
|
25
|
+
def _get_cache_path(self, url: str) -> Path:
|
|
26
|
+
"""Get the cache path for a URL using a hash-based directory structure."""
|
|
27
|
+
# Parse URL components
|
|
28
|
+
parsed_url = urllib.parse.urlparse(url)
|
|
29
|
+
|
|
30
|
+
# Create hash from URL path and query parameters for uniqueness
|
|
31
|
+
url_components = parsed_url.netloc + parsed_url.path
|
|
32
|
+
if parsed_url.query:
|
|
33
|
+
url_components += '?' + parsed_url.query
|
|
34
|
+
url_hash = hashlib.sha256(url_components.encode()).hexdigest()[:12]
|
|
35
|
+
|
|
36
|
+
# Get filename from URL or use default
|
|
37
|
+
filename = os.path.basename(parsed_url.path)
|
|
38
|
+
if not filename:
|
|
39
|
+
filename = 'download'
|
|
40
|
+
|
|
41
|
+
# Create hash directory in cache
|
|
42
|
+
cache_dir = self.get_cache_dir() / url_hash
|
|
43
|
+
cache_dir.mkdir(exist_ok=True)
|
|
44
|
+
|
|
45
|
+
return cache_dir / filename
|
|
13
46
|
uri: Optional[str] = Field(default=None) # Original location (URL or file path)
|
|
14
47
|
path: Optional[str] = None # Resolved local file path
|
|
15
48
|
content_type: Optional[str] = None # MIME type of the file
|
|
@@ -74,14 +107,21 @@ class File(BaseModel):
|
|
|
74
107
|
return parsed.scheme in ('http', 'https')
|
|
75
108
|
|
|
76
109
|
def _download_url(self) -> None:
|
|
77
|
-
"""Download the URL to
|
|
110
|
+
"""Download the URL to the cache directory and update the path."""
|
|
78
111
|
original_url = self.uri
|
|
79
|
-
|
|
112
|
+
cache_path = self._get_cache_path(original_url)
|
|
113
|
+
|
|
114
|
+
# If file exists in cache, use it
|
|
115
|
+
if cache_path.exists():
|
|
116
|
+
print(f"Using cached file: {cache_path}")
|
|
117
|
+
self.path = str(cache_path)
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
print(f"Downloading URL: {original_url} to {cache_path}")
|
|
80
121
|
try:
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
self._tmp_path = tmp_file.name
|
|
122
|
+
# Download to a temporary filename in the final directory
|
|
123
|
+
tmp_path = str(cache_path) + '.tmp'
|
|
124
|
+
self._tmp_path = tmp_path
|
|
85
125
|
|
|
86
126
|
# Set up request with user agent
|
|
87
127
|
headers = {
|
|
@@ -97,26 +137,53 @@ class File(BaseModel):
|
|
|
97
137
|
print(f"Downloading URL: {original_url} to {self._tmp_path}")
|
|
98
138
|
try:
|
|
99
139
|
with urllib.request.urlopen(req) as response:
|
|
100
|
-
|
|
140
|
+
# Safely retrieve content-length if available
|
|
141
|
+
total_size = 0
|
|
142
|
+
try:
|
|
143
|
+
if hasattr(response, 'headers') and response.headers is not None:
|
|
144
|
+
# urllib may expose headers as an email.message.Message
|
|
145
|
+
cl = response.headers.get('content-length')
|
|
146
|
+
total_size = int(cl) if cl is not None else 0
|
|
147
|
+
elif hasattr(response, 'getheader'):
|
|
148
|
+
cl = response.getheader('content-length')
|
|
149
|
+
total_size = int(cl) if cl is not None else 0
|
|
150
|
+
except Exception:
|
|
151
|
+
total_size = 0
|
|
152
|
+
|
|
101
153
|
block_size = 1024 # 1 Kibibyte
|
|
102
154
|
|
|
103
155
|
with tqdm(total=total_size, unit='iB', unit_scale=True) as pbar:
|
|
104
156
|
with open(self._tmp_path, 'wb') as out_file:
|
|
105
157
|
while True:
|
|
106
|
-
|
|
158
|
+
non_chunking = False
|
|
159
|
+
try:
|
|
160
|
+
buffer = response.read(block_size)
|
|
161
|
+
except TypeError:
|
|
162
|
+
# Some mocks (or minimal implementations) expose read() without size
|
|
163
|
+
buffer = response.read()
|
|
164
|
+
non_chunking = True
|
|
107
165
|
if not buffer:
|
|
108
166
|
break
|
|
109
167
|
out_file.write(buffer)
|
|
110
|
-
|
|
168
|
+
try:
|
|
169
|
+
pbar.update(len(buffer))
|
|
170
|
+
except Exception:
|
|
171
|
+
pass
|
|
172
|
+
if non_chunking:
|
|
173
|
+
# If we read the whole body at once, exit loop
|
|
174
|
+
break
|
|
111
175
|
|
|
112
|
-
|
|
176
|
+
# Rename the temporary file to the final name
|
|
177
|
+
os.rename(self._tmp_path, cache_path)
|
|
178
|
+
self._tmp_path = None # Prevent deletion in __del__
|
|
179
|
+
self.path = str(cache_path)
|
|
113
180
|
except (urllib.error.URLError, urllib.error.HTTPError) as e:
|
|
114
181
|
raise RuntimeError(f"Failed to download URL {original_url}: {str(e)}")
|
|
115
182
|
except IOError as e:
|
|
116
183
|
raise RuntimeError(f"Failed to write downloaded file to {self._tmp_path}: {str(e)}")
|
|
117
184
|
except Exception as e:
|
|
118
185
|
# Clean up temp file if something went wrong
|
|
119
|
-
if
|
|
186
|
+
if hasattr(self, '_tmp_path') and self._tmp_path:
|
|
120
187
|
try:
|
|
121
188
|
os.unlink(self._tmp_path)
|
|
122
189
|
except (OSError, IOError):
|
|
@@ -169,14 +236,46 @@ class File(BaseModel):
|
|
|
169
236
|
self.size = self._get_file_size() # Always update size
|
|
170
237
|
self.filename = self._get_filename()
|
|
171
238
|
|
|
239
|
+
# @classmethod
|
|
240
|
+
# def __get_pydantic_core_schema__(
|
|
241
|
+
# cls, source: Type[Any], handler: GetCoreSchemaHandler
|
|
242
|
+
# ) -> CoreSchema:
|
|
243
|
+
# """Generates a Pydantic Core schema for validation of this File class"""
|
|
244
|
+
# # Get the default schema for our class
|
|
245
|
+
# schema = handler(source)
|
|
246
|
+
|
|
247
|
+
# # Create a proper serialization schema that includes the type
|
|
248
|
+
# serialization = core_schema.plain_serializer_function_ser_schema(
|
|
249
|
+
# lambda x: x.uri if x.uri else x.path,
|
|
250
|
+
# return_schema=core_schema.str_schema(),
|
|
251
|
+
# when_used="json",
|
|
252
|
+
# )
|
|
253
|
+
|
|
254
|
+
# return core_schema.json_or_python_schema(
|
|
255
|
+
# json_schema=core_schema.union_schema([
|
|
256
|
+
# core_schema.str_schema(), # Accept string input
|
|
257
|
+
# schema, # Accept full object input
|
|
258
|
+
# ]),
|
|
259
|
+
# python_schema=schema,
|
|
260
|
+
# serialization=serialization,
|
|
261
|
+
# )
|
|
262
|
+
|
|
172
263
|
@classmethod
|
|
173
|
-
def
|
|
174
|
-
schema
|
|
175
|
-
|
|
176
|
-
|
|
264
|
+
def __get_pydantic_json_schema__(
|
|
265
|
+
cls, schema: CoreSchema, handler: GetJsonSchemaHandler
|
|
266
|
+
) -> dict[str, Any]:
|
|
267
|
+
"""Generate a simple JSON schema that accepts either a string or an object"""
|
|
268
|
+
json_schema = handler(schema)
|
|
269
|
+
if "$ref" in json_schema:
|
|
270
|
+
# If we got a ref, resolve it to the actual schema
|
|
271
|
+
json_schema = handler.resolve_ref_schema(json_schema)
|
|
272
|
+
|
|
273
|
+
# Add string as an alternative without recursion
|
|
177
274
|
return {
|
|
275
|
+
"$id": "/schemas/File",
|
|
178
276
|
"oneOf": [
|
|
179
|
-
{
|
|
180
|
-
|
|
277
|
+
{k: v for k, v in json_schema.items() if k != "$ref"}, # Remove any $ref to prevent recursion
|
|
278
|
+
{"type": "string"}
|
|
181
279
|
]
|
|
182
|
-
}
|
|
280
|
+
}
|
|
281
|
+
|