inferencesh 0.2.31__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.
@@ -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
+ )
@@ -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 tempfile
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 a temporary file and update the path."""
110
+ """Download the URL to the cache directory and update the path."""
78
111
  original_url = self.uri
79
- tmp_file = None
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
- # Create a temporary file with a suffix based on the URL path
82
- suffix = os.path.splitext(urllib.parse.urlparse(original_url).path)[1]
83
- tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
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
- total_size = int(response.headers.get('content-length', 0))
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
- buffer = response.read(block_size)
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
- pbar.update(len(buffer))
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
- self.path = self._tmp_path
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 tmp_file is not None and hasattr(self, '_tmp_path'):
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 model_json_schema(cls, **kwargs):
174
- schema = super().model_json_schema(**kwargs)
175
- schema["$id"] = "/schemas/File"
176
- # Create a schema that accepts either a string or the full object
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
- {"type": "string"}, # Accept string input
180
- schema # Accept full object input
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
+