inferencesh 0.1.24__tar.gz → 0.3.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.
Potentially problematic release.
This version of inferencesh might be problematic. Click here for more details.
- {inferencesh-0.1.24/src/inferencesh.egg-info → inferencesh-0.3.0}/PKG-INFO +4 -1
- {inferencesh-0.1.24 → inferencesh-0.3.0}/pyproject.toml +7 -1
- {inferencesh-0.1.24 → inferencesh-0.3.0}/src/inferencesh/sdk.py +92 -32
- {inferencesh-0.1.24 → inferencesh-0.3.0/src/inferencesh.egg-info}/PKG-INFO +4 -1
- inferencesh-0.3.0/src/inferencesh.egg-info/requires.txt +5 -0
- inferencesh-0.3.0/tests/test_sdk.py +162 -0
- inferencesh-0.1.24/src/inferencesh.egg-info/requires.txt +0 -1
- inferencesh-0.1.24/tests/test_sdk.py +0 -31
- {inferencesh-0.1.24 → inferencesh-0.3.0}/LICENSE +0 -0
- {inferencesh-0.1.24 → inferencesh-0.3.0}/README.md +0 -0
- {inferencesh-0.1.24 → inferencesh-0.3.0}/setup.cfg +0 -0
- {inferencesh-0.1.24 → inferencesh-0.3.0}/setup.py +0 -0
- {inferencesh-0.1.24 → inferencesh-0.3.0}/src/inferencesh/__init__.py +0 -0
- {inferencesh-0.1.24 → inferencesh-0.3.0}/src/inferencesh.egg-info/SOURCES.txt +0 -0
- {inferencesh-0.1.24 → inferencesh-0.3.0}/src/inferencesh.egg-info/dependency_links.txt +0 -0
- {inferencesh-0.1.24 → inferencesh-0.3.0}/src/inferencesh.egg-info/entry_points.txt +0 -0
- {inferencesh-0.1.24 → inferencesh-0.3.0}/src/inferencesh.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: inferencesh
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: inference.sh Python SDK
|
|
5
5
|
Author: Inference Shell Inc.
|
|
6
6
|
Author-email: "Inference Shell Inc." <hello@inference.sh>
|
|
@@ -13,6 +13,9 @@ Requires-Python: >=3.7
|
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
15
|
Requires-Dist: pydantic>=2.0.0
|
|
16
|
+
Provides-Extra: test
|
|
17
|
+
Requires-Dist: pytest>=7.0.0; extra == "test"
|
|
18
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "test"
|
|
16
19
|
Dynamic: author
|
|
17
20
|
Dynamic: license-file
|
|
18
21
|
Dynamic: requires-python
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "inferencesh"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "inference.sh Python SDK"
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "Inference Shell Inc.", email = "hello@inference.sh"},
|
|
@@ -35,3 +35,9 @@ addopts = "-v"
|
|
|
35
35
|
[tool.flake8]
|
|
36
36
|
max-line-length = 100
|
|
37
37
|
exclude = [".git", "__pycache__", "build", "dist"]
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
test = [
|
|
41
|
+
"pytest>=7.0.0",
|
|
42
|
+
"pytest-cov>=4.0.0",
|
|
43
|
+
]
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
from typing import Optional, Union
|
|
2
|
-
from pydantic import BaseModel, ConfigDict, PrivateAttr, model_validator
|
|
2
|
+
from pydantic import BaseModel, ConfigDict, PrivateAttr, model_validator, Field, field_validator
|
|
3
3
|
import mimetypes
|
|
4
4
|
import os
|
|
5
5
|
import urllib.request
|
|
6
6
|
import urllib.parse
|
|
7
7
|
import tempfile
|
|
8
|
-
from pydantic import Field
|
|
9
8
|
from typing import Any, Dict, List
|
|
10
9
|
|
|
11
10
|
import inspect
|
|
12
11
|
import ast
|
|
13
12
|
import textwrap
|
|
14
13
|
from collections import OrderedDict
|
|
14
|
+
from enum import Enum
|
|
15
|
+
import shutil
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
import hashlib
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
# inspired by https://github.com/pydantic/pydantic/issues/7580
|
|
@@ -102,39 +105,35 @@ class BaseApp(BaseModel):
|
|
|
102
105
|
|
|
103
106
|
class File(BaseModel):
|
|
104
107
|
"""A class representing a file in the inference.sh ecosystem."""
|
|
105
|
-
uri: Optional[str] = None # Original location (URL or file path)
|
|
108
|
+
uri: Optional[str] = Field(default=None) # Original location (URL or file path)
|
|
106
109
|
path: Optional[str] = None # Resolved local file path
|
|
107
110
|
content_type: Optional[str] = None # MIME type of the file
|
|
108
111
|
size: Optional[int] = None # File size in bytes
|
|
109
112
|
filename: Optional[str] = None # Original filename if available
|
|
110
113
|
_tmp_path: Optional[str] = PrivateAttr(default=None) # Internal storage for temporary file path
|
|
111
114
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
115
|
+
def __init__(self, initializer=None, **data):
|
|
116
|
+
if initializer is not None:
|
|
117
|
+
if isinstance(initializer, str):
|
|
118
|
+
data['uri'] = initializer
|
|
119
|
+
elif isinstance(initializer, File):
|
|
120
|
+
data = initializer.model_dump()
|
|
121
|
+
else:
|
|
122
|
+
raise ValueError(f'Invalid input for File: {initializer}')
|
|
123
|
+
super().__init__(**data)
|
|
124
|
+
|
|
125
|
+
@model_validator(mode='before')
|
|
122
126
|
@classmethod
|
|
123
|
-
def
|
|
124
|
-
"
|
|
125
|
-
if isinstance(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
elif isinstance(value, dict):
|
|
132
|
-
# If it's a dict, use normal validation
|
|
133
|
-
return cls(**value)
|
|
134
|
-
raise ValueError(f'Invalid input for File: {value}')
|
|
135
|
-
|
|
127
|
+
def convert_str_to_file(cls, values):
|
|
128
|
+
print(f"check_uri_or_path input: {values}")
|
|
129
|
+
if isinstance(values, str): # Only accept strings
|
|
130
|
+
return {"uri": values}
|
|
131
|
+
elif isinstance(values, dict):
|
|
132
|
+
return values
|
|
133
|
+
raise ValueError(f'Invalid input for File: {values}')
|
|
134
|
+
|
|
136
135
|
@model_validator(mode='after')
|
|
137
|
-
def
|
|
136
|
+
def validate_required_fields(self) -> 'File':
|
|
138
137
|
"""Validate that either uri or path is provided."""
|
|
139
138
|
if not self.uri and not self.path:
|
|
140
139
|
raise ValueError("Either 'uri' or 'path' must be provided")
|
|
@@ -147,7 +146,10 @@ class File(BaseModel):
|
|
|
147
146
|
self.path = os.path.abspath(self.uri)
|
|
148
147
|
elif self.uri:
|
|
149
148
|
self.path = self.uri
|
|
150
|
-
self.
|
|
149
|
+
if self.path:
|
|
150
|
+
self._populate_metadata()
|
|
151
|
+
else:
|
|
152
|
+
raise ValueError("Either 'uri' or 'path' must be provided")
|
|
151
153
|
|
|
152
154
|
def _is_url(self, path: str) -> bool:
|
|
153
155
|
"""Check if the path is a URL."""
|
|
@@ -234,13 +236,24 @@ class File(BaseModel):
|
|
|
234
236
|
|
|
235
237
|
def refresh_metadata(self) -> None:
|
|
236
238
|
"""Refresh all metadata from the file."""
|
|
237
|
-
self.
|
|
239
|
+
if os.path.exists(self.path):
|
|
240
|
+
self.content_type = self._guess_content_type()
|
|
241
|
+
self.size = self._get_file_size() # Always update size
|
|
242
|
+
self.filename = self._get_filename()
|
|
238
243
|
|
|
239
244
|
|
|
245
|
+
class ContextMessageRole(str, Enum):
|
|
246
|
+
USER = "user"
|
|
247
|
+
ASSISTANT = "assistant"
|
|
248
|
+
SYSTEM = "system"
|
|
249
|
+
|
|
250
|
+
class Message(BaseModel):
|
|
251
|
+
role: ContextMessageRole
|
|
252
|
+
content: str
|
|
253
|
+
|
|
240
254
|
class ContextMessage(BaseModel):
|
|
241
|
-
role:
|
|
255
|
+
role: ContextMessageRole = Field(
|
|
242
256
|
description="The role of the message",
|
|
243
|
-
enum=["user", "assistant", "system"]
|
|
244
257
|
)
|
|
245
258
|
text: str = Field(
|
|
246
259
|
description="The text content of the message"
|
|
@@ -300,4 +313,51 @@ class LLMInputWithImage(LLMInput):
|
|
|
300
313
|
image: Optional[File] = Field(
|
|
301
314
|
description="The image to use for the model",
|
|
302
315
|
default=None
|
|
303
|
-
)
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
class DownloadDir(str, Enum):
|
|
319
|
+
"""Standard download directories used by the SDK."""
|
|
320
|
+
DATA = "./data" # Persistent storage/cache directory
|
|
321
|
+
TEMP = "./tmp" # Temporary storage directory
|
|
322
|
+
CACHE = "./cache" # Cache directory
|
|
323
|
+
|
|
324
|
+
def download(url: str, directory: Union[str, Path, DownloadDir]) -> str:
|
|
325
|
+
"""Download a file to the specified directory and return its path.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
url: The URL to download from
|
|
329
|
+
directory: The directory to save the file to. Can be a string path,
|
|
330
|
+
Path object, or DownloadDir enum value.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
str: The path to the downloaded file
|
|
334
|
+
"""
|
|
335
|
+
# Convert directory to Path
|
|
336
|
+
dir_path = Path(directory)
|
|
337
|
+
dir_path.mkdir(exist_ok=True)
|
|
338
|
+
|
|
339
|
+
# Create hash directory from URL
|
|
340
|
+
url_hash = hashlib.sha256(url.encode()).hexdigest()[:12]
|
|
341
|
+
hash_dir = dir_path / url_hash
|
|
342
|
+
hash_dir.mkdir(exist_ok=True)
|
|
343
|
+
|
|
344
|
+
# Keep original filename
|
|
345
|
+
filename = os.path.basename(urllib.parse.urlparse(url).path)
|
|
346
|
+
if not filename:
|
|
347
|
+
filename = 'download'
|
|
348
|
+
|
|
349
|
+
output_path = hash_dir / filename
|
|
350
|
+
|
|
351
|
+
# If file exists in directory and it's not a temp directory, return it
|
|
352
|
+
if output_path.exists() and directory != DownloadDir.TEMP:
|
|
353
|
+
return str(output_path)
|
|
354
|
+
|
|
355
|
+
# Download the file
|
|
356
|
+
file = File(url)
|
|
357
|
+
if file.path:
|
|
358
|
+
shutil.copy2(file.path, output_path)
|
|
359
|
+
# Prevent the File instance from deleting its temporary file
|
|
360
|
+
file._tmp_path = None
|
|
361
|
+
return str(output_path)
|
|
362
|
+
|
|
363
|
+
raise RuntimeError(f"Failed to download {url}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: inferencesh
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: inference.sh Python SDK
|
|
5
5
|
Author: Inference Shell Inc.
|
|
6
6
|
Author-email: "Inference Shell Inc." <hello@inference.sh>
|
|
@@ -13,6 +13,9 @@ Requires-Python: >=3.7
|
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
15
|
Requires-Dist: pydantic>=2.0.0
|
|
16
|
+
Provides-Extra: test
|
|
17
|
+
Requires-Dist: pytest>=7.0.0; extra == "test"
|
|
18
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "test"
|
|
16
19
|
Dynamic: author
|
|
17
20
|
Dynamic: license-file
|
|
18
21
|
Dynamic: requires-python
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pytest
|
|
3
|
+
import tempfile
|
|
4
|
+
from inferencesh import BaseApp, BaseAppInput, BaseAppOutput, File
|
|
5
|
+
import urllib.parse
|
|
6
|
+
|
|
7
|
+
def test_file_creation():
|
|
8
|
+
# Create a temporary file
|
|
9
|
+
with open("test.txt", "w") as f:
|
|
10
|
+
f.write("test")
|
|
11
|
+
|
|
12
|
+
file = File(path="test.txt")
|
|
13
|
+
assert file.exists()
|
|
14
|
+
assert file.size > 0
|
|
15
|
+
assert file.content_type is not None
|
|
16
|
+
assert file.filename == "test.txt"
|
|
17
|
+
|
|
18
|
+
os.remove("test.txt")
|
|
19
|
+
|
|
20
|
+
def test_base_app():
|
|
21
|
+
class TestInput(BaseAppInput):
|
|
22
|
+
text: str
|
|
23
|
+
|
|
24
|
+
class TestOutput(BaseAppOutput):
|
|
25
|
+
result: str
|
|
26
|
+
|
|
27
|
+
# Use BaseApp directly, don't subclass with implementation
|
|
28
|
+
app = BaseApp()
|
|
29
|
+
import asyncio
|
|
30
|
+
with pytest.raises(NotImplementedError):
|
|
31
|
+
asyncio.run(app.run(TestInput(text="test")))
|
|
32
|
+
|
|
33
|
+
def test_file_from_local_path():
|
|
34
|
+
# Create a temporary file
|
|
35
|
+
with tempfile.NamedTemporaryFile(suffix='.txt', delete=False) as f:
|
|
36
|
+
f.write(b"test content")
|
|
37
|
+
path = f.name
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
# Test creating File from path
|
|
41
|
+
file = File(uri=path)
|
|
42
|
+
assert file.exists()
|
|
43
|
+
assert file.size == len("test content")
|
|
44
|
+
assert file.content_type == "text/plain"
|
|
45
|
+
assert file.filename == os.path.basename(path)
|
|
46
|
+
assert file.path == os.path.abspath(path)
|
|
47
|
+
assert file._tmp_path is None # Should not create temp file for local paths
|
|
48
|
+
finally:
|
|
49
|
+
os.unlink(path)
|
|
50
|
+
|
|
51
|
+
def test_file_from_relative_path():
|
|
52
|
+
# Create a file in current directory
|
|
53
|
+
with open("test_relative.txt", "w") as f:
|
|
54
|
+
f.write("relative test")
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
file = File(uri="test_relative.txt")
|
|
58
|
+
assert file.exists()
|
|
59
|
+
assert os.path.isabs(file.path)
|
|
60
|
+
assert file.filename == "test_relative.txt"
|
|
61
|
+
finally:
|
|
62
|
+
os.unlink("test_relative.txt")
|
|
63
|
+
|
|
64
|
+
def test_file_validation():
|
|
65
|
+
# Test empty initialization
|
|
66
|
+
with pytest.raises(ValueError, match="Either 'uri' or 'path' must be provided"):
|
|
67
|
+
File()
|
|
68
|
+
|
|
69
|
+
# Test invalid input type
|
|
70
|
+
with pytest.raises(ValueError, match="Invalid input for File"):
|
|
71
|
+
File(123)
|
|
72
|
+
|
|
73
|
+
# Test string input (should work)
|
|
74
|
+
with tempfile.NamedTemporaryFile(suffix='.txt', delete=False) as f:
|
|
75
|
+
f.write(b"test content")
|
|
76
|
+
path = f.name
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
file = File(path)
|
|
80
|
+
assert isinstance(file, File)
|
|
81
|
+
assert file.uri == path
|
|
82
|
+
assert file.exists()
|
|
83
|
+
finally:
|
|
84
|
+
os.unlink(path)
|
|
85
|
+
|
|
86
|
+
def test_file_from_url(monkeypatch):
|
|
87
|
+
# Mock URL download
|
|
88
|
+
def mock_urlopen(request):
|
|
89
|
+
class MockResponse:
|
|
90
|
+
def __enter__(self):
|
|
91
|
+
return self
|
|
92
|
+
|
|
93
|
+
def __exit__(self, *args):
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
def read(self):
|
|
97
|
+
return b"mocked content"
|
|
98
|
+
|
|
99
|
+
return MockResponse()
|
|
100
|
+
|
|
101
|
+
monkeypatch.setattr(urllib.request, 'urlopen', mock_urlopen)
|
|
102
|
+
|
|
103
|
+
url = "https://example.com/test.pdf"
|
|
104
|
+
file = File(uri=url)
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
assert file._is_url(url)
|
|
108
|
+
assert file.exists()
|
|
109
|
+
assert file._tmp_path is not None
|
|
110
|
+
assert file._tmp_path.endswith('.pdf') # Just check the extension
|
|
111
|
+
assert file.content_type == "application/pdf"
|
|
112
|
+
finally:
|
|
113
|
+
# Cleanup should happen in __del__ but let's be explicit for testing
|
|
114
|
+
if file._tmp_path and os.path.exists(file._tmp_path):
|
|
115
|
+
os.unlink(file._tmp_path)
|
|
116
|
+
|
|
117
|
+
def test_file_metadata_refresh():
|
|
118
|
+
with tempfile.NamedTemporaryFile(suffix='.json', delete=False) as f:
|
|
119
|
+
initial_content = b'{"test": "data"}'
|
|
120
|
+
f.write(initial_content)
|
|
121
|
+
path = f.name
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
file = File(uri=path)
|
|
125
|
+
initial_size = file.size
|
|
126
|
+
|
|
127
|
+
# Modify file with significantly more data
|
|
128
|
+
with open(path, 'ab') as f: # Open in append binary mode
|
|
129
|
+
additional_data = b'\n{"more": "data"}\n' * 10 # Add multiple lines of data
|
|
130
|
+
f.write(additional_data)
|
|
131
|
+
|
|
132
|
+
# Refresh metadata
|
|
133
|
+
file.refresh_metadata()
|
|
134
|
+
assert file.size > initial_size, f"New size {file.size} should be larger than initial size {initial_size}"
|
|
135
|
+
finally:
|
|
136
|
+
os.unlink(path)
|
|
137
|
+
|
|
138
|
+
def test_file_cleanup(monkeypatch):
|
|
139
|
+
# Mock URL download - same mock as test_file_from_url
|
|
140
|
+
def mock_urlopen(request):
|
|
141
|
+
class MockResponse:
|
|
142
|
+
def __enter__(self):
|
|
143
|
+
return self
|
|
144
|
+
|
|
145
|
+
def __exit__(self, *args):
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
def read(self):
|
|
149
|
+
return b"mocked content"
|
|
150
|
+
|
|
151
|
+
return MockResponse()
|
|
152
|
+
|
|
153
|
+
monkeypatch.setattr(urllib.request, 'urlopen', mock_urlopen)
|
|
154
|
+
|
|
155
|
+
url = "https://example.com/test.txt"
|
|
156
|
+
file = File(uri=url)
|
|
157
|
+
|
|
158
|
+
if file._tmp_path:
|
|
159
|
+
tmp_path = file._tmp_path
|
|
160
|
+
assert os.path.exists(tmp_path)
|
|
161
|
+
del file
|
|
162
|
+
assert not os.path.exists(tmp_path)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
pydantic>=2.0.0
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import pytest
|
|
3
|
-
from inferencesh import BaseApp, BaseAppInput, BaseAppOutput, File
|
|
4
|
-
|
|
5
|
-
def test_file_creation():
|
|
6
|
-
# Create a temporary file
|
|
7
|
-
with open("test.txt", "w") as f:
|
|
8
|
-
f.write("test")
|
|
9
|
-
|
|
10
|
-
file = File(path="test.txt")
|
|
11
|
-
assert file.exists()
|
|
12
|
-
assert file.size > 0
|
|
13
|
-
assert file.content_type is not None
|
|
14
|
-
assert file.filename == "test.txt"
|
|
15
|
-
|
|
16
|
-
os.remove("test.txt")
|
|
17
|
-
|
|
18
|
-
def test_base_app():
|
|
19
|
-
class TestInput(BaseAppInput):
|
|
20
|
-
text: str
|
|
21
|
-
|
|
22
|
-
class TestOutput(BaseAppOutput):
|
|
23
|
-
result: str
|
|
24
|
-
|
|
25
|
-
class TestApp(BaseApp):
|
|
26
|
-
async def run(self, app_input: TestInput) -> TestOutput:
|
|
27
|
-
return TestOutput(result=f"Processed: {app_input.text}")
|
|
28
|
-
|
|
29
|
-
app = TestApp()
|
|
30
|
-
with pytest.raises(NotImplementedError):
|
|
31
|
-
app.run(TestInput(text="test"))
|
|
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
|