mirror of
https://github.com/xtekky/gpt4free.git
synced 2025-12-06 02:30:41 -08:00
feat: enhance HAR provider, image handling, markdown upload & cache
- **g4f/Provider/har/__init__.py** - `get_models`/`create_async`: iterate over `(domain, harFile)` and filter with `domain in request_url` - `read_har_files` now yields `(domain, har_data)`; fixes file variable shadowing and uses `json.load` - remove stray `print`, add type hint for `find_str`, replace manual loops with `yield from` - small whitespace clean-up - **g4f/Provider/needs_auth/Grok.py** - `ImagePreview` now passes `auth_result.cookies` and `auth_result.headers` - **g4f/Provider/needs_auth/OpenaiChat.py** - add `Union` import; rename/refactor `get_generated_images` → `get_generated_image` - support `file-service://` and `sediment://` pointers; choose correct download URL - return `ImagePreview` or `ImageResponse` accordingly and stream each image part - propagate 422 errors, update prompt assignment and image handling paths - **g4f/client/__init__.py** - drop unused `ignore_working` parameter in sync/async `Completions` - normalise `media` argument: accept single tuple, infer filename when missing, fix index loop - `Images.create_variation` updated to use the new media logic - **g4f/gui/server/api.py** - expose `latest_version_cached` via `?cache=` query parameter - **g4f/gui/server/backend_api.py** - optional Markdown extraction via `MarkItDown`; save rendered text as `<file>.md` - upload flow rewrites: copy to temp file, move to bucket/media dir, clean temp, store filenames - introduce `has_markitdown` guard and improved logging/exception handling - **g4f/tools/files.py** - remove trailing spaces in HAR code-block header string - **g4f/version.py** - add `latest_version_cached` `@cached_property` for memoised version lookup
This commit is contained in:
parent
db47d71f4c
commit
8f63f656a2
8 changed files with 95 additions and 36 deletions
|
|
@ -17,10 +17,10 @@ class HarProvider(AsyncGeneratorProvider, ProviderModelMixin):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_models(cls):
|
def get_models(cls):
|
||||||
for harFile in read_har_files():
|
for domain, harFile in read_har_files():
|
||||||
for v in harFile['log']['entries']:
|
for v in harFile['log']['entries']:
|
||||||
request_url = v['request']['url']
|
request_url = v['request']['url']
|
||||||
if not request_url.startswith(cls.url) or "." in urlparse(request_url).path or "heartbeat" in request_url:
|
if domain not in request_url or "." in urlparse(request_url).path or "heartbeat" in request_url:
|
||||||
continue
|
continue
|
||||||
if "\n\ndata: " not in v['response']['content']['text']:
|
if "\n\ndata: " not in v['response']['content']['text']:
|
||||||
continue
|
continue
|
||||||
|
|
@ -41,11 +41,11 @@ class HarProvider(AsyncGeneratorProvider, ProviderModelMixin):
|
||||||
session_hash = str(uuid.uuid4()).replace("-", "")
|
session_hash = str(uuid.uuid4()).replace("-", "")
|
||||||
prompt = get_last_user_message(messages)
|
prompt = get_last_user_message(messages)
|
||||||
|
|
||||||
for harFile in read_har_files():
|
for domain, harFile in read_har_files():
|
||||||
async with StreamSession(impersonate="chrome") as session:
|
async with StreamSession(impersonate="chrome") as session:
|
||||||
for v in harFile['log']['entries']:
|
for v in harFile['log']['entries']:
|
||||||
request_url = v['request']['url']
|
request_url = v['request']['url']
|
||||||
if not request_url.startswith(cls.url) or "." in urlparse(request_url).path or "heartbeat" in request_url:
|
if domain not in request_url or "." in urlparse(request_url).path or "heartbeat" in request_url:
|
||||||
continue
|
continue
|
||||||
postData = None
|
postData = None
|
||||||
if "postData" in v['request']:
|
if "postData" in v['request']:
|
||||||
|
|
@ -59,8 +59,6 @@ class HarProvider(AsyncGeneratorProvider, ProviderModelMixin):
|
||||||
|
|
||||||
async with getattr(session, method)(request_url, data=postData, headers=get_headers(v), proxy=proxy) as response:
|
async with getattr(session, method)(request_url, data=postData, headers=get_headers(v), proxy=proxy) as response:
|
||||||
await raise_for_status(response)
|
await raise_for_status(response)
|
||||||
if "heartbeat" in request_url:
|
|
||||||
continue
|
|
||||||
returned_data = ""
|
returned_data = ""
|
||||||
async for line in response.iter_lines():
|
async for line in response.iter_lines():
|
||||||
if not line.startswith(b"data: "):
|
if not line.startswith(b"data: "):
|
||||||
|
|
@ -83,9 +81,9 @@ def read_har_files():
|
||||||
for file in files:
|
for file in files:
|
||||||
if not file.endswith(".har"):
|
if not file.endswith(".har"):
|
||||||
continue
|
continue
|
||||||
with open(os.path.join(root, file), 'rb') as file:
|
with open(os.path.join(root, file), 'rb') as f:
|
||||||
try:
|
try:
|
||||||
yield json.loads(file.read())
|
yield os.path.splitext(file)[0], json.load(f)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
raise RuntimeError(f"Failed to read HAR file: {file}")
|
raise RuntimeError(f"Failed to read HAR file: {file}")
|
||||||
|
|
||||||
|
|
@ -98,7 +96,7 @@ def read_str_recusive(data):
|
||||||
elif isinstance(item, str):
|
elif isinstance(item, str):
|
||||||
yield item
|
yield item
|
||||||
|
|
||||||
def find_str(data, skip=0):
|
def find_str(data, skip: int = 0):
|
||||||
for item in read_str_recusive(data):
|
for item in read_str_recusive(data):
|
||||||
if skip > 0:
|
if skip > 0:
|
||||||
skip -= 1
|
skip -= 1
|
||||||
|
|
@ -110,7 +108,6 @@ def read_list_recusive(data, key):
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
for k, v in data.items():
|
for k, v in data.items():
|
||||||
if k == key:
|
if k == key:
|
||||||
print(k, v)
|
|
||||||
yield v
|
yield v
|
||||||
else:
|
else:
|
||||||
yield from read_list_recusive(v, key)
|
yield from read_list_recusive(v, key)
|
||||||
|
|
@ -123,8 +120,7 @@ def find_list(data, key):
|
||||||
if isinstance(item, str):
|
if isinstance(item, str):
|
||||||
yield item
|
yield item
|
||||||
elif isinstance(item, list):
|
elif isinstance(item, list):
|
||||||
for sub_item in item:
|
yield from item
|
||||||
yield sub_item
|
|
||||||
|
|
||||||
def get_str_list(data):
|
def get_str_list(data):
|
||||||
for item in data:
|
for item in data:
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ class Grok(AsyncAuthedProvider, ProviderModelMixin):
|
||||||
response_data = result.get("response", {})
|
response_data = result.get("response", {})
|
||||||
image = response_data.get("streamingImageGenerationResponse", None)
|
image = response_data.get("streamingImageGenerationResponse", None)
|
||||||
if image is not None:
|
if image is not None:
|
||||||
yield ImagePreview(f'{cls.assets_url}/{image["imageUrl"]}', "", {"cookies": cookies, "headers": headers})
|
yield ImagePreview(f'{cls.assets_url}/{image["imageUrl"]}', "", {"cookies": auth_result.cookies, "headers": auth_result.headers})
|
||||||
token = response_data.get("token", result.get("token"))
|
token = response_data.get("token", result.get("token"))
|
||||||
is_thinking = response_data.get("isThinking", result.get("isThinking"))
|
is_thinking = response_data.get("isThinking", result.get("isThinking"))
|
||||||
if token:
|
if token:
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import json
|
||||||
import base64
|
import base64
|
||||||
import time
|
import time
|
||||||
import random
|
import random
|
||||||
from typing import AsyncIterator, Iterator, Optional, Generator, Dict
|
from typing import AsyncIterator, Iterator, Optional, Generator, Dict, Union
|
||||||
from copy import copy
|
from copy import copy
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -254,22 +254,43 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_generated_images(cls, session: StreamSession, auth_result: AuthResult, parts: list, prompt: str, conversation_id: str) -> AsyncIterator:
|
async def get_generated_image(cls, session: StreamSession, auth_result: AuthResult, element: Union[dict, str], prompt: str = None, conversation_id: str = None) -> AsyncIterator:
|
||||||
download_urls = []
|
download_urls = []
|
||||||
element = element.split("sediment://")[-1]
|
is_sediment = False
|
||||||
url = f"{cls.url}/backend-api/conversation/{conversation_id}/attachment/{element}/download"
|
if prompt is None:
|
||||||
debug.log(f"OpenaiChat: Downloading image: {url}")
|
try:
|
||||||
|
prompt = element["metadata"]["dalle"]["prompt"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
if "asset_pointer" in element:
|
||||||
|
element = element["asset_pointer"]
|
||||||
|
if isinstance(element, str) and element.startswith("file-service://"):
|
||||||
|
element = element.split("file-service://", 1)[-1]
|
||||||
|
if isinstance(element, str) and element.startswith("sediment://"):
|
||||||
|
is_sediment = True
|
||||||
|
element = element.split("sediment://")[-1]
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Invalid image element: {element}")
|
||||||
|
if is_sediment:
|
||||||
|
url = f"{cls.url}/backend-api/conversation/{conversation_id}/attachment/{element}/download"
|
||||||
|
else:
|
||||||
|
url =f"{cls.url}/backend-api/files/{element}/download"
|
||||||
try:
|
try:
|
||||||
async with session.get(url, headers=auth_result.headers) as response:
|
async with session.get(url, headers=auth_result.headers) as response:
|
||||||
cls._update_request_args(auth_result, session)
|
cls._update_request_args(auth_result, session)
|
||||||
await raise_for_status(response)
|
await raise_for_status(response)
|
||||||
data = await response.json()
|
data = await response.json()
|
||||||
download_url = data.get("download_url")
|
download_url = data.get("download_url")
|
||||||
download_urls.append(download_url)
|
if download_url is not None:
|
||||||
|
download_urls.append(download_url)
|
||||||
|
debug.log(f"OpenaiChat: Found image: {download_url}")
|
||||||
|
else:
|
||||||
|
debug.log("OpenaiChat: No download URL found in response: ", data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
debug.error("OpenaiChat: Download image failed")
|
debug.error("OpenaiChat: Download image failed")
|
||||||
debug.error(e)
|
debug.error(e)
|
||||||
return ImagePreview(download_urls, prompt)
|
if download_urls:
|
||||||
|
return ImagePreview(download_urls, prompt) if is_sediment else ImageResponse(download_urls, prompt)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def create_authed(
|
async def create_authed(
|
||||||
|
|
@ -402,7 +423,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
|
||||||
if conversation.conversation_id is not None:
|
if conversation.conversation_id is not None:
|
||||||
data["conversation_id"] = conversation.conversation_id
|
data["conversation_id"] = conversation.conversation_id
|
||||||
debug.log(f"OpenaiChat: Use conversation: {conversation.conversation_id}")
|
debug.log(f"OpenaiChat: Use conversation: {conversation.conversation_id}")
|
||||||
conversation.prompt = format_image_prompt(messages, prompt)
|
prompt = conversation.prompt = format_image_prompt(messages, prompt)
|
||||||
if action != "continue":
|
if action != "continue":
|
||||||
data["parent_message_id"] = getattr(conversation, "parent_message_id", conversation.message_id)
|
data["parent_message_id"] = getattr(conversation, "parent_message_id", conversation.message_id)
|
||||||
conversation.parent_message_id = None
|
conversation.parent_message_id = None
|
||||||
|
|
@ -430,6 +451,8 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
|
||||||
cls._update_request_args(auth_result, session)
|
cls._update_request_args(auth_result, session)
|
||||||
if response.status in (401, 403, 429):
|
if response.status in (401, 403, 429):
|
||||||
raise MissingAuthError("Access token is not valid")
|
raise MissingAuthError("Access token is not valid")
|
||||||
|
elif response.status == 422:
|
||||||
|
raise RuntimeError((await response.json()), data)
|
||||||
await raise_for_status(response)
|
await raise_for_status(response)
|
||||||
buffer = u""
|
buffer = u""
|
||||||
async for line in response.iter_lines():
|
async for line in response.iter_lines():
|
||||||
|
|
@ -515,7 +538,9 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
|
||||||
elif m.get("p") == "/message/metadata/image_gen_title":
|
elif m.get("p") == "/message/metadata/image_gen_title":
|
||||||
fields.prompt = m.get("v")
|
fields.prompt = m.get("v")
|
||||||
elif m.get("p") == "/message/content/parts/0/asset_pointer":
|
elif m.get("p") == "/message/content/parts/0/asset_pointer":
|
||||||
fields.generated_images = await cls.get_generated_images(session, auth_result, m.get("v"), fields.prompt, fields.conversation_id)
|
generated_images = fields.generated_images = await cls.get_generated_image(session, auth_result, m.get("v"), fields.prompt, fields.conversation_id)
|
||||||
|
if generated_images is not None:
|
||||||
|
yield generated_images
|
||||||
elif m.get("p") == "/message/metadata/search_result_groups":
|
elif m.get("p") == "/message/metadata/search_result_groups":
|
||||||
for entry in [p.get("entries") for p in m.get("v")]:
|
for entry in [p.get("entries") for p in m.get("v")]:
|
||||||
for link in entry:
|
for link in entry:
|
||||||
|
|
@ -544,7 +569,9 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
|
||||||
fields.is_thinking = True
|
fields.is_thinking = True
|
||||||
yield Reasoning(status=m.get("metadata", {}).get("initial_text"))
|
yield Reasoning(status=m.get("metadata", {}).get("initial_text"))
|
||||||
if c.get("content_type") == "multimodal_text":
|
if c.get("content_type") == "multimodal_text":
|
||||||
yield await cls.get_generated_images(session, auth_result, c.get("parts"), fields.prompt, fields.conversation_id)
|
for part in c.get("parts"):
|
||||||
|
if isinstance(part, dict) and part.get("content_type") == "image_asset_pointer":
|
||||||
|
yield await cls.get_generated_image(session, auth_result, part, fields.prompt, fields.conversation_id)
|
||||||
if m.get("author", {}).get("role") == "assistant":
|
if m.get("author", {}).get("role") == "assistant":
|
||||||
if fields.parent_message_id is None:
|
if fields.parent_message_id is None:
|
||||||
fields.parent_message_id = v.get("message", {}).get("id")
|
fields.parent_message_id = v.get("message", {}).get("id")
|
||||||
|
|
|
||||||
|
|
@ -291,16 +291,18 @@ class Completions:
|
||||||
max_tokens: Optional[int] = None,
|
max_tokens: Optional[int] = None,
|
||||||
stop: Optional[Union[list[str], str]] = None,
|
stop: Optional[Union[list[str], str]] = None,
|
||||||
api_key: Optional[str] = None,
|
api_key: Optional[str] = None,
|
||||||
ignore_working: Optional[bool] = False,
|
|
||||||
ignore_stream: Optional[bool] = False,
|
ignore_stream: Optional[bool] = False,
|
||||||
**kwargs
|
**kwargs
|
||||||
) -> ChatCompletion:
|
) -> ChatCompletion:
|
||||||
if isinstance(messages, str):
|
if isinstance(messages, str):
|
||||||
messages = [{"role": "user", "content": messages}]
|
messages = [{"role": "user", "content": messages}]
|
||||||
if image is not None:
|
if image is not None:
|
||||||
kwargs["media"] = [(image, image_name)]
|
kwargs["media"] = (image, image_name)
|
||||||
elif "images" in kwargs:
|
elif "images" in kwargs:
|
||||||
kwargs["media"] = kwargs.pop("images")
|
kwargs["media"] = kwargs.pop("images")
|
||||||
|
for idx, media in kwargs.get("media", []):
|
||||||
|
if not isinstance(media, (list, tuple)):
|
||||||
|
kwargs["media"][idx] = (media[0], media[1] if media[1] is not None else getattr(image, "name", None))
|
||||||
if provider is None:
|
if provider is None:
|
||||||
provider = self.provider
|
provider = self.provider
|
||||||
if provider is None:
|
if provider is None:
|
||||||
|
|
@ -493,7 +495,10 @@ class Images:
|
||||||
proxy = self.client.proxy
|
proxy = self.client.proxy
|
||||||
prompt = "create a variation of this image"
|
prompt = "create a variation of this image"
|
||||||
if image is not None:
|
if image is not None:
|
||||||
kwargs["media"] = [(image, None)]
|
kwargs["media"] = image
|
||||||
|
for idx, media in kwargs.get("media", []):
|
||||||
|
if not isinstance(media, (list, tuple)):
|
||||||
|
kwargs["media"][idx] = (media[0], media[1] if media[1] is not None else getattr(image, "name", None))
|
||||||
|
|
||||||
error = None
|
error = None
|
||||||
response = None
|
response = None
|
||||||
|
|
@ -591,7 +596,6 @@ class AsyncCompletions:
|
||||||
max_tokens: Optional[int] = None,
|
max_tokens: Optional[int] = None,
|
||||||
stop: Optional[Union[list[str], str]] = None,
|
stop: Optional[Union[list[str], str]] = None,
|
||||||
api_key: Optional[str] = None,
|
api_key: Optional[str] = None,
|
||||||
ignore_working: Optional[bool] = False,
|
|
||||||
ignore_stream: Optional[bool] = False,
|
ignore_stream: Optional[bool] = False,
|
||||||
**kwargs
|
**kwargs
|
||||||
) -> Awaitable[ChatCompletion]:
|
) -> Awaitable[ChatCompletion]:
|
||||||
|
|
@ -601,6 +605,10 @@ class AsyncCompletions:
|
||||||
kwargs["media"] = [(image, image_name)]
|
kwargs["media"] = [(image, image_name)]
|
||||||
elif "images" in kwargs:
|
elif "images" in kwargs:
|
||||||
kwargs["media"] = kwargs.pop("images")
|
kwargs["media"] = kwargs.pop("images")
|
||||||
|
for idx, media in kwargs.get("media", []):
|
||||||
|
if not isinstance(media, (list, tuple)):
|
||||||
|
kwargs["media"][idx] = (media[0], media[1] if media[1] is not None else getattr(image, "name", None))
|
||||||
|
|
||||||
if provider is None:
|
if provider is None:
|
||||||
provider = self.provider
|
provider = self.provider
|
||||||
if provider is None:
|
if provider is None:
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import logging
|
||||||
import os
|
import os
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
from flask import send_from_directory
|
from flask import send_from_directory, request
|
||||||
from inspect import signature
|
from inspect import signature
|
||||||
|
|
||||||
from ...errors import VersionNotFoundError, MissingAuthError
|
from ...errors import VersionNotFoundError, MissingAuthError
|
||||||
|
|
@ -87,7 +87,10 @@ class Api:
|
||||||
latest_version = None
|
latest_version = None
|
||||||
try:
|
try:
|
||||||
current_version = version.utils.current_version
|
current_version = version.utils.current_version
|
||||||
latest_version = version.utils.latest_version
|
if request.args.get("cache"):
|
||||||
|
latest_version = version.utils.latest_version_cached
|
||||||
|
else:
|
||||||
|
latest_version = version.utils.latest_version
|
||||||
except VersionNotFoundError:
|
except VersionNotFoundError:
|
||||||
pass
|
pass
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,12 @@ from pathlib import Path
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
|
|
||||||
|
try:
|
||||||
|
from markitdown import MarkItDown
|
||||||
|
has_markitdown = True
|
||||||
|
except ImportError:
|
||||||
|
has_markitdown = False
|
||||||
|
|
||||||
from ...client.service import convert_to_provider
|
from ...client.service import convert_to_provider
|
||||||
from ...providers.asyncio import to_sync_generator
|
from ...providers.asyncio import to_sync_generator
|
||||||
from ...providers.response import FinishReason
|
from ...providers.response import FinishReason
|
||||||
|
|
@ -299,8 +305,24 @@ class Backend_Api(Api):
|
||||||
filenames = []
|
filenames = []
|
||||||
media = []
|
media = []
|
||||||
for file in request.files.getlist('files'):
|
for file in request.files.getlist('files'):
|
||||||
try:
|
# Copy the file to a temporary location
|
||||||
filename = secure_filename(file.filename)
|
filename = secure_filename(file.filename)
|
||||||
|
copyfile = tempfile.NamedTemporaryFile(suffix=filename, delete=False)
|
||||||
|
shutil.copyfileobj(file.stream, copyfile)
|
||||||
|
copyfile.close()
|
||||||
|
file.stream.close()
|
||||||
|
|
||||||
|
result = None
|
||||||
|
if has_markitdown:
|
||||||
|
try:
|
||||||
|
md = MarkItDown()
|
||||||
|
result = md.convert(copyfile.name).text_content
|
||||||
|
with open(os.path.join(bucket_dir, f"{filename}.md"), 'w') as f:
|
||||||
|
f.write(f"{result.text_content}\n")
|
||||||
|
filenames.append(f"{filename}.md")
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
if not result:
|
||||||
if is_allowed_extension(filename):
|
if is_allowed_extension(filename):
|
||||||
os.makedirs(media_dir, exist_ok=True)
|
os.makedirs(media_dir, exist_ok=True)
|
||||||
newfile = os.path.join(media_dir, filename)
|
newfile = os.path.join(media_dir, filename)
|
||||||
|
|
@ -309,11 +331,10 @@ class Backend_Api(Api):
|
||||||
newfile = os.path.join(bucket_dir, filename)
|
newfile = os.path.join(bucket_dir, filename)
|
||||||
filenames.append(filename)
|
filenames.append(filename)
|
||||||
else:
|
else:
|
||||||
|
os.remove(copyfile.name)
|
||||||
continue
|
continue
|
||||||
with open(newfile, 'wb') as f:
|
shutil.copyfile(copyfile.name, newfile)
|
||||||
shutil.copyfileobj(file.stream, f)
|
os.remove(copyfile.name)
|
||||||
finally:
|
|
||||||
file.stream.close()
|
|
||||||
with open(os.path.join(bucket_dir, "files.txt"), 'w') as f:
|
with open(os.path.join(bucket_dir, "files.txt"), 'w') as f:
|
||||||
[f.write(f"{filename}\n") for filename in filenames]
|
[f.write(f"{filename}\n") for filename in filenames]
|
||||||
return {"bucket_id": bucket_id, "files": filenames, "media": media}
|
return {"bucket_id": bucket_id, "files": filenames, "media": media}
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ def stream_read_files(bucket_dir: Path, filenames: list, delete_files: bool = Fa
|
||||||
else:
|
else:
|
||||||
os.unlink(filepath)
|
os.unlink(filepath)
|
||||||
continue
|
continue
|
||||||
yield f"```{filename}\n"
|
yield f"```{filename}\n"
|
||||||
if has_pypdf2 and filename.endswith(".pdf"):
|
if has_pypdf2 and filename.endswith(".pdf"):
|
||||||
try:
|
try:
|
||||||
reader = PyPDF2.PdfReader(file_path)
|
reader = PyPDF2.PdfReader(file_path)
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,10 @@ class VersionUtils:
|
||||||
return get_github_version(GITHUB_REPOSITORY)
|
return get_github_version(GITHUB_REPOSITORY)
|
||||||
return get_pypi_version(PACKAGE_NAME)
|
return get_pypi_version(PACKAGE_NAME)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def latest_version_cached(self) -> str:
|
||||||
|
return self.latest_version
|
||||||
|
|
||||||
def check_version(self) -> None:
|
def check_version(self) -> None:
|
||||||
"""
|
"""
|
||||||
Checks if the current version of 'g4f' is up to date with the latest version.
|
Checks if the current version of 'g4f' is up to date with the latest version.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue