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.
Files changed (29) hide show
  1. {novelai_python-0.1.7 → novelai_python-0.1.8}/PKG-INFO +2 -1
  2. {novelai_python-0.1.7 → novelai_python-0.1.8}/README.md +1 -0
  3. {novelai_python-0.1.7 → novelai_python-0.1.8}/pyproject.toml +1 -1
  4. {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/__init__.py +7 -2
  5. {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/_response/__init__.py +3 -2
  6. novelai_python-0.1.8/src/novelai_python/_response/user/__init__.py +5 -0
  7. novelai_python-0.1.8/src/novelai_python/_response/user/subscription.py +52 -0
  8. {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/sdk/__init__.py +1 -1
  9. novelai_python-0.1.8/src/novelai_python/sdk/user/__init__.py +7 -0
  10. novelai_python-0.1.8/src/novelai_python/sdk/user/subscription.py +81 -0
  11. {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/server.py +21 -0
  12. novelai_python-0.1.8/tests/test_server_run.py +46 -0
  13. novelai_python-0.1.7/tests/test_server_run.py +0 -28
  14. {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/_exceptions.py +0 -0
  15. {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/_response/ai/__init__.py +0 -0
  16. {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/_response/ai/generate_image.py +0 -0
  17. {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/credential/JwtToken.py +0 -0
  18. {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/credential/__init__.py +0 -0
  19. {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/sdk/ai/__init__.py +0 -0
  20. {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/sdk/ai/generate_image.py +0 -0
  21. {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/utils/__init__.py +0 -0
  22. {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/utils/hash.py +0 -0
  23. {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/utils/random_prompt/__init__.py +0 -0
  24. {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/utils/random_prompt/tag.py +0 -0
  25. {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/utils/random_prompt/tag_artist.py +0 -0
  26. {novelai_python-0.1.7 → novelai_python-0.1.8}/src/novelai_python/utils/random_prompt/tag_nsfw.py +0 -0
  27. {novelai_python-0.1.7 → novelai_python-0.1.8}/tests/__init__.py +0 -0
  28. {novelai_python-0.1.7 → novelai_python-0.1.8}/tests/test_random_prompt.py +0 -0
  29. {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.7
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
@@ -8,6 +8,7 @@ The goal of this repository is to use Pydantic to build legitimate requests to a
8
8
  ### Roadmap 🚧
9
9
 
10
10
  - [x] /ai/generate-image
11
+ - [x] /user/subscription
11
12
  - [ ] /ai/generate-image/suggest-tags
12
13
  - [ ] /ai/annotate-image
13
14
  - [ ] /ai/classify
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "novelai-python"
3
- version = "0.1.7"
3
+ version = "0.1.8"
4
4
  description = "Novelai Python Binding With Pydantic"
5
5
  authors = [
6
6
  { name = "sudoskys", email = "coldlando@hotmail.com" },
@@ -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",
@@ -4,7 +4,8 @@
4
4
  # @File : __init__.py.py
5
5
  # @Software: PyCharm
6
6
  from .ai.generate_image import ImageGenerateResp
7
-
7
+ from .user.subscription import SubscriptionResp
8
8
  __all__ = [
9
- "ImageGenerateResp"
9
+ "ImageGenerateResp",
10
+ "SubscriptionResp",
10
11
  ]
@@ -0,0 +1,5 @@
1
+ # -*- coding: utf-8 -*-
2
+ # @Time : 2024/2/7 上午9:57
3
+ # @Author : sudoskys
4
+ # @File : __init__.py.py
5
+ # @Software: PyCharm
@@ -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
@@ -5,4 +5,4 @@
5
5
  # @Software: PyCharm
6
6
 
7
7
  from .ai.generate_image import GenerateImageInfer, ImageGenerateResp # noqa 401
8
-
8
+ from .user.subscription import Subscription, SubscriptionResp # noqa 401
@@ -0,0 +1,7 @@
1
+ # -*- coding: utf-8 -*-
2
+ # @Time : 2024/2/7 上午10:03
3
+ # @Author : sudoskys
4
+ # @File : __init__.py.py
5
+ # @Software: PyCharm
6
+
7
+ from .subscription import * # noqa: F403
@@ -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