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.
Files changed (49) hide show
  1. {novelai_python-0.2.8 → novelai_python-0.2.9}/PKG-INFO +7 -3
  2. {novelai_python-0.2.8 → novelai_python-0.2.9}/README.md +6 -2
  3. {novelai_python-0.2.8 → novelai_python-0.2.9}/pyproject.toml +1 -1
  4. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/__init__.py +4 -0
  5. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/_response/ai/generate_image.py +3 -1
  6. novelai_python-0.2.9/src/novelai_python/_response/ai/upscale.py +19 -0
  7. novelai_python-0.2.9/src/novelai_python/_response/schema.py +11 -0
  8. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/_response/user/information.py +4 -2
  9. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/_response/user/login.py +3 -2
  10. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/_response/user/subscription.py +3 -1
  11. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/credential/JwtToken.py +1 -0
  12. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/credential/UserAuth.py +1 -0
  13. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/sdk/__init__.py +1 -0
  14. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/sdk/ai/generate_image.py +19 -4
  15. novelai_python-0.2.9/src/novelai_python/sdk/ai/upscale.py +204 -0
  16. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/sdk/user/login.py +1 -1
  17. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/utils/useful.py +3 -5
  18. novelai_python-0.2.9/tests/test_upscale.py +78 -0
  19. {novelai_python-0.2.8 → novelai_python-0.2.9}/LICENSE +0 -0
  20. {novelai_python-0.2.8 → novelai_python-0.2.9}/NOTICE.MD +0 -0
  21. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/_exceptions.py +0 -0
  22. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/_response/__init__.py +0 -0
  23. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/_response/ai/__init__.py +0 -0
  24. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/_response/user/__init__.py +0 -0
  25. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/credential/ApiToken.py +0 -0
  26. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/credential/__init__.py +0 -0
  27. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/credential/_base.py +0 -0
  28. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/sdk/ai/LICENSE +0 -0
  29. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/sdk/ai/__init__.py +0 -0
  30. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/sdk/schema.py +0 -0
  31. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/sdk/user/__init__.py +0 -0
  32. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/sdk/user/information.py +0 -0
  33. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/sdk/user/subscription.py +0 -0
  34. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/server.py +0 -0
  35. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/utils/__init__.py +0 -0
  36. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/utils/encode.py +0 -0
  37. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/utils/hash.py +0 -0
  38. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/utils/random_prompt/__init__.py +0 -0
  39. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/utils/random_prompt/tag.py +0 -0
  40. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/utils/random_prompt/tag_artist.py +0 -0
  41. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/utils/random_prompt/tag_character.py +0 -0
  42. {novelai_python-0.2.8 → novelai_python-0.2.9}/src/novelai_python/utils/random_prompt/tag_nsfw.py +0 -0
  43. {novelai_python-0.2.8 → novelai_python-0.2.9}/tests/__init__.py +0 -0
  44. {novelai_python-0.2.8 → novelai_python-0.2.9}/tests/test_random_prompt.py +0 -0
  45. {novelai_python-0.2.8 → novelai_python-0.2.9}/tests/test_server.py +0 -0
  46. {novelai_python-0.2.8 → novelai_python-0.2.9}/tests/test_server_run.py +0 -0
  47. {novelai_python-0.2.8 → novelai_python-0.2.9}/tests/test_user_information.py +0 -0
  48. {novelai_python-0.2.8 → novelai_python-0.2.9}/tests/test_user_login.py +0 -0
  49. {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.8
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
- [![PyPI version](https://badge.fury.io/py/novelai_python.svg)](https://badge.fury.io/py/novelai_python)
29
+ [![PyPI version](https://badge.fury.io/py/novelai-python.svg)](https://badge.fury.io/py/novelai-python)
30
30
  [![Downloads](https://pepy.tech/badge/novelai_python)](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
- [![PyPI version](https://badge.fury.io/py/novelai_python.svg)](https://badge.fury.io/py/novelai_python)
3
+ [![PyPI version](https://badge.fury.io/py/novelai-python.svg)](https://badge.fury.io/py/novelai-python)
4
4
  [![Downloads](https://pepy.tech/badge/novelai_python)](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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "novelai-python"
3
- version = "0.2.8"
3
+ version = "0.2.9"
4
4
  description = "Novelai Python Binding With Pydantic"
5
5
  authors = [
6
6
  { name = "sudoskys", email = "coldlando@hotmail.com" },
@@ -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
 
@@ -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
- class ImageGenerateResp(BaseModel):
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
@@ -0,0 +1,11 @@
1
+ # -*- coding: utf-8 -*-
2
+ # @Time : 2024/2/13 上午11:30
3
+ # @Author : sudoskys
4
+ # @File : schema.py
5
+ # @Software: PyCharm
6
+
7
+ from pydantic import BaseModel
8
+
9
+
10
+ class RespBase(BaseModel):
11
+ pass
@@ -4,10 +4,12 @@
4
4
  # @File : information.py
5
5
  # @Software: PyCharm
6
6
 
7
- from pydantic import BaseModel, Field
7
+ from pydantic import Field
8
8
 
9
+ from ..schema import RespBase
9
10
 
10
- class InformationResp(BaseModel):
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")
@@ -3,8 +3,9 @@
3
3
  # @Author : sudoskys
4
4
  # @File : login.py
5
5
  # @Software: PyCharm
6
- from pydantic import BaseModel
7
6
 
7
+ from ..schema import RespBase
8
8
 
9
- class LoginResp(BaseModel):
9
+
10
+ class LoginResp(RespBase):
10
11
  accessToken: str
@@ -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(BaseModel):
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
- logger.debug(f"Request Data: {request_data}")
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