novelai-python 0.2.8__tar.gz → 0.2.9__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.
- {novelai_python-0.2.8 → novelai_python-0.2.9}/PKG-INFO +7 -3
- {novelai_python-0.2.8 → novelai_python-0.2.9}/README.md +6 -2
- {novelai_python-0.2.8 → novelai_python-0.2.9}/pyproject.toml +1 -1
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/__init__.py +4 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/_response/ai/generate_image.py +3 -1
- novelai_python-0.2.9/src/novelai_python/_response/ai/upscale.py +19 -0
- novelai_python-0.2.9/src/novelai_python/_response/schema.py +11 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/_response/user/information.py +4 -2
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/_response/user/login.py +3 -2
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/_response/user/subscription.py +3 -1
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/credential/JwtToken.py +1 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/credential/UserAuth.py +1 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/sdk/__init__.py +1 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/sdk/ai/generate_image.py +19 -4
- novelai_python-0.2.9/src/novelai_python/sdk/ai/upscale.py +204 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/sdk/user/login.py +1 -1
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/utils/useful.py +3 -5
- novelai_python-0.2.9/tests/test_upscale.py +78 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/LICENSE +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/NOTICE.MD +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/_exceptions.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/_response/__init__.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/_response/ai/__init__.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/_response/user/__init__.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/credential/ApiToken.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/credential/__init__.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/credential/_base.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/sdk/ai/LICENSE +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/sdk/ai/__init__.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/sdk/schema.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/sdk/user/__init__.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/sdk/user/information.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/sdk/user/subscription.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/server.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/utils/__init__.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/utils/encode.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/utils/hash.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/utils/random_prompt/__init__.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/utils/random_prompt/tag.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/utils/random_prompt/tag_artist.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/utils/random_prompt/tag_character.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/utils/random_prompt/tag_nsfw.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/tests/__init__.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/tests/test_random_prompt.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/tests/test_server.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/tests/test_server_run.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/tests/test_user_information.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/tests/test_user_login.py +0 -0
- {novelai_python-0.2.8 → novelai_python-0.2.9}/tests/test_user_subscription.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: novelai-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
4
4
|
Summary: Novelai Python Binding With Pydantic
|
|
5
5
|
Author-Email: sudoskys <coldlando@hotmail.com>
|
|
6
6
|
License: Apache License 2.0
|
|
@@ -26,7 +26,7 @@ Description-Content-Type: text/markdown
|
|
|
26
26
|
|
|
27
27
|
# novelai-python
|
|
28
28
|
|
|
29
|
-
[](https://badge.fury.io/py/novelai-python)
|
|
30
30
|
[](https://pepy.tech/project/novelai_python)
|
|
31
31
|
|
|
32
32
|
The goal of this repository is to use Pydantic to build legitimate requests to access the Novelai API service.
|
|
@@ -37,16 +37,20 @@ The goal of this repository is to use Pydantic to build legitimate requests to a
|
|
|
37
37
|
- [x] /user/subscription
|
|
38
38
|
- [x] /user/login
|
|
39
39
|
- [x] /user/information
|
|
40
|
+
- [x] /ai/upscale
|
|
40
41
|
- [ ] /ai/generate-image/suggest-tags
|
|
41
42
|
- [ ] /ai/annotate-image
|
|
42
43
|
- [ ] /ai/classify
|
|
43
|
-
- [ ] /ai/upscale
|
|
44
44
|
- [ ] /ai/generate-prompt
|
|
45
45
|
- [ ] /ai/generate
|
|
46
46
|
- [ ] /ai/generate-voice
|
|
47
47
|
|
|
48
48
|
### Usage 🖥️
|
|
49
49
|
|
|
50
|
+
```shell
|
|
51
|
+
pip install novelai-python
|
|
52
|
+
```
|
|
53
|
+
|
|
50
54
|
More examples can be found in the [playground](https://github.com/LlmKira/novelai-python/tree/main/playground) directory.
|
|
51
55
|
|
|
52
56
|
```python
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# novelai-python
|
|
2
2
|
|
|
3
|
-
[](https://badge.fury.io/py/novelai-python)
|
|
4
4
|
[](https://pepy.tech/project/novelai_python)
|
|
5
5
|
|
|
6
6
|
The goal of this repository is to use Pydantic to build legitimate requests to access the Novelai API service.
|
|
@@ -11,16 +11,20 @@ The goal of this repository is to use Pydantic to build legitimate requests to a
|
|
|
11
11
|
- [x] /user/subscription
|
|
12
12
|
- [x] /user/login
|
|
13
13
|
- [x] /user/information
|
|
14
|
+
- [x] /ai/upscale
|
|
14
15
|
- [ ] /ai/generate-image/suggest-tags
|
|
15
16
|
- [ ] /ai/annotate-image
|
|
16
17
|
- [ ] /ai/classify
|
|
17
|
-
- [ ] /ai/upscale
|
|
18
18
|
- [ ] /ai/generate-prompt
|
|
19
19
|
- [ ] /ai/generate
|
|
20
20
|
- [ ] /ai/generate-voice
|
|
21
21
|
|
|
22
22
|
### Usage 🖥️
|
|
23
23
|
|
|
24
|
+
```shell
|
|
25
|
+
pip install novelai-python
|
|
26
|
+
```
|
|
27
|
+
|
|
24
28
|
More examples can be found in the [playground](https://github.com/LlmKira/novelai-python/tree/main/playground) directory.
|
|
25
29
|
|
|
26
30
|
```python
|
|
@@ -16,11 +16,15 @@ from .sdk import GenerateImageInfer, ImageGenerateResp
|
|
|
16
16
|
from .sdk import Information, InformationResp
|
|
17
17
|
from .sdk import Login, LoginResp
|
|
18
18
|
from .sdk import Subscription, SubscriptionResp
|
|
19
|
+
from .sdk import Upscale, UpscaleResp
|
|
19
20
|
|
|
20
21
|
__all__ = [
|
|
21
22
|
"GenerateImageInfer",
|
|
22
23
|
"ImageGenerateResp",
|
|
23
24
|
|
|
25
|
+
"Upscale",
|
|
26
|
+
"UpscaleResp",
|
|
27
|
+
|
|
24
28
|
"Subscription",
|
|
25
29
|
"SubscriptionResp",
|
|
26
30
|
|
{novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/_response/ai/generate_image.py
RENAMED
|
@@ -7,8 +7,10 @@ from typing import Tuple, List
|
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel
|
|
9
9
|
|
|
10
|
+
from ..schema import RespBase
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
|
|
13
|
+
class ImageGenerateResp(RespBase):
|
|
12
14
|
class RequestParams(BaseModel):
|
|
13
15
|
endpoint: str
|
|
14
16
|
raw_request: dict = None
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/2/13 上午11:29
|
|
3
|
+
# @Author : sudoskys
|
|
4
|
+
# @File : upscale.py
|
|
5
|
+
# @Software: PyCharm
|
|
6
|
+
from typing import Tuple
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from ..schema import RespBase
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UpscaleResp(RespBase):
|
|
14
|
+
class RequestParams(BaseModel):
|
|
15
|
+
endpoint: str
|
|
16
|
+
raw_request: dict = None
|
|
17
|
+
|
|
18
|
+
meta: RequestParams
|
|
19
|
+
files: Tuple[str, bytes] = None
|
{novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/_response/user/information.py
RENAMED
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
# @File : information.py
|
|
5
5
|
# @Software: PyCharm
|
|
6
6
|
|
|
7
|
-
from pydantic import
|
|
7
|
+
from pydantic import Field
|
|
8
8
|
|
|
9
|
+
from ..schema import RespBase
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
|
|
12
|
+
class InformationResp(RespBase):
|
|
11
13
|
emailVerified: bool = Field(..., description="Email verification status")
|
|
12
14
|
emailVerificationLetterSent: bool = Field(..., description="Email verification letter sent status")
|
|
13
15
|
trialActivated: bool = Field(..., description="Trial activation status")
|
{novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/_response/user/subscription.py
RENAMED
|
@@ -7,6 +7,8 @@ from typing import Optional, Dict, Any, List
|
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel, Field
|
|
9
9
|
|
|
10
|
+
from ..schema import RespBase
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
class TrainingSteps(BaseModel):
|
|
12
14
|
fixedTrainingStepsLeft: int
|
|
@@ -30,7 +32,7 @@ class Perks(BaseModel):
|
|
|
30
32
|
contextTokens: int
|
|
31
33
|
|
|
32
34
|
|
|
33
|
-
class SubscriptionResp(
|
|
35
|
+
class SubscriptionResp(RespBase):
|
|
34
36
|
tier: int = Field(..., description="Subscription tier")
|
|
35
37
|
active: bool = Field(..., description="Subscription status")
|
|
36
38
|
expiresAt: int = Field(..., description="Subscription expiration time")
|
|
@@ -24,6 +24,7 @@ class JwtCredential(CredentialBase):
|
|
|
24
24
|
self._session = AsyncSession(timeout=timeout, headers={
|
|
25
25
|
"Accept": "*/*",
|
|
26
26
|
"Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
|
|
27
|
+
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0",
|
|
27
28
|
"Accept-Encoding": "gzip, deflate, br",
|
|
28
29
|
"Authorization": f"Bearer {self.jwt_token.get_secret_value()}",
|
|
29
30
|
"Content-Type": "application/json",
|
|
@@ -30,6 +30,7 @@ class LoginCredential(CredentialBase):
|
|
|
30
30
|
resp = await Login.build(user_name=self.username, password=self.password.get_secret_value()).request()
|
|
31
31
|
self._session = AsyncSession(timeout=timeout, headers={
|
|
32
32
|
"Accept": "*/*",
|
|
33
|
+
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0",
|
|
33
34
|
"Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
|
|
34
35
|
"Accept-Encoding": "gzip, deflate, br",
|
|
35
36
|
"Authorization": f"Bearer {resp.accessToken}",
|
|
@@ -8,3 +8,4 @@ from .ai.generate_image import GenerateImageInfer, ImageGenerateResp # noqa 401
|
|
|
8
8
|
from .user.information import Information, InformationResp # noqa 401
|
|
9
9
|
from .user.login import Login, LoginResp # noqa 401
|
|
10
10
|
from .user.subscription import Subscription, SubscriptionResp # noqa 401
|
|
11
|
+
from .ai.upscale import Upscale, UpscaleResp # noqa 401
|
|
@@ -22,7 +22,7 @@ from typing_extensions import override
|
|
|
22
22
|
|
|
23
23
|
from ..schema import ApiBaseModel
|
|
24
24
|
from ..._exceptions import APIError, AuthError, ConcurrentGenerationError, SessionHttpError
|
|
25
|
-
from ..._response import ImageGenerateResp
|
|
25
|
+
from ..._response.ai.generate_image import ImageGenerateResp
|
|
26
26
|
from ...credential import CredentialBase
|
|
27
27
|
from ...utils import try_jsonfy, NovelAiMetadata
|
|
28
28
|
|
|
@@ -102,7 +102,6 @@ class Resolution(Enum):
|
|
|
102
102
|
|
|
103
103
|
class GenerateImageInfer(ApiBaseModel):
|
|
104
104
|
_endpoint: Optional[str] = PrivateAttr("https://api.novelai.net")
|
|
105
|
-
_charge: bool = PrivateAttr(False)
|
|
106
105
|
|
|
107
106
|
class Params(BaseModel):
|
|
108
107
|
# Inpaint
|
|
@@ -412,14 +411,30 @@ class GenerateImageInfer(ApiBaseModel):
|
|
|
412
411
|
request_data = self.model_dump(mode="json", exclude_none=True)
|
|
413
412
|
# Header
|
|
414
413
|
if isinstance(session, AsyncSession):
|
|
415
|
-
session.headers.update(self.necessary_headers(request_data))
|
|
414
|
+
session.headers.update(await self.necessary_headers(request_data))
|
|
416
415
|
elif isinstance(session, CredentialBase):
|
|
417
416
|
update_header = await self.necessary_headers(request_data)
|
|
418
417
|
session = await session.get_session(update_headers=update_header)
|
|
419
418
|
if override_headers:
|
|
420
419
|
session.headers.clear()
|
|
421
420
|
session.headers.update(override_headers)
|
|
422
|
-
|
|
421
|
+
try:
|
|
422
|
+
_log_data = request_data.copy()
|
|
423
|
+
if self.action == Action.GENERATE:
|
|
424
|
+
logger.debug(f"Request Data: {_log_data}")
|
|
425
|
+
else:
|
|
426
|
+
_log_data.get("parameters", {}).update({
|
|
427
|
+
"image": "base64 data" if self.parameters.image else "None",
|
|
428
|
+
}
|
|
429
|
+
)
|
|
430
|
+
_log_data.get("parameters", {}).update(
|
|
431
|
+
{
|
|
432
|
+
"mask": "base64 data" if self.parameters.mask else "None",
|
|
433
|
+
}
|
|
434
|
+
)
|
|
435
|
+
logger.debug(f"Request Data: {request_data}")
|
|
436
|
+
except Exception as e:
|
|
437
|
+
logger.warning(f"Error when print log data: {e}")
|
|
423
438
|
try:
|
|
424
439
|
assert hasattr(session, "post"), "session must have post method."
|
|
425
440
|
response = await session.post(
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/2/13 上午10:48
|
|
3
|
+
# @Author : sudoskys
|
|
4
|
+
# @File : upscale.py
|
|
5
|
+
# @Software: PyCharm
|
|
6
|
+
import base64
|
|
7
|
+
import json
|
|
8
|
+
from io import BytesIO
|
|
9
|
+
from typing import Optional, Union
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
from zipfile import ZipFile
|
|
12
|
+
|
|
13
|
+
import curl_cffi
|
|
14
|
+
import httpx
|
|
15
|
+
from curl_cffi.requests import AsyncSession
|
|
16
|
+
from loguru import logger
|
|
17
|
+
from pydantic import ConfigDict, PrivateAttr, model_validator
|
|
18
|
+
|
|
19
|
+
from ..schema import ApiBaseModel
|
|
20
|
+
from ..._exceptions import APIError, AuthError, SessionHttpError
|
|
21
|
+
from ..._response.ai.upscale import UpscaleResp
|
|
22
|
+
from ...credential import CredentialBase
|
|
23
|
+
from ...utils import try_jsonfy, NovelAiMetadata
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Upscale(ApiBaseModel):
|
|
27
|
+
_endpoint: Optional[str] = PrivateAttr("https://api.novelai.net")
|
|
28
|
+
image: Union[str, bytes] # base64
|
|
29
|
+
width: Optional[int] = None
|
|
30
|
+
height: Optional[int] = None
|
|
31
|
+
scale: float = 4
|
|
32
|
+
model_config = ConfigDict(extra="ignore")
|
|
33
|
+
|
|
34
|
+
@model_validator(mode="after")
|
|
35
|
+
def validate_model(self):
|
|
36
|
+
if isinstance(self.image, str) and self.image.startswith("data:image/"):
|
|
37
|
+
raise ValueError("Invalid image format, must be base64 encoded.")
|
|
38
|
+
if isinstance(self.image, bytes):
|
|
39
|
+
self.image = base64.b64encode(self.image).decode("utf-8")
|
|
40
|
+
# Auto detect image size
|
|
41
|
+
try:
|
|
42
|
+
from PIL import Image
|
|
43
|
+
with Image.open(BytesIO(base64.b64decode(self.image))) as img:
|
|
44
|
+
width, height = img.size
|
|
45
|
+
except Exception as e:
|
|
46
|
+
logger.warning(f"Error when validate image size: {e}")
|
|
47
|
+
if self.width is None or self.height is None:
|
|
48
|
+
raise ValueError("Invalid image size and cant auto detect, must be set width and height.")
|
|
49
|
+
else:
|
|
50
|
+
if self.width is None:
|
|
51
|
+
self.width = width
|
|
52
|
+
if self.height is None:
|
|
53
|
+
self.height = height
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def base_url(self):
|
|
58
|
+
return f"{self.endpoint.strip('/')}/ai/upscale"
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def endpoint(self):
|
|
62
|
+
return self._endpoint
|
|
63
|
+
|
|
64
|
+
@endpoint.setter
|
|
65
|
+
def endpoint(self, value):
|
|
66
|
+
self._endpoint = value
|
|
67
|
+
|
|
68
|
+
async def necessary_headers(self, request_data) -> dict:
|
|
69
|
+
"""
|
|
70
|
+
:param request_data:
|
|
71
|
+
:return:
|
|
72
|
+
"""
|
|
73
|
+
return {
|
|
74
|
+
"Host": urlparse(self.endpoint).netloc,
|
|
75
|
+
"Accept": "*/*",
|
|
76
|
+
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0",
|
|
77
|
+
"Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
|
|
78
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
79
|
+
"Referer": "https://novelai.net/",
|
|
80
|
+
"Content-Type": "application/json",
|
|
81
|
+
"Origin": "https://novelai.net",
|
|
82
|
+
"Content-Length": str(len(json.dumps(request_data).encode("utf-8"))),
|
|
83
|
+
"Connection": "keep-alive",
|
|
84
|
+
"Sec-Fetch-Dest": "empty",
|
|
85
|
+
"Sec-Fetch-Mode": "cors",
|
|
86
|
+
"Sec-Fetch-Site": "same-site",
|
|
87
|
+
"Pragma": "no-cache",
|
|
88
|
+
"Cache-Control": "no-cache",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async def request(self,
|
|
92
|
+
session: Union[AsyncSession, "CredentialBase"],
|
|
93
|
+
*,
|
|
94
|
+
override_headers: Optional[dict] = None,
|
|
95
|
+
remove_sign: bool = False
|
|
96
|
+
) -> UpscaleResp:
|
|
97
|
+
"""
|
|
98
|
+
生成图片
|
|
99
|
+
:param override_headers:
|
|
100
|
+
:param session: session
|
|
101
|
+
:param remove_sign: 移除追踪信息
|
|
102
|
+
:return:
|
|
103
|
+
"""
|
|
104
|
+
# Data Build
|
|
105
|
+
request_data = self.model_dump(mode="json", exclude_none=True)
|
|
106
|
+
# Header
|
|
107
|
+
if isinstance(session, AsyncSession):
|
|
108
|
+
session.headers.update(await self.necessary_headers(request_data))
|
|
109
|
+
elif isinstance(session, CredentialBase):
|
|
110
|
+
update_header = await self.necessary_headers(request_data)
|
|
111
|
+
session = await session.get_session(update_headers=update_header)
|
|
112
|
+
if override_headers:
|
|
113
|
+
session.headers.clear()
|
|
114
|
+
session.headers.update(override_headers)
|
|
115
|
+
try:
|
|
116
|
+
_log_data = request_data.copy()
|
|
117
|
+
_log_data.update({"image": "base64 data"}) if isinstance(_log_data.get("image"), str) else None
|
|
118
|
+
logger.info(f"Upscale request data: {_log_data}")
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.warning(f"Error when print log data: {e}")
|
|
121
|
+
try:
|
|
122
|
+
assert hasattr(session, "post"), "session must have post method."
|
|
123
|
+
response = await session.post(
|
|
124
|
+
self.base_url,
|
|
125
|
+
data=json.dumps(request_data).encode("utf-8")
|
|
126
|
+
)
|
|
127
|
+
if response.headers.get('Content-Type') not in ['binary/octet-stream', 'application/x-zip-compressed']:
|
|
128
|
+
logger.error(
|
|
129
|
+
f"Error with content type: {response.headers.get('Content-Type')} and code: {response.status_code}"
|
|
130
|
+
)
|
|
131
|
+
try:
|
|
132
|
+
_msg = response.json()
|
|
133
|
+
except Exception as e:
|
|
134
|
+
logger.warning(e)
|
|
135
|
+
if not isinstance(response.content, str) and len(response.content) < 50:
|
|
136
|
+
raise APIError(
|
|
137
|
+
message=f"Unexpected content type: {response.headers.get('Content-Type')}",
|
|
138
|
+
request=request_data,
|
|
139
|
+
code=response.status_code,
|
|
140
|
+
response=try_jsonfy(response.content)
|
|
141
|
+
)
|
|
142
|
+
else:
|
|
143
|
+
_msg = {"statusCode": response.status_code, "message": response.content}
|
|
144
|
+
status_code = _msg.get("statusCode", response.status_code)
|
|
145
|
+
message = _msg.get("message", "Unknown error")
|
|
146
|
+
if status_code in [400, 401, 402]:
|
|
147
|
+
# 400 : validation error
|
|
148
|
+
# 401 : unauthorized
|
|
149
|
+
# 402 : payment required
|
|
150
|
+
# 409 : conflict
|
|
151
|
+
raise AuthError(message, request=request_data, code=status_code, response=_msg)
|
|
152
|
+
if status_code in [409]:
|
|
153
|
+
# conflict error
|
|
154
|
+
raise APIError(message, request=request_data, code=status_code, response=_msg)
|
|
155
|
+
"""
|
|
156
|
+
if status_code in [429]:
|
|
157
|
+
# concurrent error
|
|
158
|
+
raise ConcurrentGenerationError(
|
|
159
|
+
message=message,
|
|
160
|
+
request=request_data,
|
|
161
|
+
code=status_code,
|
|
162
|
+
response=_msg
|
|
163
|
+
)
|
|
164
|
+
"""
|
|
165
|
+
raise APIError(message, request=request_data, code=status_code, response=_msg)
|
|
166
|
+
zip_file = ZipFile(BytesIO(response.content))
|
|
167
|
+
unzip_content = []
|
|
168
|
+
with zip_file as zf:
|
|
169
|
+
file_list = zf.namelist()
|
|
170
|
+
if not file_list:
|
|
171
|
+
raise APIError(
|
|
172
|
+
message="No file in zip",
|
|
173
|
+
request=request_data,
|
|
174
|
+
code=response.status_code,
|
|
175
|
+
response=try_jsonfy(response.content)
|
|
176
|
+
)
|
|
177
|
+
for filename in file_list:
|
|
178
|
+
data = zip_file.read(filename)
|
|
179
|
+
if remove_sign:
|
|
180
|
+
try:
|
|
181
|
+
data = NovelAiMetadata.rehash(BytesIO(data), remove_stealth=True)
|
|
182
|
+
if not isinstance(data, bytes):
|
|
183
|
+
data = data.getvalue()
|
|
184
|
+
except Exception as e:
|
|
185
|
+
logger.exception(f"SdkWarn:Remove sign error: {e}")
|
|
186
|
+
unzip_content.append((filename, data))
|
|
187
|
+
return UpscaleResp(
|
|
188
|
+
meta=UpscaleResp.RequestParams(
|
|
189
|
+
endpoint=self.base_url,
|
|
190
|
+
raw_request=request_data,
|
|
191
|
+
),
|
|
192
|
+
files=unzip_content[0]
|
|
193
|
+
)
|
|
194
|
+
except curl_cffi.requests.errors.RequestsError as exc:
|
|
195
|
+
logger.exception(exc)
|
|
196
|
+
raise SessionHttpError("An AsyncSession RequestsError occurred, maybe SSL error. Try again later!")
|
|
197
|
+
except httpx.HTTPError as exc:
|
|
198
|
+
logger.exception(exc)
|
|
199
|
+
raise SessionHttpError("An HTTPError occurred, maybe SSL error. Try again later!")
|
|
200
|
+
except APIError as e:
|
|
201
|
+
raise e
|
|
202
|
+
except Exception as e:
|
|
203
|
+
logger.opt(exception=e).exception("An Unexpected error occurred")
|
|
204
|
+
raise e
|
|
@@ -77,7 +77,7 @@ class Login(ApiBaseModel):
|
|
|
77
77
|
# Data Build
|
|
78
78
|
request_data = self.model_dump(mode="json", exclude_none=True)
|
|
79
79
|
if isinstance(session, AsyncSession):
|
|
80
|
-
session.headers.update(self.necessary_headers(request_data))
|
|
80
|
+
session.headers.update(await self.necessary_headers(request_data))
|
|
81
81
|
elif isinstance(session, CredentialBase):
|
|
82
82
|
session = await session.get_session(update_headers=await self.necessary_headers(request_data))
|
|
83
83
|
# Header
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# @Software: PyCharm
|
|
6
6
|
import collections
|
|
7
7
|
import random
|
|
8
|
-
from typing import List
|
|
8
|
+
from typing import List, Union
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def enum_to_list(enum_):
|
|
@@ -22,7 +22,8 @@ class QueSelect(object):
|
|
|
22
22
|
self.used = collections.deque()
|
|
23
23
|
self.users = {}
|
|
24
24
|
|
|
25
|
-
def get(self, user_id: int) -> str:
|
|
25
|
+
def get(self, user_id: Union[int, str]) -> str:
|
|
26
|
+
user_id = str(user_id)
|
|
26
27
|
if user_id not in self.users:
|
|
27
28
|
self.users[user_id] = {'data': self.data.copy(), 'used': collections.deque()}
|
|
28
29
|
|
|
@@ -41,6 +42,3 @@ class QueSelect(object):
|
|
|
41
42
|
user_used.append(selected)
|
|
42
43
|
|
|
43
44
|
return selected
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/2/13 下午12:03
|
|
3
|
+
# @Author : sudoskys
|
|
4
|
+
# @File : test_upscale.py
|
|
5
|
+
# @Software: PyCharm
|
|
6
|
+
# -*- coding: utf-8 -*-
|
|
7
|
+
# @Time : 2024/2/14 下午4:20
|
|
8
|
+
# @Author : sudoskys
|
|
9
|
+
# @File : test_upscale.py
|
|
10
|
+
# @Software: PyCharm
|
|
11
|
+
from unittest import mock
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
from curl_cffi.requests import AsyncSession
|
|
15
|
+
|
|
16
|
+
from novelai_python import APIError, Upscale, AuthError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.mark.asyncio
|
|
20
|
+
async def test_validation_error_during_upscale():
|
|
21
|
+
validation_error_response = mock.Mock()
|
|
22
|
+
validation_error_response.headers = {"Content-Type": "application/json"}
|
|
23
|
+
validation_error_response.status_code = 400
|
|
24
|
+
validation_error_response.json.return_value = {
|
|
25
|
+
"statusCode": 400,
|
|
26
|
+
"message": "A validation error occurred."
|
|
27
|
+
}
|
|
28
|
+
session = mock.MagicMock(spec=AsyncSession)
|
|
29
|
+
session.post = mock.AsyncMock(return_value=validation_error_response)
|
|
30
|
+
session.headers = {}
|
|
31
|
+
|
|
32
|
+
upscale = Upscale(image="base64_encoded_image", height=100, width=100)
|
|
33
|
+
with pytest.raises(AuthError) as e:
|
|
34
|
+
await upscale.request(session=session)
|
|
35
|
+
assert e.type is AuthError
|
|
36
|
+
expected_message = 'A validation error occurred.'
|
|
37
|
+
assert expected_message == str(e.value)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.asyncio
|
|
41
|
+
async def test_unauthorized_error_during_upscale():
|
|
42
|
+
unauthorized_error_response = mock.Mock()
|
|
43
|
+
unauthorized_error_response.headers = {"Content-Type": "application/json"}
|
|
44
|
+
unauthorized_error_response.status_code = 401
|
|
45
|
+
unauthorized_error_response.json.return_value = {
|
|
46
|
+
"statusCode": 401,
|
|
47
|
+
"message": "Unauthorized."
|
|
48
|
+
}
|
|
49
|
+
session = mock.MagicMock(spec=AsyncSession)
|
|
50
|
+
session.post = mock.AsyncMock(return_value=unauthorized_error_response)
|
|
51
|
+
session.headers = {}
|
|
52
|
+
|
|
53
|
+
upscale = Upscale(image="base64_encoded_image", height=100, width=100)
|
|
54
|
+
with pytest.raises(APIError) as e:
|
|
55
|
+
await upscale.request(session=session)
|
|
56
|
+
assert e.type is AuthError
|
|
57
|
+
expected_message = 'Unauthorized.'
|
|
58
|
+
assert expected_message == str(e.value)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.asyncio
|
|
62
|
+
async def test_unknown_error_during_upscale():
|
|
63
|
+
unknown_error_response = mock.Mock()
|
|
64
|
+
unknown_error_response.headers = {"Content-Type": "application/json"}
|
|
65
|
+
unknown_error_response.status_code = 500
|
|
66
|
+
unknown_error_response.json.return_value = {
|
|
67
|
+
"statusCode": 500,
|
|
68
|
+
"message": "Unknown error occurred."
|
|
69
|
+
}
|
|
70
|
+
session = mock.MagicMock(spec=AsyncSession)
|
|
71
|
+
session.post = mock.AsyncMock(return_value=unknown_error_response)
|
|
72
|
+
session.headers = {}
|
|
73
|
+
|
|
74
|
+
upscale = Upscale(image="base64_encoded_image", height=100, width=100)
|
|
75
|
+
with pytest.raises(APIError) as e:
|
|
76
|
+
await upscale.request(session=session)
|
|
77
|
+
expected_message = 'Unknown error occurred.'
|
|
78
|
+
assert expected_message == str(e.value)
|
|
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
|
|
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
|
|
File without changes
|
{novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/utils/random_prompt/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/utils/random_prompt/tag_artist.py
RENAMED
|
File without changes
|
|
File without changes
|
{novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/utils/random_prompt/tag_nsfw.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|