tumblrbot 1.9.2__tar.gz → 1.9.3__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.
@@ -217,5 +217,5 @@ __marimo__/
217
217
 
218
218
  data
219
219
  *.jsonl
220
- config.toml
220
+ *.toml
221
221
  tumblrbot.exe.lnk
@@ -1,10 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tumblrbot
3
- Version: 1.9.2
3
+ Version: 1.9.3
4
4
  Summary: An updated bot that posts to Tumblr, based on your very own blog!
5
5
  Requires-Python: >= 3.13
6
6
  Description-Content-Type: text/markdown
7
- Requires-Dist: keyring
8
7
  Requires-Dist: openai
9
8
  Requires-Dist: pwinput
10
9
  Requires-Dist: pydantic
@@ -25,7 +24,6 @@ Project-URL: Source, https://github.com/MaidScientistIzutsumiMarin/tumblrbot
25
24
  [JSON Lines Validator]: https://jsonlines.org/validator
26
25
 
27
26
  [pip]: https://pypi.org
28
- [keyring]: https://pypi.org/project/keyring
29
27
  [Rich]: https://pypi.org/project/rich
30
28
 
31
29
  [OpenAI]: https://pypi.org/project/openai
@@ -60,7 +58,6 @@ Features:
60
58
 
61
59
  - An [interactive console][Main] for all steps of generating posts for the blog:
62
60
  1. Asks for [OpenAI] and [Tumblr] tokens.
63
- - Stores API tokens using [keyring].
64
61
  1. Retrieves [Tumblr] [OAuth] tokens.
65
62
  1. [Downloads posts][Download] from specified blogs ([configurable]).
66
63
  - Skips redownloading already downloaded posts.
@@ -102,7 +99,6 @@ Features:
102
99
  1. Install the [pip] package: `pip install tumblrbot`
103
100
  - Alternatively, you can install from this repository: `pip install git+https://github.com/MaidThatPrograms/tumblrbot.git`
104
101
  - On Linux, you will have to make a virtual environment or use the flag to install packages system-wide.
105
- - See [keyring] for additional requirements if you are not on Windows.
106
102
 
107
103
  ## Usage
108
104
 
@@ -7,7 +7,6 @@
7
7
  [JSON Lines Validator]: https://jsonlines.org/validator
8
8
 
9
9
  [pip]: https://pypi.org
10
- [keyring]: https://pypi.org/project/keyring
11
10
  [Rich]: https://pypi.org/project/rich
12
11
 
13
12
  [OpenAI]: https://pypi.org/project/openai
@@ -42,7 +41,6 @@ Features:
42
41
 
43
42
  - An [interactive console][Main] for all steps of generating posts for the blog:
44
43
  1. Asks for [OpenAI] and [Tumblr] tokens.
45
- - Stores API tokens using [keyring].
46
44
  1. Retrieves [Tumblr] [OAuth] tokens.
47
45
  1. [Downloads posts][Download] from specified blogs ([configurable]).
48
46
  - Skips redownloading already downloaded posts.
@@ -84,7 +82,6 @@ Features:
84
82
  1. Install the [pip] package: `pip install tumblrbot`
85
83
  - Alternatively, you can install from this repository: `pip install git+https://github.com/MaidThatPrograms/tumblrbot.git`
86
84
  - On Linux, you will have to make a virtual environment or use the flag to install packages system-wide.
87
- - See [keyring] for additional requirements if you are not on Windows.
88
85
 
89
86
  ## Usage
90
87
 
@@ -1,11 +1,10 @@
1
1
  [project]
2
2
  name = "tumblrbot"
3
- version = "1.9.2"
3
+ version = "1.9.3"
4
4
  description = "An updated bot that posts to Tumblr, based on your very own blog!"
5
5
  readme = "README.md"
6
6
  requires-python = ">= 3.13"
7
7
  dependencies = [
8
- "keyring",
9
8
  "openai",
10
9
  "pwinput",
11
10
  "pydantic",
@@ -48,7 +48,6 @@ class DraftGenerator(FlowClass):
48
48
  return Post(
49
49
  content=[Post.Block(type="text", text=text)],
50
50
  tags=tags or [],
51
- state="draft",
52
51
  parent_tumblelog_uuid=original.blog.uuid,
53
52
  parent_post_id=original.id,
54
53
  reblog_key=original.reblog_key,
@@ -1,20 +1,17 @@
1
- import tomllib
2
- from abc import abstractmethod
3
1
  from collections.abc import Generator
4
2
  from pathlib import Path
5
- from typing import Annotated, Any, ClassVar, Literal, Self, override
3
+ from tomllib import loads
4
+ from typing import Annotated, Any, Literal, Self, override
6
5
 
7
6
  import rich
8
- import tomlkit
9
- from keyring import get_password, set_password
10
7
  from openai.types import ChatModel
11
8
  from pwinput import pwinput
12
9
  from pydantic import BaseModel, ConfigDict, Field, NonNegativeFloat, NonNegativeInt, PlainSerializer, PositiveFloat, PositiveInt, model_validator
13
10
  from pydantic.json_schema import SkipJsonSchema
14
11
  from requests_oauthlib import OAuth1Session
15
12
  from rich.panel import Panel
16
- from rich.prompt import Confirm, Prompt
17
- from tomlkit import comment, document
13
+ from rich.prompt import Prompt
14
+ from tomlkit import comment, document, dumps # pyright: ignore[reportUnknownVariableType]
18
15
 
19
16
 
20
17
  class FullyValidatedModel(BaseModel):
@@ -29,22 +26,32 @@ class FullyValidatedModel(BaseModel):
29
26
 
30
27
  class FileSyncSettings(FullyValidatedModel):
31
28
  @classmethod
32
- @abstractmethod
33
- def read(cls) -> Self | dict[str, object] | str | None: ...
29
+ def get_toml_file(cls) -> Path:
30
+ return Path(f"{cls.__name__.lower()}.toml")
34
31
 
35
32
  @classmethod
36
33
  def load(cls) -> Self:
37
- data = cls.read() or {}
38
- return cls.model_validate_json(data) if isinstance(data, str) else cls.model_validate(data)
34
+ toml_file = cls.get_toml_file()
35
+ data = loads(toml_file.read_text("utf_8")) if toml_file.exists() else {}
36
+ return cls.model_validate(data)
39
37
 
40
38
  @model_validator(mode="after")
41
- @abstractmethod
42
- def write(self) -> Self: ...
39
+ def dump(self) -> Self:
40
+ toml_table = document()
41
+
42
+ for (name, field), value in zip(self.__class__.model_fields.items(), self.model_dump(mode="json").values(), strict=True):
43
+ if field.description is not None:
44
+ for line in field.description.split(". "):
45
+ toml_table.add(comment(f"{line.removesuffix('.')}."))
43
46
 
47
+ toml_table[name] = value
44
48
 
45
- class Config(FileSyncSettings):
46
- toml_file: ClassVar = Path("config.toml")
49
+ self.get_toml_file().write_text(dumps(toml_table), encoding="utf_8")
47
50
 
51
+ return self
52
+
53
+
54
+ class Config(FileSyncSettings):
48
55
  # Downloading Posts & Writing Examples
49
56
  download_blog_identifiers: list[str] = Field([], description="The identifiers of the blogs which post data will be downloaded from.")
50
57
  data_directory: Path = Field(Path("data"), description="Where to store downloaded post data.")
@@ -78,16 +85,10 @@ class Config(FileSyncSettings):
78
85
  tags_developer_message: str = Field("You will be provided with a block of text, and your task is to extract a very short list of the most important subjects from it.", description="The developer message used to generate tags.")
79
86
  reblog_blog_identifiers: list[str] = Field([], description="The identifiers of blogs that can be reblogged from when generating drafts.")
80
87
  reblog_chance: NonNegativeFloat = Field(0.1, description="The chance to generate a reblog of a random post. This will use more OpenAI tokens.")
81
- reblog_user_message: str = Field("Please write a comical Tumblr post in response to the following post:\n{}", description="The format string for the user message used to reblog posts.")
82
-
83
- @classmethod
84
- @override
85
- def read(cls) -> dict[str, object] | None:
86
- return tomllib.loads(cls.toml_file.read_text("utf_8")) if cls.toml_file.exists() else None
88
+ reblog_user_message: str = Field("Please write a comical Tumblr post in response to the following post:\n\n{}", description="The format string for the user message used to reblog posts.")
87
89
 
88
- @model_validator(mode="after")
89
90
  @override
90
- def write(self) -> Self:
91
+ def model_post_init(self, _: object) -> None:
91
92
  if not self.download_blog_identifiers:
92
93
  rich.print("Enter the [cyan]identifiers of your blogs[/] that data should be [bold purple]downloaded[/] from, separated by commas.")
93
94
  self.download_blog_identifiers = list(map(str.strip, Prompt.ask("[bold][Example] [dim]staff.tumblr.com,changes").split(",")))
@@ -96,19 +97,6 @@ class Config(FileSyncSettings):
96
97
  rich.print("Enter the [cyan]identifier of your blog[/] that drafts should be [bold purple]uploaded[/] to.")
97
98
  self.upload_blog_identifier = Prompt.ask("[bold][Example] [dim]staff.tumblr.com or changes").strip()
98
99
 
99
- toml_table = document()
100
-
101
- for (name, field), value in zip(self.__class__.model_fields.items(), self.model_dump(mode="json").values(), strict=True):
102
- if field.description is not None:
103
- for line in field.description.split(". "):
104
- toml_table.add(comment(f"{line.removesuffix('.')}."))
105
-
106
- toml_table[name] = value
107
-
108
- self.toml_file.write_text(tomlkit.dumps(toml_table), encoding="utf_8")
109
-
110
- return self
111
-
112
100
 
113
101
  class Tokens(FileSyncSettings):
114
102
  class Tumblr(FullyValidatedModel):
@@ -117,39 +105,16 @@ class Tokens(FileSyncSettings):
117
105
  resource_owner_key: str = ""
118
106
  resource_owner_secret: str = ""
119
107
 
120
- service_name: ClassVar = "tumblrbot"
121
- username: ClassVar = "tokens"
122
-
123
108
  openai_api_key: str = ""
124
109
  tumblr: Tumblr = Tumblr()
125
110
 
126
- @staticmethod
127
- def get_oauth_tokens(token: dict[str, str]) -> tuple[str, str]:
128
- return token["oauth_token"], token["oauth_token_secret"]
129
-
130
- @staticmethod
131
- def online_token_prompt(url: str, *tokens: str) -> Generator[str]:
132
- formatted_token_string = " and ".join(f"[cyan]{token}[/]" for token in tokens)
133
-
134
- rich.print(f"Retrieve your {formatted_token_string} from: {url}")
135
- for token in tokens:
136
- yield pwinput(f"Enter your {token} (masked): ").strip()
137
-
138
- rich.print()
139
-
140
- @classmethod
141
- @override
142
- def read(cls) -> str | None:
143
- return get_password(cls.service_name, cls.username)
144
-
145
- @model_validator(mode="after")
146
111
  @override
147
- def write(self) -> Self:
112
+ def model_post_init(self, _: object) -> None:
148
113
  # Check if any tokens are missing or if the user wants to reset them, then set tokens if necessary.
149
- if not self.openai_api_key or Confirm.ask("Reset OpenAI API key?", default=False):
114
+ if not self.openai_api_key:
150
115
  (self.openai_api_key,) = self.online_token_prompt("https://platform.openai.com/api-keys", "API key")
151
116
 
152
- if not all(self.tumblr.model_dump().values()) or Confirm.ask("Reset Tumblr API tokens?", default=False):
117
+ if not all(self.tumblr.model_dump().values()):
153
118
  self.tumblr.client_key, self.tumblr.client_secret = self.online_token_prompt("https://tumblr.com/oauth/apps", "consumer key", "consumer secret")
154
119
 
155
120
  # This is the whole OAuth 1.0 process.
@@ -174,11 +139,19 @@ class Tokens(FileSyncSettings):
174
139
 
175
140
  self.tumblr.resource_owner_key, self.tumblr.resource_owner_secret = self.get_oauth_tokens(oauth_tokens)
176
141
 
177
- # Regardless of whether any values were changed, we may as well write to the keyring.
178
- # Any unchanged values will be set to the value they already were, since this is run after reading from the keyring.
179
- set_password(self.service_name, self.username, self.model_dump_json())
142
+ @staticmethod
143
+ def online_token_prompt(url: str, *tokens: str) -> Generator[str]:
144
+ formatted_token_string = " and ".join(f"[cyan]{token}[/]" for token in tokens)
180
145
 
181
- return self
146
+ rich.print(f"Retrieve your {formatted_token_string} from: {url}")
147
+ for token in tokens:
148
+ yield pwinput(f"Enter your {token} (masked): ").strip()
149
+
150
+ rich.print()
151
+
152
+ @staticmethod
153
+ def get_oauth_tokens(token: dict[str, str]) -> tuple[str, str]:
154
+ return token["oauth_token"], token["oauth_token_secret"]
182
155
 
183
156
 
184
157
  class Blog(FullyValidatedModel):
@@ -209,7 +182,7 @@ class Post(FullyValidatedModel):
209
182
 
210
183
  timestamp: SkipJsonSchema[int] = 0
211
184
  tags: Annotated[list[str], PlainSerializer(",".join)] = []
212
- state: SkipJsonSchema[Literal["published", "queued", "draft", "private", "unapproved"]] = "published"
185
+ state: SkipJsonSchema[Literal["published", "queued", "draft", "private", "unapproved"]] = "draft"
213
186
 
214
187
  content: SkipJsonSchema[list[Block]] = []
215
188
  layout: SkipJsonSchema[list[Block]] = []
File without changes
File without changes