novelai-python 0.2.3__tar.gz → 0.2.5__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.3 → novelai_python-0.2.5}/PKG-INFO +2 -2
- {novelai_python-0.2.3 → novelai_python-0.2.5}/README.md +1 -1
- {novelai_python-0.2.3 → novelai_python-0.2.5}/pyproject.toml +1 -1
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/_exceptions.py +7 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/_response/user/subscription.py +28 -2
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/credential/ApiToken.py +7 -1
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/credential/JwtToken.py +8 -1
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/credential/UserAuth.py +7 -1
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/credential/_base.py +1 -1
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/sdk/ai/generate_image.py +133 -22
- novelai_python-0.2.5/src/novelai_python/sdk/schema.py +36 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/sdk/user/__init__.py +1 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/sdk/user/information.py +31 -8
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/sdk/user/login.py +23 -5
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/sdk/user/subscription.py +38 -8
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/server.py +1 -1
- novelai_python-0.2.5/src/novelai_python/utils/useful.py +7 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/tests/test_server_run.py +1 -1
- {novelai_python-0.2.3 → novelai_python-0.2.5}/tests/test_user_information.py +3 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/tests/test_user_login.py +5 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/tests/test_user_subscription.py +4 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/LICENSE +0 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/NOTICE.MD +0 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/__init__.py +0 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/_response/__init__.py +0 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/_response/ai/__init__.py +0 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/_response/ai/generate_image.py +0 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/_response/user/__init__.py +0 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/_response/user/information.py +0 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/_response/user/login.py +0 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/credential/__init__.py +0 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/sdk/__init__.py +0 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/sdk/ai/LICENSE +0 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/sdk/ai/__init__.py +0 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/utils/__init__.py +0 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/utils/encode.py +0 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/utils/hash.py +0 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/utils/random_prompt/__init__.py +0 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/utils/random_prompt/tag.py +0 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/utils/random_prompt/tag_artist.py +0 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/utils/random_prompt/tag_character.py +0 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/utils/random_prompt/tag_nsfw.py +0 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/tests/__init__.py +0 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/tests/test_random_prompt.py +0 -0
- {novelai_python-0.2.3 → novelai_python-0.2.5}/tests/test_server.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.5
|
|
4
4
|
Summary: Novelai Python Binding With Pydantic
|
|
5
5
|
Author-Email: sudoskys <coldlando@hotmail.com>
|
|
6
6
|
License: Apache License 2.0
|
|
@@ -80,7 +80,7 @@ async def main():
|
|
|
80
80
|
cost = gen.calculate_cost(is_opus=True)
|
|
81
81
|
print(f"charge: {cost} if you are vip3")
|
|
82
82
|
|
|
83
|
-
resp = gen.
|
|
83
|
+
resp = gen.request(session=globe_s)
|
|
84
84
|
resp: ImageGenerateResp
|
|
85
85
|
print(resp.meta)
|
|
86
86
|
|
|
@@ -13,6 +13,13 @@ class NovelAiError(Exception):
|
|
|
13
13
|
pass
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
class InvalidRequestHeader(NovelAiError):
|
|
17
|
+
"""
|
|
18
|
+
InvalidRequestHeader is raised when the request header is invalid.
|
|
19
|
+
"""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
16
23
|
class APIError(NovelAiError):
|
|
17
24
|
"""
|
|
18
25
|
APIError is raised when the API returns an error.
|
{novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/_response/user/subscription.py
RENAMED
|
@@ -45,8 +45,34 @@ class SubscriptionResp(BaseModel):
|
|
|
45
45
|
|
|
46
46
|
@property
|
|
47
47
|
def anlas_left(self):
|
|
48
|
-
return self.trainingStepsLeft.fixedTrainingStepsLeft
|
|
48
|
+
return self.trainingStepsLeft.fixedTrainingStepsLeft + self.trainingStepsLeft.purchasedTrainingSteps
|
|
49
49
|
|
|
50
50
|
@property
|
|
51
51
|
def is_unlimited_image_generation(self):
|
|
52
|
-
return self.perks.unlimitedImageGeneration
|
|
52
|
+
return self.perks.unlimitedImageGeneration and self.perks.imageGeneration
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def get_tier_name(self):
|
|
56
|
+
if self.tier == 0:
|
|
57
|
+
return "Paper"
|
|
58
|
+
elif self.tier == 1:
|
|
59
|
+
return "Tablet"
|
|
60
|
+
elif self.tier == 2:
|
|
61
|
+
return "Scroll"
|
|
62
|
+
elif self.tier == 3:
|
|
63
|
+
return "Opus"
|
|
64
|
+
else:
|
|
65
|
+
return "Unknown"
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def limit_perks(self):
|
|
69
|
+
perks = []
|
|
70
|
+
if not self.perks.imageGeneration:
|
|
71
|
+
perks.append("imageGeneration")
|
|
72
|
+
if not self.perks.voiceGeneration:
|
|
73
|
+
perks.append("voiceGeneration")
|
|
74
|
+
if not self.perks.unlimitedImageGeneration:
|
|
75
|
+
perks.append("unlimitedImageGeneration")
|
|
76
|
+
if not self.perks.unlimitedMaxPriority:
|
|
77
|
+
perks.append("unlimitedMaxPriority")
|
|
78
|
+
return perks
|
|
@@ -17,14 +17,20 @@ class ApiCredential(CredentialBase):
|
|
|
17
17
|
api_token: SecretStr = Field(None, description="api token")
|
|
18
18
|
_session: AsyncSession = None
|
|
19
19
|
|
|
20
|
-
async def get_session(self, timeout: int = 180):
|
|
20
|
+
async def get_session(self, timeout: int = 180, update_headers: dict = None):
|
|
21
|
+
if update_headers is None:
|
|
22
|
+
update_headers = {}
|
|
21
23
|
if not self._session:
|
|
22
24
|
self._session = AsyncSession(timeout=timeout, headers={
|
|
25
|
+
"Accept": "*/*",
|
|
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
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
23
28
|
"Authorization": f"Bearer {self.api_token.get_secret_value()}",
|
|
24
29
|
"Content-Type": "application/json",
|
|
25
30
|
"Origin": "https://novelai.net",
|
|
26
31
|
"Referer": "https://novelai.net/",
|
|
27
32
|
}, impersonate="chrome110")
|
|
33
|
+
self._session.headers.update(update_headers)
|
|
28
34
|
return self._session
|
|
29
35
|
|
|
30
36
|
@field_validator('api_token')
|
|
@@ -17,14 +17,21 @@ class JwtCredential(CredentialBase):
|
|
|
17
17
|
jwt_token: SecretStr = Field(None, description="jwt token")
|
|
18
18
|
_session: AsyncSession = None
|
|
19
19
|
|
|
20
|
-
async def get_session(self, timeout: int = 180):
|
|
20
|
+
async def get_session(self, timeout: int = 180, update_headers: dict = None):
|
|
21
|
+
if update_headers is None:
|
|
22
|
+
update_headers = {}
|
|
21
23
|
if not self._session:
|
|
22
24
|
self._session = AsyncSession(timeout=timeout, headers={
|
|
25
|
+
"Accept": "*/*",
|
|
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
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
23
28
|
"Authorization": f"Bearer {self.jwt_token.get_secret_value()}",
|
|
24
29
|
"Content-Type": "application/json",
|
|
25
30
|
"Origin": "https://novelai.net",
|
|
26
31
|
"Referer": "https://novelai.net/",
|
|
27
32
|
}, impersonate="chrome110")
|
|
33
|
+
assert isinstance(update_headers, dict), "update_headers must be a dict"
|
|
34
|
+
self._session.headers.update(update_headers)
|
|
28
35
|
return self._session
|
|
29
36
|
|
|
30
37
|
@field_validator('jwt_token')
|
|
@@ -21,16 +21,22 @@ class LoginCredential(CredentialBase):
|
|
|
21
21
|
_session: Optional[AsyncSession] = None
|
|
22
22
|
_update_at: Optional[int] = None
|
|
23
23
|
|
|
24
|
-
async def get_session(self, timeout: int = 180):
|
|
24
|
+
async def get_session(self, timeout: int = 180, update_headers: dict = None):
|
|
25
|
+
if update_headers is None:
|
|
26
|
+
update_headers = {}
|
|
25
27
|
# 30 天有效期
|
|
26
28
|
if not self._session or int(time.time()) - self._update_at > 29 * 24 * 60 * 60:
|
|
27
29
|
from ..sdk import Login
|
|
28
30
|
resp = await Login.build(user_name=self.username, password=self.password.get_secret_value()).request()
|
|
29
31
|
self._session = AsyncSession(timeout=timeout, headers={
|
|
32
|
+
"Accept": "*/*",
|
|
33
|
+
"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
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
30
35
|
"Authorization": f"Bearer {resp.accessToken}",
|
|
31
36
|
"Content-Type": "application/json",
|
|
32
37
|
"Origin": "https://novelai.net",
|
|
33
38
|
"Referer": "https://novelai.net/",
|
|
34
39
|
}, impersonate="chrome110")
|
|
35
40
|
self._update_at = int(time.time())
|
|
41
|
+
self._session.headers.update(update_headers)
|
|
36
42
|
return self._session
|
|
@@ -3,20 +3,24 @@
|
|
|
3
3
|
# @Author : sudoskys
|
|
4
4
|
# @File : generate_image.py
|
|
5
5
|
# @Software: PyCharm
|
|
6
|
+
import base64
|
|
6
7
|
import json
|
|
7
8
|
import math
|
|
8
9
|
import random
|
|
9
10
|
from enum import Enum, IntEnum
|
|
10
11
|
from io import BytesIO
|
|
11
12
|
from typing import Optional, Union
|
|
13
|
+
from urllib.parse import urlparse
|
|
12
14
|
from zipfile import ZipFile
|
|
13
15
|
|
|
16
|
+
import curl_cffi
|
|
14
17
|
import httpx
|
|
15
|
-
from curl_cffi.requests import AsyncSession
|
|
18
|
+
from curl_cffi.requests import AsyncSession
|
|
16
19
|
from loguru import logger
|
|
17
20
|
from pydantic import BaseModel, ConfigDict, PrivateAttr, field_validator, model_validator, Field
|
|
18
21
|
from typing_extensions import override
|
|
19
22
|
|
|
23
|
+
from ..schema import ApiBaseModel
|
|
20
24
|
from ..._exceptions import APIError, AuthError
|
|
21
25
|
from ..._response import ImageGenerateResp
|
|
22
26
|
from ...credential import CredentialBase
|
|
@@ -48,14 +52,39 @@ class UCPreset(IntEnum):
|
|
|
48
52
|
|
|
49
53
|
class Action(Enum):
|
|
50
54
|
GENERATE = "generate"
|
|
55
|
+
"""Generate Image"""
|
|
51
56
|
IMG2IMG = "img2img"
|
|
57
|
+
"""Image to Image"""
|
|
52
58
|
INFILL = "infill"
|
|
59
|
+
"""Inpainting"""
|
|
53
60
|
|
|
54
61
|
|
|
55
62
|
class Model(Enum):
|
|
56
63
|
NAI_DIFFUSION_3 = "nai-diffusion-3"
|
|
57
64
|
NAI_DIFFUSION_3_INPAINTING = "nai-diffusion-3-inpainting"
|
|
58
65
|
|
|
66
|
+
NAI_DIFFUSION = "nai-diffusion"
|
|
67
|
+
NAI_DIFFUSION_INPAINTING = "nai-diffusion-inpainting"
|
|
68
|
+
|
|
69
|
+
SAFE_DIFFUSION = "safe-diffusion"
|
|
70
|
+
SAFE_DIFFUSION_INPAINTING = "safe-diffusion-inpainting"
|
|
71
|
+
|
|
72
|
+
NAI_DIFFUSION_FURRY = "nai-diffusion-furry"
|
|
73
|
+
FURRY_DIFFUSION_INPAINTING = "furry-diffusion-inpainting"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ControlNetModel(Enum):
|
|
77
|
+
HED = "hed"
|
|
78
|
+
"""边缘检测"""
|
|
79
|
+
MIDAS = "midas"
|
|
80
|
+
"""景深"""
|
|
81
|
+
FAKE_SCRIBBLE = "fake_scribble"
|
|
82
|
+
"""伪涂鸦"""
|
|
83
|
+
M_LSD = "mlsd"
|
|
84
|
+
"""(建筑)线条检测"""
|
|
85
|
+
LANDSCAPER = "uniformer"
|
|
86
|
+
"""风景生成"""
|
|
87
|
+
|
|
59
88
|
|
|
60
89
|
class Resolution(Enum):
|
|
61
90
|
RES_512_768 = (512, 768)
|
|
@@ -71,25 +100,28 @@ class Resolution(Enum):
|
|
|
71
100
|
RES_1920_1088 = (1920, 1088)
|
|
72
101
|
|
|
73
102
|
|
|
74
|
-
class GenerateImageInfer(
|
|
103
|
+
class GenerateImageInfer(ApiBaseModel):
|
|
75
104
|
_endpoint: Optional[str] = PrivateAttr("https://api.novelai.net")
|
|
76
105
|
_charge: bool = PrivateAttr(False)
|
|
77
106
|
|
|
78
107
|
class Params(BaseModel):
|
|
79
108
|
# Inpaint
|
|
80
109
|
add_original_image: Optional[bool] = False
|
|
81
|
-
mask: Optional[str] = None
|
|
110
|
+
mask: Optional[Union[str, bytes]] = None # img2img,base64
|
|
82
111
|
|
|
83
112
|
cfg_rescale: Optional[float] = Field(0, ge=0, le=1, multiple_of=0.02)
|
|
113
|
+
"""Prompt Guidance Rescale"""
|
|
84
114
|
controlnet_strength: Optional[float] = Field(1.0, ge=0.1, le=2, multiple_of=0.1)
|
|
115
|
+
"""ControlNet Strength"""
|
|
85
116
|
dynamic_thresholding: Optional[bool] = False
|
|
117
|
+
"""Reduce artifacts caused by high prompt guidance values."""
|
|
86
118
|
height: Optional[int] = Field(1216, ge=64, le=49152)
|
|
87
119
|
# Img2Img
|
|
88
|
-
image: Optional[str] = None # img2img,base64
|
|
89
|
-
strength: Optional[float] = Field(default=0.
|
|
120
|
+
image: Optional[Union[str, bytes]] = None # img2img,base64
|
|
121
|
+
strength: Optional[float] = Field(default=0.5, ge=0.01, le=0.99, multiple_of=0.01)
|
|
90
122
|
noise: Optional[float] = Field(default=0, ge=0, le=0.99, multiple_of=0.01)
|
|
91
123
|
controlnet_condition: Optional[str] = None
|
|
92
|
-
controlnet_model: Optional[
|
|
124
|
+
controlnet_model: Optional[ControlNetModel] = None
|
|
93
125
|
|
|
94
126
|
n_samples: Optional[int] = Field(1, ge=1, le=8)
|
|
95
127
|
negative_prompt: Optional[str] = ''
|
|
@@ -103,6 +135,7 @@ class GenerateImageInfer(BaseModel):
|
|
|
103
135
|
qualityToggle: Optional[bool] = True
|
|
104
136
|
sampler: Optional[Sampler] = Sampler.K_EULER
|
|
105
137
|
scale: Optional[float] = Field(6.0, ge=0, le=10, multiple_of=0.1)
|
|
138
|
+
"""Prompt Guidance"""
|
|
106
139
|
# Seed
|
|
107
140
|
seed: Optional[int] = Field(
|
|
108
141
|
default_factory=lambda: random.randint(0, 4294967295 - 7),
|
|
@@ -120,6 +153,7 @@ class GenerateImageInfer(BaseModel):
|
|
|
120
153
|
steps: Optional[int] = Field(28, ge=1, le=50)
|
|
121
154
|
ucPreset: Optional[UCPreset] = 0
|
|
122
155
|
uncond_scale: Optional[float] = Field(1.0, ge=0, le=1.5, multiple_of=0.05)
|
|
156
|
+
"""Undesired Content Strength"""
|
|
123
157
|
width: Optional[int] = Field(832, ge=64, le=49152)
|
|
124
158
|
|
|
125
159
|
@model_validator(mode="after")
|
|
@@ -130,6 +164,22 @@ class GenerateImageInfer(BaseModel):
|
|
|
130
164
|
raise ValueError('Invalid Model Params For img2img2 mode... image should match add_original_image!')
|
|
131
165
|
return self
|
|
132
166
|
|
|
167
|
+
@field_validator('mask')
|
|
168
|
+
def mask_validator(cls, v: Union[str, bytes]):
|
|
169
|
+
if isinstance(v, str) and v.startswith("data:image/"):
|
|
170
|
+
raise ValueError("Invalid image format, must be base64 encoded.")
|
|
171
|
+
if isinstance(v, bytes):
|
|
172
|
+
return base64.b64encode(v).decode("utf-8")
|
|
173
|
+
return v
|
|
174
|
+
|
|
175
|
+
@field_validator('image')
|
|
176
|
+
def image_validator(cls, v: Union[str, bytes]):
|
|
177
|
+
if isinstance(v, str) and v.startswith("data:image/"):
|
|
178
|
+
raise ValueError("Invalid image format, must be base64 encoded.")
|
|
179
|
+
if isinstance(v, bytes):
|
|
180
|
+
return base64.b64encode(v).decode("utf-8")
|
|
181
|
+
return v
|
|
182
|
+
|
|
133
183
|
@field_validator('width')
|
|
134
184
|
def width_validator(cls, v: int):
|
|
135
185
|
"""
|
|
@@ -188,6 +238,12 @@ class GenerateImageInfer(BaseModel):
|
|
|
188
238
|
if self.parameters.qualityToggle:
|
|
189
239
|
self.input += ", best quality, amazing quality, very aesthetic, absurdres"
|
|
190
240
|
|
|
241
|
+
@model_validator(mode="after")
|
|
242
|
+
def validate_model(self):
|
|
243
|
+
if self.action == Action.INFILL and not self.parameters.mask:
|
|
244
|
+
logger.warning("Mask maybe required for infill mode.")
|
|
245
|
+
return self
|
|
246
|
+
|
|
191
247
|
@property
|
|
192
248
|
def base_url(self):
|
|
193
249
|
return f"{self.endpoint.strip('/')}/ai/generate-image"
|
|
@@ -250,28 +306,42 @@ class GenerateImageInfer(BaseModel):
|
|
|
250
306
|
negative_prompt: str = "",
|
|
251
307
|
seed: int = None,
|
|
252
308
|
steps: int = 28,
|
|
309
|
+
scale: float = 6.0,
|
|
253
310
|
cfg_rescale: int = 0,
|
|
254
311
|
sampler: Union[Sampler, str] = Sampler.K_EULER,
|
|
255
312
|
width: int = 832,
|
|
256
313
|
height: int = 1216,
|
|
257
314
|
qualityToggle: bool = True,
|
|
258
315
|
ucPreset: Union[UCPreset, int] = UCPreset.TYPE0,
|
|
316
|
+
image: Union[str, bytes] = None,
|
|
317
|
+
add_original_image: bool = None,
|
|
318
|
+
strength: float = None,
|
|
319
|
+
mask: Union[str, bytes] = None,
|
|
320
|
+
controlnet_model: Union[ControlNetModel, str] = None,
|
|
321
|
+
controlnet_condition: str = None,
|
|
259
322
|
**kwargs
|
|
260
323
|
):
|
|
261
324
|
"""
|
|
262
|
-
|
|
263
|
-
:param ucPreset: 0:
|
|
264
|
-
:param qualityToggle:
|
|
265
|
-
:param prompt:
|
|
266
|
-
:param model:
|
|
325
|
+
The build Function, more convenient to create a GenerateImageInfer instance.
|
|
326
|
+
:param ucPreset: 0: Heavy, 1: Light, 2: Character
|
|
327
|
+
:param qualityToggle: Is quality toggle
|
|
328
|
+
:param prompt: Input prompt for generate image
|
|
329
|
+
:param model: The model for generate image
|
|
267
330
|
:param action: Mode for img generate [generate, img2img, infill]
|
|
268
|
-
:param negative_prompt:
|
|
269
|
-
:param seed:
|
|
270
|
-
:param steps:
|
|
271
|
-
:param
|
|
272
|
-
:param
|
|
331
|
+
:param negative_prompt: The content of negative prompt
|
|
332
|
+
:param seed: The seed for generate image
|
|
333
|
+
:param steps: The steps for generate image
|
|
334
|
+
:param scale: Prompt Guidance
|
|
335
|
+
:param cfg_rescale: Prompt Guidance Rescale 0-1 lower is more creative
|
|
336
|
+
:param sampler: The sampler for generate image
|
|
273
337
|
:param width: 宽
|
|
274
338
|
:param height: 高
|
|
339
|
+
:param image: 图片
|
|
340
|
+
:param add_original_image: 是否添加原始图片
|
|
341
|
+
:param strength: IMG2IMG 强度
|
|
342
|
+
:param mask: Inpainting mask
|
|
343
|
+
:param controlnet_model: 控制网络模型
|
|
344
|
+
:param controlnet_condition: 控制网络条件
|
|
275
345
|
:return: self
|
|
276
346
|
"""
|
|
277
347
|
assert isinstance(prompt, str)
|
|
@@ -280,12 +350,19 @@ class GenerateImageInfer(BaseModel):
|
|
|
280
350
|
"negative_prompt": _negative_prompt,
|
|
281
351
|
"seed": seed,
|
|
282
352
|
"steps": steps,
|
|
353
|
+
"scale": scale,
|
|
283
354
|
"cfg_rescale": cfg_rescale,
|
|
284
355
|
"sampler": sampler,
|
|
285
356
|
"width": width,
|
|
286
357
|
"height": height,
|
|
287
358
|
"qualityToggle": qualityToggle,
|
|
288
359
|
"ucPreset": ucPreset,
|
|
360
|
+
"image": image,
|
|
361
|
+
"add_original_image": add_original_image,
|
|
362
|
+
"strength": strength,
|
|
363
|
+
"mask": mask,
|
|
364
|
+
"controlnet_model": controlnet_model,
|
|
365
|
+
"controlnet_condition": controlnet_condition
|
|
289
366
|
})
|
|
290
367
|
# 清理空值
|
|
291
368
|
param = {k: v for k, v in kwargs.items() if v is not None}
|
|
@@ -296,18 +373,52 @@ class GenerateImageInfer(BaseModel):
|
|
|
296
373
|
parameters=cls.Params(**param)
|
|
297
374
|
)
|
|
298
375
|
|
|
299
|
-
async def
|
|
300
|
-
|
|
301
|
-
|
|
376
|
+
async def necessary_headers(self, request_data) -> dict:
|
|
377
|
+
"""
|
|
378
|
+
:param request_data:
|
|
379
|
+
:return:
|
|
380
|
+
"""
|
|
381
|
+
return {
|
|
382
|
+
"Host": urlparse(self.endpoint).netloc,
|
|
383
|
+
"Accept": "*/*",
|
|
384
|
+
"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",
|
|
385
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
386
|
+
"Referer": "https://novelai.net/",
|
|
387
|
+
"Content-Type": "application/json",
|
|
388
|
+
"Origin": "https://novelai.net",
|
|
389
|
+
"Content-Length": str(len(json.dumps(request_data).encode("utf-8"))),
|
|
390
|
+
"Connection": "keep-alive",
|
|
391
|
+
"Sec-Fetch-Dest": "empty",
|
|
392
|
+
"Sec-Fetch-Mode": "cors",
|
|
393
|
+
"Sec-Fetch-Site": "same-site",
|
|
394
|
+
"Pragma": "no-cache",
|
|
395
|
+
"Cache-Control": "no-cache",
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async def request(self,
|
|
399
|
+
session: Union[AsyncSession, "CredentialBase"],
|
|
400
|
+
*,
|
|
401
|
+
override_headers: Optional[dict] = None,
|
|
402
|
+
remove_sign: bool = False
|
|
403
|
+
) -> ImageGenerateResp:
|
|
302
404
|
"""
|
|
303
405
|
生成图片
|
|
406
|
+
:param override_headers:
|
|
304
407
|
:param session: session
|
|
305
408
|
:param remove_sign: 移除追踪信息
|
|
306
409
|
:return:
|
|
307
410
|
"""
|
|
308
|
-
|
|
309
|
-
session = await session.get_session()
|
|
411
|
+
# Data Build
|
|
310
412
|
request_data = self.model_dump(mode="json", exclude_none=True)
|
|
413
|
+
# Header
|
|
414
|
+
if isinstance(session, AsyncSession):
|
|
415
|
+
session.headers.update(self.necessary_headers(request_data))
|
|
416
|
+
elif isinstance(session, CredentialBase):
|
|
417
|
+
update_header = await self.necessary_headers(request_data)
|
|
418
|
+
session = await session.get_session(update_headers=update_header)
|
|
419
|
+
if override_headers:
|
|
420
|
+
session.headers.clear()
|
|
421
|
+
session.headers.update(override_headers)
|
|
311
422
|
logger.debug(f"Request Data: {request_data}")
|
|
312
423
|
try:
|
|
313
424
|
assert hasattr(session, "post"), "session must have post method."
|
|
@@ -365,7 +476,7 @@ class GenerateImageInfer(BaseModel):
|
|
|
365
476
|
),
|
|
366
477
|
files=unzip_content
|
|
367
478
|
)
|
|
368
|
-
except RequestsError as exc:
|
|
479
|
+
except curl_cffi.requests.errors.RequestsError as exc:
|
|
369
480
|
logger.exception(exc)
|
|
370
481
|
raise RuntimeError(f"An AsyncSession error occurred: {exc}")
|
|
371
482
|
except httpx.HTTPError as exc:
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/2/10 上午11:15
|
|
3
|
+
# @Author : sudoskys
|
|
4
|
+
# @File : schema.py
|
|
5
|
+
# @Software: PyCharm
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import Optional, Union
|
|
8
|
+
|
|
9
|
+
from curl_cffi.requests import AsyncSession
|
|
10
|
+
from pydantic import BaseModel, PrivateAttr
|
|
11
|
+
|
|
12
|
+
from ..credential import CredentialBase
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ApiBaseModel(BaseModel, ABC):
|
|
16
|
+
_endpoint: Optional[str] = PrivateAttr()
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def endpoint(self):
|
|
20
|
+
return self._endpoint
|
|
21
|
+
|
|
22
|
+
@endpoint.setter
|
|
23
|
+
def endpoint(self, value):
|
|
24
|
+
self._endpoint = value
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
async def necessary_headers(self, request_data) -> dict:
|
|
28
|
+
raise NotImplementedError()
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
async def request(self,
|
|
32
|
+
session: Union[AsyncSession, CredentialBase],
|
|
33
|
+
*,
|
|
34
|
+
override_headers: Optional[dict] = None
|
|
35
|
+
):
|
|
36
|
+
raise NotImplementedError()
|
|
@@ -5,18 +5,20 @@
|
|
|
5
5
|
# @Software: PyCharm
|
|
6
6
|
from typing import Optional, Union
|
|
7
7
|
|
|
8
|
+
import curl_cffi
|
|
8
9
|
import httpx
|
|
9
|
-
from curl_cffi.requests import AsyncSession
|
|
10
|
+
from curl_cffi.requests import AsyncSession
|
|
10
11
|
from loguru import logger
|
|
11
|
-
from pydantic import
|
|
12
|
+
from pydantic import PrivateAttr
|
|
12
13
|
|
|
14
|
+
from ..schema import ApiBaseModel
|
|
13
15
|
from ..._exceptions import APIError, AuthError
|
|
14
16
|
from ..._response.user.information import InformationResp
|
|
15
17
|
from ...credential import CredentialBase
|
|
16
18
|
from ...utils import try_jsonfy
|
|
17
19
|
|
|
18
20
|
|
|
19
|
-
class Information(
|
|
21
|
+
class Information(ApiBaseModel):
|
|
20
22
|
_endpoint: Optional[str] = PrivateAttr("https://api.novelai.net")
|
|
21
23
|
|
|
22
24
|
@property
|
|
@@ -31,22 +33,43 @@ class Information(BaseModel):
|
|
|
31
33
|
def endpoint(self, value):
|
|
32
34
|
self._endpoint = value
|
|
33
35
|
|
|
36
|
+
async def necessary_headers(self, request_data) -> dict:
|
|
37
|
+
return {
|
|
38
|
+
"Host": "api.novelai.net",
|
|
39
|
+
"Accept": "*/*",
|
|
40
|
+
"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",
|
|
41
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
42
|
+
"Referer": "https://novelai.net/",
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
"Origin": "https://novelai.net"
|
|
45
|
+
}
|
|
46
|
+
|
|
34
47
|
async def request(self,
|
|
35
48
|
session: Union[AsyncSession, CredentialBase],
|
|
49
|
+
*,
|
|
50
|
+
override_headers: Optional[dict] = None
|
|
36
51
|
) -> InformationResp:
|
|
37
52
|
"""
|
|
38
53
|
Request to get user subscription information
|
|
39
|
-
:param
|
|
54
|
+
:param override_headers:
|
|
55
|
+
:param session:
|
|
40
56
|
:return:
|
|
41
57
|
"""
|
|
42
|
-
if isinstance(session, CredentialBase):
|
|
43
|
-
session = await session.get_session()
|
|
44
58
|
request_data = {}
|
|
59
|
+
# Data Build
|
|
60
|
+
if isinstance(session, AsyncSession):
|
|
61
|
+
session.headers.update(await self.necessary_headers(request_data))
|
|
62
|
+
elif isinstance(session, CredentialBase):
|
|
63
|
+
session = await session.get_session(update_headers=await self.necessary_headers(request_data))
|
|
64
|
+
# Header
|
|
65
|
+
if override_headers:
|
|
66
|
+
session.headers.clear()
|
|
67
|
+
session.headers.update(override_headers)
|
|
45
68
|
logger.debug("Information")
|
|
46
69
|
try:
|
|
47
70
|
assert hasattr(session, "get"), "session must have get method."
|
|
48
71
|
response = await session.get(
|
|
49
|
-
self.base_url
|
|
72
|
+
self.base_url
|
|
50
73
|
)
|
|
51
74
|
if "application/json" not in response.headers.get('Content-Type') or response.status_code != 200:
|
|
52
75
|
logger.error(
|
|
@@ -74,7 +97,7 @@ class Information(BaseModel):
|
|
|
74
97
|
raise APIError(message, request=request_data, status_code=status_code, response=_msg)
|
|
75
98
|
raise APIError(message, request=request_data, status_code=status_code, response=_msg)
|
|
76
99
|
return InformationResp.model_validate(response.json())
|
|
77
|
-
except RequestsError as exc:
|
|
100
|
+
except curl_cffi.requests.errors.RequestsError as exc:
|
|
78
101
|
logger.exception(exc)
|
|
79
102
|
raise RuntimeError(f"An AsyncSession error occurred: {exc}")
|
|
80
103
|
except httpx.HTTPError as exc:
|
|
@@ -4,19 +4,22 @@
|
|
|
4
4
|
# @File : login.py
|
|
5
5
|
# @Software: PyCharm
|
|
6
6
|
import json
|
|
7
|
-
from typing import Optional
|
|
7
|
+
from typing import Optional, Union
|
|
8
8
|
|
|
9
|
+
import curl_cffi
|
|
9
10
|
import httpx
|
|
10
|
-
from curl_cffi.requests import AsyncSession
|
|
11
|
+
from curl_cffi.requests import AsyncSession
|
|
11
12
|
from loguru import logger
|
|
12
|
-
from pydantic import
|
|
13
|
+
from pydantic import PrivateAttr, Field
|
|
13
14
|
|
|
15
|
+
from ..schema import ApiBaseModel
|
|
14
16
|
from ..._exceptions import APIError
|
|
15
17
|
from ..._response.user.login import LoginResp
|
|
18
|
+
from ...credential import CredentialBase
|
|
16
19
|
from ...utils import try_jsonfy, encode_access_key
|
|
17
20
|
|
|
18
21
|
|
|
19
|
-
class Login(
|
|
22
|
+
class Login(ApiBaseModel):
|
|
20
23
|
_endpoint: Optional[str] = PrivateAttr("https://api.novelai.net")
|
|
21
24
|
_session: Optional[AsyncSession] = PrivateAttr(None)
|
|
22
25
|
key: str = Field(..., description="User's key")
|
|
@@ -59,13 +62,28 @@ class Login(BaseModel):
|
|
|
59
62
|
"""
|
|
60
63
|
return cls(key=encode_access_key(user_name, password))
|
|
61
64
|
|
|
65
|
+
async def necessary_headers(self, request_data) -> dict:
|
|
66
|
+
return {}
|
|
67
|
+
|
|
62
68
|
async def request(self,
|
|
69
|
+
session: Union[AsyncSession, CredentialBase] = None,
|
|
70
|
+
*,
|
|
71
|
+
override_headers: Optional[dict] = None,
|
|
63
72
|
) -> LoginResp:
|
|
64
73
|
"""
|
|
65
74
|
Request to get user access token
|
|
66
75
|
:return:
|
|
67
76
|
"""
|
|
77
|
+
# Data Build
|
|
68
78
|
request_data = self.model_dump(mode="json", exclude_none=True)
|
|
79
|
+
if isinstance(session, AsyncSession):
|
|
80
|
+
session.headers.update(self.necessary_headers(request_data))
|
|
81
|
+
elif isinstance(session, CredentialBase):
|
|
82
|
+
session = await session.get_session(update_headers=await self.necessary_headers(request_data))
|
|
83
|
+
# Header
|
|
84
|
+
if override_headers:
|
|
85
|
+
session.headers.clear()
|
|
86
|
+
session.headers.update(override_headers)
|
|
69
87
|
logger.debug("Login")
|
|
70
88
|
try:
|
|
71
89
|
assert hasattr(self.session, "post"), "session must have get method."
|
|
@@ -97,7 +115,7 @@ class Login(BaseModel):
|
|
|
97
115
|
raise APIError(message, request=request_data, status_code=status_code, response=_msg)
|
|
98
116
|
raise APIError(message, request=request_data, status_code=status_code, response=_msg)
|
|
99
117
|
return LoginResp.model_validate(response.json())
|
|
100
|
-
except RequestsError as exc:
|
|
118
|
+
except curl_cffi.requests.errors.RequestsError as exc:
|
|
101
119
|
logger.exception(exc)
|
|
102
120
|
raise RuntimeError(f"An AsyncSession error occurred: {exc}")
|
|
103
121
|
except httpx.HTTPError as exc:
|
|
@@ -5,18 +5,20 @@
|
|
|
5
5
|
# @Software: PyCharm
|
|
6
6
|
from typing import Optional, Union
|
|
7
7
|
|
|
8
|
+
import curl_cffi
|
|
8
9
|
import httpx
|
|
9
|
-
from curl_cffi.requests import AsyncSession
|
|
10
|
+
from curl_cffi.requests import AsyncSession
|
|
10
11
|
from loguru import logger
|
|
11
|
-
from pydantic import
|
|
12
|
+
from pydantic import PrivateAttr
|
|
12
13
|
|
|
14
|
+
from ..schema import ApiBaseModel
|
|
13
15
|
from ..._exceptions import APIError, AuthError
|
|
14
16
|
from ..._response.user.subscription import SubscriptionResp
|
|
15
17
|
from ...credential import CredentialBase
|
|
16
18
|
from ...utils import try_jsonfy
|
|
17
19
|
|
|
18
20
|
|
|
19
|
-
class Subscription(
|
|
21
|
+
class Subscription(ApiBaseModel):
|
|
20
22
|
_endpoint: Optional[str] = PrivateAttr("https://api.novelai.net")
|
|
21
23
|
|
|
22
24
|
@property
|
|
@@ -31,22 +33,50 @@ class Subscription(BaseModel):
|
|
|
31
33
|
def endpoint(self, value):
|
|
32
34
|
self._endpoint = value
|
|
33
35
|
|
|
36
|
+
async def necessary_headers(self, request_data) -> dict:
|
|
37
|
+
return {
|
|
38
|
+
"Host": "api.novelai.net",
|
|
39
|
+
"Accept": "*/*",
|
|
40
|
+
"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",
|
|
41
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
42
|
+
"Referer": "https://novelai.net/",
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
"Origin": "https://novelai.net",
|
|
45
|
+
"Connection": "keep-alive",
|
|
46
|
+
"Sec-Fetch-Dest": "empty",
|
|
47
|
+
"Sec-Fetch-Mode": "cors",
|
|
48
|
+
"Sec-Fetch-Site": "same-site",
|
|
49
|
+
"Pragma": "no-cache",
|
|
50
|
+
"Cache-Control": "no-cache",
|
|
51
|
+
"TE": "trailers",
|
|
52
|
+
}
|
|
53
|
+
|
|
34
54
|
async def request(self,
|
|
35
55
|
session: Union[AsyncSession, CredentialBase],
|
|
56
|
+
*,
|
|
57
|
+
override_headers: Optional[dict] = None
|
|
36
58
|
) -> SubscriptionResp:
|
|
37
59
|
"""
|
|
38
60
|
Request to get user subscription information
|
|
39
|
-
:param
|
|
61
|
+
:param override_headers:
|
|
62
|
+
:param session:
|
|
40
63
|
:return:
|
|
41
64
|
"""
|
|
42
|
-
|
|
43
|
-
session = await session.get_session()
|
|
65
|
+
# Data Build
|
|
44
66
|
request_data = {}
|
|
67
|
+
if isinstance(session, AsyncSession):
|
|
68
|
+
session.headers.update(await self.necessary_headers(request_data))
|
|
69
|
+
elif isinstance(session, CredentialBase):
|
|
70
|
+
session = await session.get_session(update_headers=await self.necessary_headers(request_data))
|
|
71
|
+
# Header
|
|
72
|
+
if override_headers:
|
|
73
|
+
session.headers.clear()
|
|
74
|
+
session.headers.update(override_headers)
|
|
45
75
|
logger.debug("Subscription")
|
|
46
76
|
try:
|
|
47
77
|
assert hasattr(session, "get"), "session must have get method."
|
|
48
78
|
response = await session.get(
|
|
49
|
-
self.base_url,
|
|
79
|
+
url=self.base_url,
|
|
50
80
|
)
|
|
51
81
|
if "application/json" not in response.headers.get('Content-Type') or response.status_code != 200:
|
|
52
82
|
logger.error(
|
|
@@ -74,7 +104,7 @@ class Subscription(BaseModel):
|
|
|
74
104
|
raise APIError(message, request=request_data, status_code=status_code, response=_msg)
|
|
75
105
|
raise APIError(message, request=request_data, status_code=status_code, response=_msg)
|
|
76
106
|
return SubscriptionResp.model_validate(response.json())
|
|
77
|
-
except RequestsError as exc:
|
|
107
|
+
except curl_cffi.requests.errors.RequestsError as exc:
|
|
78
108
|
logger.exception(exc)
|
|
79
109
|
raise RuntimeError(f"An AsyncSession error occurred: {exc}")
|
|
80
110
|
except httpx.HTTPError as exc:
|
|
@@ -102,7 +102,7 @@ async def generate_image(
|
|
|
102
102
|
:return:
|
|
103
103
|
"""
|
|
104
104
|
try:
|
|
105
|
-
_result = await req.
|
|
105
|
+
_result = await req.request(session=get_session(current_token), remove_sign=True)
|
|
106
106
|
zip_file_bytes = io.BytesIO()
|
|
107
107
|
with zipfile.ZipFile(zip_file_bytes, mode="w", compression=zipfile.ZIP_DEFLATED) as zip_file:
|
|
108
108
|
for file in _result.files:
|
|
@@ -28,7 +28,7 @@ def test_subscription_without_api_key(mock_subscription):
|
|
|
28
28
|
|
|
29
29
|
@patch('src.novelai_python.server.GenerateImageInfer')
|
|
30
30
|
def test_generate_image_without_api_key(mock_generate_image):
|
|
31
|
-
mock_generate_image.return_value.
|
|
31
|
+
mock_generate_image.return_value.request.return_value = {"status": "image generated"}
|
|
32
32
|
response = client.post("/ai/generate_image")
|
|
33
33
|
assert response.status_code == 403
|
|
34
34
|
|
|
@@ -34,6 +34,7 @@ async def test_request_method_successful():
|
|
|
34
34
|
}
|
|
35
35
|
session = mock.MagicMock(spec=AsyncSession)
|
|
36
36
|
session.get = mock.AsyncMock(return_value=successful_response)
|
|
37
|
+
session.headers = {}
|
|
37
38
|
|
|
38
39
|
info = Information()
|
|
39
40
|
resp = await info.request(session)
|
|
@@ -48,6 +49,7 @@ async def test_request_method_unauthorized_error():
|
|
|
48
49
|
auth_response.json.return_value = {"statusCode": 401, "message": "Access Token is incorrect."}
|
|
49
50
|
|
|
50
51
|
session = mock.MagicMock(spec=AsyncSession)
|
|
52
|
+
session.headers = {}
|
|
51
53
|
session.get = mock.AsyncMock(return_value=auth_response)
|
|
52
54
|
|
|
53
55
|
info = Information()
|
|
@@ -66,6 +68,7 @@ async def test_request_method_unknown_error():
|
|
|
66
68
|
unknown_error_response.json.return_value = {"statusCode": 500, "message": "An unknown error occurred."}
|
|
67
69
|
|
|
68
70
|
session = mock.MagicMock(spec=AsyncSession)
|
|
71
|
+
session.headers = {}
|
|
69
72
|
session.get = mock.AsyncMock(return_value=unknown_error_response)
|
|
70
73
|
|
|
71
74
|
info = Information()
|
|
@@ -21,6 +21,7 @@ async def test_successful_user_login():
|
|
|
21
21
|
}
|
|
22
22
|
session = mock.MagicMock(spec=AsyncSession)
|
|
23
23
|
session.post = mock.AsyncMock(return_value=successful_login_response)
|
|
24
|
+
session.headers = {}
|
|
24
25
|
|
|
25
26
|
login = Login(key="encoded_key")
|
|
26
27
|
login.session = session
|
|
@@ -40,6 +41,7 @@ async def test_validation_error_during_login():
|
|
|
40
41
|
}
|
|
41
42
|
session = mock.MagicMock(spec=AsyncSession)
|
|
42
43
|
session.post = mock.AsyncMock(return_value=validation_error_response)
|
|
44
|
+
session.headers = {}
|
|
43
45
|
|
|
44
46
|
login = Login(key="encoded_key")
|
|
45
47
|
login.session = session
|
|
@@ -61,6 +63,7 @@ async def test_incorrect_access_key_during_login():
|
|
|
61
63
|
}
|
|
62
64
|
session = mock.MagicMock(spec=AsyncSession)
|
|
63
65
|
session.post = mock.AsyncMock(return_value=incorrect_key_response)
|
|
66
|
+
session.headers = {}
|
|
64
67
|
|
|
65
68
|
login = Login(key="encoded_key")
|
|
66
69
|
login.session = session
|
|
@@ -82,6 +85,8 @@ async def test_unknown_error_during_login():
|
|
|
82
85
|
}
|
|
83
86
|
session = mock.MagicMock(spec=AsyncSession)
|
|
84
87
|
session.post = mock.AsyncMock(return_value=unknown_error_response)
|
|
88
|
+
session.headers = {}
|
|
89
|
+
|
|
85
90
|
login = Login(key="encoded_key")
|
|
86
91
|
login.session = session
|
|
87
92
|
with pytest.raises(APIError) as e:
|
|
@@ -49,6 +49,8 @@ async def test_successful_subscription_request():
|
|
|
49
49
|
}
|
|
50
50
|
session = mock.MagicMock(spec=AsyncSession)
|
|
51
51
|
session.get = mock.AsyncMock(return_value=successful_response)
|
|
52
|
+
session.headers = {}
|
|
53
|
+
|
|
52
54
|
subscription = Subscription()
|
|
53
55
|
resp = await subscription.request(session)
|
|
54
56
|
assert isinstance(resp, SubscriptionResp)
|
|
@@ -65,6 +67,7 @@ async def test_incorrect_access_token_subscription_request():
|
|
|
65
67
|
}
|
|
66
68
|
session = mock.MagicMock(spec=AsyncSession)
|
|
67
69
|
session.get = mock.AsyncMock(return_value=incorrect_token_response)
|
|
70
|
+
session.headers = {}
|
|
68
71
|
|
|
69
72
|
subscription = Subscription()
|
|
70
73
|
with pytest.raises(AuthError) as e:
|
|
@@ -85,6 +88,7 @@ async def test_unknown_error_subscription_request():
|
|
|
85
88
|
}
|
|
86
89
|
session = mock.MagicMock(spec=AsyncSession)
|
|
87
90
|
session.get = mock.AsyncMock(return_value=unknown_error_response)
|
|
91
|
+
session.headers = {}
|
|
88
92
|
|
|
89
93
|
subscription = Subscription()
|
|
90
94
|
with pytest.raises(APIError) as e:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/_response/ai/generate_image.py
RENAMED
|
File without changes
|
|
File without changes
|
{novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/_response/user/information.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
|
|
File without changes
|
{novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/utils/random_prompt/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/utils/random_prompt/tag_artist.py
RENAMED
|
File without changes
|
|
File without changes
|
{novelai_python-0.2.3 → novelai_python-0.2.5}/src/novelai_python/utils/random_prompt/tag_nsfw.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|