tumblrbot 1.2.0__tar.gz → 1.3.0__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.
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: tumblrbot
3
+ Version: 1.3.0
4
+ Summary: An updated bot that posts to Tumblr, based on your very own blog!
5
+ Requires-Python: >= 3.13
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: keyring
8
+ Requires-Dist: more-itertools
9
+ Requires-Dist: openai
10
+ Requires-Dist: pydantic
11
+ Requires-Dist: pydantic-settings
12
+ Requires-Dist: requests
13
+ Requires-Dist: requests-oauthlib
14
+ Requires-Dist: rich
15
+ Requires-Dist: tiktoken
16
+ Requires-Dist: tomlkit
17
+ Project-URL: Source, https://github.com/MaidThatPrograms/tumblrbot
18
+
19
+ [OAuth]: https://oauth.net/1
20
+ [OpenAI]: https://pypi.org/project/openai
21
+ [Python]: https://python.org/download
22
+ [Tumblr]: https://tumblr.com
23
+
24
+ [keyring]: https://pypi.org/project/keyring
25
+ [Rich]: https://pypi.org/project/rich
26
+
27
+ [Moderation API]: https://platform.openai.com/docs/api-reference/moderations
28
+ [pip]: https://pypi.org
29
+
30
+ [Download]: src/tumblrbot/flow/download.py
31
+ [Examples]: src/tumblrbot/flow/examples.py
32
+ [Fine-Tune]: src/tumblrbot/flow/fine_tune.py
33
+ [Generate]: src/tumblrbot/flow/generate.py
34
+ [Main]: src/tumblrbot/__main__.py
35
+ [README.md]: README.md
36
+
37
+ [config]: #configuration
38
+
39
+ # tumblrbot
40
+ [![PyPI - Version](https://img.shields.io/pypi/v/tumblrbot)](https://python.org/pypi/tumblrbot)
41
+
42
+ Description of original project:
43
+ > 4tv-tumblrbot was a collaborative project I embarked on with my close friend Dima, who goes by @smoqueen on Tumblr. The aim of this endeavor was straightforward yet silly: to develop a Tumblr bot powered by a machine-learning model. This bot would be specifically trained on the content from a particular Tumblr blog or a selected set of blogs, allowing it to mimic the style, tone, and thematic essence of the original posts.
44
+
45
+ This fork is largely a rewrite of the source code with similarities in its structure and process.
46
+
47
+ Features:
48
+ - An [interactive console][Main] for all steps of generating posts for the blog:
49
+ 1. Asks for [OpenAI] and [Tumblr] tokens.
50
+ - Stores API tokens using [keyring].
51
+ - Prevents API tokens from printing to the console.
52
+ 1. Retrieves [Tumblr] [OAuth] tokens.
53
+ 1. [Downloads posts][Download] from the [configured][config] [Tumblr] blogs.
54
+ - Skips redownloading already downloaded posts.
55
+ - Shows progress and previews the current post.
56
+ 1. [Creates examples][Examples] to fine-tune the model from your posts.
57
+ - Filters out posts that contain more than just text data.
58
+ - Filters out any posts flagged by the [OpenAI] [Moderation API] (optional).
59
+ - Shows progress and previews the current post.
60
+ - Formats asks as the user message and the responses as the assistant response.
61
+ - Adds custom user messages and assistant responses to the dataset from the [configured][config] file.
62
+ 1. Provides cost estimates if the currently saved examples are used to fine-tune the [configured][config] model.
63
+ 1. [Uploads examples][Fine-Tune] to [OpenAI] and begins the fine-tuning process.
64
+ - Resumes monitoring the same fine-tuning process when restarted.
65
+ - Stores the output model automatically when fine-tuning is completed.
66
+ 1. [Generates and uploads posts][Generate] to the [configured][config] [Tumblr] blog using the [configured][config] fine-tuned model.
67
+ - Creates tags by extracting keywords at the [configured][config] frequency using the [configured][config] model.
68
+ - Uploads posts as drafts to the [configured][config] [Tumblr] blog.
69
+ - Shows progress and previews the current post.
70
+ - Colorful output, progress bars, and post previews using [rich].
71
+ - Automatically keeps the [config] file up-to-date and recreates it if missing.
72
+
73
+ **To-Do:**
74
+ - Add documentation.
75
+ - Finish updating [README.md].
76
+
77
+
78
+ **Please submit an issue or contact us for features you want added/reimplemented.**
79
+
80
+ ## Installation
81
+ 1. Install the latest version of [Python]:
82
+ - Windows: `winget install python3`
83
+ - Linux (apt): `apt install python-pip`
84
+ - Linux (pacman): `pacman install python-pip`
85
+ 1. Install the [pip] package: `pip install tumblrbot`
86
+ - Alternatively, you can install from this repository: `pip install git+https://github.com/MaidThatPrograms/tumblrbot.git`
87
+ - On Linux, you will have to make a virtual environment.
88
+
89
+ ## Usage
90
+ Run `tumblrbot` from anywhere. Run `tumblrbot --help` for command-line options. Every command-line option corresponds to a value from the [config](#configuration).
91
+
92
+ ## Obtaining Tokens
93
+ - The [OpenAI] API token can be created [here](https://platform.openai.com/settings/organization/api-keys).
94
+ 1. Leave everything at the defaults and set `Project` to `Default Project`.
95
+ 1. Press `Create secret key`.
96
+ 1. Press `Copy` to copy the API token to your clipboard.
97
+ - The [Tumblr] API tokens can be created [here](https://tumblr.com/oauth/apps).
98
+ 1. Press `+ Register Application`.
99
+ 1. Enter anything for `Application Name` and `Application Description`.
100
+ 1. Enter any URL for `Application Website` and `Default callback URL`, like `https://example.com`.
101
+ 1. Enter any email address for `Administrative contact email`. It probably doesn't need to be one you have access to.
102
+ 1. Press the checkbox next to `I'm not a robot` and complete the CAPTCHA.
103
+ 1. Press `Register`.
104
+ 1. You now have access to your `consumer key` next to `Oauth Consumer Key`.
105
+ 1. Press `Show secret key` to see your `Consumer Secret`.
106
+
107
+ When running this program, you will be prompted to enter all of these tokens. **The fields are password-protected, so there will be no output to the console.** If something goes wrong while entering the tokens, you can always reset them by running the program again and answering `y` to the relevant prompt.
108
+
109
+ After inputting the [Tumblr] tokens, you will be given a URL that you need to open in your browser. Press `Allow`, then copy and paste the URL of the page you are redirected to into the console.
110
+
111
+ ## Configuration
112
+ All config options can be found in `config.toml` after running the program once. This will be kept up-to-date if there are changes to the config's format in a future update. This also means it may be worthwhile to double-check the config file after an update. Any changes to the config should be in the changelog for a given version.
113
+ > WIP: There will be more information about the config options soon.
114
+
@@ -0,0 +1,95 @@
1
+ [OAuth]: https://oauth.net/1
2
+ [OpenAI]: https://pypi.org/project/openai
3
+ [Python]: https://python.org/download
4
+ [Tumblr]: https://tumblr.com
5
+
6
+ [keyring]: https://pypi.org/project/keyring
7
+ [Rich]: https://pypi.org/project/rich
8
+
9
+ [Moderation API]: https://platform.openai.com/docs/api-reference/moderations
10
+ [pip]: https://pypi.org
11
+
12
+ [Download]: src/tumblrbot/flow/download.py
13
+ [Examples]: src/tumblrbot/flow/examples.py
14
+ [Fine-Tune]: src/tumblrbot/flow/fine_tune.py
15
+ [Generate]: src/tumblrbot/flow/generate.py
16
+ [Main]: src/tumblrbot/__main__.py
17
+ [README.md]: README.md
18
+
19
+ [config]: #configuration
20
+
21
+ # tumblrbot
22
+ [![PyPI - Version](https://img.shields.io/pypi/v/tumblrbot)](https://python.org/pypi/tumblrbot)
23
+
24
+ Description of original project:
25
+ > 4tv-tumblrbot was a collaborative project I embarked on with my close friend Dima, who goes by @smoqueen on Tumblr. The aim of this endeavor was straightforward yet silly: to develop a Tumblr bot powered by a machine-learning model. This bot would be specifically trained on the content from a particular Tumblr blog or a selected set of blogs, allowing it to mimic the style, tone, and thematic essence of the original posts.
26
+
27
+ This fork is largely a rewrite of the source code with similarities in its structure and process.
28
+
29
+ Features:
30
+ - An [interactive console][Main] for all steps of generating posts for the blog:
31
+ 1. Asks for [OpenAI] and [Tumblr] tokens.
32
+ - Stores API tokens using [keyring].
33
+ - Prevents API tokens from printing to the console.
34
+ 1. Retrieves [Tumblr] [OAuth] tokens.
35
+ 1. [Downloads posts][Download] from the [configured][config] [Tumblr] blogs.
36
+ - Skips redownloading already downloaded posts.
37
+ - Shows progress and previews the current post.
38
+ 1. [Creates examples][Examples] to fine-tune the model from your posts.
39
+ - Filters out posts that contain more than just text data.
40
+ - Filters out any posts flagged by the [OpenAI] [Moderation API] (optional).
41
+ - Shows progress and previews the current post.
42
+ - Formats asks as the user message and the responses as the assistant response.
43
+ - Adds custom user messages and assistant responses to the dataset from the [configured][config] file.
44
+ 1. Provides cost estimates if the currently saved examples are used to fine-tune the [configured][config] model.
45
+ 1. [Uploads examples][Fine-Tune] to [OpenAI] and begins the fine-tuning process.
46
+ - Resumes monitoring the same fine-tuning process when restarted.
47
+ - Stores the output model automatically when fine-tuning is completed.
48
+ 1. [Generates and uploads posts][Generate] to the [configured][config] [Tumblr] blog using the [configured][config] fine-tuned model.
49
+ - Creates tags by extracting keywords at the [configured][config] frequency using the [configured][config] model.
50
+ - Uploads posts as drafts to the [configured][config] [Tumblr] blog.
51
+ - Shows progress and previews the current post.
52
+ - Colorful output, progress bars, and post previews using [rich].
53
+ - Automatically keeps the [config] file up-to-date and recreates it if missing.
54
+
55
+ **To-Do:**
56
+ - Add documentation.
57
+ - Finish updating [README.md].
58
+
59
+
60
+ **Please submit an issue or contact us for features you want added/reimplemented.**
61
+
62
+ ## Installation
63
+ 1. Install the latest version of [Python]:
64
+ - Windows: `winget install python3`
65
+ - Linux (apt): `apt install python-pip`
66
+ - Linux (pacman): `pacman install python-pip`
67
+ 1. Install the [pip] package: `pip install tumblrbot`
68
+ - Alternatively, you can install from this repository: `pip install git+https://github.com/MaidThatPrograms/tumblrbot.git`
69
+ - On Linux, you will have to make a virtual environment.
70
+
71
+ ## Usage
72
+ Run `tumblrbot` from anywhere. Run `tumblrbot --help` for command-line options. Every command-line option corresponds to a value from the [config](#configuration).
73
+
74
+ ## Obtaining Tokens
75
+ - The [OpenAI] API token can be created [here](https://platform.openai.com/settings/organization/api-keys).
76
+ 1. Leave everything at the defaults and set `Project` to `Default Project`.
77
+ 1. Press `Create secret key`.
78
+ 1. Press `Copy` to copy the API token to your clipboard.
79
+ - The [Tumblr] API tokens can be created [here](https://tumblr.com/oauth/apps).
80
+ 1. Press `+ Register Application`.
81
+ 1. Enter anything for `Application Name` and `Application Description`.
82
+ 1. Enter any URL for `Application Website` and `Default callback URL`, like `https://example.com`.
83
+ 1. Enter any email address for `Administrative contact email`. It probably doesn't need to be one you have access to.
84
+ 1. Press the checkbox next to `I'm not a robot` and complete the CAPTCHA.
85
+ 1. Press `Register`.
86
+ 1. You now have access to your `consumer key` next to `Oauth Consumer Key`.
87
+ 1. Press `Show secret key` to see your `Consumer Secret`.
88
+
89
+ When running this program, you will be prompted to enter all of these tokens. **The fields are password-protected, so there will be no output to the console.** If something goes wrong while entering the tokens, you can always reset them by running the program again and answering `y` to the relevant prompt.
90
+
91
+ After inputting the [Tumblr] tokens, you will be given a URL that you need to open in your browser. Press `Allow`, then copy and paste the URL of the page you are redirected to into the console.
92
+
93
+ ## Configuration
94
+ All config options can be found in `config.toml` after running the program once. This will be kept up-to-date if there are changes to the config's format in a future update. This also means it may be worthwhile to double-check the config file after an update. Any changes to the config should be in the changelog for a given version.
95
+ > WIP: There will be more information about the config options soon.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tumblrbot"
3
- version = "1.2.0"
3
+ version = "1.3.0"
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"
@@ -6,14 +6,15 @@ from tumblrbot.flow.download import PostDownloader
6
6
  from tumblrbot.flow.examples import ExamplesWriter
7
7
  from tumblrbot.flow.fine_tune import FineTuner
8
8
  from tumblrbot.flow.generate import DraftGenerator
9
- from tumblrbot.utils.common import TumblrClient
10
- from tumblrbot.utils.settings import Tokens
9
+ from tumblrbot.utils.models import Tokens
10
+ from tumblrbot.utils.tumblr import TumblrClient
11
11
 
12
12
 
13
13
  def main() -> None:
14
14
  install()
15
+
15
16
  tokens = Tokens()
16
- with OpenAI(api_key=tokens.openai_api_key.get_secret_value()) as openai, TumblrClient(tokens) as tumblr:
17
+ with OpenAI(api_key=tokens.openai_api_key.get_secret_value()) as openai, TumblrClient(tokens=tokens) as tumblr:
17
18
  post_downloader = PostDownloader(openai, tumblr)
18
19
  if Confirm.ask("Download latest posts?", default=False):
19
20
  post_downloader.download()
@@ -2,11 +2,11 @@ from io import TextIOBase
2
2
  from json import dump
3
3
  from pathlib import Path
4
4
 
5
- from tumblrbot.utils.common import PreviewLive, UtilClass
5
+ from tumblrbot.utils.common import FlowClass, PreviewLive
6
6
  from tumblrbot.utils.models import Post
7
7
 
8
8
 
9
- class PostDownloader(UtilClass):
9
+ class PostDownloader(FlowClass):
10
10
  def paginate_posts(self, blog_identifier: str, completed: int, after: int, fp: TextIOBase, live: PreviewLive) -> None:
11
11
  task_id = live.progress.add_task(f"Downloading posts from '{blog_identifier}'...", total=None, completed=completed)
12
12
 
@@ -8,17 +8,18 @@ from typing import IO
8
8
 
9
9
  import rich
10
10
  from more_itertools import chunked
11
- from openai import BadRequestError
11
+ from openai import BadRequestError, OpenAI
12
12
  from rich.console import Console
13
13
  from rich.prompt import Confirm
14
14
  from tiktoken import encoding_for_model, get_encoding
15
15
 
16
- from tumblrbot.utils.common import PreviewLive, UtilClass
16
+ from tumblrbot.utils.common import FlowClass, PreviewLive
17
17
  from tumblrbot.utils.models import Example, Post
18
18
 
19
19
 
20
20
  @dataclass
21
- class ExamplesWriter(UtilClass):
21
+ class ExamplesWriter(FlowClass):
22
+ openai: OpenAI
22
23
  data_paths: list[Path]
23
24
 
24
25
  def count_tokens(self) -> Generator[int]:
@@ -52,7 +53,7 @@ class ExamplesWriter(UtilClass):
52
53
  with data_path.open(encoding="utf_8") as fp:
53
54
  for line in fp:
54
55
  post = Post.model_validate_json(line)
55
- if post.get_text_content() and not (post.is_submission or post.trail):
56
+ if not (post.is_submission or post.trail) and post.only_text_blocks() and post.get_response_content():
56
57
  yield post
57
58
 
58
59
  def get_filtered_posts(self) -> Generator[Post]:
@@ -67,7 +68,7 @@ class ExamplesWriter(UtilClass):
67
68
  ceil(len(posts) / chunk_size),
68
69
  description="Removing flagged posts...",
69
70
  ):
70
- response = self.openai.moderations.create(input=list(map(Post.get_text_content, chunk)))
71
+ response = self.openai.moderations.create(input=["\n".join(post.get_text_content()) for post in chunk])
71
72
  for post, moderation in zip(chunk, response.results, strict=True):
72
73
  if moderation.flagged:
73
74
  removed += 1
@@ -88,9 +89,11 @@ class ExamplesWriter(UtilClass):
88
89
 
89
90
  with self.config.examples_file.open("w", encoding="utf_8") as fp:
90
91
  for post in self.get_filtered_posts():
92
+ ask_content, response_content = post.get_text_content()
93
+
91
94
  self.write_example(
92
- None,
93
- post.get_text_content(),
95
+ ask_content or self.config.user_message,
96
+ response_content,
94
97
  fp,
95
98
  )
96
99
 
@@ -103,15 +106,12 @@ class ExamplesWriter(UtilClass):
103
106
 
104
107
  rich.print(f"[bold]The examples file can be found at: '{self.config.examples_file}'\n")
105
108
 
106
- def write_example(self, user_input: str | None, assistant_message: str, fp: IO[str]) -> None:
109
+ def write_example(self, user_message: str, assistant_message: str, fp: IO[str]) -> None:
107
110
  example = Example(
108
111
  messages=[
109
112
  Example.Message(role="developer", content=self.config.developer_message),
113
+ Example.Message(role="user", content=user_message),
110
114
  Example.Message(role="assistant", content=assistant_message),
111
115
  ],
112
116
  )
113
-
114
- if user_input:
115
- example.messages.insert(1, Example.Message(role="user", content=user_input))
116
-
117
117
  fp.write(f"{example.model_dump_json()}\n")
@@ -6,11 +6,11 @@ from time import sleep
6
6
  import rich
7
7
  from openai.types.fine_tuning import FineTuningJob
8
8
 
9
- from tumblrbot.utils.common import PreviewLive, UtilClass
9
+ from tumblrbot.utils.common import FlowClass, PreviewLive
10
10
 
11
11
 
12
12
  @dataclass
13
- class FineTuner(UtilClass):
13
+ class FineTuner(FlowClass):
14
14
  estimated_tokens: int
15
15
 
16
16
  @staticmethod
@@ -2,11 +2,11 @@ from random import random
2
2
 
3
3
  import rich
4
4
 
5
- from tumblrbot.utils.common import PreviewLive, UtilClass
5
+ from tumblrbot.utils.common import FlowClass, PreviewLive
6
6
  from tumblrbot.utils.models import Post
7
7
 
8
8
 
9
- class DraftGenerator(UtilClass):
9
+ class DraftGenerator(FlowClass):
10
10
  def generate_tags(self, content: Post.Block) -> Post | None:
11
11
  if random() < self.config.tags_chance: # noqa: S311
12
12
  return self.openai.responses.parse(
@@ -47,8 +47,8 @@ class DraftGenerator(UtilClass):
47
47
  post = self.generate_post()
48
48
  self.tumblr.create_post(self.config.upload_blog_identifier, post)
49
49
  live.custom_update(post)
50
- except BaseException as exc:
51
- exc.add_note(f"📉 An error occurred! Generated {i} draft(s) before failing. {message}")
50
+ except BaseException as exception:
51
+ exception.add_note(f"📉 An error occurred! Generated {i} draft(s) before failing. {message}")
52
52
  raise
53
53
 
54
54
  rich.print(f":chart_increasing: [bold green]Generated {self.config.draft_count} draft(s).[/] {message}")
@@ -9,7 +9,7 @@ from rich.live import Live
9
9
  from rich.progress import MofNCompleteColumn, Progress, SpinnerColumn, TimeElapsedColumn
10
10
  from rich.table import Table
11
11
 
12
- from tumblrbot.utils.settings import Config
12
+ from tumblrbot.utils.config import Config
13
13
  from tumblrbot.utils.tumblr import TumblrClient
14
14
 
15
15
 
@@ -41,7 +41,7 @@ class PreviewLive(Live):
41
41
 
42
42
 
43
43
  @dataclass
44
- class UtilClass:
44
+ class FlowClass:
45
45
  config: ClassVar = Config() # pyright: ignore[reportCallIssue]
46
46
 
47
47
  openai: OpenAI
@@ -1,16 +1,13 @@
1
- import json
2
- from collections.abc import Generator, Sequence
1
+ from collections.abc import Sequence
3
2
  from pathlib import Path
4
- from typing import TYPE_CHECKING, Any, ClassVar, Self, override
3
+ from typing import TYPE_CHECKING, Self, override
5
4
 
6
5
  import rich
7
6
  import tomlkit
8
- from keyring import get_password, set_password
9
7
  from openai.types import ChatModel
10
8
  from pydantic import Field, PositiveFloat, PositiveInt, Secret, model_validator
11
9
  from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, TomlConfigSettingsSource
12
- from requests_oauthlib import OAuth2Session
13
- from rich.prompt import Confirm, Prompt
10
+ from rich.prompt import Prompt
14
11
  from tomlkit import comment, document
15
12
 
16
13
  if TYPE_CHECKING:
@@ -92,70 +89,3 @@ class Config(BaseSettings):
92
89
  tomlkit.dumps(toml_table), # pyright: ignore[reportUnknownMemberType]
93
90
  encoding="utf_8",
94
91
  )
95
-
96
-
97
- class Tokens(BaseSettings):
98
- service_name: ClassVar = "tumblrbot"
99
- model_config = SettingsConfigDict(toml_file="env.toml")
100
-
101
- openai_api_key: Secret[str] = Secret("")
102
- tumblr_client_id: Secret[str] = Secret("")
103
- tumblr_client_secret: Secret[str] = Secret("")
104
- tumblr_token: Secret[Any] = Secret({})
105
-
106
- @staticmethod
107
- def online_token_prompt(url: str, *tokens: str) -> Generator[Secret[str]]:
108
- formatted_tokens = [f"[cyan]{token}[/]" for token in tokens]
109
- formatted_token_string = " and ".join(formatted_tokens)
110
-
111
- rich.print(f"Retrieve your {formatted_token_string} from: {url}")
112
- for token in formatted_tokens:
113
- prompt = f"Enter your {token} [yellow](hidden)"
114
- yield Secret(Prompt.ask(prompt, password=True).strip())
115
-
116
- rich.print()
117
-
118
- @override
119
- def model_post_init(self, context: object) -> None:
120
- super().model_post_init(context)
121
-
122
- for name, _ in self:
123
- if value := get_password(self.service_name, name):
124
- setattr(self, name, Secret(json.loads(value)))
125
-
126
- @model_validator(mode="after")
127
- def write_to_keyring(self) -> Self:
128
- if not self.openai_api_key.get_secret_value() or Confirm.ask("Reset OpenAI API key?", default=False):
129
- (self.openai_api_key,) = self.online_token_prompt("https://platform.openai.com/api-keys", "API key")
130
-
131
- if not all(
132
- map(
133
- Secret[Any].get_secret_value,
134
- [
135
- self.tumblr_client_id,
136
- self.tumblr_client_secret,
137
- self.tumblr_token,
138
- ],
139
- ),
140
- ) or Confirm.ask("Reset Tumblr API tokens?", default=False):
141
- self.tumblr_client_id, self.tumblr_client_secret = self.online_token_prompt("https://tumblr.com/oauth/apps", "consumer key", "consumer secret")
142
-
143
- oauth = OAuth2Session(
144
- self.tumblr_client_id.get_secret_value(),
145
- scope=["basic", "write", "offline_access"],
146
- )
147
- authorization_url, _ = oauth.authorization_url("https://tumblr.com/oauth2/authorize") # pyright: ignore[reportUnknownMemberType]
148
- rich.print(f"Please go to {authorization_url} and authorize access.")
149
- self.tumblr_token = Secret(
150
- oauth.fetch_token( # pyright: ignore[reportUnknownMemberType]
151
- "https://api.tumblr.com/v2/oauth2/token",
152
- authorization_response=Prompt.ask("Enter the full callback URL"),
153
- client_secret=self.tumblr_client_secret.get_secret_value(),
154
- ),
155
- )
156
-
157
- for name, value in self:
158
- if isinstance(value, Secret):
159
- set_password(self.service_name, name, json.dumps(value.get_secret_value()))
160
-
161
- return self
@@ -0,0 +1,131 @@
1
+ from collections.abc import Generator
2
+ from typing import Annotated, Any, ClassVar, Literal, override
3
+
4
+ import rich
5
+ from keyring import get_password, set_password
6
+ from openai import BaseModel
7
+ from pydantic import ConfigDict, PlainSerializer, SecretStr
8
+ from pydantic.json_schema import SkipJsonSchema
9
+ from requests_oauthlib import OAuth1Session
10
+ from rich.panel import Panel
11
+ from rich.prompt import Confirm, Prompt
12
+
13
+
14
+ class FullyValidatedModel(BaseModel):
15
+ model_config = ConfigDict(
16
+ extra="ignore",
17
+ validate_assignment=True,
18
+ validate_default=True,
19
+ validate_return=True,
20
+ validate_by_name=True,
21
+ )
22
+
23
+
24
+ class Tokens(FullyValidatedModel):
25
+ service_name: ClassVar = "tumblrbot"
26
+
27
+ openai_api_key: SecretStr = SecretStr("")
28
+ tumblr_client_key: SecretStr = SecretStr("")
29
+ tumblr_client_secret: SecretStr = SecretStr("")
30
+ tumblr_resource_owner_key: SecretStr = SecretStr("")
31
+ tumblr_resource_owner_secret: SecretStr = SecretStr("")
32
+
33
+ @staticmethod
34
+ def online_token_prompt(url: str, *tokens: str) -> Generator[SecretStr]:
35
+ formatted_tokens = [f"[cyan]{token}[/]" for token in tokens]
36
+ formatted_token_string = " and ".join(formatted_tokens)
37
+
38
+ rich.print(f"Retrieve your {formatted_token_string} from: {url}")
39
+ for token in formatted_tokens:
40
+ prompt = f"Enter your {token} [yellow](hidden)"
41
+ yield SecretStr(Prompt.ask(prompt, password=True).strip())
42
+
43
+ rich.print()
44
+
45
+ @override
46
+ def model_post_init(self, context: object) -> None:
47
+ super().model_post_init(context)
48
+
49
+ for name, _ in self:
50
+ if value := get_password(self.service_name, name):
51
+ setattr(self, name, value)
52
+
53
+ if not self.openai_api_key.get_secret_value() or Confirm.ask("Reset OpenAI API key?", default=False):
54
+ (self.openai_api_key,) = self.online_token_prompt("https://platform.openai.com/api-keys", "API key")
55
+
56
+ if not all(self.get_tumblr_tokens()) or Confirm.ask("Reset Tumblr API tokens?", default=False):
57
+ self.tumblr_client_key, self.tumblr_client_secret = self.online_token_prompt("https://tumblr.com/oauth/apps", "consumer key", "consumer secret")
58
+
59
+ oauth_session = OAuth1Session(*self.get_tumblr_tokens()[:2])
60
+ fetch_response = oauth_session.fetch_request_token("http://tumblr.com/oauth/request_token") # pyright: ignore[reportUnknownMemberType]
61
+ full_authorize_url = oauth_session.authorization_url("http://tumblr.com/oauth/authorize") # pyright: ignore[reportUnknownMemberType]
62
+ (redirect_response,) = self.online_token_prompt(full_authorize_url, "full redirect URL")
63
+ oauth_response = oauth_session.parse_authorization_response(redirect_response.get_secret_value())
64
+ oauth_session = OAuth1Session(
65
+ *self.get_tumblr_tokens()[:2],
66
+ fetch_response["oauth_token"],
67
+ fetch_response["oauth_token_secret"],
68
+ verifier=oauth_response["oauth_verifier"],
69
+ )
70
+ oauth_tokens = oauth_session.fetch_access_token("http://tumblr.com/oauth/access_token") # pyright: ignore[reportUnknownMemberType]
71
+ self.tumblr_resource_owner_key = oauth_tokens["oauth_token"]
72
+ self.tumblr_resource_owner_secret = oauth_tokens["oauth_token_secret"]
73
+
74
+ for name, value in self:
75
+ if isinstance(value, SecretStr):
76
+ set_password(self.service_name, name, value.get_secret_value())
77
+
78
+ def get_tumblr_tokens(self) -> tuple[str, str, str, str]:
79
+ return (
80
+ self.tumblr_client_key.get_secret_value(),
81
+ self.tumblr_client_secret.get_secret_value(),
82
+ self.tumblr_resource_owner_key.get_secret_value(),
83
+ self.tumblr_resource_owner_secret.get_secret_value(),
84
+ )
85
+
86
+
87
+ class Post(FullyValidatedModel):
88
+ class Block(FullyValidatedModel):
89
+ type: str = ""
90
+ text: str = ""
91
+ blocks: list[int] = [] # noqa: RUF012
92
+
93
+ timestamp: SkipJsonSchema[int] = 0
94
+ tags: Annotated[list[str], PlainSerializer(",".join)] = [] # noqa: RUF012
95
+ state: SkipJsonSchema[Literal["published", "queued", "draft", "private", "unapproved"]] = "published"
96
+
97
+ content: SkipJsonSchema[list[Block]] = [] # noqa: RUF012
98
+ layout: SkipJsonSchema[list[Block]] = [] # noqa: RUF012
99
+ trail: SkipJsonSchema[list[Any]] = [] # noqa: RUF012
100
+
101
+ is_submission: SkipJsonSchema[bool] = False
102
+
103
+ def __rich__(self) -> Panel:
104
+ return Panel(
105
+ self.get_text_content(),
106
+ title="Preview",
107
+ subtitle=" ".join(f"#{tag}" for tag in self.tags),
108
+ subtitle_align="left",
109
+ )
110
+
111
+ @override
112
+ def model_post_init(self, context: object) -> None:
113
+ super().model_post_init(context)
114
+
115
+ indices: set[int] = set()
116
+ for block in self.layout:
117
+ if block.type == "ask":
118
+ indices.update(block.blocks)
119
+
120
+ self.content = [block for i, block in enumerate(self.content) if i not in indices and block.type == "text"]
121
+
122
+ def get_text_content(self) -> str:
123
+ return "\n\n".join(block.text for block in self.content)
124
+
125
+
126
+ class Example(FullyValidatedModel):
127
+ class Message(FullyValidatedModel):
128
+ role: Literal["developer", "user", "assistant"]
129
+ content: str
130
+
131
+ messages: list[Message]
@@ -1,35 +1,20 @@
1
1
  from dataclasses import dataclass
2
2
 
3
- from pydantic import Secret
4
3
  from requests import HTTPError, Response
5
- from requests_oauthlib import OAuth2Session
4
+ from requests_oauthlib import OAuth1Session
6
5
 
7
- from tumblrbot.utils.models import Post
8
- from tumblrbot.utils.settings import Tokens
6
+ from tumblrbot.utils.models import Post, Tokens
9
7
 
10
8
 
11
9
  @dataclass
12
- class TumblrClient(OAuth2Session):
10
+ class TumblrClient(OAuth1Session):
13
11
  tokens: Tokens
14
12
 
15
13
  def __post_init__(self) -> None:
16
- super().__init__( # pyright: ignore[reportUnknownMemberType]
17
- self.tokens.tumblr_client_id.get_secret_value(),
18
- auto_refresh_url="https://api.tumblr.com/v2/oauth2/token",
19
- auto_refresh_kwargs={
20
- "client_id": self.tokens.tumblr_client_id.get_secret_value(),
21
- "client_secret": self.tokens.tumblr_client_secret.get_secret_value(),
22
- "token": self.tokens.tumblr_token.get_secret_value(),
23
- },
24
- token=self.tokens.tumblr_token.get_secret_value(),
25
- token_updater=self.token_updater,
26
- )
14
+ super().__init__(*self.tokens.get_tumblr_tokens()) # pyright: ignore[reportUnknownMemberType]
27
15
 
28
16
  self.hooks["response"].append(self.response_hook)
29
17
 
30
- def token_updater(self, token: object) -> None:
31
- self.tokens.tumblr_token = Secret(token)
32
-
33
18
  def response_hook(self, response: Response, **_: object) -> None:
34
19
  try:
35
20
  response.raise_for_status()
tumblrbot-1.2.0/PKG-INFO DELETED
@@ -1,109 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: tumblrbot
3
- Version: 1.2.0
4
- Summary: An updated bot that posts to Tumblr, based on your very own blog!
5
- Requires-Python: >= 3.13
6
- Description-Content-Type: text/markdown
7
- Requires-Dist: keyring
8
- Requires-Dist: more-itertools
9
- Requires-Dist: openai
10
- Requires-Dist: pydantic
11
- Requires-Dist: pydantic-settings
12
- Requires-Dist: requests
13
- Requires-Dist: requests-oauthlib
14
- Requires-Dist: rich
15
- Requires-Dist: tiktoken
16
- Requires-Dist: tomlkit
17
- Project-URL: Source, https://github.com/MaidThatPrograms/tumblrbot
18
-
19
- [OpenAI]: https://pypi.org/project/openai
20
- [Python]: https://python.org/download
21
- [Rich]: https://pypi.org/project/rich
22
-
23
- [gpt-4.1-nano-2025-04-14]: https://platform.openai.com/docs/models/gpt-4.1-nano
24
- [Moderation API]: https://platform.openai.com/docs/api-reference/moderations
25
- [New Post Format]: https://tumblr.com/docs/npf
26
- [OAuth 2.0]: https://www.tumblr.com/docs/en/api/v2#oauth2-authorization
27
- [pip]: https://pypi.org
28
-
29
- [Download]: tumblrbot/flow/download.py
30
- [Examples]: tumblrbot/flow/examples.py
31
- [Fine-Tune]: tumblrbot/flow/fine_tune.py
32
- [Generate]: tumblrbot/flow/generate.py
33
- [Settings]: tumblrbot/utils/settings.py
34
- [Main]: __main__.py
35
- [README.md]: README.md
36
-
37
- # tumblrbot
38
- [![PyPI - Version](https://img.shields.io/pypi/v/tumblrbot)](https://python.org/pypi/tumblrbot)
39
-
40
- Description of original project:
41
- > 4tv-tumblrbot was a collaborative project I embarked on with my close friend Dima, who goes by @smoqueen on Tumblr. The aim of this endeavor was straightforward yet silly: to develop a Tumblr bot powered by a machine-learning model. This bot would be specifically trained on the content from a particular Tumblr blog or a selected set of blogs, allowing it to mimic the style, tone, and thematic essence of the original posts.
42
-
43
- This fork is largely a rewrite of the source code with similarities in its structure and process:
44
- - Updates:
45
- - Updated to [OAuth 2.0].
46
- - Updated to the [New Post Format].
47
- - Updated to the latest version of [OpenAI].
48
- - Updated the [base model version][Settings] to [gpt-4.1-nano-2025-04-14].
49
- - Removed features:
50
- - [Generation][Generate]:
51
- - Removed clearing drafts behavior.
52
- - [Training][Examples]:
53
- - Removed exports that had HTML or reblogs.
54
- - Removed special word-replacement behavior.
55
- - Removed filtering by year.
56
- - Removed setup and related files.
57
- - Changed/Added features:
58
- - [Generation][Generate]:
59
- - Added a link to the blog's draft page.
60
- - Added error checking for uploading drafts.
61
- - [Training][Examples]:
62
- - Added the option to [Download] the latest posts from the [specified blogs][Settings].
63
- - Added the option to remove posts flagged by the [Moderation API].
64
- - Added the option to automatically [Fine-Tune] the examples on the [specified base model][Settings].
65
- - Added the ability to add custom prompts and responses to the example data.
66
- - Changed to now escape examples automatically.
67
- - Set encoding for reading post data to `UTF-8` to fix decoding errors.
68
- - Added newlines between paragraphs.
69
- - Removed "ALT", submission, ask, and poll text from posts.
70
- - Improved the estimated token counts and costs.
71
- - Changed to [Rich] for output.
72
- - Added progress bars.
73
- - Added post previews.
74
- - Added color, formatting, and more information to output.
75
- - Created a [guided utility][Main] for every step of building your bot blog.
76
- - Maid scripts wait for user input before the console closes.
77
- - Added comand-line options to override [Settings] options.
78
- - Added behavior to regenerate the default [config.toml][Settings] and [env.toml][Settings] if missing.
79
- - Renamed several files.
80
- - Renamed several [Settings] options.
81
- - Changed the value of several [Settings] options.
82
- - Added full type-checking coverage (fully importable from third-party scripts).
83
-
84
- To-Do:
85
- - Add documentation.
86
- - Finish updating [README.md].
87
- - Change the differences list to instead just be a list of features.
88
-
89
-
90
- **Please submit an issue or contact us for features you want to added/reimplemented.**
91
-
92
- ## Installation
93
- 1. Install the latest version of [Python]:
94
- - Windows: `winget install python3`
95
- - Linux (apt): `apt install python-pip`
96
- - Linux (pacman): `pacman install python-pip`
97
- 1. Install the [pip] package: `pip install tumblrbot`
98
- - Alternatively, you can install from this repository: `pip install git+https://github.com/MaidThatPrograms/tumblrbot.git`
99
- - On Linux, you will have to make a virtual environment.
100
-
101
- ## Usage
102
- Run `tumblrbot` from anywhere. Run `tumblrbot --help` for command-line options.
103
-
104
- ## Obtaining Tokens
105
- > WIP
106
-
107
- ## Configuration
108
- > WIP
109
-
tumblrbot-1.2.0/README.md DELETED
@@ -1,90 +0,0 @@
1
- [OpenAI]: https://pypi.org/project/openai
2
- [Python]: https://python.org/download
3
- [Rich]: https://pypi.org/project/rich
4
-
5
- [gpt-4.1-nano-2025-04-14]: https://platform.openai.com/docs/models/gpt-4.1-nano
6
- [Moderation API]: https://platform.openai.com/docs/api-reference/moderations
7
- [New Post Format]: https://tumblr.com/docs/npf
8
- [OAuth 2.0]: https://www.tumblr.com/docs/en/api/v2#oauth2-authorization
9
- [pip]: https://pypi.org
10
-
11
- [Download]: tumblrbot/flow/download.py
12
- [Examples]: tumblrbot/flow/examples.py
13
- [Fine-Tune]: tumblrbot/flow/fine_tune.py
14
- [Generate]: tumblrbot/flow/generate.py
15
- [Settings]: tumblrbot/utils/settings.py
16
- [Main]: __main__.py
17
- [README.md]: README.md
18
-
19
- # tumblrbot
20
- [![PyPI - Version](https://img.shields.io/pypi/v/tumblrbot)](https://python.org/pypi/tumblrbot)
21
-
22
- Description of original project:
23
- > 4tv-tumblrbot was a collaborative project I embarked on with my close friend Dima, who goes by @smoqueen on Tumblr. The aim of this endeavor was straightforward yet silly: to develop a Tumblr bot powered by a machine-learning model. This bot would be specifically trained on the content from a particular Tumblr blog or a selected set of blogs, allowing it to mimic the style, tone, and thematic essence of the original posts.
24
-
25
- This fork is largely a rewrite of the source code with similarities in its structure and process:
26
- - Updates:
27
- - Updated to [OAuth 2.0].
28
- - Updated to the [New Post Format].
29
- - Updated to the latest version of [OpenAI].
30
- - Updated the [base model version][Settings] to [gpt-4.1-nano-2025-04-14].
31
- - Removed features:
32
- - [Generation][Generate]:
33
- - Removed clearing drafts behavior.
34
- - [Training][Examples]:
35
- - Removed exports that had HTML or reblogs.
36
- - Removed special word-replacement behavior.
37
- - Removed filtering by year.
38
- - Removed setup and related files.
39
- - Changed/Added features:
40
- - [Generation][Generate]:
41
- - Added a link to the blog's draft page.
42
- - Added error checking for uploading drafts.
43
- - [Training][Examples]:
44
- - Added the option to [Download] the latest posts from the [specified blogs][Settings].
45
- - Added the option to remove posts flagged by the [Moderation API].
46
- - Added the option to automatically [Fine-Tune] the examples on the [specified base model][Settings].
47
- - Added the ability to add custom prompts and responses to the example data.
48
- - Changed to now escape examples automatically.
49
- - Set encoding for reading post data to `UTF-8` to fix decoding errors.
50
- - Added newlines between paragraphs.
51
- - Removed "ALT", submission, ask, and poll text from posts.
52
- - Improved the estimated token counts and costs.
53
- - Changed to [Rich] for output.
54
- - Added progress bars.
55
- - Added post previews.
56
- - Added color, formatting, and more information to output.
57
- - Created a [guided utility][Main] for every step of building your bot blog.
58
- - Maid scripts wait for user input before the console closes.
59
- - Added comand-line options to override [Settings] options.
60
- - Added behavior to regenerate the default [config.toml][Settings] and [env.toml][Settings] if missing.
61
- - Renamed several files.
62
- - Renamed several [Settings] options.
63
- - Changed the value of several [Settings] options.
64
- - Added full type-checking coverage (fully importable from third-party scripts).
65
-
66
- To-Do:
67
- - Add documentation.
68
- - Finish updating [README.md].
69
- - Change the differences list to instead just be a list of features.
70
-
71
-
72
- **Please submit an issue or contact us for features you want to added/reimplemented.**
73
-
74
- ## Installation
75
- 1. Install the latest version of [Python]:
76
- - Windows: `winget install python3`
77
- - Linux (apt): `apt install python-pip`
78
- - Linux (pacman): `pacman install python-pip`
79
- 1. Install the [pip] package: `pip install tumblrbot`
80
- - Alternatively, you can install from this repository: `pip install git+https://github.com/MaidThatPrograms/tumblrbot.git`
81
- - On Linux, you will have to make a virtual environment.
82
-
83
- ## Usage
84
- Run `tumblrbot` from anywhere. Run `tumblrbot --help` for command-line options.
85
-
86
- ## Obtaining Tokens
87
- > WIP
88
-
89
- ## Configuration
90
- > WIP
@@ -1,63 +0,0 @@
1
- from typing import Annotated, Any, Literal, override
2
-
3
- from openai import BaseModel
4
- from pydantic import ConfigDict, PlainSerializer
5
- from pydantic.json_schema import SkipJsonSchema
6
- from rich.panel import Panel
7
-
8
-
9
- class FullyValidatedModel(BaseModel):
10
- model_config = ConfigDict(
11
- extra="ignore",
12
- validate_assignment=True,
13
- validate_default=True,
14
- validate_return=True,
15
- validate_by_name=True,
16
- )
17
-
18
-
19
- class Post(FullyValidatedModel):
20
- class Block(FullyValidatedModel):
21
- type: str = ""
22
- text: str = ""
23
- blocks: list[int] = [] # noqa: RUF012
24
-
25
- timestamp: SkipJsonSchema[int] = 0
26
- tags: Annotated[SkipJsonSchema[list[str]], PlainSerializer(",".join)] = [] # noqa: RUF012
27
- state: SkipJsonSchema[Literal["published", "queued", "draft", "private", "unapproved"]] = "published"
28
-
29
- content: SkipJsonSchema[list[Block]] = [] # noqa: RUF012
30
- layout: SkipJsonSchema[list[Block]] = [] # noqa: RUF012
31
- trail: SkipJsonSchema[list[Any]] = [] # noqa: RUF012
32
-
33
- is_submission: SkipJsonSchema[bool] = False
34
-
35
- def __rich__(self) -> Panel:
36
- return Panel(
37
- self.get_text_content(),
38
- title="Preview",
39
- subtitle=" ".join(f"#{tag}" for tag in self.tags),
40
- subtitle_align="left",
41
- )
42
-
43
- @override
44
- def model_post_init(self, context: object) -> None:
45
- super().model_post_init(context)
46
-
47
- indices: set[int] = set()
48
- for block in self.layout:
49
- if block.type == "ask":
50
- indices.update(block.blocks)
51
-
52
- self.content = [block for i, block in enumerate(self.content) if i not in indices and block.type == "text"]
53
-
54
- def get_text_content(self) -> str:
55
- return "\n\n".join(block.text for block in self.content)
56
-
57
-
58
- class Example(FullyValidatedModel):
59
- class Message(FullyValidatedModel):
60
- role: Literal["developer", "user", "assistant"]
61
- content: str
62
-
63
- messages: list[Message]
File without changes
File without changes