viscribe 1.0.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.
viscribe/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .async_client import AsyncClient
2
+ from .client import Client
3
+
4
+ __all__ = ["Client", "AsyncClient"]
@@ -0,0 +1,316 @@
1
+ import asyncio
2
+ from typing import Any, Optional, Type, Union
3
+
4
+ from aiohttp import ClientSession, ClientTimeout, TCPConnector
5
+ from aiohttp.client_exceptions import ClientError
6
+ from pydantic import BaseModel
7
+
8
+ from viscribe.config import API_BASE_URL, DEFAULT_HEADERS
9
+ from viscribe.exceptions import APIError
10
+ from viscribe.logger import viscribe_logger as logger
11
+ from viscribe.models.image import (
12
+ CreditsResponse,
13
+ ExtractField,
14
+ FeedbackRequest,
15
+ FeedbackResponse,
16
+ ImageAskRequest,
17
+ ImageAskResponse,
18
+ ImageClassifyRequest,
19
+ ImageClassifyResponse,
20
+ ImageCompareRequest,
21
+ ImageCompareResponse,
22
+ ImageDescribeRequest,
23
+ ImageDescribeResponse,
24
+ ImageExtractRequest,
25
+ ImageExtractResponse,
26
+ )
27
+ from viscribe.utils.helpers import handle_async_response, validate_api_key
28
+
29
+
30
+ class AsyncClient:
31
+ @classmethod
32
+ def from_env(
33
+ cls,
34
+ verify_ssl: bool = True,
35
+ timeout: Optional[float] = None,
36
+ max_retries: int = 3,
37
+ retry_delay: float = 1.0,
38
+ ):
39
+ """Initialize AsyncClient using API key from environment variable.
40
+
41
+ Args:
42
+ verify_ssl: Whether to verify SSL certificates
43
+ timeout: Request timeout in seconds. None means no timeout (infinite)
44
+ max_retries: Maximum number of retry attempts
45
+ retry_delay: Delay between retries in seconds
46
+ """
47
+ from os import getenv
48
+
49
+ api_key = getenv("VISCRIBE_API_KEY")
50
+ if not api_key:
51
+ raise ValueError("VISCRIBE_API_KEY environment variable not set")
52
+ return cls(
53
+ api_key=api_key,
54
+ verify_ssl=verify_ssl,
55
+ timeout=timeout,
56
+ max_retries=max_retries,
57
+ retry_delay=retry_delay,
58
+ )
59
+
60
+ def __init__(
61
+ self,
62
+ api_key: str = None,
63
+ verify_ssl: bool = True,
64
+ timeout: Optional[float] = None,
65
+ max_retries: int = 3,
66
+ retry_delay: float = 1.0,
67
+ ):
68
+ """Initialize AsyncClient with configurable parameters.
69
+
70
+ Args:
71
+ api_key: API key for authentication. If None, will try to load from environment
72
+ verify_ssl: Whether to verify SSL certificates
73
+ timeout: Request timeout in seconds. None means no timeout (infinite)
74
+ max_retries: Maximum number of retry attempts
75
+ retry_delay: Delay between retries in seconds
76
+ """
77
+ logger.info("🔑 Initializing AsyncClient")
78
+
79
+ # Try to get API key from environment if not provided
80
+ if api_key is None:
81
+ from os import getenv
82
+
83
+ api_key = getenv("VISCRIBE_API_KEY")
84
+ if not api_key:
85
+ raise ValueError(
86
+ "VISCRIBE_API_KEY not provided and not found in environment"
87
+ )
88
+
89
+ validate_api_key(api_key)
90
+ logger.debug(
91
+ f"🛠️ Configuration: verify_ssl={verify_ssl}, timeout={timeout}, max_retries={max_retries}"
92
+ )
93
+ self.api_key = api_key
94
+ self.headers = {**DEFAULT_HEADERS, "VISCRIBE-APIKEY": api_key}
95
+ self.max_retries = max_retries
96
+ self.retry_delay = retry_delay
97
+
98
+ ssl = None if verify_ssl else False
99
+ self.timeout = ClientTimeout(total=timeout) if timeout is not None else None
100
+
101
+ self.session = ClientSession(
102
+ headers=self.headers, connector=TCPConnector(ssl=ssl), timeout=self.timeout
103
+ )
104
+
105
+ logger.info("✅ AsyncClient initialized successfully")
106
+
107
+ async def _make_request(self, method: str, url: str, **kwargs) -> Any:
108
+ """Make HTTP request with retry logic."""
109
+ for attempt in range(self.max_retries):
110
+ try:
111
+ logger.info(
112
+ f"🚀 Making {method} request to {url} (Attempt {attempt + 1}/{self.max_retries})"
113
+ )
114
+ logger.debug(f"🔍 Request parameters: {kwargs}")
115
+
116
+ async with self.session.request(method, url, **kwargs) as response:
117
+ logger.debug(f"📥 Response status: {response.status}")
118
+ result = await handle_async_response(response)
119
+ logger.info(f"✅ Request completed successfully: {method} {url}")
120
+ return result
121
+
122
+ except ClientError as e:
123
+ logger.warning(f"⚠️ Request attempt {attempt + 1} failed: {str(e)}")
124
+ if hasattr(e, "status") and e.status is not None:
125
+ try:
126
+ error_data = await e.response.json()
127
+ error_msg = error_data.get("error", str(e))
128
+ logger.error(f"🔴 API Error: {error_msg}")
129
+ raise APIError(error_msg, status_code=e.status)
130
+ except ValueError:
131
+ logger.error("🔴 Could not parse error response")
132
+ raise APIError(
133
+ str(e),
134
+ status_code=e.status if hasattr(e, "status") else None,
135
+ )
136
+
137
+ if attempt == self.max_retries - 1:
138
+ logger.error(f"❌ All retry attempts failed for {method} {url}")
139
+ raise ConnectionError(f"Failed to connect to API: {str(e)}")
140
+
141
+ retry_delay = self.retry_delay * (attempt + 1)
142
+ logger.info(f"⏳ Waiting {retry_delay}s before retry {attempt + 2}")
143
+ await asyncio.sleep(retry_delay)
144
+
145
+ async def submit_feedback(self, request_id: str, rating: int, feedback_text: Optional[str] = None) -> FeedbackResponse:
146
+ """Submit feedback for a request"""
147
+ logger.info(f"📝 Submitting feedback for request {request_id}")
148
+ feedback = FeedbackRequest(
149
+ request_id=request_id, rating=rating, feedback_text=feedback_text
150
+ )
151
+ payload = feedback.model_dump(exclude_none=True)
152
+ result = await self._make_request("POST", f"{API_BASE_URL}/feedback", json=payload)
153
+ return FeedbackResponse(**result)
154
+
155
+ async def get_credits(self) -> CreditsResponse:
156
+ """Get credits information"""
157
+ logger.info("💳 Fetching credits information")
158
+ result = await self._make_request("GET", f"{API_BASE_URL}/credits")
159
+ return CreditsResponse(**result)
160
+
161
+ async def close(self):
162
+ """Close the session to free up resources"""
163
+ logger.info("🔒 Closing AsyncClient session")
164
+ await self.session.close()
165
+ logger.debug("✅ Session closed successfully")
166
+
167
+ async def __aenter__(self):
168
+ return self
169
+
170
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
171
+ await self.close()
172
+
173
+ async def describe_image(
174
+ self,
175
+ image_url: str = None,
176
+ image_base64: str = None,
177
+ instruction: str = None,
178
+ generate_tags: bool = True,
179
+ ) -> ImageDescribeResponse:
180
+ """Send a describe image request with explicit arguments and validate with Pydantic."""
181
+ logger.info("🔍 Starting image describe request")
182
+
183
+ req = ImageDescribeRequest(
184
+ image_url=image_url,
185
+ image_base64=image_base64,
186
+ instruction=instruction,
187
+ generate_tags=generate_tags,
188
+ )
189
+ payload = req.model_dump(exclude_none=True)
190
+ result = await self._make_request(
191
+ "POST", f"{API_BASE_URL}/images/describe", json=payload
192
+ )
193
+ return ImageDescribeResponse(**result)
194
+
195
+ async def extract_image(
196
+ self,
197
+ image_url: str = None,
198
+ image_base64: str = None,
199
+ fields: list = None,
200
+ advanced_schema: Union[dict, BaseModel, Type[BaseModel]] = None,
201
+ instruction: str = None,
202
+ ) -> ImageExtractResponse:
203
+ """Send an extract structured data from image request with explicit arguments and validate with Pydantic.
204
+
205
+ Args:
206
+ image_url: URL of the image
207
+ image_base64: Base64 encoded string of the image
208
+ fields: List of dictionaries with 'name', 'type', and optional 'description' keys.
209
+ Each field type can be 'text', 'number', 'array_text' (max 5), or 'array_number' (max 5).
210
+ Max 10 fields allowed.
211
+ advanced_schema: Full JSON schema for complex extraction (dict), a Pydantic BaseModel instance,
212
+ or a Pydantic BaseModel class. If a BaseModel is provided, it will be serialized
213
+ to its JSON schema. Use this for nested structures.
214
+ instruction: Optional instruction to guide the extraction process.
215
+
216
+ Note: Either fields or advanced_schema must be provided, not both.
217
+ """
218
+ logger.info("🔍 Starting image extract request")
219
+
220
+ # Convert dictionaries to ExtractField models if fields are provided
221
+ validated_fields = None
222
+ if fields is not None:
223
+ validated_fields = [ExtractField(**field) if isinstance(field, dict) else field for field in fields]
224
+
225
+ # Convert Pydantic BaseModel to JSON schema if advanced_schema is a BaseModel
226
+ validated_schema = advanced_schema
227
+ if advanced_schema is not None:
228
+ if isinstance(advanced_schema, BaseModel):
229
+ # BaseModel instance
230
+ validated_schema = advanced_schema.model_json_schema()
231
+ elif isinstance(advanced_schema, type) and issubclass(advanced_schema, BaseModel):
232
+ # BaseModel class
233
+ validated_schema = advanced_schema.model_json_schema()
234
+
235
+ req = ImageExtractRequest(
236
+ image_url=image_url,
237
+ image_base64=image_base64,
238
+ fields=validated_fields,
239
+ advanced_schema=validated_schema,
240
+ instruction=instruction,
241
+ )
242
+ payload = req.model_dump(exclude_none=True)
243
+ result = await self._make_request(
244
+ "POST", f"{API_BASE_URL}/images/extract", json=payload
245
+ )
246
+ return ImageExtractResponse(**result)
247
+
248
+ async def classify_image(
249
+ self,
250
+ image_url: str = None,
251
+ image_base64: str = None,
252
+ classes: list = None,
253
+ class_descriptions: dict = None,
254
+ instruction: str = None,
255
+ multi_label: bool = False,
256
+ ) -> ImageClassifyResponse:
257
+ """Send an image classify request with explicit arguments and validate with Pydantic."""
258
+ logger.info("🔍 Starting image classify request")
259
+
260
+ req = ImageClassifyRequest(
261
+ image_url=image_url,
262
+ image_base64=image_base64,
263
+ classes=classes,
264
+ class_descriptions=class_descriptions,
265
+ instruction=instruction,
266
+ multi_label=multi_label,
267
+ )
268
+ payload = req.model_dump(exclude_none=True)
269
+ result = await self._make_request(
270
+ "POST", f"{API_BASE_URL}/images/classify", json=payload
271
+ )
272
+ return ImageClassifyResponse(**result)
273
+
274
+ async def ask_image(
275
+ self,
276
+ image_url: str = None,
277
+ image_base64: str = None,
278
+ question: str = None,
279
+ ) -> ImageAskResponse:
280
+ """Send an image VQA (ask) request with explicit arguments and validate with Pydantic."""
281
+ logger.info("🔍 Starting image ask (VQA) request")
282
+
283
+ req = ImageAskRequest(
284
+ image_url=image_url,
285
+ image_base64=image_base64,
286
+ question=question,
287
+ )
288
+ payload = req.model_dump(exclude_none=True)
289
+ result = await self._make_request("POST", f"{API_BASE_URL}/images/ask", json=payload)
290
+ return ImageAskResponse(**result)
291
+
292
+ async def compare_images(
293
+ self,
294
+ image1_url: str = None,
295
+ image1_base64: str = None,
296
+ image2_url: str = None,
297
+ image2_base64: str = None,
298
+ instruction: str = None,
299
+ ) -> ImageCompareResponse:
300
+ """Send an image compare request with explicit arguments and validate with Pydantic."""
301
+ logger.info("🔍 Starting image compare request")
302
+
303
+ req = ImageCompareRequest(
304
+ image1_url=image1_url,
305
+ image1_base64=image1_base64,
306
+ image2_url=image2_url,
307
+ image2_base64=image2_base64,
308
+ instruction=instruction,
309
+ )
310
+ payload = req.model_dump(exclude_none=True)
311
+ result = await self._make_request(
312
+ "POST", f"{API_BASE_URL}/images/compare", json=payload
313
+ )
314
+ return ImageCompareResponse(**result)
315
+
316
+
viscribe/client.py ADDED
@@ -0,0 +1,327 @@
1
+ # Client implementation goes here
2
+ from typing import Any, Optional, Type, Union
3
+
4
+ import requests
5
+ import urllib3
6
+ from pydantic import BaseModel
7
+ from requests.exceptions import RequestException
8
+
9
+ from viscribe.config import API_BASE_URL, DEFAULT_HEADERS
10
+ from viscribe.exceptions import APIError
11
+ from viscribe.logger import viscribe_logger as logger
12
+ from viscribe.models.image import CreditsResponse, FeedbackRequest, FeedbackResponse
13
+ from viscribe.models.image import (
14
+ ExtractField,
15
+ ImageDescribeRequest,
16
+ ImageDescribeResponse,
17
+ ImageExtractRequest,
18
+ ImageExtractResponse,
19
+ ImageClassifyRequest,
20
+ ImageClassifyResponse,
21
+ ImageAskRequest,
22
+ ImageAskResponse,
23
+ ImageCompareRequest, ImageCompareResponse
24
+ )
25
+ from viscribe.utils.helpers import handle_sync_response, validate_api_key
26
+
27
+
28
+ class Client:
29
+ @classmethod
30
+ def from_env(
31
+ cls,
32
+ verify_ssl: bool = True,
33
+ timeout: Optional[float] = None,
34
+ max_retries: int = 3,
35
+ retry_delay: float = 1.0,
36
+ ):
37
+ """Initialize Client using API key from environment variable.
38
+
39
+ Args:
40
+ verify_ssl: Whether to verify SSL certificates
41
+ timeout: Request timeout in seconds. None means no timeout (infinite)
42
+ max_retries: Maximum number of retry attempts
43
+ retry_delay: Delay between retries in seconds
44
+ """
45
+ from os import getenv
46
+
47
+ api_key = getenv("VISCRIBE_API_KEY")
48
+ if not api_key:
49
+ raise ValueError("VISCRIBE_API_KEY environment variable not set")
50
+ return cls(
51
+ api_key=api_key,
52
+ verify_ssl=verify_ssl,
53
+ timeout=timeout,
54
+ max_retries=max_retries,
55
+ retry_delay=retry_delay,
56
+ )
57
+
58
+ def __init__(
59
+ self,
60
+ api_key: str = None,
61
+ verify_ssl: bool = True,
62
+ timeout: Optional[float] = None,
63
+ max_retries: int = 3,
64
+ retry_delay: float = 1.0,
65
+ ):
66
+ """Initialize Client with configurable parameters.
67
+
68
+ Args:
69
+ api_key: API key for authentication. If None, will try to load from environment
70
+ verify_ssl: Whether to verify SSL certificates
71
+ timeout: Request timeout in seconds. None means no timeout (infinite)
72
+ max_retries: Maximum number of retry attempts
73
+ retry_delay: Delay between retries in seconds
74
+ """
75
+ logger.info("🔑 Initializing Client")
76
+
77
+ # Try to get API key from environment if not provided
78
+ if api_key is None:
79
+ from os import getenv
80
+
81
+ api_key = getenv("VISCRIBE_API_KEY")
82
+ if not api_key:
83
+ raise ValueError(
84
+ "VISCRIBE_API_KEY not provided and not found in environment"
85
+ )
86
+
87
+ validate_api_key(api_key)
88
+ logger.debug(
89
+ f"🛠️ Configuration: verify_ssl={verify_ssl}, timeout={timeout}, max_retries={max_retries}"
90
+ )
91
+
92
+ self.api_key = api_key
93
+ self.headers = {**DEFAULT_HEADERS, "VISCRIBE-APIKEY": api_key}
94
+ self.timeout = timeout
95
+ self.max_retries = max_retries
96
+ self.retry_delay = retry_delay
97
+
98
+ # Create a session for connection pooling
99
+ self.session = requests.Session()
100
+ self.session.headers.update(self.headers)
101
+ self.session.verify = verify_ssl
102
+
103
+ # Configure retries
104
+ adapter = requests.adapters.HTTPAdapter(
105
+ max_retries=requests.urllib3.Retry(
106
+ total=max_retries,
107
+ backoff_factor=retry_delay,
108
+ status_forcelist=[500, 502, 503, 504],
109
+ )
110
+ )
111
+ self.session.mount("http://", adapter)
112
+ self.session.mount("https://", adapter)
113
+
114
+ # Add warning suppression if verify_ssl is False
115
+ if not verify_ssl:
116
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
117
+
118
+ logger.info("✅ Client initialized successfully")
119
+
120
+ def _make_request(self, method: str, url: str, **kwargs) -> Any:
121
+ """Make HTTP request with error handling."""
122
+ try:
123
+ logger.info(f"🚀 Making {method} request to {url}")
124
+ logger.debug(f"🔍 Request parameters: {kwargs}")
125
+
126
+ response = self.session.request(method, url, timeout=self.timeout, **kwargs)
127
+ logger.debug(f"📥 Response status: {response.status_code}")
128
+
129
+ result = handle_sync_response(response)
130
+ logger.info(f"✅ Request completed successfully: {method} {url}")
131
+ return result
132
+
133
+ except RequestException as e:
134
+ logger.error(f"❌ Request failed: {str(e)}")
135
+ if hasattr(e, "response") and e.response is not None:
136
+ try:
137
+ error_data = e.response.json()
138
+ error_msg = error_data.get("error", str(e))
139
+ logger.error(f"🔴 API Error: {error_msg}")
140
+ raise APIError(error_msg, status_code=e.response.status_code)
141
+ except ValueError:
142
+ logger.error("🔴 Could not parse error response")
143
+ raise APIError(
144
+ str(e),
145
+ status_code=(
146
+ e.response.status_code
147
+ if hasattr(e.response, "status_code")
148
+ else None
149
+ ),
150
+ )
151
+ logger.error(f"🔴 Connection Error: {str(e)}")
152
+ raise ConnectionError(f"Failed to connect to API: {str(e)}")
153
+
154
+ def submit_feedback(self, request_id: str, rating: int, feedback_text: Optional[str] = None) -> FeedbackResponse:
155
+ """Submit feedback for a request"""
156
+ logger.info(f"📝 Submitting feedback for request {request_id}")
157
+ request = FeedbackRequest(
158
+ request_id=request_id,
159
+ rating=rating,
160
+ feedback_text=feedback_text,
161
+ )
162
+ payload = request.model_dump(exclude_none=True)
163
+ result = self._make_request(
164
+ "POST", f"{API_BASE_URL}/feedback", json=payload
165
+ )
166
+ return FeedbackResponse(**result)
167
+
168
+ def get_credits(self) -> CreditsResponse:
169
+ """Get credits information"""
170
+ logger.info("💳 Fetching credits information")
171
+ result = self._make_request("GET", f"{API_BASE_URL}/credits")
172
+ return CreditsResponse(**result)
173
+
174
+ def describe_image(
175
+ self,
176
+ image_url: str = None,
177
+ image_base64: str = None,
178
+ instruction: str = None,
179
+ generate_tags: bool = True,
180
+ ):
181
+ """Send a describe image request with explicit arguments and validate with Pydantic."""
182
+ logger.info("🔍 Starting image describe request")
183
+
184
+ req = ImageDescribeRequest(
185
+ image_url=image_url,
186
+ image_base64=image_base64,
187
+ instruction=instruction,
188
+ generate_tags=generate_tags,
189
+ )
190
+ payload = req.model_dump(exclude_none=True)
191
+ result = self._make_request(
192
+ "POST", f"{API_BASE_URL}/images/describe", json=payload
193
+ )
194
+ return ImageDescribeResponse(**result)
195
+
196
+ def extract_image(
197
+ self,
198
+ image_url: str = None,
199
+ image_base64: str = None,
200
+ fields: list = None,
201
+ advanced_schema: Union[dict, BaseModel, Type[BaseModel]] = None,
202
+ instruction: str = None,
203
+ ):
204
+ """Send an extract structured data from image request with explicit arguments and validate with Pydantic.
205
+
206
+ Args:
207
+ image_url: URL of the image
208
+ image_base64: Base64 encoded string of the image
209
+ fields: List of dictionaries with 'name', 'type', and optional 'description' keys.
210
+ Each field type can be 'text', 'number', 'array_text' (max 5), or 'array_number' (max 5).
211
+ Max 10 fields allowed.
212
+ advanced_schema: Full JSON schema for complex extraction (dict), a Pydantic BaseModel instance,
213
+ or a Pydantic BaseModel class. If a BaseModel is provided, it will be serialized
214
+ to its JSON schema. Use this for nested structures.
215
+ instruction: Optional instruction to guide the extraction process.
216
+
217
+ Note: Either fields or advanced_schema must be provided, not both.
218
+ """
219
+ logger.info("🔍 Starting image extract request")
220
+
221
+ # Convert dictionaries to ExtractField models if fields are provided
222
+ validated_fields = None
223
+ if fields is not None:
224
+ validated_fields = [ExtractField(**field) if isinstance(field, dict) else field for field in fields]
225
+
226
+ # Convert Pydantic BaseModel to JSON schema if advanced_schema is a BaseModel
227
+ validated_schema = advanced_schema
228
+ if advanced_schema is not None:
229
+ if isinstance(advanced_schema, BaseModel):
230
+ # BaseModel instance
231
+ validated_schema = advanced_schema.model_json_schema()
232
+ elif isinstance(advanced_schema, type) and issubclass(advanced_schema, BaseModel):
233
+ # BaseModel class
234
+ validated_schema = advanced_schema.model_json_schema()
235
+
236
+ req = ImageExtractRequest(
237
+ image_url=image_url,
238
+ image_base64=image_base64,
239
+ fields=validated_fields,
240
+ advanced_schema=validated_schema,
241
+ instruction=instruction,
242
+ )
243
+ payload = req.model_dump(exclude_none=True)
244
+ result = self._make_request(
245
+ "POST", f"{API_BASE_URL}/images/extract", json=payload
246
+ )
247
+ return ImageExtractResponse(**result)
248
+
249
+ def classify_image(
250
+ self,
251
+ image_url: str = None,
252
+ image_base64: str = None,
253
+ classes: list = None,
254
+ class_descriptions: dict = None,
255
+ instruction: str = None,
256
+ multi_label: bool = False,
257
+ ):
258
+ """Send an image classify request with explicit arguments and validate with Pydantic."""
259
+ logger.info("🔍 Starting image classify request")
260
+
261
+ req = ImageClassifyRequest(
262
+ image_url=image_url,
263
+ image_base64=image_base64,
264
+ classes=classes,
265
+ class_descriptions=class_descriptions,
266
+ instruction=instruction,
267
+ multi_label=multi_label,
268
+ )
269
+ payload = req.model_dump(exclude_none=True)
270
+ result = self._make_request(
271
+ "POST", f"{API_BASE_URL}/images/classify", json=payload
272
+ )
273
+ return ImageClassifyResponse(**result)
274
+
275
+ def ask_image(
276
+ self,
277
+ image_url: str = None,
278
+ image_base64: str = None,
279
+ question: str = None,
280
+ ):
281
+ """Send an image VQA (ask) request with explicit arguments and validate with Pydantic."""
282
+ logger.info("🔍 Starting image ask (VQA) request")
283
+
284
+ req = ImageAskRequest(
285
+ image_url=image_url,
286
+ image_base64=image_base64,
287
+ question=question,
288
+ )
289
+ payload = req.model_dump(exclude_none=True)
290
+ result = self._make_request("POST", f"{API_BASE_URL}/images/ask", json=payload)
291
+ return ImageAskResponse(**result)
292
+
293
+ def compare_images(
294
+ self,
295
+ image1_url: str = None,
296
+ image1_base64: str = None,
297
+ image2_url: str = None,
298
+ image2_base64: str = None,
299
+ instruction: str = None,
300
+ ):
301
+ """Send an image compare request with explicit arguments and validate with Pydantic."""
302
+ logger.info("🔍 Starting image compare request")
303
+
304
+ req = ImageCompareRequest(
305
+ image1_url=image1_url,
306
+ image1_base64=image1_base64,
307
+ image2_url=image2_url,
308
+ image2_base64=image2_base64,
309
+ instruction=instruction,
310
+ )
311
+ payload = req.model_dump(exclude_none=True)
312
+ result = self._make_request(
313
+ "POST", f"{API_BASE_URL}/images/compare", json=payload
314
+ )
315
+ return ImageCompareResponse(**result)
316
+
317
+ def close(self):
318
+ """Close the session to free up resources"""
319
+ logger.info("🔒 Closing Client session")
320
+ self.session.close()
321
+ logger.debug("✅ Session closed successfully")
322
+
323
+ def __enter__(self):
324
+ return self
325
+
326
+ def __exit__(self, exc_type, exc_val, exc_tb):
327
+ self.close()
viscribe/config.py ADDED
@@ -0,0 +1,6 @@
1
+ # Configuration and constants
2
+ API_BASE_URL = "https://api.viscribe.ai/v1"
3
+ DEFAULT_HEADERS = {
4
+ "accept": "application/json",
5
+ "Content-Type": "application/json",
6
+ }
viscribe/exceptions.py ADDED
@@ -0,0 +1,7 @@
1
+ class APIError(Exception):
2
+ """Base exception for API errors."""
3
+
4
+ def __init__(self, message: str, status_code: int = None):
5
+ self.status_code = status_code
6
+ self.message = message
7
+ super().__init__(f"[{status_code}] {message}")
viscribe/logger.py ADDED
@@ -0,0 +1,119 @@
1
+ import logging
2
+ import logging.handlers
3
+ from typing import Dict, Optional
4
+
5
+ # Emoji mappings for different log levels
6
+ LOG_EMOJIS: Dict[int, str] = {
7
+ logging.DEBUG: "🐛",
8
+ logging.INFO: "💬",
9
+ logging.WARNING: "⚠️",
10
+ logging.ERROR: "❌",
11
+ logging.CRITICAL: "🚨",
12
+ }
13
+
14
+
15
+ class EmojiFormatter(logging.Formatter):
16
+ """Custom formatter that adds emojis to log messages"""
17
+
18
+ def format(self, record: logging.LogRecord) -> str:
19
+ # Add emoji based on log level
20
+ emoji = LOG_EMOJIS.get(record.levelno, "")
21
+ record.emoji = emoji
22
+ return super().format(record)
23
+
24
+
25
+ class ViscribeLogger:
26
+ """Class to manage Viscribe logging configuration"""
27
+
28
+ _instance = None
29
+ _initialized = False
30
+
31
+ def __new__(cls):
32
+ if cls._instance is None:
33
+ cls._instance = super(ViscribeLogger, cls).__new__(cls)
34
+ return cls._instance
35
+
36
+ def __init__(self):
37
+ if not self._initialized:
38
+ self.logger = logging.getLogger("viscribe")
39
+ self.logger.setLevel(logging.INFO)
40
+ self.enabled = False
41
+ self._initialized = True
42
+
43
+ def set_logging(
44
+ self,
45
+ level: Optional[str] = None,
46
+ log_file: Optional[str] = None,
47
+ log_format: Optional[str] = None,
48
+ ) -> None:
49
+ """
50
+ Configure logging settings. If level is None, logging will be disabled.
51
+
52
+ Args:
53
+ level: Logging level (e.g., 'DEBUG', 'INFO'). None to disable logging.
54
+ log_file: Optional file path to write logs to
55
+ log_format: Optional custom log format string
56
+ """
57
+ # Clear existing handlers
58
+ self.logger.handlers.clear()
59
+
60
+ if level is None:
61
+ # Disable logging
62
+ self.enabled = False
63
+ return
64
+
65
+ # Enable logging with specified level
66
+ self.enabled = True
67
+ level = getattr(logging, level.upper(), logging.INFO)
68
+ self.logger.setLevel(level)
69
+
70
+ # Default format if none provided
71
+ if not log_format:
72
+ log_format = "%(emoji)s %(asctime)-15s %(message)s"
73
+
74
+ formatter = EmojiFormatter(log_format)
75
+
76
+ # Console handler
77
+ console_handler = logging.StreamHandler()
78
+ console_handler.setFormatter(formatter)
79
+ self.logger.addHandler(console_handler)
80
+
81
+ # File handler if log_file specified
82
+ if log_file:
83
+ file_handler = logging.FileHandler(log_file)
84
+ file_handler.setFormatter(formatter)
85
+ self.logger.addHandler(file_handler)
86
+
87
+ def disable(self) -> None:
88
+ """Disable all logging"""
89
+ self.logger.handlers.clear()
90
+ self.enabled = False
91
+
92
+ def debug(self, message: str) -> None:
93
+ """Log debug message if logging is enabled"""
94
+ if self.enabled:
95
+ self.logger.debug(message)
96
+
97
+ def info(self, message: str) -> None:
98
+ """Log info message if logging is enabled"""
99
+ if self.enabled:
100
+ self.logger.info(message)
101
+
102
+ def warning(self, message: str) -> None:
103
+ """Log warning message if logging is enabled"""
104
+ if self.enabled:
105
+ self.logger.warning(message)
106
+
107
+ def error(self, message: str) -> None:
108
+ """Log error message if logging is enabled"""
109
+ if self.enabled:
110
+ self.logger.error(message)
111
+
112
+ def critical(self, message: str) -> None:
113
+ """Log critical message if logging is enabled"""
114
+ if self.enabled:
115
+ self.logger.critical(message)
116
+
117
+
118
+ # Default logger instance
119
+ viscribe_logger = ViscribeLogger()
@@ -0,0 +1,16 @@
1
+ from .image import (
2
+ CreditsResponse,
3
+ ExtractField,
4
+ FeedbackRequest,
5
+ FeedbackResponse,
6
+ ImageAskRequest,
7
+ ImageAskResponse,
8
+ ImageClassifyRequest,
9
+ ImageClassifyResponse,
10
+ ImageCompareRequest,
11
+ ImageCompareResponse,
12
+ ImageDescribeRequest,
13
+ ImageDescribeResponse,
14
+ ImageExtractRequest,
15
+ ImageExtractResponse,
16
+ )
@@ -0,0 +1,188 @@
1
+ from datetime import datetime
2
+ from typing import Any, Dict, List, Optional
3
+ from uuid import UUID
4
+
5
+ from pydantic import BaseModel, Field, field_validator, model_validator
6
+
7
+ from viscribe.utils.helpers import validate_base64_image, validate_url_format
8
+
9
+ # 1. Image Endpoints
10
+
11
+
12
+ class ImageSourceBase(BaseModel):
13
+ image_url: Optional[str] = Field(default=None, description="URL of the image.")
14
+ image_base64: Optional[str] = Field(
15
+ default=None, description="Base64 encoded string of the image."
16
+ )
17
+
18
+ @model_validator(mode="before")
19
+ @classmethod
20
+ def check_image_source(cls, values):
21
+ """Ensure exactly one image source is provided and validate formats."""
22
+ url = values.get("image_url")
23
+ b64 = values.get("image_base64")
24
+
25
+ if not url and not b64:
26
+ raise ValueError("Either image_url or image_base64 must be provided.")
27
+ if url and b64:
28
+ raise ValueError("Provide either image_url or image_base64, not both.")
29
+
30
+ # Validate URL format if provided
31
+ if url:
32
+ try:
33
+ validate_url_format(url)
34
+ except ValueError as e:
35
+ raise ValueError(f"Invalid image_url: {str(e)}")
36
+
37
+ # Validate base64 format if provided
38
+ if b64:
39
+ try:
40
+ validate_base64_image(b64)
41
+ except ValueError as e:
42
+ raise ValueError(f"Invalid image_base64: {str(e)}")
43
+
44
+ return values
45
+
46
+
47
+ class ImageDescribeRequest(ImageSourceBase):
48
+ instruction: Optional[str] = None
49
+ generate_tags: bool = True
50
+
51
+
52
+ class ImageDescribeResponse(BaseModel):
53
+ request_id: str
54
+ credits_used: int
55
+ image_description: str
56
+ tags: Optional[List[str]] = None
57
+
58
+
59
+ class ExtractField(BaseModel):
60
+ """Simple field definition for extraction."""
61
+
62
+ name: str = Field(..., description="Field name")
63
+ type: str = Field(
64
+ ..., description="Field type: 'text', 'number', 'array_text', or 'array_number'"
65
+ )
66
+ description: Optional[str] = Field(
67
+ None, description="Optional description to guide extraction"
68
+ )
69
+
70
+ @field_validator("type")
71
+ @classmethod
72
+ def validate_type(cls, v):
73
+ valid_types = ["text", "number", "array_text", "array_number"]
74
+ if v not in valid_types:
75
+ raise ValueError(f"type must be one of {valid_types}")
76
+ return v
77
+
78
+
79
+ class ImageExtractRequest(ImageSourceBase):
80
+ fields: Optional[List[ExtractField]] = Field(
81
+ default=None,
82
+ description="Simple list of fields to extract (max 10). Each field can be text, number, array_text (max 5), or array_number (max 5).",
83
+ max_length=10,
84
+ )
85
+ advanced_schema: Optional[dict] = Field(
86
+ default=None,
87
+ description="Advanced: Full JSON schema for complex extraction. Use this for nested structures.",
88
+ )
89
+ instruction: Optional[str] = Field(
90
+ default=None,
91
+ description="Optional instruction to guide the extraction process.",
92
+ )
93
+
94
+ @model_validator(mode="after")
95
+ def check_schema_or_fields(self):
96
+ """Ensure either fields or advanced_schema is provided, not both."""
97
+ if not self.fields and not self.advanced_schema:
98
+ raise ValueError("Either 'fields' or 'advanced_schema' must be provided")
99
+ if self.fields and self.advanced_schema:
100
+ raise ValueError("Provide either 'fields' or 'advanced_schema', not both")
101
+
102
+ # Validate advanced_schema if provided
103
+ if self.advanced_schema:
104
+ if (
105
+ not isinstance(self.advanced_schema, dict)
106
+ or self.advanced_schema.get("type") != "object"
107
+ or not self.advanced_schema.get("properties")
108
+ ):
109
+ raise ValueError(
110
+ "advanced_schema must be a valid JSON object schema with 'type': 'object' and a 'properties' field."
111
+ )
112
+ return self
113
+
114
+
115
+ class ImageExtractResponse(BaseModel):
116
+ request_id: str
117
+ credits_used: int
118
+ extracted_data: Dict[str, Any]
119
+
120
+
121
+ class ImageClassifyRequest(ImageSourceBase):
122
+ classes: Optional[List[str]] = None
123
+ class_descriptions: Optional[Dict[str, str]] = None
124
+ instruction: Optional[str] = None
125
+ multi_label: bool = False
126
+
127
+
128
+ class ImageClassifyResponse(BaseModel):
129
+ request_id: str
130
+ credits_used: int
131
+ classification: List[str]
132
+
133
+
134
+ class ImageAskRequest(ImageSourceBase):
135
+ question: str
136
+
137
+
138
+ class ImageAskResponse(BaseModel):
139
+ request_id: str
140
+ credits_used: int
141
+ answer: str
142
+
143
+
144
+ class ImageCompareRequest(BaseModel):
145
+ image1_url: Optional[str] = None
146
+ image1_base64: Optional[str] = None
147
+ image2_url: Optional[str] = None
148
+ image2_base64: Optional[str] = None
149
+ instruction: Optional[str] = Field(
150
+ default="Describe the similarities and differences between these two images."
151
+ )
152
+
153
+
154
+ class ImageCompareResponse(BaseModel):
155
+ request_id: str
156
+ credits_used: int
157
+ comparison_result: str
158
+
159
+
160
+ # 2. User Endpoints
161
+
162
+
163
+ class CreditsResponse(BaseModel):
164
+ remaining_credits: int
165
+ total_credits_used: int
166
+
167
+
168
+ class FeedbackRequest(BaseModel):
169
+ """Request model for feedback endpoint"""
170
+
171
+ request_id: str = Field(..., example="123e4567-e89b-12d3-a456-426614174000")
172
+ rating: int = Field(..., ge=1, le=5, example=5)
173
+ feedback_text: Optional[str] = Field(None, example="Great results!")
174
+
175
+ @model_validator(mode="after")
176
+ def validate_request_id(self) -> "FeedbackRequest":
177
+ try:
178
+ UUID(self.request_id)
179
+ except ValueError:
180
+ raise ValueError("request_id must be a valid UUID")
181
+ return self
182
+
183
+
184
+ class FeedbackResponse(BaseModel):
185
+ feedback_id: UUID
186
+ request_id: UUID
187
+ message: str
188
+ feedback_timestamp: datetime
File without changes
@@ -0,0 +1,80 @@
1
+ # Utility functions go here
2
+
3
+ import base64
4
+ from typing import Any, Dict
5
+ from urllib.parse import urlparse
6
+ from uuid import UUID
7
+
8
+ import aiohttp
9
+ from requests import Response
10
+
11
+ from viscribe.exceptions import APIError
12
+
13
+
14
+ def validate_api_key(api_key: str) -> bool:
15
+ if not api_key.startswith("vscrb-"):
16
+ raise ValueError("Invalid API key format. API key must start with 'vscrb-'")
17
+ uuid_part = api_key[5:] # Strip out 'vscrb-'
18
+ try:
19
+ UUID(uuid_part)
20
+ except ValueError:
21
+ raise ValueError(
22
+ "Invalid API key format. API key must be 'vscrb-' followed by a valid UUID. You can get one at https://app.viscribe.ai/"
23
+ )
24
+ return True
25
+
26
+
27
+ def handle_sync_response(response: Response) -> Dict[str, Any]:
28
+ data = response.json()
29
+
30
+ if response.status_code >= 400:
31
+ error_msg = data.get("error", "Unknown error occurred")
32
+ raise APIError(error_msg, status_code=response.status_code)
33
+
34
+ return data
35
+
36
+
37
+ async def handle_async_response(response: aiohttp.ClientResponse) -> Dict[str, Any]:
38
+ data = await response.json()
39
+
40
+ if response.status >= 400:
41
+ error_msg = data.get("error", "Unknown error occurred")
42
+ raise APIError(error_msg, status_code=response.status)
43
+
44
+ return data
45
+
46
+
47
+ def validate_url_format(url: str) -> bool:
48
+ """Validate URL format."""
49
+ try:
50
+ result = urlparse(url)
51
+ if not all([result.scheme, result.netloc]):
52
+ raise ValueError("URL must have a valid scheme and netloc")
53
+ if result.scheme not in ["http", "https"]:
54
+ raise ValueError("URL scheme must be http or https")
55
+ return True
56
+ except ValueError:
57
+ raise
58
+ except Exception as e:
59
+ raise ValueError(f"Invalid URL format: {str(e)}")
60
+
61
+
62
+ def validate_base64_image(b64: str) -> bool:
63
+ """Validate base64 encoded image format."""
64
+ try:
65
+ # Check if it's a data URL
66
+ if b64.startswith("data:image/"):
67
+ # Extract the base64 part after the comma
68
+ b64_part = b64.split(",", 1)[1] if "," in b64 else b64
69
+ else:
70
+ b64_part = b64
71
+
72
+ # Try to decode
73
+ decoded = base64.b64decode(b64_part, validate=True)
74
+ if len(decoded) == 0:
75
+ raise ValueError("Base64 string is empty after decoding")
76
+ return True
77
+ except ValueError:
78
+ raise
79
+ except Exception as e:
80
+ raise ValueError(f"Invalid base64 image format: {str(e)}")
@@ -0,0 +1,235 @@
1
+ Metadata-Version: 2.3
2
+ Name: viscribe
3
+ Version: 1.0.1
4
+ Summary: ViscribeAI SDK
5
+ Author-email: viscriber0 <contact@viscribe.ai>
6
+ License: MIT
7
+ Keywords: ai,api,artificial intelligence,gpt,image,image2text,llm,machine learning,multimodal,natural language processing,nlp,openai,sdk
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
12
+ Requires-Python: <4.0,>=3.10
13
+ Requires-Dist: aiohttp>=3.10
14
+ Requires-Dist: beautifulsoup4>=4.13.4
15
+ Requires-Dist: pydantic>=2.10.2
16
+ Requires-Dist: python-dotenv>=1.0.1
17
+ Requires-Dist: requests>=2.32.3
18
+ Provides-Extra: docs
19
+ Requires-Dist: furo==2024.5.6; extra == 'docs'
20
+ Requires-Dist: sphinx==6.0; extra == 'docs'
21
+ Description-Content-Type: text/markdown
22
+
23
+ <div align="center">
24
+ <a href="https://viscribe.ai"><img src="assets/viscribe-logo.png" alt="Viscribe Logo" width="200"></a>
25
+ </div>
26
+
27
+ # 🌐 ViscribeAI - Python SDK
28
+
29
+ [![PyPI version](https://badge.fury.io/py/viscribe.svg)](https://badge.fury.io/py/viscribe)
30
+ [![Python Support](https://img.shields.io/pypi/pyversions/viscribe.svg)](https://pypi.org/project/viscribe/)
31
+ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
32
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
33
+ [![Documentation Status](https://readthedocs.org/projects/viscribe/badge/?version=latest)](https://docs.viscribe.ai)
34
+
35
+ Official [Python SDK](https://viscribe.ai) for ViscribeAI - AI-powered image understanding and analysis.
36
+
37
+ > 🎁 **Get started with free credits!** Visit [dashboard.viscribe.ai](https://dashboard.viscribe.ai) to sign up and get your API key.
38
+
39
+ ## 📦 Installation
40
+
41
+ ```bash
42
+ pip install viscribe
43
+ ```
44
+
45
+ ## 🚀 Features
46
+
47
+ - 🖼️ AI-powered image description, extraction, classification, VQA (Visual Question Answering), and comparison
48
+ - 🔄 Both sync and async clients
49
+ - 📊 Structured output with Pydantic schemas
50
+ - 🔍 Detailed logging
51
+ - ⚡ Automatic retries
52
+ - 🔐 Secure authentication
53
+
54
+ ## 🎯 Quick Start
55
+
56
+ ```python
57
+ from viscribe import Client
58
+
59
+ client = Client(api_key="your-api-key-here")
60
+ ```
61
+
62
+ > **Note:**
63
+ > You can set the `VISCRIBE_API_KEY` environment variable and initialize the client without parameters: `client = Client()`
64
+
65
+ ## 📚 Image Endpoints
66
+
67
+ ### 1. Describe Image
68
+ Generate a natural language description of an image, optionally with tags.
69
+
70
+ ```python
71
+ from viscribe.client import Client
72
+ client = Client(api_key="your-api-key-here")
73
+
74
+ resp = client.describe_image(
75
+ image_url="https://img.com/cat.jpg",
76
+ generate_tags=True
77
+ )
78
+ print(resp)
79
+ ```
80
+
81
+ ### 2. Classify Image
82
+ Classify an image into one or more categories.
83
+
84
+ ```python
85
+ resp = client.classify_image(
86
+ image_url="https://img.com/cat.jpg",
87
+ classes=["cat", "dog"]
88
+ )
89
+ print(resp)
90
+ ```
91
+
92
+ ### 3. Visual Question Answering (VQA)
93
+ Ask a question about the content of an image and get an answer.
94
+
95
+ ```python
96
+ resp = client.ask_image(
97
+ image_url="https://img.com/car.jpg",
98
+ question="What color is the car?"
99
+ )
100
+ print(resp)
101
+ ```
102
+
103
+ ### 4. Extract Structured Data from Image
104
+ Extract structured data from an image using either simple fields or an advanced schema.
105
+
106
+ #### Simple Fields (Recommended for basic extraction)
107
+ Use simple fields for straightforward data extraction (max 10 fields):
108
+
109
+ ```python
110
+ resp = client.extract_image(
111
+ image_url="https://img.com/prod.jpg",
112
+ fields=[
113
+ {"name": "product_name", "type": "text", "description": "Name of the product"},
114
+ {"name": "price", "type": "number", "description": "Product price"},
115
+ {"name": "tags", "type": "array_text", "description": "Product tags"},
116
+ ]
117
+ )
118
+ print(resp.extracted_data)
119
+ ```
120
+
121
+ **Field Types:**
122
+ - `text`: Single text value
123
+ - `number`: Single numeric value
124
+ - `array_text`: Array of text values (max 5 items)
125
+ - `array_number`: Array of numeric values (max 5 items)
126
+
127
+ #### Advanced Schema (For complex/nested structures)
128
+ Use advanced schema for complex nested structures or when you need more control:
129
+
130
+ ```python
131
+ from pydantic import BaseModel
132
+
133
+ class Product(BaseModel):
134
+ product_name: str
135
+ price: float
136
+ specifications: dict
137
+
138
+ resp = client.extract_image(
139
+ image_url="https://img.com/prod.jpg",
140
+ advanced_schema=Product # Pass the class directly
141
+ )
142
+ print(resp.extracted_data)
143
+ ```
144
+
145
+ > **Note:** Either `fields` or `advanced_schema` must be provided, not both.
146
+
147
+ ### 5. Compare Images
148
+ Compare two images and get a description of their similarities and differences.
149
+
150
+ ```python
151
+ resp = client.compare_images(
152
+ image1_url="https://img.com/cat1.jpg",
153
+ image2_url="https://img.com/cat2.jpg"
154
+ )
155
+ print(resp)
156
+ ```
157
+
158
+ ## 👤 User Endpoints
159
+
160
+ Check credits and submit feedback.
161
+
162
+ ### Get Credits
163
+ ```python
164
+ credits = client.get_credits()
165
+ print(credits)
166
+ ```
167
+
168
+ ### Submit Feedback
169
+ ```python
170
+ feedback_response = client.submit_feedback(
171
+ request_id="your-request-id",
172
+ rating=5, # Rating from 1-5
173
+ feedback_text="Perfect image description!",
174
+ )
175
+ print(feedback_response)
176
+ ```
177
+
178
+ ## ⚡ Async Usage
179
+
180
+ All endpoints support async operations:
181
+
182
+ ```python
183
+ import asyncio
184
+ from viscribe.async_client import AsyncClient
185
+
186
+ async def main():
187
+ client = AsyncClient(api_key="your-api-key-here")
188
+ resp = await client.describe_image({"image_url": "https://img.com/cat.jpg"})
189
+ print(resp)
190
+ # ... use other endpoints as above
191
+
192
+ asyncio.run(main())
193
+ ```
194
+
195
+ ## 📖 Documentation
196
+
197
+ For detailed documentation, visit [docs.viscribe.ai](https://docs.viscribe.ai)
198
+
199
+ ## 🛠️ Development
200
+
201
+ For information about setting up the development environment and contributing to the project, see our [Contributing Guide](CONTRIBUTING.md).
202
+
203
+ ## 💬 Support & Feedback
204
+
205
+ - 📧 Email: support@viscribe.ai
206
+ - 💻 GitHub Issues: [Create an issue](https://github.com/ViscribeAI/python-sdk/issues)
207
+ - 🌟 Feature Requests: [Request a feature](https://github.com/ViscribeAI/python-sdk/issues/new)
208
+ - ⭐ API Feedback: You can also submit feedback programmatically using the feedback endpoint:
209
+ ```python
210
+ from viscribe import Client
211
+
212
+ client = Client(api_key="your-api-key-here")
213
+
214
+ client.submit_feedback(
215
+ request_id="your-request-id",
216
+ rating=5,
217
+ feedback_text="Great results!"
218
+ )
219
+ ```
220
+
221
+ ## 📄 License
222
+
223
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
224
+
225
+ ## 🔗 Links
226
+
227
+ - [Website](https://viscribe.ai)
228
+ - [Documentation](https://docs.viscribe.ai)
229
+ - [GitHub](https://github.com/ViscribeAI/python-sdk)
230
+
231
+ ---
232
+
233
+ > 🎁 **Get started with free credits!** Visit [dashboard.viscribe.ai](https://dashboard.viscribe.ai) to sign up and get your API key.
234
+
235
+ Made with ❤️ by [ViscribeAI](https://viscribe.ai)
@@ -0,0 +1,14 @@
1
+ viscribe/__init__.py,sha256=9v9X-o8CGf5goWydq-BeHY0U8BVEiDrTk_o15YQXIp4,106
2
+ viscribe/async_client.py,sha256=_kuI_B1yI4YImEl0j3-7FjOgkuo6uSPsXwrKwbyGwa0,12387
3
+ viscribe/client.py,sha256=aI2w4TUQBBVKWJGVp0RMdkhTxUVY9icrieeiJ1qw2t8,12277
4
+ viscribe/config.py,sha256=0Qp5Tf-J1q2c-rNFdEViZjQfW8U5xJCzzcUUWKEghYg,170
5
+ viscribe/exceptions.py,sha256=TFROaihQQAmLO56s3J-st0BR74xeqjHT_tpzgrMaM74,264
6
+ viscribe/logger.py,sha256=Sdli1r1KG2NzMaPRHVCiNHiI6_ykifHKPh7hor-MrsY,3555
7
+ viscribe/models/__init__.py,sha256=uvr4voFaDXYc4JSomF8SXrFJNmFhNPgX-gfDEuVAGDA,356
8
+ viscribe/models/image.py,sha256=rsDGq32Q9-iO67G1wJ692xdk8R8Vxyoaksyad3662to,5706
9
+ viscribe/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ viscribe/utils/helpers.py,sha256=fzdRK_QhQBsOEWEJxaedVraSEjdzuE3GFLqqtH9u2eY,2431
11
+ viscribe-1.0.1.dist-info/METADATA,sha256=KlFdb_GwQxrdTuvepk-aKryhfHOhvIXvkVJO3kPxcYQ,6718
12
+ viscribe-1.0.1.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
13
+ viscribe-1.0.1.dist-info/licenses/LICENSE,sha256=PO1JKSHKXkev7dpsYZIgd-wXxIzigpPunGgQQa3dNA4,1067
14
+ viscribe-1.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.26.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ViscribeAI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.