chunkr-ai 0.0.17__py3-none-any.whl → 0.0.18__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.
- chunkr_ai/__init__.py +1 -2
- chunkr_ai/api/chunkr.py +46 -39
- chunkr_ai/api/chunkr_base.py +142 -8
- chunkr_ai/api/config.py +18 -45
- chunkr_ai/api/decorators.py +58 -0
- chunkr_ai/api/misc.py +0 -2
- chunkr_ai/api/protocol.py +0 -2
- chunkr_ai/api/task_response.py +119 -0
- chunkr_ai/models.py +3 -12
- {chunkr_ai-0.0.17.dist-info → chunkr_ai-0.0.18.dist-info}/METADATA +1 -2
- chunkr_ai-0.0.18.dist-info/RECORD +17 -0
- chunkr_ai/api/base.py +0 -183
- chunkr_ai/api/chunkr_async.py +0 -120
- chunkr_ai/api/schema.py +0 -136
- chunkr_ai/api/task.py +0 -66
- chunkr_ai/api/task_async.py +0 -69
- chunkr_ai/api/task_base.py +0 -85
- chunkr_ai-0.0.17.dist-info/RECORD +0 -21
- {chunkr_ai-0.0.17.dist-info → chunkr_ai-0.0.18.dist-info}/LICENSE +0 -0
- {chunkr_ai-0.0.17.dist-info → chunkr_ai-0.0.18.dist-info}/WHEEL +0 -0
- {chunkr_ai-0.0.17.dist-info → chunkr_ai-0.0.18.dist-info}/top_level.txt +0 -0
chunkr_ai/__init__.py
CHANGED
chunkr_ai/api/chunkr.py
CHANGED
@@ -1,78 +1,85 @@
|
|
1
|
-
from .chunkr_base import ChunkrBase
|
2
|
-
from .config import Configuration
|
3
|
-
from .task import TaskResponse
|
4
1
|
from pathlib import Path
|
5
2
|
from PIL import Image
|
6
|
-
import requests
|
7
3
|
from typing import Union, BinaryIO
|
8
|
-
from .misc import prepare_upload_data
|
9
4
|
|
5
|
+
from .config import Configuration
|
6
|
+
from .decorators import anywhere, ensure_client
|
7
|
+
from .misc import prepare_upload_data
|
8
|
+
from .task_response import TaskResponse
|
9
|
+
from .chunkr_base import ChunkrBase
|
10
10
|
|
11
11
|
class Chunkr(ChunkrBase):
|
12
|
-
"""Chunkr API client"""
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
def upload(
|
12
|
+
"""Chunkr API client that works in both sync and async contexts"""
|
13
|
+
|
14
|
+
@anywhere()
|
15
|
+
@ensure_client()
|
16
|
+
async def upload(
|
19
17
|
self,
|
20
18
|
file: Union[str, Path, BinaryIO, Image.Image],
|
21
19
|
config: Configuration = None,
|
22
20
|
) -> TaskResponse:
|
23
|
-
task = self.create_task(file, config)
|
24
|
-
return task.poll()
|
21
|
+
task = await self.create_task(file, config)
|
22
|
+
return await task.poll()
|
25
23
|
|
26
|
-
|
27
|
-
|
28
|
-
|
24
|
+
@anywhere()
|
25
|
+
@ensure_client()
|
26
|
+
async def update(self, task_id: str, config: Configuration) -> TaskResponse:
|
27
|
+
task = await self.update_task(task_id, config)
|
28
|
+
return await task.poll()
|
29
29
|
|
30
|
-
|
30
|
+
@anywhere()
|
31
|
+
@ensure_client()
|
32
|
+
async def create_task(
|
31
33
|
self,
|
32
34
|
file: Union[str, Path, BinaryIO, Image.Image],
|
33
35
|
config: Configuration = None,
|
34
36
|
) -> TaskResponse:
|
35
37
|
files = prepare_upload_data(file, config)
|
36
|
-
|
37
|
-
raise ValueError("Session not found")
|
38
|
-
r = self._session.post(
|
38
|
+
r = await self._client.post(
|
39
39
|
f"{self.url}/api/v1/task", files=files, headers=self._headers()
|
40
40
|
)
|
41
41
|
r.raise_for_status()
|
42
42
|
return TaskResponse(**r.json()).with_client(self)
|
43
43
|
|
44
|
-
|
44
|
+
@anywhere()
|
45
|
+
@ensure_client()
|
46
|
+
async def update_task(self, task_id: str, config: Configuration) -> TaskResponse:
|
45
47
|
files = prepare_upload_data(None, config)
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
48
|
+
r = await self._client.patch(
|
49
|
+
f"{self.url}/api/v1/task/{task_id}",
|
50
|
+
files=files,
|
51
|
+
headers=self._headers(),
|
50
52
|
)
|
51
|
-
|
52
53
|
r.raise_for_status()
|
53
54
|
return TaskResponse(**r.json()).with_client(self)
|
54
55
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
r = self.
|
56
|
+
@anywhere()
|
57
|
+
@ensure_client()
|
58
|
+
async def get_task(self, task_id: str) -> TaskResponse:
|
59
|
+
r = await self._client.get(
|
59
60
|
f"{self.url}/api/v1/task/{task_id}", headers=self._headers()
|
60
61
|
)
|
61
62
|
r.raise_for_status()
|
62
63
|
return TaskResponse(**r.json()).with_client(self)
|
63
64
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
r = self.
|
65
|
+
@anywhere()
|
66
|
+
@ensure_client()
|
67
|
+
async def delete_task(self, task_id: str) -> None:
|
68
|
+
r = await self._client.delete(
|
68
69
|
f"{self.url}/api/v1/task/{task_id}", headers=self._headers()
|
69
70
|
)
|
70
71
|
r.raise_for_status()
|
71
72
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
r = self.
|
73
|
+
@ensure_client()
|
74
|
+
@anywhere()
|
75
|
+
async def cancel_task(self, task_id: str) -> None:
|
76
|
+
r = await self._client.get(
|
76
77
|
f"{self.url}/api/v1/task/{task_id}/cancel", headers=self._headers()
|
77
78
|
)
|
78
79
|
r.raise_for_status()
|
80
|
+
|
81
|
+
@anywhere()
|
82
|
+
async def close(self):
|
83
|
+
if self._client:
|
84
|
+
await self._client.aclose()
|
85
|
+
self._client = None
|
chunkr_ai/api/chunkr_base.py
CHANGED
@@ -1,13 +1,16 @@
|
|
1
1
|
from .config import Configuration
|
2
|
-
from .
|
3
|
-
from .task_async import TaskResponseAsync
|
2
|
+
from .task_response import TaskResponse
|
4
3
|
from .auth import HeadersMixin
|
5
4
|
from abc import abstractmethod
|
6
5
|
from dotenv import load_dotenv
|
6
|
+
import httpx
|
7
|
+
import io
|
8
|
+
import json
|
7
9
|
import os
|
8
10
|
from pathlib import Path
|
9
11
|
from PIL import Image
|
10
|
-
|
12
|
+
import requests
|
13
|
+
from typing import BinaryIO, Tuple, Union
|
11
14
|
|
12
15
|
|
13
16
|
class ChunkrBase(HeadersMixin):
|
@@ -23,13 +26,138 @@ class ChunkrBase(HeadersMixin):
|
|
23
26
|
)
|
24
27
|
|
25
28
|
self.url = self.url.rstrip("/")
|
29
|
+
self._client = httpx.AsyncClient()
|
30
|
+
|
31
|
+
def _prepare_file(
|
32
|
+
self, file: Union[str, Path, BinaryIO, Image.Image]
|
33
|
+
) -> Tuple[str, BinaryIO]:
|
34
|
+
"""Convert various file types into a tuple of (filename, file-like object).
|
35
|
+
|
36
|
+
Args:
|
37
|
+
file: Input file, can be:
|
38
|
+
- String or Path to a file
|
39
|
+
- URL string starting with http:// or https://
|
40
|
+
- Base64 string
|
41
|
+
- Opened binary file (mode='rb')
|
42
|
+
- PIL/Pillow Image object
|
43
|
+
|
44
|
+
Returns:
|
45
|
+
Tuple[str, BinaryIO]: (filename, file-like object) ready for upload
|
46
|
+
|
47
|
+
Raises:
|
48
|
+
FileNotFoundError: If the file path doesn't exist
|
49
|
+
TypeError: If the file type is not supported
|
50
|
+
ValueError: If the URL is invalid or unreachable
|
51
|
+
ValueError: If the MIME type is unsupported
|
52
|
+
"""
|
53
|
+
# Handle URLs
|
54
|
+
if isinstance(file, str) and (
|
55
|
+
file.startswith("http://") or file.startswith("https://")
|
56
|
+
):
|
57
|
+
response = requests.get(file)
|
58
|
+
response.raise_for_status()
|
59
|
+
file_obj = io.BytesIO(response.content)
|
60
|
+
filename = Path(file.split("/")[-1]).name or "downloaded_file"
|
61
|
+
return filename, file_obj
|
62
|
+
|
63
|
+
# Handle base64 strings
|
64
|
+
if isinstance(file, str) and "," in file and ";base64," in file:
|
65
|
+
try:
|
66
|
+
# Split header and data
|
67
|
+
header, base64_data = file.split(",", 1)
|
68
|
+
import base64
|
69
|
+
|
70
|
+
file_bytes = base64.b64decode(base64_data)
|
71
|
+
file_obj = io.BytesIO(file_bytes)
|
72
|
+
|
73
|
+
# Try to determine format from header
|
74
|
+
format = "bin"
|
75
|
+
mime_type = header.split(":")[-1].split(";")[0].lower()
|
76
|
+
|
77
|
+
# Map MIME types to file extensions
|
78
|
+
mime_to_ext = {
|
79
|
+
"application/pdf": "pdf",
|
80
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
|
81
|
+
"application/msword": "doc",
|
82
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx",
|
83
|
+
"application/vnd.ms-powerpoint": "ppt",
|
84
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
|
85
|
+
"application/vnd.ms-excel": "xls",
|
86
|
+
"image/jpeg": "jpg",
|
87
|
+
"image/png": "png",
|
88
|
+
"image/jpg": "jpg",
|
89
|
+
}
|
90
|
+
|
91
|
+
if mime_type in mime_to_ext:
|
92
|
+
format = mime_to_ext[mime_type]
|
93
|
+
else:
|
94
|
+
raise ValueError(f"Unsupported MIME type: {mime_type}")
|
95
|
+
|
96
|
+
return f"file.{format}", file_obj
|
97
|
+
except Exception as e:
|
98
|
+
raise ValueError(f"Invalid base64 string: {str(e)}")
|
99
|
+
|
100
|
+
# Handle file paths
|
101
|
+
if isinstance(file, (str, Path)):
|
102
|
+
path = Path(file).resolve()
|
103
|
+
if not path.exists():
|
104
|
+
raise FileNotFoundError(f"File not found: {file}")
|
105
|
+
return path.name, open(path, "rb")
|
106
|
+
|
107
|
+
# Handle PIL Images
|
108
|
+
if isinstance(file, Image.Image):
|
109
|
+
img_byte_arr = io.BytesIO()
|
110
|
+
format = file.format or "PNG"
|
111
|
+
file.save(img_byte_arr, format=format)
|
112
|
+
img_byte_arr.seek(0)
|
113
|
+
return f"image.{format.lower()}", img_byte_arr
|
114
|
+
|
115
|
+
# Handle file-like objects
|
116
|
+
if hasattr(file, "read") and hasattr(file, "seek"):
|
117
|
+
# Try to get the filename from the file object if possible
|
118
|
+
name = (
|
119
|
+
getattr(file, "name", "document")
|
120
|
+
if hasattr(file, "name")
|
121
|
+
else "document"
|
122
|
+
)
|
123
|
+
return Path(name).name, file
|
124
|
+
|
125
|
+
raise TypeError(f"Unsupported file type: {type(file)}")
|
126
|
+
|
127
|
+
def _prepare_upload_data(
|
128
|
+
self,
|
129
|
+
file: Union[str, Path, BinaryIO, Image.Image],
|
130
|
+
config: Configuration = None,
|
131
|
+
) -> Tuple[dict, dict]:
|
132
|
+
"""Prepare files and data dictionaries for upload.
|
133
|
+
|
134
|
+
Args:
|
135
|
+
file: The file to upload
|
136
|
+
config: Optional configuration settings
|
137
|
+
|
138
|
+
Returns:
|
139
|
+
Tuple[dict, dict]: (files dict, data dict) ready for upload
|
140
|
+
"""
|
141
|
+
filename, file_obj = self._prepare_file(file)
|
142
|
+
files = {"file": (filename, file_obj)}
|
143
|
+
data = {}
|
144
|
+
|
145
|
+
if config:
|
146
|
+
config_dict = config.model_dump(mode="json", exclude_none=True)
|
147
|
+
for key, value in config_dict.items():
|
148
|
+
if isinstance(value, dict):
|
149
|
+
files[key] = (None, json.dumps(value), "application/json")
|
150
|
+
else:
|
151
|
+
data[key] = value
|
152
|
+
|
153
|
+
return files, data
|
26
154
|
|
27
155
|
@abstractmethod
|
28
156
|
def upload(
|
29
157
|
self,
|
30
158
|
file: Union[str, Path, BinaryIO, Image.Image],
|
31
159
|
config: Configuration = None,
|
32
|
-
) ->
|
160
|
+
) -> TaskResponse:
|
33
161
|
"""Upload a file and wait for processing to complete.
|
34
162
|
|
35
163
|
Args:
|
@@ -64,7 +192,7 @@ class ChunkrBase(HeadersMixin):
|
|
64
192
|
@abstractmethod
|
65
193
|
def update(
|
66
194
|
self, task_id: str, config: Configuration
|
67
|
-
) ->
|
195
|
+
) -> TaskResponse:
|
68
196
|
"""Update a task by its ID and wait for processing to complete.
|
69
197
|
|
70
198
|
Args:
|
@@ -81,7 +209,7 @@ class ChunkrBase(HeadersMixin):
|
|
81
209
|
self,
|
82
210
|
file: Union[str, Path, BinaryIO, Image.Image],
|
83
211
|
config: Configuration = None,
|
84
|
-
) ->
|
212
|
+
) -> TaskResponse:
|
85
213
|
"""Upload a file for processing and immediately return the task response. It will not wait for processing to complete. To wait for the full processing to complete, use `task.poll()`.
|
86
214
|
|
87
215
|
Args:
|
@@ -117,7 +245,7 @@ class ChunkrBase(HeadersMixin):
|
|
117
245
|
@abstractmethod
|
118
246
|
def update_task(
|
119
247
|
self, task_id: str, config: Configuration
|
120
|
-
) ->
|
248
|
+
) -> TaskResponse:
|
121
249
|
"""Update a task by its ID and immediately return the task response. It will not wait for processing to complete. To wait for the full processing to complete, use `task.poll()`.
|
122
250
|
|
123
251
|
Args:
|
@@ -130,7 +258,7 @@ class ChunkrBase(HeadersMixin):
|
|
130
258
|
pass
|
131
259
|
|
132
260
|
@abstractmethod
|
133
|
-
def get_task(self, task_id: str) ->
|
261
|
+
def get_task(self, task_id: str) -> TaskResponse:
|
134
262
|
"""Get a task response by its ID.
|
135
263
|
|
136
264
|
Args:
|
@@ -158,3 +286,9 @@ class ChunkrBase(HeadersMixin):
|
|
158
286
|
task_id: The ID of the task to cancel
|
159
287
|
"""
|
160
288
|
pass
|
289
|
+
|
290
|
+
@abstractmethod
|
291
|
+
def close(self) -> None:
|
292
|
+
"""Close the client connection.
|
293
|
+
This should be called when you're done using the client to properly clean up resources."""
|
294
|
+
pass
|
chunkr_ai/api/config.py
CHANGED
@@ -1,26 +1,21 @@
|
|
1
1
|
from pydantic import BaseModel, Field, model_validator, ConfigDict
|
2
2
|
from enum import Enum
|
3
3
|
from typing import Optional, List, Dict, Union, Type
|
4
|
-
from .schema import from_pydantic
|
5
|
-
|
6
4
|
|
7
5
|
class GenerationStrategy(str, Enum):
|
8
6
|
LLM = "LLM"
|
9
7
|
AUTO = "Auto"
|
10
8
|
|
11
|
-
|
12
9
|
class CroppingStrategy(str, Enum):
|
13
10
|
ALL = "All"
|
14
11
|
AUTO = "Auto"
|
15
12
|
|
16
|
-
|
17
13
|
class GenerationConfig(BaseModel):
|
18
14
|
html: Optional[GenerationStrategy] = None
|
19
15
|
llm: Optional[str] = None
|
20
16
|
markdown: Optional[GenerationStrategy] = None
|
21
17
|
crop_image: Optional[CroppingStrategy] = None
|
22
18
|
|
23
|
-
|
24
19
|
class SegmentProcessing(BaseModel):
|
25
20
|
model_config = ConfigDict(populate_by_name=True, alias_generator=str.title)
|
26
21
|
|
@@ -39,46 +34,38 @@ class SegmentProcessing(BaseModel):
|
|
39
34
|
page_footer: Optional[GenerationConfig] = Field(default=None, alias="PageFooter")
|
40
35
|
page: Optional[GenerationConfig] = Field(default=None, alias="Page")
|
41
36
|
|
42
|
-
|
43
37
|
class ChunkProcessing(BaseModel):
|
44
38
|
target_length: Optional[int] = None
|
45
39
|
|
46
|
-
|
47
40
|
class Property(BaseModel):
|
48
41
|
name: str
|
49
42
|
prop_type: str
|
50
43
|
description: Optional[str] = None
|
51
44
|
default: Optional[str] = None
|
52
45
|
|
53
|
-
|
54
46
|
class JsonSchema(BaseModel):
|
55
47
|
title: str
|
56
48
|
properties: List[Property]
|
57
49
|
|
58
|
-
|
59
50
|
class OcrStrategy(str, Enum):
|
60
51
|
ALL = "All"
|
61
52
|
AUTO = "Auto"
|
62
53
|
|
63
|
-
|
64
54
|
class SegmentationStrategy(str, Enum):
|
65
55
|
LAYOUT_ANALYSIS = "LayoutAnalysis"
|
66
56
|
PAGE = "Page"
|
67
57
|
|
68
|
-
|
69
58
|
class BoundingBox(BaseModel):
|
70
59
|
left: float
|
71
60
|
top: float
|
72
61
|
width: float
|
73
62
|
height: float
|
74
63
|
|
75
|
-
|
76
64
|
class OCRResult(BaseModel):
|
77
65
|
bbox: BoundingBox
|
78
66
|
text: str
|
79
67
|
confidence: Optional[float]
|
80
68
|
|
81
|
-
|
82
69
|
class SegmentType(str, Enum):
|
83
70
|
CAPTION = "Caption"
|
84
71
|
FOOTNOTE = "Footnote"
|
@@ -93,7 +80,6 @@ class SegmentType(str, Enum):
|
|
93
80
|
TEXT = "Text"
|
94
81
|
TITLE = "Title"
|
95
82
|
|
96
|
-
|
97
83
|
class Segment(BaseModel):
|
98
84
|
bbox: BoundingBox
|
99
85
|
content: str
|
@@ -107,42 +93,41 @@ class Segment(BaseModel):
|
|
107
93
|
segment_id: str
|
108
94
|
segment_type: SegmentType
|
109
95
|
|
110
|
-
|
111
96
|
class Chunk(BaseModel):
|
112
97
|
chunk_id: str
|
113
98
|
chunk_length: int
|
114
99
|
segments: List[Segment]
|
115
100
|
|
116
|
-
|
117
101
|
class ExtractedJson(BaseModel):
|
118
102
|
data: Dict
|
119
103
|
|
120
|
-
|
121
104
|
class OutputResponse(BaseModel):
|
122
105
|
chunks: List[Chunk]
|
123
|
-
|
124
|
-
|
106
|
+
file_name: Optional[str]
|
107
|
+
page_count: Optional[int]
|
108
|
+
pdf_url: Optional[str]
|
125
109
|
|
126
110
|
class Model(str, Enum):
|
127
111
|
FAST = "Fast"
|
128
112
|
HIGH_QUALITY = "HighQuality"
|
129
113
|
|
130
|
-
class
|
114
|
+
class Pipeline(str, Enum):
|
131
115
|
AZURE = "Azure"
|
132
116
|
|
133
117
|
class Configuration(BaseModel):
|
134
|
-
chunk_processing: Optional[ChunkProcessing] =
|
135
|
-
expires_in: Optional[int] =
|
136
|
-
high_resolution: Optional[bool] =
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
118
|
+
chunk_processing: Optional[ChunkProcessing] = None
|
119
|
+
expires_in: Optional[int] = None
|
120
|
+
high_resolution: Optional[bool] = None
|
121
|
+
model: Optional[Model] = None
|
122
|
+
ocr_strategy: Optional[OcrStrategy] = None
|
123
|
+
segment_processing: Optional[SegmentProcessing] = None
|
124
|
+
segmentation_strategy: Optional[SegmentationStrategy] = None
|
125
|
+
pipeline: Optional[Pipeline] = None
|
126
|
+
|
127
|
+
class OutputConfiguration(Configuration):
|
128
|
+
input_file_url: Optional[str] = None
|
129
|
+
json_schema: Optional[Union[JsonSchema, Type[BaseModel], BaseModel]] = None
|
130
|
+
|
146
131
|
@model_validator(mode="before")
|
147
132
|
def map_deprecated_fields(cls, values: Dict) -> Dict:
|
148
133
|
if isinstance(values, dict) and "target_chunk_length" in values:
|
@@ -151,19 +136,7 @@ class Configuration(BaseModel):
|
|
151
136
|
values["chunk_processing"] = values.get("chunk_processing", {}) or {}
|
152
137
|
values["chunk_processing"]["target_length"] = target_length
|
153
138
|
return values
|
154
|
-
|
155
|
-
@model_validator(mode="after")
|
156
|
-
def convert_json_schema(self) -> "Configuration":
|
157
|
-
if self.json_schema is not None and not isinstance(
|
158
|
-
self.json_schema, JsonSchema
|
159
|
-
):
|
160
|
-
if isinstance(self.json_schema, (BaseModel, type)) and issubclass(
|
161
|
-
getattr(self.json_schema, "__class__", type), BaseModel
|
162
|
-
):
|
163
|
-
self.json_schema = JsonSchema(**from_pydantic(self.json_schema))
|
164
|
-
return self
|
165
|
-
|
166
|
-
|
139
|
+
|
167
140
|
class Status(str, Enum):
|
168
141
|
STARTING = "Starting"
|
169
142
|
PROCESSING = "Processing"
|
@@ -0,0 +1,58 @@
|
|
1
|
+
import functools
|
2
|
+
import asyncio
|
3
|
+
import httpx
|
4
|
+
from typing import Callable, Any, TypeVar, Awaitable, ParamSpec, Union, overload
|
5
|
+
|
6
|
+
T = TypeVar('T')
|
7
|
+
P = ParamSpec('P')
|
8
|
+
|
9
|
+
_sync_loop = None
|
10
|
+
|
11
|
+
@overload
|
12
|
+
def anywhere() -> Callable[[Callable[P, Awaitable[T]]], Callable[P, Union[Awaitable[T], T]]]: ...
|
13
|
+
|
14
|
+
def anywhere():
|
15
|
+
"""Decorator that allows an async function to run anywhere - sync or async context."""
|
16
|
+
def decorator(async_func: Callable[P, Awaitable[T]]) -> Callable[P, Union[Awaitable[T], T]]:
|
17
|
+
@functools.wraps(async_func)
|
18
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> Union[Awaitable[T], T]:
|
19
|
+
global _sync_loop
|
20
|
+
try:
|
21
|
+
loop = asyncio.get_running_loop()
|
22
|
+
return async_func(*args, **kwargs)
|
23
|
+
except RuntimeError:
|
24
|
+
if _sync_loop is None:
|
25
|
+
_sync_loop = asyncio.new_event_loop()
|
26
|
+
asyncio.set_event_loop(_sync_loop)
|
27
|
+
try:
|
28
|
+
return _sync_loop.run_until_complete(async_func(*args, **kwargs))
|
29
|
+
finally:
|
30
|
+
asyncio.set_event_loop(None)
|
31
|
+
return wrapper
|
32
|
+
return decorator
|
33
|
+
|
34
|
+
def ensure_client() -> Callable[[Callable[P, Awaitable[T]]], Callable[P, Awaitable[T]]]:
|
35
|
+
"""Decorator that ensures a valid httpx.AsyncClient exists before executing the method"""
|
36
|
+
def decorator(async_func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
|
37
|
+
@functools.wraps(async_func)
|
38
|
+
async def wrapper(self: Any, *args: P.args, **kwargs: P.kwargs) -> T:
|
39
|
+
if not self._client or self._client.is_closed:
|
40
|
+
self._client = httpx.AsyncClient()
|
41
|
+
return await async_func(self, *args, **kwargs)
|
42
|
+
return wrapper
|
43
|
+
return decorator
|
44
|
+
|
45
|
+
def require_task() -> Callable[[Callable[P, Awaitable[T]]], Callable[P, Awaitable[T]]]:
|
46
|
+
"""Decorator that ensures task has required attributes and valid client before execution"""
|
47
|
+
def decorator(async_func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
|
48
|
+
@functools.wraps(async_func)
|
49
|
+
async def wrapper(self: Any, *args: P.args, **kwargs: P.kwargs) -> T:
|
50
|
+
if not self.task_url:
|
51
|
+
raise ValueError("Task URL not found")
|
52
|
+
if not self._client:
|
53
|
+
raise ValueError("Client not found")
|
54
|
+
if not self._client._client or self._client._client.is_closed:
|
55
|
+
self._client._client = httpx.AsyncClient()
|
56
|
+
return await async_func(self, *args, **kwargs)
|
57
|
+
return wrapper
|
58
|
+
return decorator
|
chunkr_ai/api/misc.py
CHANGED
@@ -3,11 +3,9 @@ import io
|
|
3
3
|
import json
|
4
4
|
from pathlib import Path
|
5
5
|
from PIL import Image
|
6
|
-
from pydantic import BaseModel
|
7
6
|
import requests
|
8
7
|
from typing import Union, Tuple, BinaryIO, Optional
|
9
8
|
|
10
|
-
|
11
9
|
def prepare_file(file: Union[str, Path, BinaryIO, Image.Image]) -> Tuple[str, BinaryIO]:
|
12
10
|
"""Convert various file types into a tuple of (filename, file-like object)."""
|
13
11
|
# Handle URLs
|
chunkr_ai/api/protocol.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
from typing import Optional, runtime_checkable, Protocol
|
2
|
-
from requests import Session
|
3
2
|
from httpx import AsyncClient
|
4
3
|
|
5
4
|
|
@@ -9,7 +8,6 @@ class ChunkrClientProtocol(Protocol):
|
|
9
8
|
|
10
9
|
url: str
|
11
10
|
_api_key: str
|
12
|
-
_session: Optional[Session] = None
|
13
11
|
_client: Optional[AsyncClient] = None
|
14
12
|
|
15
13
|
def get_api_key(self) -> str:
|