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.
- {inferencesh-0.1.24/src/inferencesh.egg-info → inferencesh-0.2.1}/PKG-INFO +5 -1
- {inferencesh-0.1.24 → inferencesh-0.2.1}/pyproject.toml +8 -1
- {inferencesh-0.1.24 → inferencesh-0.2.1}/src/inferencesh/sdk.py +106 -35
- {inferencesh-0.1.24 → inferencesh-0.2.1/src/inferencesh.egg-info}/PKG-INFO +5 -1
- inferencesh-0.2.1/src/inferencesh.egg-info/requires.txt +6 -0
- inferencesh-0.2.1/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.2.1}/LICENSE +0 -0
- {inferencesh-0.1.24 → inferencesh-0.2.1}/README.md +0 -0
- {inferencesh-0.1.24 → inferencesh-0.2.1}/setup.cfg +0 -0
- {inferencesh-0.1.24 → inferencesh-0.2.1}/setup.py +0 -0
- {inferencesh-0.1.24 → inferencesh-0.2.1}/src/inferencesh/__init__.py +0 -0
- {inferencesh-0.1.24 → inferencesh-0.2.1}/src/inferencesh.egg-info/SOURCES.txt +0 -0
- {inferencesh-0.1.24 → inferencesh-0.2.1}/src/inferencesh.egg-info/dependency_links.txt +0 -0
- {inferencesh-0.1.24 → inferencesh-0.2.1}/src/inferencesh.egg-info/entry_points.txt +0 -0
- {inferencesh-0.1.24 → inferencesh-0.2.1}/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.1
|
|
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
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
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."""
|
|
@@ -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
|
|
181
|
-
|
|
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.
|
|
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:
|
|
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
|
|
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,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
|