novelai-python 0.8.0__tar.gz → 0.8.1__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 (125) hide show
  1. {novelai_python-0.8.0 → novelai_python-0.8.1}/PKG-INFO +2 -2
  2. {novelai_python-0.8.0 → novelai_python-0.8.1}/README.md +1 -1
  3. {novelai_python-0.8.0 → novelai_python-0.8.1}/pyproject.toml +1 -1
  4. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/_response/user/subscription.py +1 -1
  5. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/credential/UserAuth.py +22 -1
  6. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/ai/_enum.py +8 -3
  7. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/ai/generate_voice/__init__.py +3 -2
  8. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/server.py +4 -2
  9. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/test_from_metadata.py +15 -0
  10. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/test_login_proxy.py +30 -0
  11. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/test_presets_and_usage.py +1 -2
  12. {novelai_python-0.8.0 → novelai_python-0.8.1}/LICENSE +0 -0
  13. {novelai_python-0.8.0 → novelai_python-0.8.1}/NOTICE.MD +0 -0
  14. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/__init__.py +0 -0
  15. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/_enum.py +0 -0
  16. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/_exceptions.py +0 -0
  17. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/_response/__init__.py +0 -0
  18. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/_response/ai/__init__.py +0 -0
  19. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/_response/ai/generate.py +0 -0
  20. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/_response/ai/generate_image.py +0 -0
  21. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/_response/ai/generate_stream.py +0 -0
  22. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/_response/ai/generate_voice.py +0 -0
  23. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/_response/ai/upscale.py +0 -0
  24. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/_response/schema.py +0 -0
  25. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/_response/user/__init__.py +0 -0
  26. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/_response/user/data.py +0 -0
  27. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/_response/user/information.py +0 -0
  28. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/_response/user/login.py +0 -0
  29. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/_response/user/priority.py +0 -0
  30. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/_response/user/trial_status.py +0 -0
  31. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/_retry.py +0 -0
  32. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/credential/ApiToken.py +0 -0
  33. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/credential/JwtToken.py +0 -0
  34. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/credential/__init__.py +0 -0
  35. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/credential/_base.py +0 -0
  36. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/__init__.py +0 -0
  37. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/ai/LICENSE +0 -0
  38. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/ai/__init__.py +0 -0
  39. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/ai/_const.py +0 -0
  40. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/ai/_cost.py +0 -0
  41. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/ai/augment_image/__init__.py +0 -0
  42. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/ai/augment_image/_enum.py +0 -0
  43. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/ai/encode_vibe.py +0 -0
  44. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/ai/generate/__init__.py +0 -0
  45. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/ai/generate/_const.py +0 -0
  46. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/ai/generate/_enum.py +0 -0
  47. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/ai/generate/_schema.py +0 -0
  48. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/ai/generate/themes.json +0 -0
  49. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/ai/generate_image/__init__.py +0 -0
  50. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/ai/generate_image/auto_text.py +0 -0
  51. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/ai/generate_image/params.py +0 -0
  52. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/ai/generate_image/schema.py +0 -0
  53. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/ai/generate_image/suggest_tags.py +0 -0
  54. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/ai/generate_image/tokenizer.py +0 -0
  55. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/ai/generate_stream.py +0 -0
  56. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/ai/generate_voice/_enum.py +0 -0
  57. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/ai/openai_compat.py +0 -0
  58. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/ai/upscale.py +0 -0
  59. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/schema.py +0 -0
  60. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/user/__init__.py +0 -0
  61. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/user/data.py +0 -0
  62. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/user/information.py +0 -0
  63. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/user/login.py +0 -0
  64. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/user/persistent_token.py +0 -0
  65. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/user/priority.py +0 -0
  66. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/user/subscription.py +0 -0
  67. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/sdk/user/trial_status.py +0 -0
  68. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/tokenizer/__init__.py +0 -0
  69. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/tokenizer/clip_tokenizer.py +0 -0
  70. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/tool/__init__.py +0 -0
  71. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/tool/image_metadata/__init__.py +0 -0
  72. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/tool/image_metadata/bch.py +0 -0
  73. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/tool/image_metadata/bch_utils.py +0 -0
  74. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/tool/image_metadata/lsb_extractor.py +0 -0
  75. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/tool/image_metadata/lsb_injector.py +0 -0
  76. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/tool/paint_mask/__init__.py +0 -0
  77. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/tool/random_prompt/__init__.py +0 -0
  78. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/tool/random_prompt/data/__init__.py +0 -0
  79. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/tool/random_prompt/data/anime_v3_data.py +0 -0
  80. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/tool/random_prompt/data/anime_v4_data.py +0 -0
  81. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/tool/random_prompt/data/furry_data.py +0 -0
  82. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/tool/random_prompt/data/holiday.py +0 -0
  83. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/tool/random_prompt/data/nsfw_data.py +0 -0
  84. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/tool/random_prompt/generate_scene_composition.py +0 -0
  85. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/tool/random_prompt/generate_scene_tags.py +0 -0
  86. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/tool/random_prompt/generate_tags.py +0 -0
  87. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/tool/random_prompt/generators/__init__.py +0 -0
  88. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/tool/random_prompt/generators/anime_v3.py +0 -0
  89. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/tool/random_prompt/generators/anime_v4.py +0 -0
  90. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/tool/random_prompt/generators/furry.py +0 -0
  91. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/tool/random_prompt/generators/holiday.py +0 -0
  92. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/tool/random_prompt/logic.py +0 -0
  93. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/utils/__init__.py +0 -0
  94. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/utils/daily_limit.py +0 -0
  95. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/utils/encode.py +0 -0
  96. {novelai_python-0.8.0 → novelai_python-0.8.1}/src/novelai_python/utils/useful.py +0 -0
  97. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/__init__.py +0 -0
  98. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/conftest.py +0 -0
  99. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/fixtures/bch_vectors.json +0 -0
  100. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/fixtures/dependency_golden.json +0 -0
  101. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/fixtures/mask_ori.png +0 -0
  102. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/fixtures/mask_sketch.png +0 -0
  103. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/fixtures/v5_signed.webp +0 -0
  104. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/test_auto_text.py +0 -0
  105. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/test_bch.py +0 -0
  106. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/test_browser_identity.py +0 -0
  107. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/test_cost.py +0 -0
  108. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/test_daily_limit.py +0 -0
  109. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/test_dependency_golden.py +0 -0
  110. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/test_generate_image_v5.py +0 -0
  111. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/test_generate_text.py +0 -0
  112. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/test_generate_voice.py +0 -0
  113. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/test_image_metadata_v5.py +0 -0
  114. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/test_new_endpoints.py +0 -0
  115. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/test_random_prompt.py +0 -0
  116. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/test_retry_session.py +0 -0
  117. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/test_server.py +0 -0
  118. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/test_server_roundtrip.py +0 -0
  119. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/test_server_run.py +0 -0
  120. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/test_suggest_tags.py +0 -0
  121. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/test_tokenizer.py +0 -0
  122. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/test_upscale.py +0 -0
  123. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/test_user_information.py +0 -0
  124. {novelai_python-0.8.0 → novelai_python-0.8.1}/tests/test_user_login.py +0 -0
  125. {novelai_python-0.8.0 → novelai_python-0.8.1}/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.8.0
