inferencesh 0.3.0__py3-none-any.whl → 0.3.1__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 inferencesh might be problematic. Click here for more details.

@@ -0,0 +1,6 @@
1
+ """Utilities package for inference.sh SDK."""
2
+
3
+ from .storage import StorageDir
4
+ from .download import download
5
+
6
+ __all__ = ["StorageDir", "download"]
@@ -0,0 +1,51 @@
1
+ import hashlib
2
+ import os
3
+ import urllib.parse
4
+ import shutil
5
+ from pathlib import Path
6
+ from typing import Union
7
+
8
+ from ..models.file import File
9
+ from .storage import StorageDir
10
+
11
+
12
+ def download(url: str, directory: Union[str, Path, StorageDir]) -> str:
13
+ """Download a file to the specified directory and return its path.
14
+
15
+ Args:
16
+ url: The URL to download from
17
+ directory: The directory to save the file to. Can be a string path,
18
+ Path object, or StorageDir enum value.
19
+
20
+ Returns:
21
+ str: The path to the downloaded file
22
+ """
23
+ # Convert directory to Path
24
+ dir_path = Path(directory)
25
+ dir_path.mkdir(exist_ok=True)
26
+
27
+ # Create hash directory from URL
28
+ url_hash = hashlib.sha256(url.encode()).hexdigest()[:12]
29
+ hash_dir = dir_path / url_hash
30
+ hash_dir.mkdir(exist_ok=True)
31
+
32
+ # Keep original filename
33
+ filename = os.path.basename(urllib.parse.urlparse(url).path)
34
+ if not filename:
35
+ filename = 'download'
36
+
37
+ output_path = hash_dir / filename
38
+
39
+ # If file exists in directory and it's not a temp directory, return it
40
+ if output_path.exists() and directory != StorageDir.TEMP:
41
+ return str(output_path)
42
+
43
+ # Download the file
44
+ file = File(url)
45
+ if file.path:
46
+ shutil.copy2(file.path, output_path)
47
+ # Prevent the File instance from deleting its temporary file
48
+ file._tmp_path = None
49
+ return str(output_path)
50
+
51
+ raise RuntimeError(f"Failed to download {url}")
@@ -0,0 +1,16 @@
1
+ from enum import Enum
2
+ from pathlib import Path
3
+
4
+
5
+ class StorageDir(str, Enum):
6
+ """Standard storage directories used by the SDK."""
7
+ DATA = "/app/data" # Persistent storage/cache directory
8
+ TEMP = "/app/tmp" # Temporary storage directory
9
+ CACHE = "/app/cache" # Cache directory
10
+
11
+ @property
12
+ def path(self) -> Path:
13
+ """Get the Path object for this storage directory, ensuring it exists."""
14
+ path = Path(self.value)
15
+ path.mkdir(parents=True, exist_ok=True)
16
+ return path
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inferencesh
3
- Version: 0.3.0
3
+ Version: 0.3.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,9 +13,14 @@ 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
+ Requires-Dist: requests>=2.31.0
16
18
  Provides-Extra: test
17
19
  Requires-Dist: pytest>=7.0.0; extra == "test"
18
20
  Requires-Dist: pytest-cov>=4.0.0; extra == "test"
21
+ Provides-Extra: async
22
+ Requires-Dist: aiohttp>=3.9.0; python_version >= "3.8" and extra == "async"
23
+ Requires-Dist: aiofiles>=23.2.1; python_version >= "3.8" and extra == "async"
19
24
  Dynamic: author
20
25
  Dynamic: license-file
21
26
  Dynamic: requires-python
