novelai-python 0.1.7__tar.gz → 0.1.8__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.1.7 → novelai_python-0.1.8}/PKG-INFO +2 -1
- {novelai_python-0.1.7 → novelai_python-0.1.8}/README.md +1 -0
- {novelai_python-0.1.7 → novelai_python-0.1.8}/pyproject.toml +1 -1
- {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/__init__.py +7 -2
- {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/_response/__init__.py +3 -2
- novelai_python-0.1.8/src/novelai_python/_response/user/__init__.py +5 -0
- novelai_python-0.1.8/src/novelai_python/_response/user/subscription.py +52 -0
- {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/sdk/__init__.py +1 -1
- novelai_python-0.1.8/src/novelai_python/sdk/user/__init__.py +7 -0
- novelai_python-0.1.8/src/novelai_python/sdk/user/subscription.py +81 -0
- {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/server.py +21 -0
- novelai_python-0.1.8/tests/test_server_run.py +46 -0
- novelai_python-0.1.7/tests/test_server_run.py +0 -28
- {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/_exceptions.py +0 -0
- {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/_response/ai/__init__.py +0 -0
- {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/_response/ai/generate_image.py +0 -0
- {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/credential/JwtToken.py +0 -0
- {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/credential/__init__.py +0 -0
- {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/sdk/ai/__init__.py +0 -0
- {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/sdk/ai/generate_image.py +0 -0
- {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/utils/__init__.py +0 -0
- {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/utils/hash.py +0 -0
- {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/utils/random_prompt/__init__.py +0 -0
- {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/utils/random_prompt/tag.py +0 -0
- {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/utils/random_prompt/tag_artist.py +0 -0
- {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/utils/random_prompt/tag_nsfw.py +0 -0
- {novelai_python-0.1.7 → novelai_python-0.1.8}/tests/__init__.py +0 -0
- {novelai_python-0.1.7 → novelai_python-0.1.8}/tests/test_random_prompt.py +0 -0
- {novelai_python-0.1.7 → novelai_python-0.1.8}/tests/test_server.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: novelai-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: Novelai Python Binding With Pydantic
|
|
5
5
|
Author-Email: sudoskys <coldlando@hotmail.com>
|
|
6
6
|
License: Apache 2.0
|
|
@@ -32,6 +32,7 @@ The goal of this repository is to use Pydantic to build legitimate requests to a
|
|
|
32
32
|
### Roadmap 🚧
|
|
33
33
|
|
|
34
34
|
- [x] /ai/generate-image
|
|
35
|
+
- [x] /user/subscription
|
|
35
36
|
- [ ] /ai/generate-image/suggest-tags
|
|
36
37
|
- [ ] /ai/annotate-image
|
|
37
38
|
- [ ] /ai/classify
|
|
@@ -9,14 +9,19 @@ from ._exceptions import (
|
|
|
9
9
|
AuthError,
|
|
10
10
|
NovelAiError,
|
|
11
11
|
) # noqa: F401, F403
|
|
12
|
-
from ._response import ImageGenerateResp # noqa: F401, F403
|
|
13
12
|
from .credential import JwtCredential # noqa: F401, F403
|
|
14
|
-
from .sdk.ai import GenerateImageInfer # noqa: F401, F403
|
|
13
|
+
from .sdk.ai import GenerateImageInfer, ImageGenerateResp # noqa: F401, F403
|
|
14
|
+
from .sdk.user import Subscription, SubscriptionResp # noqa: F401, F403
|
|
15
15
|
|
|
16
16
|
__all__ = [
|
|
17
17
|
"GenerateImageInfer",
|
|
18
18
|
"ImageGenerateResp",
|
|
19
|
+
|
|
20
|
+
"Subscription",
|
|
21
|
+
"SubscriptionResp",
|
|
22
|
+
|
|
19
23
|
"JwtCredential",
|
|
24
|
+
|
|
20
25
|
"APIError",
|
|
21
26
|
"AuthError",
|
|
22
27
|
"NovelAiError",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/2/7 上午9:57
|
|
3
|
+
# @Author : sudoskys
|
|
4
|
+
# @File : subscription.py
|
|
5
|
+
# @Software: PyCharm
|
|
6
|
+
from typing import Optional, Dict, Any, List
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TrainingSteps(BaseModel):
|
|
12
|
+
fixedTrainingStepsLeft: int
|
|
13
|
+
purchasedTrainingSteps: int
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ImageGenerationLimit(BaseModel):
|
|
17
|
+
resolution: int
|
|
18
|
+
maxPrompts: int
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Perks(BaseModel):
|
|
22
|
+
maxPriorityActions: int
|
|
23
|
+
startPriority: int
|
|
24
|
+
moduleTrainingSteps: int
|
|
25
|
+
unlimitedMaxPriority: bool
|
|
26
|
+
voiceGeneration: bool
|
|
27
|
+
imageGeneration: bool
|
|
28
|
+
unlimitedImageGeneration: bool
|
|
29
|
+
unlimitedImageGenerationLimits: List[ImageGenerationLimit]
|
|
30
|
+
contextTokens: int
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SubscriptionResp(BaseModel):
|
|
34
|
+
tier: int = Field(..., description="Subscription tier")
|
|
35
|
+
active: bool = Field(..., description="Subscription status")
|
|
36
|
+
expiresAt: int = Field(..., description="Subscription expiration time")
|
|
37
|
+
perks: Perks = Field(..., description="Subscription perks")
|
|
38
|
+
paymentProcessorData: Optional[Dict[Any, Any]]
|
|
39
|
+
trainingStepsLeft: TrainingSteps = Field(..., description="Training steps left")
|
|
40
|
+
accountType: int = Field(..., description="Account type")
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def is_active(self):
|
|
44
|
+
return self.active
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def anlas_left(self):
|
|
48
|
+
return self.trainingStepsLeft.fixedTrainingStepsLeft
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def is_unlimited_image_generation(self):
|
|
52
|
+
return self.perks.unlimitedImageGeneration
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/2/7 上午10:04
|
|
3
|
+
# @Author : sudoskys
|
|
4
|
+
# @File : subscription.py.py
|
|
5
|
+
# @Software: PyCharm
|
|
6
|
+
from typing import Optional, Union
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from curl_cffi.requests import AsyncSession
|
|
10
|
+
from loguru import logger
|
|
11
|
+
from pydantic import BaseModel, PrivateAttr
|
|
12
|
+
|
|
13
|
+
from ... import APIError, AuthError
|
|
14
|
+
from ..._response.user.subscription import SubscriptionResp
|
|
15
|
+
from ...credential import JwtCredential
|
|
16
|
+
from ...utils import try_jsonfy
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Subscription(BaseModel):
|
|
20
|
+
_endpoint: Optional[str] = PrivateAttr("https://api.novelai.net")
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def base_url(self):
|
|
24
|
+
return f"{self.endpoint.strip('/')}/user/subscription"
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def endpoint(self):
|
|
28
|
+
return self._endpoint
|
|
29
|
+
|
|
30
|
+
@endpoint.setter
|
|
31
|
+
def endpoint(self, value):
|
|
32
|
+
self._endpoint = value
|
|
33
|
+
|
|
34
|
+
async def request(self,
|
|
35
|
+
session: Union[AsyncSession, JwtCredential],
|
|
36
|
+
) -> SubscriptionResp:
|
|
37
|
+
"""
|
|
38
|
+
Request to get user subscription information
|
|
39
|
+
:param session:
|
|
40
|
+
:return:
|
|
41
|
+
"""
|
|
42
|
+
if isinstance(session, JwtCredential):
|
|
43
|
+
session = session.session
|
|
44
|
+
request_data = {}
|
|
45
|
+
logger.debug("Subscription")
|
|
46
|
+
try:
|
|
47
|
+
assert hasattr(session, "get"), "session must have get method."
|
|
48
|
+
response = await session.get(
|
|
49
|
+
self.base_url,
|
|
50
|
+
)
|
|
51
|
+
if "application/json" not in response.headers.get('Content-Type'):
|
|
52
|
+
logger.error(f"Unexpected content type: {response.headers.get('Content-Type')}")
|
|
53
|
+
try:
|
|
54
|
+
_msg = response.json()
|
|
55
|
+
except Exception:
|
|
56
|
+
raise APIError(
|
|
57
|
+
message=f"Unexpected content type: {response.headers.get('Content-Type')}",
|
|
58
|
+
request=request_data,
|
|
59
|
+
status_code=response.status_code,
|
|
60
|
+
response=try_jsonfy(response.content)
|
|
61
|
+
)
|
|
62
|
+
status_code = _msg.get("statusCode", response.status_code)
|
|
63
|
+
message = _msg.get("message", "Unknown error")
|
|
64
|
+
if status_code in [400, 401, 402]:
|
|
65
|
+
# 400 : validation error
|
|
66
|
+
# 401 : unauthorized
|
|
67
|
+
# 402 : payment required
|
|
68
|
+
# 409 : conflict
|
|
69
|
+
raise AuthError(message, request=request_data, status_code=status_code, response=_msg)
|
|
70
|
+
if status_code in [500]:
|
|
71
|
+
# An unknown error occured.
|
|
72
|
+
raise APIError(message, request=request_data, status_code=status_code, response=_msg)
|
|
73
|
+
raise APIError(message, request=request_data, status_code=status_code, response=_msg)
|
|
74
|
+
return SubscriptionResp.model_validate(response.json())
|
|
75
|
+
except httpx.HTTPError as exc:
|
|
76
|
+
raise RuntimeError(f"An HTTP error occurred: {exc}")
|
|
77
|
+
except APIError as e:
|
|
78
|
+
raise e
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.opt(exception=e).exception("An Unexpected error occurred")
|
|
81
|
+
raise e
|
|
@@ -10,10 +10,12 @@ import zipfile
|
|
|
10
10
|
import uvicorn
|
|
11
11
|
from fastapi import FastAPI, Depends, Security
|
|
12
12
|
from fastapi.security import APIKeyHeader
|
|
13
|
+
from loguru import logger
|
|
13
14
|
from starlette.responses import JSONResponse, StreamingResponse
|
|
14
15
|
|
|
15
16
|
from .credential import JwtCredential, SecretStr
|
|
16
17
|
from .sdk.ai.generate_image import GenerateImageInfer
|
|
18
|
+
from .sdk.user.subscription import Subscription
|
|
17
19
|
|
|
18
20
|
app = FastAPI()
|
|
19
21
|
token_key = APIKeyHeader(name="Authorization")
|
|
@@ -35,6 +37,24 @@ async def health():
|
|
|
35
37
|
return {"status": "ok"}
|
|
36
38
|
|
|
37
39
|
|
|
40
|
+
@app.get("/user/subscription")
|
|
41
|
+
async def subscription(
|
|
42
|
+
current_token: str = Depends(get_current_token)
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
订阅
|
|
46
|
+
:param current_token: Authorization
|
|
47
|
+
:param req: Subscription
|
|
48
|
+
:return:
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
_result = await Subscription().request(session=get_session(current_token))
|
|
52
|
+
return _result.model_dump()
|
|
53
|
+
except Exception as e:
|
|
54
|
+
logger.exception(e)
|
|
55
|
+
return JSONResponse(status_code=500, content=e.__dict__)
|
|
56
|
+
|
|
57
|
+
|
|
38
58
|
@app.post("/ai/generate_image")
|
|
39
59
|
async def generate_image(
|
|
40
60
|
req: GenerateImageInfer,
|
|
@@ -58,6 +78,7 @@ async def generate_image(
|
|
|
58
78
|
'Content-Disposition': 'attachment;filename=multiple_files.zip'
|
|
59
79
|
})
|
|
60
80
|
except Exception as e:
|
|
81
|
+
logger.exception(e)
|
|
61
82
|
return JSONResponse(status_code=500, content=e.__dict__)
|
|
62
83
|
|
|
63
84
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/1/30 下午11:52
|
|
3
|
+
# @Author : sudoskys
|
|
4
|
+
# @File : test_server_run.py
|
|
5
|
+
# @Software: PyCharm
|
|
6
|
+
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
from fastapi.testclient import TestClient
|
|
10
|
+
|
|
11
|
+
from src.novelai_python.server import app, get_session
|
|
12
|
+
|
|
13
|
+
client = TestClient(app)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_health_check():
|
|
17
|
+
response = client.get("/health")
|
|
18
|
+
assert response.status_code == 200
|
|
19
|
+
assert response.json() == {"status": "ok"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@patch('src.novelai_python.server.Subscription')
|
|
23
|
+
def test_subscription_without_api_key(mock_subscription):
|
|
24
|
+
mock_subscription.return_value.request.return_value.model_dump.return_value = {"status": "subscribed"}
|
|
25
|
+
response = client.get("/user/subscription")
|
|
26
|
+
assert response.status_code == 403
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@patch('src.novelai_python.server.GenerateImageInfer')
|
|
30
|
+
def test_generate_image_without_api_key(mock_generate_image):
|
|
31
|
+
mock_generate_image.return_value.generate.return_value = {"status": "image generated"}
|
|
32
|
+
response = client.post("/ai/generate_image")
|
|
33
|
+
assert response.status_code == 403
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_get_session_new_token():
|
|
37
|
+
token = "new_token"
|
|
38
|
+
session = get_session(token)
|
|
39
|
+
assert session.jwt_token.get_secret_value() == token
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_get_session_existing_token():
|
|
43
|
+
token = "existing_token"
|
|
44
|
+
get_session(token)
|
|
45
|
+
session = get_session(token)
|
|
46
|
+
assert session.jwt_token.get_secret_value() == token
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
# @Time : 2024/1/30 下午11:52
|
|
3
|
-
# @Author : sudoskys
|
|
4
|
-
# @File : test_server_run.py
|
|
5
|
-
# @Software: PyCharm
|
|
6
|
-
from fastapi.testclient import TestClient
|
|
7
|
-
|
|
8
|
-
from src.novelai_python.sdk.ai.generate_image import GenerateImageInfer
|
|
9
|
-
from src.novelai_python.server import app, get_session
|
|
10
|
-
|
|
11
|
-
client = TestClient(app)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def test_health_check():
|
|
15
|
-
response = client.get("/health")
|
|
16
|
-
assert response.status_code == 200
|
|
17
|
-
assert response.json() == {"status": "ok"}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def test_generate_image_with_valid_token():
|
|
21
|
-
valid_token = "valid_token"
|
|
22
|
-
get_session(valid_token) # to simulate a valid session
|
|
23
|
-
response = client.post(
|
|
24
|
-
"/ai/generate_image",
|
|
25
|
-
headers={"Authorization": valid_token},
|
|
26
|
-
json=GenerateImageInfer(input="1girl").model_dump()
|
|
27
|
-
)
|
|
28
|
-
assert response.status_code == 500
|
|
File without changes
|
|
File without changes
|
{novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/_response/ai/generate_image.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/utils/random_prompt/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/utils/random_prompt/tag_artist.py
RENAMED
|
File without changes
|
{novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/utils/random_prompt/tag_nsfw.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|