3
+ Version: 0.8.1
4
4
  Summary: NovelAI Python Binding With Pydantic
5
5
  Author-Email: sudoskys <coldlando@hotmail.com>
6
6
  License: Apache License 2.0
@@ -101,7 +101,7 @@ Some changes do not raise an error, so check these when you upgrade:
101
101
  ### 📦 Usage
102
102
 
103
103
  ```shell
104
- pip -U install novelai-python
104
+ pip install -U novelai-python
105
105
  ```
106
106
 
107
107
  All API users must adhere to the NovelAI Terms of Service: https://novelai.net/terms.
@@ -71,7 +71,7 @@ Some changes do not raise an error, so check these when you upgrade:
71
71
  ### 📦 Usage
72
72
 
73
73
  ```shell
74
- pip -U install novelai-python
74
+ pip install -U novelai-python
75
75
  ```
76
76
 
77
77
  All API users must adhere to the NovelAI Terms of Service: https://novelai.net/terms.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "novelai-python"
3
- version = "0.8.0"
3
+ version = "0.8.1"
4
4
  description = "NovelAI Python Binding With Pydantic"
5
5
  authors = [
6
6
  { name = "sudoskys", email = "coldlando@hotmail.com" },
@@ -63,7 +63,7 @@ class SubscriptionResp(RespBase):
63
63
  active: bool = Field(..., description="Subscription status")
64
64
  expiresAt: int = Field(..., description="Subscription expiration time")
65
65
  perks: Perks = Field(..., description="Subscription perks")
66
- paymentProcessorData: dict[Any, Any] | None
66
+ paymentProcessorData: dict[Any, Any] | None = None # absent or null on some accounts
67
67
  trainingStepsLeft: TrainingSteps = Field(..., description="Training steps left")
68
68
  accountType: int = Field(..., description="Account type")
69
69
  usage: SubscriptionUsage | None = Field(default=None, description="Opus V5 usage bar (absent on non-Opus)")
@@ -10,6 +10,20 @@ from pydantic import Field, PrivateAttr, SecretStr
10
10
  from ._base import CredentialBase, _correlation_id, _initiated_at, browser_headers
11
11
 
12
12
 
13
+ class _LoginSession(AsyncSession):
14
+ """A session that drops the credential's cached token when NovelAI answers 401 to it."""
15
+
16
+ def __init__(self, credential: "LoginCredential", **kwargs) -> None:
17
+ super().__init__(**kwargs)
18
+ self._credential = credential
19
+
20
+ async def request(self, *args, **kwargs): # pyright: ignore[reportIncompatibleMethodOverride]
21
+ response = await super().request(*args, **kwargs)
22
+ if response.status_code == 401:
23
+ self._credential.forget_token(self.headers.get("authorization"))
24
+ return response
25
+
26
+
13
27
  class LoginCredential(CredentialBase):
14
28
  """
15
29
  JwtCredential is the base class for all credential.
@@ -31,6 +45,13 @@ class LoginCredential(CredentialBase):
31
45
  not self.session_headers or self.update_at is None or int(time.time()) - self.update_at > 29 * 24 * 60 * 60
32
46
  )
33
47
 
48
+ def forget_token(self, authorization: str | None = None) -> None:
49
+ """Log in again on the next request. With ``authorization``, only if that is still the cached token
50
+ (a session made before a newer login must not drop the newer token)."""
51
+ if authorization is None or self.session_headers.get("authorization") == authorization:
52
+ self.session_headers = {}
53
+ self.update_at = None
54
+
34
55
  def _lock(self) -> asyncio.Lock:
35
56
  # an asyncio.Lock belongs to one event loop; a credential can outlive it (several asyncio.run)
36
57
  loop = asyncio.get_running_loop()
@@ -66,4 +87,4 @@ class LoginCredential(CredentialBase):
66
87
  **(update_headers or {}),
67
88
  }
68
89
  )
69
- return AsyncSession(**self.session_kwargs(timeout, headers))
90
+ return _LoginSession(self, **self.session_kwargs(timeout, headers))
@@ -263,6 +263,12 @@ PROMOTION = {
263
263
  "NovelAI Diffusion V4 7ABFFA2A": Model.NAI_DIFFUSION_4_CURATED_PREVIEW,
264
264
  "NovelAI Diffusion V4 C1CCBA86": Model.NAI_DIFFUSION_4_CURATED_PREVIEW,
265
265
  "NovelAI Diffusion V4 770A9E12": Model.NAI_DIFFUSION_4_CURATED_PREVIEW,
266
+ # the web client's metadata import (rQ): the V5 Source names no variant, only the hash
267
+ "NovelAI Diffusion V5 657484A5": Model.NAI_DIFFUSION_5_FULL,
268
+ "NovelAI Diffusion V5 0ADF9AB7": Model.NAI_DIFFUSION_5_FULL,
269
+ "NovelAI Diffusion V5 93F4BD30": Model.NAI_DIFFUSION_5_FULL_MEDIUM,
270
+ "NovelAI Diffusion V5 70AB5786": Model.NAI_DIFFUSION_5_FULL_MEDIUM,
271
+ "NovelAI Diffusion V5 DB276663": Model.NAI_DIFFUSION_5_CURATED,
266
272
  }
267
273
 
268
274
  ModelTypeAlias = Model | str | None
@@ -604,9 +610,8 @@ def get_quality_suffix(model: ModelTypeAlias, preset_id: str | QualityPreset | N
604
610
 
605
611
  def find_model_by_hashcode(hashcode: str) -> ModelTypeAlias:
606
612
  if "NovelAI Diffusion V5" in hashcode:
607
- if "Curated" in hashcode:
608
- return PROMOTION.get(hashcode, Model.NAI_DIFFUSION_5_CURATED)
609
- return PROMOTION.get(hashcode, Model.NAI_DIFFUSION_5_FULL)
613
+ # an unknown V5 hash is Curated, as in the web client
614
+ return PROMOTION.get(hashcode, Model.NAI_DIFFUSION_5_CURATED)
610
615
  if "NovelAI Diffusion V4.5" in hashcode:
611
616
  return PROMOTION.get(hashcode, Model.NAI_DIFFUSION_4_5_FULL)
612
617
  if "NovelAI Diffusion V4" in hashcode:
@@ -91,9 +91,10 @@ class VoiceGenerate(ApiBaseModel):
91
91
  # Request
92
92
  try:
93
93
  response = await sess.post(url=self.base_url, json=request_data)
94
- header_type = response.headers.get("Content-Type")
94
+ # opus=true answers WebM/Opus, maybe as 'audio/webm;codecs="opus"'
95
+ media_type = (response.headers.get("Content-Type") or "").split(";")[0].strip().lower()
95
96
  if (
96
- header_type not in ["audio/mpeg", "audio/ogg", "audio/opus", "audio/wav", "audio/webm"]
97
+ media_type not in ["audio/mpeg", "audio/ogg", "audio/opus", "audio/wav", "audio/webm"]
97
98
  or response.status_code >= 400
98
99
  ):
99
100
  self._raise_for_status(
@@ -299,10 +299,12 @@ async def generate_voice(
299
299
  """
300
300
  req = VoiceGenerate(text=text, voice=voice, seed=seed, opus=opus, version=version)
301
301
  result = await req.request(session=get_session(current_token))
302
+ # opus=true is WebM/Opus, as the web client plays it; otherwise MP3
303
+ media_type, filename = ("audio/webm", "audio.webm") if opus else ("audio/mpeg", "audio.mp3")
302
304
  return StreamingResponse(
303
305
  io.BytesIO(result.audio),
304
- media_type="audio/mpeg",
305
- headers={"Content-Disposition": "attachment;filename=audio.mp3"},
306
+ media_type=media_type,
307
+ headers={"Content-Disposition": f"attachment;filename={filename}"},
306
308
  )
307
309
 
308
310
 
@@ -94,6 +94,21 @@ def test_img2img_needs_the_source_image():
94
94
  assert (p.strength, p.noise, p.extra_noise_seed, p.color_correct) == (0.8, 0.1, 41, False)
95
95
 
96
96
 
97
+ @pytest.mark.parametrize(
98
+ ("source", "model"),
99
+ [
100
+ ("NovelAI Diffusion V5 0ADF9AB7", Model.NAI_DIFFUSION_5_FULL),
101
+ ("NovelAI Diffusion V5 93F4BD30", Model.NAI_DIFFUSION_5_FULL_MEDIUM),
102
+ ("NovelAI Diffusion V5 DB276663", Model.NAI_DIFFUSION_5_CURATED),
103
+ ("NovelAI Diffusion V5 00000000", Model.NAI_DIFFUSION_5_CURATED), # the web client's default
104
+ ],
105
+ )
106
+ def test_v5_model_from_source_hash(source, model):
107
+ from novelai_python.sdk.ai._enum import find_model_by_hashcode
108
+
109
+ assert find_model_by_hashcode(source) == model
110
+
111
+
97
112
  def test_unknown_model_and_missing_comment():
98
113
  with pytest.raises(ValueError):
99
114
  GenerateImageInfer.from_metadata(ImageMetadata(Comment=CommentModel(prompt="x"), Source="Stable Diffusion"))
@@ -97,3 +97,33 @@ def test_a_request_body_cannot_set_the_login_proxy():
97
97
  login = Login.model_validate({"key": "x", "proxy": "http://attacker:1"})
98
98
  assert login.session.proxies in (None, {})
99
99
  assert "proxy" not in login.model_dump()
100
+
101
+
102
+ @pytest.mark.asyncio
103
+ async def test_a_401_drops_the_cached_login_token():
104
+ import asyncio
105
+ import time
106
+
107
+ from novelai_python import LoginCredential
108
+
109
+ async def handle(reader, writer):
110
+ await reader.readuntil(b"\r\n\r\n")
111
+ writer.write(b"HTTP/1.1 401 Unauthorized\r\nContent-Length: 2\r\nConnection: close\r\n\r\n{}")
112
+ await writer.drain()
113
+ writer.close()
114
+
115
+ server = await asyncio.start_server(handle, "127.0.0.1", 0)
116
+ port = server.sockets[0].getsockname()[1]
117
+ credential = LoginCredential(
118
+ username="a@b.c", password="pw", session_headers={"authorization": "Bearer old"}, update_at=int(time.time())
119
+ )
120
+ stale = await credential.get_session()
121
+ credential.session_headers = {"authorization": "Bearer newer"} # a newer login happened meanwhile
122
+ async with stale as sess:
123
+ await sess.get(f"http://127.0.0.1:{port}/")
124
+ assert credential.session_headers == {"authorization": "Bearer newer"}
125
+
126
+ async with await credential.get_session() as sess:
127
+ await sess.get(f"http://127.0.0.1:{port}/")
128
+ assert credential.session_headers == {} and credential.update_at is None
129
+ server.close()
@@ -33,8 +33,7 @@ def test_subscription_usage_is_optional_and_derived():
33
33
  "tier": 3,
34
34
  "active": True,
35
35
  "expiresAt": 1,
36
- "paymentProcessorData": None,
37
- "accountType": 0,
36
+ "accountType": 0, # no paymentProcessorData: some accounts omit it
38
37
  "trainingStepsLeft": {"fixedTrainingStepsLeft": 10000, "purchasedTrainingSteps": 0},
39
38
  "perks": {
40
39
  "maxPriorityActions": 1,
File without changes
File without changes