tumblrbot 1.4.0__tar.gz → 1.4.2__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.
- {tumblrbot-1.4.0 → tumblrbot-1.4.2}/PKG-INFO +6 -2
- {tumblrbot-1.4.0 → tumblrbot-1.4.2}/README.md +4 -1
- {tumblrbot-1.4.0 → tumblrbot-1.4.2}/pyproject.toml +2 -1
- {tumblrbot-1.4.0 → tumblrbot-1.4.2}/src/tumblrbot/__main__.py +7 -4
- {tumblrbot-1.4.0 → tumblrbot-1.4.2}/src/tumblrbot/flow/examples.py +1 -1
- {tumblrbot-1.4.0 → tumblrbot-1.4.2}/src/tumblrbot/flow/fine_tune.py +25 -16
- {tumblrbot-1.4.0 → tumblrbot-1.4.2}/src/tumblrbot/utils/common.py +2 -2
- {tumblrbot-1.4.0 → tumblrbot-1.4.2}/src/tumblrbot/utils/models.py +46 -36
- {tumblrbot-1.4.0 → tumblrbot-1.4.2}/src/tumblrbot/utils/tumblr.py +14 -14
- {tumblrbot-1.4.0 → tumblrbot-1.4.2}/.github/dependabot.yml +0 -0
- {tumblrbot-1.4.0 → tumblrbot-1.4.2}/.gitignore +0 -0
- {tumblrbot-1.4.0 → tumblrbot-1.4.2}/UNLICENSE +0 -0
- {tumblrbot-1.4.0 → tumblrbot-1.4.2}/src/tumblrbot/__init__.py +0 -0
- {tumblrbot-1.4.0 → tumblrbot-1.4.2}/src/tumblrbot/flow/__init__.py +0 -0
- {tumblrbot-1.4.0 → tumblrbot-1.4.2}/src/tumblrbot/flow/download.py +0 -0
- {tumblrbot-1.4.0 → tumblrbot-1.4.2}/src/tumblrbot/flow/generate.py +0 -0
- {tumblrbot-1.4.0 → tumblrbot-1.4.2}/src/tumblrbot/utils/__init__.py +0 -0
- {tumblrbot-1.4.0 → tumblrbot-1.4.2}/src/tumblrbot/utils/config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tumblrbot
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.2
|
|
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
|
|
@@ -11,6 +11,7 @@ Requires-Dist: niquests[speedups, http3]
|
|
|
11
11
|
Requires-Dist: openai
|
|
12
12
|
Requires-Dist: pydantic
|
|
13
13
|
Requires-Dist: pydantic-settings
|
|
14
|
+
Requires-Dist: requests
|
|
14
15
|
Requires-Dist: requests-cache
|
|
15
16
|
Requires-Dist: requests-oauthlib
|
|
16
17
|
Requires-Dist: rich
|
|
@@ -73,6 +74,8 @@ Features:
|
|
|
73
74
|
|
|
74
75
|
**To-Do:**
|
|
75
76
|
- Add code documentation.
|
|
77
|
+
- Fix inaccurate post counts when downloading posts.
|
|
78
|
+
- Fix file not found error when starting fine-tuning.
|
|
76
79
|
|
|
77
80
|
|
|
78
81
|
**Please submit an issue or contact us for features you want added/reimplemented.**
|
|
@@ -84,7 +87,8 @@ Features:
|
|
|
84
87
|
- Linux (pacman): `pacman install python-pip`
|
|
85
88
|
1. Install the [pip] package: `pip install tumblrbot`
|
|
86
89
|
- 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.
|
|
90
|
+
- On Linux, you will have to make a virtual environment or use the flag to install packages system-wide.
|
|
91
|
+
- See [keyring] for additional requirements if you are not on Windows.
|
|
88
92
|
|
|
89
93
|
## Usage
|
|
90
94
|
Run `tumblrbot` from anywhere. Run `tumblrbot --help` for command-line options. Every command-line option corresponds to a value from the [config](#configuration).
|
|
@@ -53,6 +53,8 @@ Features:
|
|
|
53
53
|
|
|
54
54
|
**To-Do:**
|
|
55
55
|
- Add code documentation.
|
|
56
|
+
- Fix inaccurate post counts when downloading posts.
|
|
57
|
+
- Fix file not found error when starting fine-tuning.
|
|
56
58
|
|
|
57
59
|
|
|
58
60
|
**Please submit an issue or contact us for features you want added/reimplemented.**
|
|
@@ -64,7 +66,8 @@ Features:
|
|
|
64
66
|
- Linux (pacman): `pacman install python-pip`
|
|
65
67
|
1. Install the [pip] package: `pip install tumblrbot`
|
|
66
68
|
- Alternatively, you can install from this repository: `pip install git+https://github.com/MaidThatPrograms/tumblrbot.git`
|
|
67
|
-
- On Linux, you will have to make a virtual environment.
|
|
69
|
+
- On Linux, you will have to make a virtual environment or use the flag to install packages system-wide.
|
|
70
|
+
- See [keyring] for additional requirements if you are not on Windows.
|
|
68
71
|
|
|
69
72
|
## Usage
|
|
70
73
|
Run `tumblrbot` from anywhere. Run `tumblrbot --help` for command-line options. Every command-line option corresponds to a value from the [config](#configuration).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tumblrbot"
|
|
3
|
-
version = "1.4.
|
|
3
|
+
version = "1.4.2"
|
|
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"
|
|
@@ -12,6 +12,7 @@ dependencies = [
|
|
|
12
12
|
"openai",
|
|
13
13
|
"pydantic",
|
|
14
14
|
"pydantic-settings",
|
|
15
|
+
"requests",
|
|
15
16
|
"requests-cache",
|
|
16
17
|
"requests-oauthlib",
|
|
17
18
|
"rich",
|
|
@@ -6,17 +6,18 @@ 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 FlowClass
|
|
9
10
|
from tumblrbot.utils.models import Tokens
|
|
10
|
-
from tumblrbot.utils.tumblr import
|
|
11
|
+
from tumblrbot.utils.tumblr import TumblrSession
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
def main() -> None:
|
|
14
15
|
install()
|
|
15
16
|
|
|
16
|
-
tokens = Tokens()
|
|
17
|
+
tokens = Tokens.read_from_keyring()
|
|
17
18
|
with (
|
|
18
19
|
OpenAI(api_key=tokens.openai_api_key.get_secret_value(), http_client=DefaultHttpxClient(http2=True)) as openai,
|
|
19
|
-
|
|
20
|
+
TumblrSession(tokens=tokens) as tumblr,
|
|
20
21
|
):
|
|
21
22
|
post_downloader = PostDownloader(openai, tumblr)
|
|
22
23
|
if Confirm.ask("Download latest posts?", default=False):
|
|
@@ -30,7 +31,9 @@ def main() -> None:
|
|
|
30
31
|
|
|
31
32
|
fine_tuner = FineTuner(openai, tumblr, estimated_tokens)
|
|
32
33
|
fine_tuner.print_estimates()
|
|
33
|
-
|
|
34
|
+
|
|
35
|
+
message = "Resume monitoring the previous fine-tuning process?" if FlowClass.config.job_id else "Upload data to OpenAI for fine-tuning?"
|
|
36
|
+
if Confirm.ask(f"{message} [bold]You must do this to set the model to generate drafts from. Alternatively, manually enter a model into the config", default=False):
|
|
34
37
|
fine_tuner.fine_tune()
|
|
35
38
|
|
|
36
39
|
if Confirm.ask("Generate drafts?", default=False):
|
|
@@ -59,7 +59,7 @@ class ExamplesWriter(FlowClass):
|
|
|
59
59
|
def get_filtered_posts(self) -> Generator[Post]:
|
|
60
60
|
posts = list(self.get_valid_posts())
|
|
61
61
|
|
|
62
|
-
if Confirm.ask("Remove posts flagged by the OpenAI moderation? This can sometimes resolve errors with fine-tuning validation, but is slow.", default=False):
|
|
62
|
+
if Confirm.ask("[gray62]Remove posts flagged by the OpenAI moderation? This can sometimes resolve errors with fine-tuning validation, but is slow.", default=False):
|
|
63
63
|
removed = 0
|
|
64
64
|
chunk_size = self.get_moderation_chunk_limit()
|
|
65
65
|
with PreviewLive() as live:
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from datetime import datetime
|
|
3
3
|
from textwrap import dedent
|
|
4
|
-
from time import sleep
|
|
4
|
+
from time import sleep, time
|
|
5
5
|
|
|
6
6
|
import rich
|
|
7
7
|
from openai.types.fine_tuning import FineTuningJob
|
|
8
|
+
from rich import progress
|
|
9
|
+
from rich.prompt import Confirm
|
|
8
10
|
|
|
9
11
|
from tumblrbot.utils.common import FlowClass, PreviewLive
|
|
10
12
|
|
|
@@ -18,26 +20,27 @@ class FineTuner(FlowClass):
|
|
|
18
20
|
rich.print(dedent(text).lstrip())
|
|
19
21
|
|
|
20
22
|
def fine_tune(self) -> None:
|
|
21
|
-
|
|
22
|
-
job = self.create_job(live)
|
|
23
|
+
job = self.create_job()
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
self.dedent_print(f"""
|
|
26
|
+
[bold]Fine-tuning is starting...[/]
|
|
27
|
+
View it online at: https://platform.openai.com/finetune/{job.id}
|
|
28
|
+
Created at: {datetime.fromtimestamp(job.created_at)}
|
|
29
|
+
Base Model: {job.model}
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
[italic dim]Closing this terminal will not stop the fine-tuning. This will take a while...\
|
|
32
|
+
""") # noqa: DTZ006
|
|
32
33
|
|
|
34
|
+
with PreviewLive() as live:
|
|
33
35
|
task_id = live.progress.add_task("", total=None)
|
|
34
36
|
|
|
35
|
-
while job.status
|
|
37
|
+
while job.status in {"validating_files", "queued", "running"}:
|
|
36
38
|
job = self.poll_job_status()
|
|
37
39
|
|
|
38
40
|
live.progress.update(
|
|
39
41
|
task_id,
|
|
40
|
-
total=job.estimated_finish,
|
|
42
|
+
total=job.estimated_finish - job.created_at if job.estimated_finish else None,
|
|
43
|
+
completed=time() - job.created_at,
|
|
41
44
|
description=f"Fine-tuning: [italic]{job.status.replace('_', ' ').title()}[/]...",
|
|
42
45
|
)
|
|
43
46
|
|
|
@@ -45,15 +48,16 @@ class FineTuner(FlowClass):
|
|
|
45
48
|
|
|
46
49
|
self.process_completed_job(job)
|
|
47
50
|
|
|
48
|
-
def create_job(self
|
|
51
|
+
def create_job(self) -> FineTuningJob:
|
|
49
52
|
if self.config.job_id:
|
|
50
53
|
return self.poll_job_status()
|
|
51
54
|
|
|
52
|
-
with
|
|
55
|
+
with progress.open(self.config.examples_file, "rb", description=f"Uploading [purple]{self.config.examples_file}[/]...") as fp:
|
|
53
56
|
file = self.openai.files.create(
|
|
54
57
|
file=fp,
|
|
55
58
|
purpose="fine-tune",
|
|
56
59
|
)
|
|
60
|
+
rich.print()
|
|
57
61
|
|
|
58
62
|
job = self.openai.fine_tuning.jobs.create(
|
|
59
63
|
model=self.config.base_model,
|
|
@@ -86,8 +90,13 @@ class FineTuner(FlowClass):
|
|
|
86
90
|
|
|
87
91
|
self.config.job_id = ""
|
|
88
92
|
|
|
89
|
-
if job.status
|
|
90
|
-
|
|
93
|
+
if job.status != "succeeded":
|
|
94
|
+
if Confirm.ask("[gray62]Delete uploaded examples file?", default=False):
|
|
95
|
+
self.openai.files.delete(job.training_file)
|
|
96
|
+
rich.print()
|
|
97
|
+
|
|
98
|
+
if job.status == "failed" and job.error is not None:
|
|
99
|
+
raise RuntimeError(job.error.message)
|
|
91
100
|
|
|
92
101
|
if job.fine_tuned_model:
|
|
93
102
|
self.config.fine_tuned_model = job.fine_tuned_model or ""
|
|
@@ -10,7 +10,7 @@ from rich.progress import MofNCompleteColumn, Progress, SpinnerColumn, TimeElaps
|
|
|
10
10
|
from rich.table import Table
|
|
11
11
|
|
|
12
12
|
from tumblrbot.utils.config import Config
|
|
13
|
-
from tumblrbot.utils.tumblr import
|
|
13
|
+
from tumblrbot.utils.tumblr import TumblrSession
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
@dataclass
|
|
@@ -18,7 +18,7 @@ class FlowClass:
|
|
|
18
18
|
config: ClassVar = Config() # pyright: ignore[reportCallIssue]
|
|
19
19
|
|
|
20
20
|
openai: OpenAI
|
|
21
|
-
tumblr:
|
|
21
|
+
tumblr: TumblrSession
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
class PreviewLive(Live):
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from collections.abc import Generator
|
|
2
|
-
from typing import Annotated, Any, ClassVar, Literal, override
|
|
2
|
+
from typing import Annotated, Any, ClassVar, Literal, Self, override
|
|
3
3
|
|
|
4
4
|
import rich
|
|
5
5
|
from keyring import get_password, set_password
|
|
@@ -10,6 +10,14 @@ from requests_oauthlib import OAuth1Session
|
|
|
10
10
|
from rich.panel import Panel
|
|
11
11
|
from rich.prompt import Confirm, Prompt
|
|
12
12
|
|
|
13
|
+
type SerializableSecretStr = Annotated[
|
|
14
|
+
SecretStr,
|
|
15
|
+
PlainSerializer(
|
|
16
|
+
SecretStr.get_secret_value,
|
|
17
|
+
when_used="json-unless-none",
|
|
18
|
+
),
|
|
19
|
+
]
|
|
20
|
+
|
|
13
21
|
|
|
14
22
|
class FullyValidatedModel(BaseModel):
|
|
15
23
|
model_config = ConfigDict(
|
|
@@ -22,13 +30,17 @@ class FullyValidatedModel(BaseModel):
|
|
|
22
30
|
|
|
23
31
|
|
|
24
32
|
class Tokens(FullyValidatedModel):
|
|
33
|
+
class Tumblr(FullyValidatedModel):
|
|
34
|
+
client_key: SerializableSecretStr = SecretStr("")
|
|
35
|
+
client_secret: SerializableSecretStr = SecretStr("")
|
|
36
|
+
resource_owner_key: SerializableSecretStr = SecretStr("")
|
|
37
|
+
resource_owner_secret: SerializableSecretStr = SecretStr("")
|
|
38
|
+
|
|
25
39
|
service_name: ClassVar = "tumblrbot"
|
|
40
|
+
username: ClassVar = "tokens"
|
|
26
41
|
|
|
27
|
-
openai_api_key:
|
|
28
|
-
|
|
29
|
-
tumblr_client_secret: SecretStr = SecretStr("")
|
|
30
|
-
tumblr_resource_owner_key: SecretStr = SecretStr("")
|
|
31
|
-
tumblr_resource_owner_secret: SecretStr = SecretStr("")
|
|
42
|
+
openai_api_key: SerializableSecretStr = SecretStr("")
|
|
43
|
+
tumblr: Tumblr = Tumblr()
|
|
32
44
|
|
|
33
45
|
@staticmethod
|
|
34
46
|
def online_token_prompt(url: str, *tokens: str) -> Generator[SecretStr]:
|
|
@@ -42,46 +54,44 @@ class Tokens(FullyValidatedModel):
|
|
|
42
54
|
|
|
43
55
|
rich.print()
|
|
44
56
|
|
|
57
|
+
@classmethod
|
|
58
|
+
def read_from_keyring(cls) -> Self:
|
|
59
|
+
if json_data := get_password(cls.service_name, cls.username):
|
|
60
|
+
return cls.model_validate_json(json_data)
|
|
61
|
+
return cls()
|
|
62
|
+
|
|
45
63
|
@override
|
|
46
64
|
def model_post_init(self, context: object) -> None:
|
|
47
65
|
super().model_post_init(context)
|
|
48
66
|
|
|
49
|
-
for name, _ in self:
|
|
50
|
-
if value := get_password(self.service_name, name):
|
|
51
|
-
setattr(self, name, value)
|
|
52
|
-
|
|
53
67
|
if not self.openai_api_key.get_secret_value() or Confirm.ask("Reset OpenAI API key?", default=False):
|
|
54
68
|
(self.openai_api_key,) = self.online_token_prompt("https://platform.openai.com/api-keys", "API key")
|
|
55
69
|
|
|
56
|
-
if not all(self.
|
|
57
|
-
self.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
if not all(self.tumblr.model_dump(mode="json").values()) or Confirm.ask("Reset Tumblr API tokens?", default=False):
|
|
71
|
+
self.tumblr.client_key, self.tumblr.client_secret = self.online_token_prompt("https://tumblr.com/oauth/apps", "consumer key", "consumer secret")
|
|
72
|
+
|
|
73
|
+
with OAuth1Session(
|
|
74
|
+
self.tumblr.client_key.get_secret_value(),
|
|
75
|
+
self.tumblr.client_secret.get_secret_value(),
|
|
76
|
+
) as oauth_session:
|
|
77
|
+
fetch_response = oauth_session.fetch_request_token("http://tumblr.com/oauth/request_token")
|
|
78
|
+
full_authorize_url = oauth_session.authorization_url("http://tumblr.com/oauth/authorize")
|
|
79
|
+
(redirect_response,) = self.online_token_prompt(full_authorize_url, "full redirect URL")
|
|
80
|
+
oauth_response = oauth_session.parse_authorization_response(redirect_response.get_secret_value())
|
|
81
|
+
|
|
82
|
+
with OAuth1Session(
|
|
83
|
+
self.tumblr.client_key.get_secret_value(),
|
|
84
|
+
self.tumblr.client_secret.get_secret_value(),
|
|
66
85
|
fetch_response["oauth_token"],
|
|
67
86
|
fetch_response["oauth_token_secret"],
|
|
68
87
|
verifier=oauth_response["oauth_verifier"],
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
self.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
)
|
|
88
|
+
) as oauth_session:
|
|
89
|
+
oauth_tokens = oauth_session.fetch_access_token("http://tumblr.com/oauth/access_token")
|
|
90
|
+
|
|
91
|
+
self.tumblr.resource_owner_key = oauth_tokens["oauth_token"]
|
|
92
|
+
self.tumblr.resource_owner_secret = oauth_tokens["oauth_token_secret"]
|
|
93
|
+
|
|
94
|
+
set_password(self.service_name, self.username, self.model_dump_json())
|
|
85
95
|
|
|
86
96
|
|
|
87
97
|
class Post(FullyValidatedModel):
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from typing import Self
|
|
3
3
|
|
|
4
|
-
from niquests import HTTPError,
|
|
4
|
+
from niquests import HTTPError, Session
|
|
5
|
+
from requests import Response
|
|
5
6
|
from requests_cache import CacheMixin
|
|
6
7
|
from requests_oauthlib import OAuth1
|
|
7
8
|
|
|
@@ -9,36 +10,35 @@ from tumblrbot.utils.models import Post, Tokens
|
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
@dataclass
|
|
12
|
-
class
|
|
13
|
+
class TumblrSession(CacheMixin, Session): # pyright: ignore[reportIncompatibleMethodOverride, reportIncompatibleVariableOverride]
|
|
13
14
|
tokens: Tokens
|
|
14
15
|
|
|
15
16
|
def __post_init__(self) -> None:
|
|
16
|
-
super().__init__(happy_eyeballs=True)
|
|
17
17
|
CacheMixin.__init__(self, use_cache_dir=True)
|
|
18
|
+
Session.__init__(self, happy_eyeballs=True)
|
|
18
19
|
|
|
19
|
-
self.auth = OAuth1(
|
|
20
|
+
self.auth = OAuth1(**self.tokens.tumblr.model_dump(mode="json"))
|
|
20
21
|
self.hooks["response"].append(self.response_hook)
|
|
21
22
|
|
|
22
23
|
def __enter__(self) -> Self:
|
|
23
24
|
super().__enter__()
|
|
24
25
|
return self
|
|
25
26
|
|
|
26
|
-
def response_hook(self, response:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
raise
|
|
27
|
+
def response_hook(self, response: Response, **_: object) -> None:
|
|
28
|
+
try:
|
|
29
|
+
response.raise_for_status()
|
|
30
|
+
except HTTPError as error:
|
|
31
|
+
if response.text:
|
|
32
|
+
error.add_note(response.text)
|
|
33
|
+
raise
|
|
34
34
|
|
|
35
35
|
def retrieve_published_posts(self, blog_identifier: str, after: int) -> Response:
|
|
36
36
|
return self.get(
|
|
37
37
|
f"https://api.tumblr.com/v2/blog/{blog_identifier}/posts",
|
|
38
38
|
params={
|
|
39
|
-
"after":
|
|
39
|
+
"after": after,
|
|
40
40
|
"sort": "asc",
|
|
41
|
-
"npf":
|
|
41
|
+
"npf": True,
|
|
42
42
|
},
|
|
43
43
|
)
|
|
44
44
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|