inferencesh 0.1.23__tar.gz → 0.2.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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inferencesh
3
- Version: 0.1.23
3
+ Version: 0.2.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.1.23"
7
+ version = "0.2.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
- from typing import Optional, Union, ClassVar
2
- from pydantic import BaseModel, ConfigDict, PrivateAttr, model_validator
1
+ from typing import Optional, Union
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,20 +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
- model_config = ConfigDict(
113
- arbitrary_types_allowed=True,
114
- populate_by_name=True
115
- )
116
-
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')
126
+ @classmethod
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
+
117
135
  @model_validator(mode='after')
118
- def check_uri_or_path(self) -> 'File':
136
+ def validate_required_fields(self) -> 'File':
119
137
  """Validate that either uri or path is provided."""
120
138
  if not self.uri and not self.path:
121
139
  raise ValueError("Either 'uri' or 'path' must be provided")
@@ -128,7 +146,10 @@ class File(BaseModel):
128
146
  self.path = os.path.abspath(self.uri)
129
147
  elif self.uri:
130
148
  self.path = self.uri
131
- self._populate_metadata()
149
+ if self.path:
150
+ self._populate_metadata()
151
+ else:
152
+ raise ValueError("Either 'uri' or 'path' must be provided")
132
153
 
133
154
  def _is_url(self, path: str) -> bool:
134
155
  """Check if the path is a URL."""
@@ -170,7 +191,7 @@ class File(BaseModel):
170
191
  if tmp_file is not None and hasattr(self, '_tmp_path'):
171
192
  try:
172
193
  os.unlink(self._tmp_path)
173
- except:
194
+ except (OSError, IOError):
174
195
  pass
175
196
  raise RuntimeError(f"Error downloading URL {original_url}: {str(e)}")
176
197
 
@@ -179,7 +200,7 @@ class File(BaseModel):
179
200
  if hasattr(self, '_tmp_path') and self._tmp_path:
180
201
  try:
181
202
  os.unlink(self._tmp_path)
182
- except:
203
+ except (OSError, IOError):
183
204
  pass
184
205
 
185
206
  def _populate_metadata(self) -> None:
@@ -215,13 +236,24 @@ class File(BaseModel):
215
236
 
216
237
  def refresh_metadata(self) -> None:
217
238
  """Refresh all metadata from the file."""
218
- self._populate_metadata()
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()
243
+
219
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
220
253
 
221
254
  class ContextMessage(BaseModel):
222
- role: str = Field(
255
+ role: ContextMessageRole = Field(
223
256
  description="The role of the message",
224
- enum=["user", "assistant", "system"]
225
257
  )
226
258
  text: str = Field(
227
259
  description="The text content of the message"
@@ -281,4 +313,51 @@ class LLMInputWithImage(LLMInput):
281
313
  image: Optional[File] = Field(
282
314
  description="The image to use for the model",
283
315
  default=None
284
- )
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.1.23
3
+ Version: 0.2.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,5 @@
1
+ pydantic>=2.0.0
2
+
3
+ [test]
4
+ pytest>=7.0.0
5
+ pytest-cov>=4.0.0
@@ -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