@@ -0,0 +1,15 @@
1
+ inferencesh/__init__.py,sha256=dY3l3yCkWoMtGX0gNXgxFnrprFRl6PPWjH8V7Qedx5g,772
2
+ inferencesh/client.py,sha256=cm7E-8LxP8jyb7JnANmcBtF1ya2i7sMBW2Pq-Oh-mcM,31318
3
+ inferencesh/models/__init__.py,sha256=FDwcdtT6c4hbRitymjmN-hZMlQa8RbKSftkZZyjtUXA,536
4
+ inferencesh/models/base.py,sha256=4gZQRi8J7y9U6PrGD9pRIehd1MJVJAqGakPQDs2AKFM,3251
5
+ inferencesh/models/file.py,sha256=0CSbIoFTvGT1CmGnMjkNmGR1N-zg64bmheVUWiCczxE,8714
6
+ inferencesh/models/llm.py,sha256=GLcEkDizBbgcfc-zC719wDe44th3EGf3FpKERjIAPE8,27755
7
+ inferencesh/utils/__init__.py,sha256=-xiD6uo2XzcrPAWFb_fUbaimmnW4KFKc-8IvBzaxNd4,148
8
+ inferencesh/utils/download.py,sha256=7n5twvoNYDcFnKJyefImaj2YfzRI7vddQw4usZbj38c,1521
9
+ inferencesh/utils/storage.py,sha256=E4J8emd4eFKdmdDgAqzz3TpaaDd3n0l8gYlMHuY8yIU,519
10
+ inferencesh-0.3.1.dist-info/licenses/LICENSE,sha256=OsgqEWIh2el_QMj0y8O1A5Q5Dl-dxqqYbFE6fszuR4s,1086
11
+ inferencesh-0.3.1.dist-info/METADATA,sha256=gU67lD_F5c66kAhn375nGfLpP57ueG5PPrlJsvPD8uY,2964
12
+ inferencesh-0.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
+ inferencesh-0.3.1.dist-info/entry_points.txt,sha256=6IC-fyozAqW3ljsMLGCXxJ0_ui2Jb-2fLHtoH1RTnEE,45
14
+ inferencesh-0.3.1.dist-info/top_level.txt,sha256=TSMHg3T1ThMl1HGAWmzBClwOYH1ump5neof9BfHIwaA,12
15
+ inferencesh-0.3.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.3.1)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
inferencesh/sdk.py DELETED
@@ -1,363 +0,0 @@
1
- from typing import Optional, Union
2
- from pydantic import BaseModel, ConfigDict, PrivateAttr, model_validator, Field, field_validator
3
- import mimetypes
4
- import os
5
- import urllib.request
6
- import urllib.parse
7
- import tempfile
8
- from typing import Any, Dict, List
9
-
10
- import inspect
11
- import ast
12
- import textwrap
13
- from collections import OrderedDict
14
- from enum import Enum
15
- import shutil
16
- from pathlib import Path
17
- import hashlib
18
-
19
-
20
- # inspired by https://github.com/pydantic/pydantic/issues/7580
21
- class OrderedSchemaModel(BaseModel):
22
- """A base model that ensures the JSON schema properties and required fields are in the order of field definition."""
23
-
24
- @classmethod
25
- def model_json_schema(cls, by_alias: bool = True, **kwargs: Any) -> Dict[str, Any]:
26
- schema = super().model_json_schema(by_alias=by_alias, **kwargs)
27
-
28
- field_order = cls._get_field_order()
29
-
30
- if field_order:
31
- # Order properties
32
- ordered_properties = OrderedDict()
33
- for field_name in field_order:
34
- if field_name in schema['properties']:
35
- ordered_properties[field_name] = schema['properties'][field_name]
36
-
37
- # Add any remaining properties that weren't in field_order
38
- for field_name, field_schema in schema['properties'].items():
39
- if field_name not in ordered_properties:
40
- ordered_properties[field_name] = field_schema
41
-
42
- schema['properties'] = ordered_properties
43
-
44
- # Order required fields
45
- if 'required' in schema:
46
- ordered_required = [field for field in field_order if field in schema['required']]
47
- # Add any remaining required fields that weren't in field_order
48
- ordered_required.extend([field for field in schema['required'] if field not in ordered_required])
49
- schema['required'] = ordered_required
50
-
51
- return schema
52
-
53
- @classmethod
54
- def _get_field_order(cls) -> List[str]:
55
- """Get the order of fields as they were defined in the class."""
56
- source = inspect.getsource(cls)
57
-
58
- # Unindent the entire source code
59
- source = textwrap.dedent(source)
60
-
61
- try:
62
- module = ast.parse(source)
63
- except IndentationError:
64
- # If we still get an IndentationError, wrap the class in a dummy module
65
- source = f"class DummyModule:\n{textwrap.indent(source, ' ')}"
66
- module = ast.parse(source)
67
- # Adjust to look at the first class def inside DummyModule
68
- # noinspection PyUnresolvedReferences
69
- class_def = module.body[0].body[0]
70
- else:
71
- # Find the class definition
72
- class_def = next(
73
- node for node in module.body if isinstance(node, ast.ClassDef) and node.name == cls.__name__
74
- )
75
-
76
- # Extract field names in the order they were defined
77
- field_order = []
78
- for node in class_def.body:
79
- if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
80
- field_order.append(node.target.id)
81
-
82
- return field_order
83
-
84
- class BaseAppInput(OrderedSchemaModel):
85
- pass
86
-
87
- class BaseAppOutput(OrderedSchemaModel):
88
- pass
89
-
90
- class BaseApp(BaseModel):
91
- model_config = ConfigDict(
92
- arbitrary_types_allowed=True,
93
- extra='allow'
94
- )
95
-
96
- async def setup(self):
97
- pass
98
-
99
- async def run(self, app_input: BaseAppInput) -> BaseAppOutput:
100
- raise NotImplementedError("run method must be implemented")
101
-
102
- async def unload(self):
103
- pass
104
-
105
-
106
- class File(BaseModel):
107
- """A class representing a file in the inference.sh ecosystem."""
108
- uri: Optional[str] = Field(default=None) # Original location (URL or file path)
109
- path: Optional[str] = None # Resolved local file path
110
- content_type: Optional[str] = None # MIME type of the file
111
- size: Optional[int] = None # File size in bytes
112
- filename: Optional[str] = None # Original filename if available
113
- _tmp_path: Optional[str] = PrivateAttr(default=None) # Internal storage for temporary file path
114
-
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
-
135
- @model_validator(mode='after')
136
- def validate_required_fields(self) -> 'File':
137
- """Validate that either uri or path is provided."""
138
- if not self.uri and not self.path:
139
- raise ValueError("Either 'uri' or 'path' must be provided")
140
- return self
141
-
142
- def model_post_init(self, _: Any) -> None:
143
- if self.uri and self._is_url(self.uri):
144
- self._download_url()
145
- elif self.uri and not os.path.isabs(self.uri):
146
- self.path = os.path.abspath(self.uri)
147
- elif self.uri:
148
- self.path = self.uri
149
- if self.path:
150
- self._populate_metadata()
151
- else:
152
- raise ValueError("Either 'uri' or 'path' must be provided")
153
-
154
- def _is_url(self, path: str) -> bool:
155
- """Check if the path is a URL."""
156
- parsed = urllib.parse.urlparse(path)
157
- return parsed.scheme in ('http', 'https')
158
-
159
- def _download_url(self) -> None:
160
- """Download the URL to a temporary file and update the path."""
161
- original_url = self.uri
162
- tmp_file = None
163
- try:
164
- # Create a temporary file with a suffix based on the URL path
165
- suffix = os.path.splitext(urllib.parse.urlparse(original_url).path)[1]
166
- tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
167
- self._tmp_path = tmp_file.name
168
-
169
- # Set up request with user agent
170
- headers = {
171
- 'User-Agent': (
172
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
173
- 'AppleWebKit/537.36 (KHTML, like Gecko) '
174
- 'Chrome/91.0.4472.124 Safari/537.36'
175
- )
176
- }
177
- req = urllib.request.Request(original_url, headers=headers)
178
-
179
- # Download the file
180
- print(f"Downloading URL: {original_url} to {self._tmp_path}")
181
- try:
182
- with urllib.request.urlopen(req) as response, open(self._tmp_path, 'wb') as out_file:
183
- out_file.write(response.read())
184
- self.path = self._tmp_path
185
- except (urllib.error.URLError, urllib.error.HTTPError) as e:
186
- raise RuntimeError(f"Failed to download URL {original_url}: {str(e)}")
187
- except IOError as e:
188
- raise RuntimeError(f"Failed to write downloaded file to {self._tmp_path}: {str(e)}")
189
- except Exception as e:
190
- # Clean up temp file if something went wrong
191
- if tmp_file is not None and hasattr(self, '_tmp_path'):
192
- try:
193
- os.unlink(self._tmp_path)
194
- except (OSError, IOError):
195
- pass
196
- raise RuntimeError(f"Error downloading URL {original_url}: {str(e)}")
197
-
198
- def __del__(self):
199
- """Cleanup temporary file if it exists."""
200
- if hasattr(self, '_tmp_path') and self._tmp_path:
201
- try:
202
- os.unlink(self._tmp_path)
203
- except (OSError, IOError):
204
- pass
205
-
206
- def _populate_metadata(self) -> None:
207
- """Populate file metadata from the path if it exists."""
208
- if os.path.exists(self.path):
209
- if not self.content_type:
210
- self.content_type = self._guess_content_type()
211
- if not self.size:
212
- self.size = self._get_file_size()
213
- if not self.filename:
214
- self.filename = self._get_filename()
215
-
216
- @classmethod
217
- def from_path(cls, path: Union[str, os.PathLike]) -> 'File':
218
- """Create a File instance from a file path."""
219
- return cls(uri=str(path))
220
-
221
- def _guess_content_type(self) -> Optional[str]:
222
- """Guess the MIME type of the file."""
223
- return mimetypes.guess_type(self.path)[0]
224
-
225
- def _get_file_size(self) -> int:
226
- """Get the size of the file in bytes."""
227
- return os.path.getsize(self.path)
228
-
229
- def _get_filename(self) -> str:
230
- """Get the base filename from the path."""
231
- return os.path.basename(self.path)
232
-
233
- def exists(self) -> bool:
234
- """Check if the file exists."""
235
- return os.path.exists(self.path)
236
-
237
- def refresh_metadata(self) -> None:
238
- """Refresh all metadata from the file."""
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
-
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
-
254
- class ContextMessage(BaseModel):
255
- role: ContextMessageRole = Field(
256
- description="The role of the message",
257
- )
258
- text: str = Field(
259
- description="The text content of the message"
260
- )
261
-
262
- class ContextMessageWithImage(ContextMessage):
263
- image: Optional[File] = Field(
264
- description="The image url of the message",
265
- default=None
266
- )
267
-
268
- class LLMInput(BaseAppInput):
269
- system_prompt: str = Field(
270
- description="The system prompt to use for the model",
271
- default="You are a helpful assistant that can answer questions and help with tasks.",
272
- examples=[
273
- "You are a helpful assistant that can answer questions and help with tasks.",
274
- "You are a certified medical professional who can provide accurate health information.",
275
- "You are a certified financial advisor who can give sound investment guidance.",
276
- "You are a certified cybersecurity expert who can explain security best practices.",
277
- "You are a certified environmental scientist who can discuss climate and sustainability.",
278
- ]
279
- )
280
- context: list[ContextMessage] = Field(
281
- description="The context to use for the model",
282
- examples=[
283
- [
284
- {"role": "user", "content": [{"type": "text", "text": "What is the capital of France?"}]},
285
- {"role": "assistant", "content": [{"type": "text", "text": "The capital of France is Paris."}]}
286
- ],
287
- [
288
- {"role": "user", "content": [{"type": "text", "text": "What is the weather like today?"}]},
289
- {"role": "assistant", "content": [{"type": "text", "text": "I apologize, but I don't have access to real-time weather information. You would need to check a weather service or app to get current weather conditions for your location."}]}
290
- ],
291
- [
292
- {"role": "user", "content": [{"type": "text", "text": "Can you help me write a poem about spring?"}]},
293
- {"role": "assistant", "content": [{"type": "text", "text": "Here's a short poem about spring:\n\nGreen buds awakening,\nSoft rain gently falling down,\nNew life springs anew.\n\nWarm sun breaks through clouds,\nBirds return with joyful song,\nNature's sweet rebirth."}]}
294
- ],
295
- [
296
- {"role": "user", "content": [{"type": "text", "text": "Explain quantum computing in simple terms"}]},
297
- {"role": "assistant", "content": [{"type": "text", "text": "Quantum computing is like having a super-powerful calculator that can solve many problems at once instead of one at a time. While regular computers use bits (0s and 1s), quantum computers use quantum bits or \"qubits\" that can be both 0 and 1 at the same time - kind of like being in two places at once! This allows them to process huge amounts of information much faster than regular computers for certain types of problems."}]}
298
- ]
299
- ],
300
- default=[]
301
- )
302
- text: str = Field(
303
- description="The user prompt to use for the model",
304
- examples=[
305
- "What is the capital of France?",
306
- "What is the weather like today?",
307
- "Can you help me write a poem about spring?",
308
- "Explain quantum computing in simple terms"
309
- ],
310
- )
311
-
312
- class LLMInputWithImage(LLMInput):
313
- image: Optional[File] = Field(
314
- description="The image to use for the model",
315
- default=None
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,8 +0,0 @@
1
- inferencesh/__init__.py,sha256=hbKkgHCh0lCdhWyHs3FHHRd8JfLeHkTd1bT4v79Fi8M,192
2
- inferencesh/sdk.py,sha256=Yebi85zvWYHVnnf5CARHg-eOpOPM2mZ7SafRlaJuwdA,14554
3
- inferencesh-0.3.0.dist-info/licenses/LICENSE,sha256=OsgqEWIh2el_QMj0y8O1A5Q5Dl-dxqqYbFE6fszuR4s,1086
4
- inferencesh-0.3.0.dist-info/METADATA,sha256=_06FcZ_2WJ7dS7DHhWmWDfmjR6rspTYZL7x9X0mUr5M,2728
5
- inferencesh-0.3.0.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
6
- inferencesh-0.3.0.dist-info/entry_points.txt,sha256=6IC-fyozAqW3ljsMLGCXxJ0_ui2Jb-2fLHtoH1RTnEE,45
7
- inferencesh-0.3.0.dist-info/top_level.txt,sha256=TSMHg3T1ThMl1HGAWmzBClwOYH1ump5neof9BfHIwaA,12
8
- inferencesh-0.3.0.dist-info/RECORD,,