inferencesh 0.1.24__tar.gz → 0.2.1__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.24
3
+ Version: 0.2.1
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,10 @@ 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
+ Requires-Dist: tqdm>=4.67.0
17
+ Provides-Extra: test
18
+ Requires-Dist: pytest>=7.0.0; extra == "test"
19
+ Requires-Dist: pytest-cov>=4.0.0; extra == "test"
16
20
  Dynamic: author
17
21
  Dynamic: license-file
18
22
  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.24"
7
+ version = "0.2.1"
8
8
  description = "inference.sh Python SDK"
9
9
  authors = [
10
10
  {name = "Inference Shell Inc.", email = "hello@inference.sh"},
@@ -18,6 +18,7 @@ classifiers = [
18
18
  ]
19
19
  dependencies = [
20
20
  "pydantic>=2.0.0",
21
+ "tqdm>=4.67.0",
21
22
  ]
22
23
 
23
24
  [project.urls]
@@ -35,3 +36,9 @@ addopts = "-v"
35
36
  [tool.flake8]
36
37
  max-line-length = 100
37
38
  exclude = [".git", "__pycache__", "build", "dist"]
39
+
40
+ [project.optional-dependencies]
41
+ test = [
42
+ "pytest>=7.0.0",
43
+ "pytest-cov>=4.0.0",
44
+ ]
@@ -1,17 +1,21 @@
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
18
+ from tqdm import tqdm
15
19
 
16
20
 
17
21
  # inspired by https://github.com/pydantic/pydantic/issues/7580
@@ -102,39 +106,34 @@ class BaseApp(BaseModel):
102
106
 
103
107
  class File(BaseModel):
104
108
  """A class representing a file in the inference.sh ecosystem."""
105
- uri: Optional[str] = None # Original location (URL or file path)
109
+ uri: Optional[str] = Field(default=None) # Original location (URL or file path)
106
110
  path: Optional[str] = None # Resolved local file path
107
111
  content_type: Optional[str] = None # MIME type of the file
108
112
  size: Optional[int] = None # File size in bytes
109
113
  filename: Optional[str] = None # Original filename if available
110
114
  _tmp_path: Optional[str] = PrivateAttr(default=None) # Internal storage for temporary file path
111
115
 
112
- model_config = ConfigDict(
113
- arbitrary_types_allowed=True,
114
- populate_by_name=True
115
- )
116
-
117
- @classmethod
118
- def __get_validators__(cls):
119
- # First yield the default validators
120
- yield cls.validate
121
-
116
+ def __init__(self, initializer=None, **data):
117
+ if initializer is not None:
118
+ if isinstance(initializer, str):
119
+ data['uri'] = initializer
120
+ elif isinstance(initializer, File):
121
+ data = initializer.model_dump()
122
+ else:
123
+ raise ValueError(f'Invalid input for File: {initializer}')
124
+ super().__init__(**data)
125
+
126
+ @model_validator(mode='before')
122
127
  @classmethod
123
- def validate(cls, value):
124
- """Convert string values to File objects."""
125
- if isinstance(value, str):
126
- # If it's a string, treat it as a uri
127
- return cls(uri=value)
128
- elif isinstance(value, cls):
129
- # If it's already a File instance, return it as is
130
- return value
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
-
128
+ def convert_str_to_file(cls, 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 check_uri_or_path(self) -> 'File':
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._populate_metadata()
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."""
@@ -174,11 +176,22 @@ class File(BaseModel):
174
176
  }
175
177
  req = urllib.request.Request(original_url, headers=headers)
176
178
 
177
- # Download the file
179
+ # Download the file with progress bar
178
180
  print(f"Downloading URL: {original_url} to {self._tmp_path}")
179
181
  try:
180
- with urllib.request.urlopen(req) as response, open(self._tmp_path, 'wb') as out_file:
181
- out_file.write(response.read())
182
+ with urllib.request.urlopen(req) as response:
183
+ total_size = int(response.headers.get('content-length', 0))
184
+ block_size = 1024 # 1 Kibibyte
185
+
186
+ with tqdm(total=total_size, unit='iB', unit_scale=True) as pbar:
187
+ with open(self._tmp_path, 'wb') as out_file:
188
+ while True:
189
+ buffer = response.read(block_size)
190
+ if not buffer:
191
+ break
192
+ out_file.write(buffer)
193
+ pbar.update(len(buffer))
194
+
182
195
  self.path = self._tmp_path
183
196
  except (urllib.error.URLError, urllib.error.HTTPError) as e:
184
197
  raise RuntimeError(f"Failed to download URL {original_url}: {str(e)}")
@@ -234,13 +247,24 @@ class File(BaseModel):
234
247
 
235
248
  def refresh_metadata(self) -> None:
236
249
  """Refresh all metadata from the file."""
237
- self._populate_metadata()
250
+ if os.path.exists(self.path):
251
+ self.content_type = self._guess_content_type()
252
+ self.size = self._get_file_size() # Always update size
253
+ self.filename = self._get_filename()
238
254
 
239
255
 
256
+ class ContextMessageRole(str, Enum):
257
+ USER = "user"
258
+ ASSISTANT = "assistant"
259
+ SYSTEM = "system"
260
+
261
+ class Message(BaseModel):
262
+ role: ContextMessageRole
263
+ content: str
264
+
240
265
  class ContextMessage(BaseModel):
241
- role: str = Field(
266
+ role: ContextMessageRole = Field(
242
267
  description="The role of the message",
243
- enum=["user", "assistant", "system"]
244
268
  )
245
269
  text: str = Field(
246
270
  description="The text content of the message"
@@ -300,4 +324,51 @@ class LLMInputWithImage(LLMInput):
300
324
  image: Optional[File] = Field(
301
325
  description="The image to use for the model",
302
326
  default=None
303
- )
327
+ )
328
+
329
+ class DownloadDir(str, Enum):
330
+ """Standard download directories used by the SDK."""
331
+ DATA = "./data" # Persistent storage/cache directory
332
+ TEMP = "./tmp" # Temporary storage directory
333
+ CACHE = "./cache" # Cache directory
334
+
335
+ def download(url: str, directory: Union[str, Path, DownloadDir]) -> str:
336
+ """Download a file to the specified directory and return its path.
337
+
338
+ Args:
339
+ url: The URL to download from
340
+ directory: The directory to save the file to. Can be a string path,
341
+ Path object, or DownloadDir enum value.
342
+
343
+ Returns:
344
+ str: The path to the downloaded file
345
+ """
346
+ # Convert directory to Path
347
+ dir_path = Path(directory)
348
+ dir_path.mkdir(exist_ok=True)
349
+
350
+ # Create hash directory from URL
351
+ url_hash = hashlib.sha256(url.encode()).hexdigest()[:12]
352
+ hash_dir = dir_path / url_hash
353
+ hash_dir.mkdir(exist_ok=True)
354
+
355
+ # Keep original filename
356
+ filename = os.path.basename(urllib.parse.urlparse(url).path)
357
+ if not filename:
358
+ filename = 'download'
359
+
360
+ output_path = hash_dir / filename
361
+
362
+ # If file exists in directory and it's not a temp directory, return it
363
+ if output_path.exists() and directory != DownloadDir.TEMP:
364
+ return str(output_path)
365
+
366
+ # Download the file
367
+ file = File(url)
368
+ if file.path:
369
+ shutil.copy2(file.path, output_path)
370
+ # Prevent the File instance from deleting its temporary file
371
+ file._tmp_path = None
372
+ return str(output_path)
373
+
374
+ 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.24
3
+ Version: 0.2.1
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,10 @@ 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
+ Requires-Dist: tqdm>=4.67.0
17
+ Provides-Extra: test
18
+ Requires-Dist: pytest>=7.0.0; extra == "test"
19
+ Requires-Dist: pytest-cov>=4.0.0; extra == "test"
16
20
  Dynamic: author
17
21
  Dynamic: license-file
18
22
  Dynamic: requires-python
@@ -0,0 +1,6 @@
1
+ pydantic>=2.0.0
2
+ tqdm>=4.67.0
3
+
4
+ [test]
5
+ pytest>=7.0.0
6
+ 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