tumblrbot 1.4.5__tar.gz → 1.4.6__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.5 → tumblrbot-1.4.6}/PKG-INFO +2 -4
- {tumblrbot-1.4.5 → tumblrbot-1.4.6}/pyproject.toml +4 -6
- {tumblrbot-1.4.5 → tumblrbot-1.4.6}/src/tumblrbot/__main__.py +2 -5
- {tumblrbot-1.4.5 → tumblrbot-1.4.6}/src/tumblrbot/flow/examples.py +11 -12
- {tumblrbot-1.4.5 → tumblrbot-1.4.6}/src/tumblrbot/utils/config.py +3 -3
- {tumblrbot-1.4.5 → tumblrbot-1.4.6}/src/tumblrbot/utils/models.py +23 -32
- tumblrbot-1.4.6/src/tumblrbot/utils/tumblr.py +40 -0
- tumblrbot-1.4.5/src/tumblrbot/utils/tumblr.py +0 -47
- {tumblrbot-1.4.5 → tumblrbot-1.4.6}/.github/dependabot.yml +0 -0
- {tumblrbot-1.4.5 → tumblrbot-1.4.6}/.gitignore +0 -0
- {tumblrbot-1.4.5 → tumblrbot-1.4.6}/README.md +0 -0
- {tumblrbot-1.4.5 → tumblrbot-1.4.6}/UNLICENSE +0 -0
- {tumblrbot-1.4.5 → tumblrbot-1.4.6}/src/tumblrbot/__init__.py +0 -0
- {tumblrbot-1.4.5 → tumblrbot-1.4.6}/src/tumblrbot/flow/__init__.py +0 -0
- {tumblrbot-1.4.5 → tumblrbot-1.4.6}/src/tumblrbot/flow/download.py +0 -0
- {tumblrbot-1.4.5 → tumblrbot-1.4.6}/src/tumblrbot/flow/fine_tune.py +0 -0
- {tumblrbot-1.4.5 → tumblrbot-1.4.6}/src/tumblrbot/flow/generate.py +0 -0
- {tumblrbot-1.4.5 → tumblrbot-1.4.6}/src/tumblrbot/utils/__init__.py +0 -0
- {tumblrbot-1.4.5 → tumblrbot-1.4.6}/src/tumblrbot/utils/common.py +0 -0
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tumblrbot
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.6
|
|
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: httpx[http2]
|
|
8
7
|
Requires-Dist: keyring
|
|
9
|
-
Requires-Dist: more-itertools
|
|
10
|
-
Requires-Dist: niquests[speedups, http3]
|
|
11
8
|
Requires-Dist: openai
|
|
12
9
|
Requires-Dist: pwinput
|
|
13
10
|
Requires-Dist: pydantic
|
|
14
11
|
Requires-Dist: pydantic-settings
|
|
12
|
+
Requires-Dist: requests
|
|
15
13
|
Requires-Dist: requests-oauthlib
|
|
16
14
|
Requires-Dist: rich
|
|
17
15
|
Requires-Dist: tiktoken
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tumblrbot"
|
|
3
|
-
version = "1.4.
|
|
3
|
+
version = "1.4.6"
|
|
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
|
-
"httpx[http2]",
|
|
9
8
|
"keyring",
|
|
10
|
-
"more-itertools",
|
|
11
|
-
"niquests[speedups,http3]",
|
|
12
9
|
"openai",
|
|
13
10
|
"pwinput",
|
|
14
11
|
"pydantic",
|
|
15
12
|
"pydantic-settings",
|
|
13
|
+
"requests",
|
|
16
14
|
"requests-oauthlib",
|
|
17
15
|
"rich",
|
|
18
16
|
"tiktoken",
|
|
@@ -26,5 +24,5 @@ Source = "https://github.com/MaidThatPrograms/tumblrbot"
|
|
|
26
24
|
tumblrbot = "tumblrbot.__main__:main"
|
|
27
25
|
|
|
28
26
|
[build-system]
|
|
29
|
-
requires = ["
|
|
30
|
-
build-backend = "
|
|
27
|
+
requires = ["flit_core"]
|
|
28
|
+
build-backend = "flit_core.buildapi"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from openai import
|
|
1
|
+
from openai import OpenAI
|
|
2
2
|
from rich.prompt import Confirm
|
|
3
3
|
from rich.traceback import install
|
|
4
4
|
|
|
@@ -15,10 +15,7 @@ def main() -> None:
|
|
|
15
15
|
install()
|
|
16
16
|
|
|
17
17
|
tokens = Tokens.read_from_keyring()
|
|
18
|
-
with (
|
|
19
|
-
OpenAI(api_key=tokens.openai_api_key.get_secret_value(), http_client=DefaultHttpxClient(http2=True)) as openai,
|
|
20
|
-
TumblrSession(tokens=tokens) as tumblr,
|
|
21
|
-
):
|
|
18
|
+
with OpenAI(api_key=tokens.openai_api_key) as openai, TumblrSession(tokens) as tumblr:
|
|
22
19
|
if Confirm.ask("Download latest posts?", default=False):
|
|
23
20
|
PostDownloader(openai=openai, tumblr=tumblr).main()
|
|
24
21
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
from collections.abc import Generator
|
|
2
|
+
from itertools import batched
|
|
2
3
|
from json import loads
|
|
3
4
|
from math import ceil
|
|
4
5
|
from re import search
|
|
5
6
|
from typing import IO, override
|
|
6
7
|
|
|
7
8
|
import rich
|
|
8
|
-
from more_itertools import chunked
|
|
9
9
|
from openai import BadRequestError
|
|
10
10
|
from rich.prompt import Confirm
|
|
11
11
|
|
|
@@ -58,18 +58,18 @@ class ExamplesWriter(FlowClass):
|
|
|
58
58
|
posts = self.get_valid_posts()
|
|
59
59
|
|
|
60
60
|
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):
|
|
61
|
-
|
|
61
|
+
batch_size = self.get_moderation_batch_size()
|
|
62
62
|
posts = list(posts)
|
|
63
63
|
removed = 0
|
|
64
64
|
|
|
65
65
|
with PreviewLive() as live:
|
|
66
|
-
for
|
|
67
|
-
|
|
68
|
-
ceil(len(posts) /
|
|
66
|
+
for batch in live.progress.track(
|
|
67
|
+
batched(posts, batch_size, strict=False),
|
|
68
|
+
ceil(len(posts) / batch_size),
|
|
69
69
|
description="Removing flagged posts...",
|
|
70
70
|
):
|
|
71
|
-
response = self.openai.moderations.create(input=list(map(Post.get_content_text,
|
|
72
|
-
for post, moderation in zip(
|
|
71
|
+
response = self.openai.moderations.create(input=list(map(Post.get_content_text, batch)))
|
|
72
|
+
for post, moderation in zip(batch, response.results, strict=True):
|
|
73
73
|
if moderation.flagged:
|
|
74
74
|
removed += 1
|
|
75
75
|
live.custom_update(post)
|
|
@@ -84,15 +84,14 @@ class ExamplesWriter(FlowClass):
|
|
|
84
84
|
with data_path.open(encoding="utf_8") as fp:
|
|
85
85
|
for line in fp:
|
|
86
86
|
post = Post.model_validate_json(line)
|
|
87
|
-
if
|
|
87
|
+
if post.valid_text_post():
|
|
88
88
|
yield post
|
|
89
89
|
|
|
90
|
-
def
|
|
91
|
-
test_n = 1000
|
|
90
|
+
def get_moderation_batch_size(self) -> int:
|
|
92
91
|
try:
|
|
93
|
-
self.openai.moderations.create(input=[""] *
|
|
92
|
+
self.openai.moderations.create(input=[""] * self.config.max_moderation_batch_size)
|
|
94
93
|
except BadRequestError as error:
|
|
95
94
|
message = error.response.json()["error"]["message"]
|
|
96
95
|
if match := search(r"(\d+)\.", message):
|
|
97
96
|
return int(match.group(1))
|
|
98
|
-
return
|
|
97
|
+
return self.config.max_moderation_batch_size
|
|
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Self, override
|
|
|
5
5
|
import rich
|
|
6
6
|
import tomlkit
|
|
7
7
|
from openai.types import ChatModel
|
|
8
|
-
from pydantic import Field, NonNegativeFloat, PositiveFloat, PositiveInt,
|
|
8
|
+
from pydantic import Field, NonNegativeFloat, PositiveFloat, PositiveInt, model_validator
|
|
9
9
|
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, TomlConfigSettingsSource
|
|
10
10
|
from rich.prompt import Prompt
|
|
11
11
|
from tomlkit import comment, document
|
|
@@ -31,6 +31,7 @@ class Config(BaseSettings):
|
|
|
31
31
|
data_directory: Path = Field(Path("data"), description="Where to store downloaded post data.")
|
|
32
32
|
|
|
33
33
|
# Writing Examples
|
|
34
|
+
max_moderation_batch_size: PositiveInt = Field(100, description="How many posts, at most, to submit to the OpenAI moderation API. This is also capped by the API.")
|
|
34
35
|
custom_prompts_file: Path = Field(Path("custom_prompts.jsonl"), description="Where to read in custom prompts from.")
|
|
35
36
|
|
|
36
37
|
# Writing Examples & Fine-Tuning
|
|
@@ -88,8 +89,7 @@ class Config(BaseSettings):
|
|
|
88
89
|
for line in field.description.split(". "):
|
|
89
90
|
toml_table.add(comment(f"{line.removesuffix('.')}."))
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
toml_table[name] = value.get_secret_value() if isinstance(value, Secret) else dumped_model[name]
|
|
92
|
+
toml_table[name] = dumped_model[name]
|
|
93
93
|
|
|
94
94
|
Path(toml_file).write_text(
|
|
95
95
|
tomlkit.dumps(toml_table),
|
|
@@ -3,23 +3,14 @@ from typing import Annotated, Any, ClassVar, Literal, Self, override
|
|
|
3
3
|
|
|
4
4
|
import rich
|
|
5
5
|
from keyring import get_password, set_password
|
|
6
|
-
from niquests import Session
|
|
7
6
|
from openai import BaseModel
|
|
8
7
|
from pwinput import pwinput
|
|
9
|
-
from pydantic import ConfigDict, PlainSerializer
|
|
8
|
+
from pydantic import ConfigDict, PlainSerializer
|
|
10
9
|
from pydantic.json_schema import SkipJsonSchema
|
|
11
10
|
from requests_oauthlib import OAuth1Session
|
|
12
11
|
from rich.panel import Panel
|
|
13
12
|
from rich.prompt import Confirm
|
|
14
13
|
|
|
15
|
-
type SerializableSecretStr = Annotated[
|
|
16
|
-
SecretStr,
|
|
17
|
-
PlainSerializer(
|
|
18
|
-
SecretStr.get_secret_value,
|
|
19
|
-
when_used="json-unless-none",
|
|
20
|
-
),
|
|
21
|
-
]
|
|
22
|
-
|
|
23
14
|
|
|
24
15
|
class FullyValidatedModel(BaseModel):
|
|
25
16
|
model_config = ConfigDict(
|
|
@@ -33,24 +24,28 @@ class FullyValidatedModel(BaseModel):
|
|
|
33
24
|
|
|
34
25
|
class Tokens(FullyValidatedModel):
|
|
35
26
|
class Tumblr(FullyValidatedModel):
|
|
36
|
-
client_key:
|
|
37
|
-
client_secret:
|
|
38
|
-
resource_owner_key:
|
|
39
|
-
resource_owner_secret:
|
|
27
|
+
client_key: str = ""
|
|
28
|
+
client_secret: str = ""
|
|
29
|
+
resource_owner_key: str = ""
|
|
30
|
+
resource_owner_secret: str = ""
|
|
40
31
|
|
|
41
32
|
service_name: ClassVar = "tumblrbot"
|
|
42
33
|
username: ClassVar = "tokens"
|
|
43
34
|
|
|
44
|
-
openai_api_key:
|
|
35
|
+
openai_api_key: str = ""
|
|
45
36
|
tumblr: Tumblr = Tumblr()
|
|
46
37
|
|
|
47
38
|
@staticmethod
|
|
48
|
-
def
|
|
39
|
+
def get_oauth_tokens(token: dict[str, str]) -> tuple[str, str]:
|
|
40
|
+
return token["oauth_token"], token["oauth_token_secret"]
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def online_token_prompt(url: str, *tokens: str) -> Generator[str]:
|
|
49
44
|
formatted_token_string = " and ".join(f"[cyan]{token}[/]" for token in tokens)
|
|
50
45
|
|
|
51
46
|
rich.print(f"Retrieve your {formatted_token_string} from: {url}")
|
|
52
47
|
for token in tokens:
|
|
53
|
-
yield
|
|
48
|
+
yield pwinput(f"Enter your {token} (masked): ").strip()
|
|
54
49
|
|
|
55
50
|
rich.print()
|
|
56
51
|
|
|
@@ -64,34 +59,30 @@ class Tokens(FullyValidatedModel):
|
|
|
64
59
|
def model_post_init(self, context: object) -> None:
|
|
65
60
|
super().model_post_init(context)
|
|
66
61
|
|
|
67
|
-
if not self.openai_api_key
|
|
62
|
+
if not self.openai_api_key or Confirm.ask("Reset OpenAI API key?", default=False):
|
|
68
63
|
(self.openai_api_key,) = self.online_token_prompt("https://platform.openai.com/api-keys", "API key")
|
|
69
64
|
|
|
70
|
-
if not all(self.tumblr.model_dump(
|
|
65
|
+
if not all(self.tumblr.model_dump().values()) or Confirm.ask("Reset Tumblr API tokens?", default=False):
|
|
71
66
|
self.tumblr.client_key, self.tumblr.client_secret = self.online_token_prompt("https://tumblr.com/oauth/apps", "consumer key", "consumer secret")
|
|
72
67
|
|
|
73
|
-
OAuth1Session.__bases__ = (Session,)
|
|
74
|
-
|
|
75
68
|
with OAuth1Session(
|
|
76
|
-
self.tumblr.client_key
|
|
77
|
-
self.tumblr.client_secret
|
|
69
|
+
self.tumblr.client_key,
|
|
70
|
+
self.tumblr.client_secret,
|
|
78
71
|
) as oauth_session:
|
|
79
72
|
fetch_response = oauth_session.fetch_request_token("http://tumblr.com/oauth/request_token")
|
|
80
73
|
full_authorize_url = oauth_session.authorization_url("http://tumblr.com/oauth/authorize")
|
|
81
74
|
(redirect_response,) = self.online_token_prompt(full_authorize_url, "full redirect URL")
|
|
82
|
-
oauth_response = oauth_session.parse_authorization_response(redirect_response
|
|
75
|
+
oauth_response = oauth_session.parse_authorization_response(redirect_response)
|
|
83
76
|
|
|
84
77
|
with OAuth1Session(
|
|
85
|
-
self.tumblr.client_key
|
|
86
|
-
self.tumblr.client_secret
|
|
87
|
-
fetch_response
|
|
88
|
-
fetch_response["oauth_token_secret"],
|
|
78
|
+
self.tumblr.client_key,
|
|
79
|
+
self.tumblr.client_secret,
|
|
80
|
+
*self.get_oauth_tokens(fetch_response),
|
|
89
81
|
verifier=oauth_response["oauth_verifier"],
|
|
90
82
|
) as oauth_session:
|
|
91
83
|
oauth_tokens = oauth_session.fetch_access_token("http://tumblr.com/oauth/access_token")
|
|
92
84
|
|
|
93
|
-
self.tumblr.resource_owner_key = oauth_tokens
|
|
94
|
-
self.tumblr.resource_owner_secret = oauth_tokens["oauth_token_secret"]
|
|
85
|
+
self.tumblr.resource_owner_key, self.tumblr.resource_owner_secret = self.get_oauth_tokens(oauth_tokens)
|
|
95
86
|
|
|
96
87
|
set_password(self.service_name, self.username, self.model_dump_json())
|
|
97
88
|
|
|
@@ -120,8 +111,8 @@ class Post(FullyValidatedModel):
|
|
|
120
111
|
subtitle_align="left",
|
|
121
112
|
)
|
|
122
113
|
|
|
123
|
-
def
|
|
124
|
-
return all(block.type == "text" for block in self.content) and not any(block.type == "ask" for block in self.layout)
|
|
114
|
+
def valid_text_post(self) -> bool:
|
|
115
|
+
return bool(self.content) and all(block.type == "text" for block in self.content) and not (self.is_submission or self.trail or any(block.type == "ask" for block in self.layout))
|
|
125
116
|
|
|
126
117
|
def get_content_text(self) -> str:
|
|
127
118
|
return "\n\n".join(block.text for block in self.content)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from typing import Self
|
|
2
|
+
|
|
3
|
+
from requests import HTTPError, Response
|
|
4
|
+
from requests_oauthlib import OAuth1Session
|
|
5
|
+
|
|
6
|
+
from tumblrbot.utils.models import Post, Tokens
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TumblrSession(OAuth1Session):
|
|
10
|
+
def __init__(self, tokens: Tokens) -> None:
|
|
11
|
+
super().__init__(**tokens.tumblr.model_dump())
|
|
12
|
+
self.hooks["response"].append(self.response_hook)
|
|
13
|
+
|
|
14
|
+
def __enter__(self) -> Self:
|
|
15
|
+
super().__enter__()
|
|
16
|
+
return self
|
|
17
|
+
|
|
18
|
+
def response_hook(self, response: Response, *_args: object, **_kwargs: object) -> None:
|
|
19
|
+
try:
|
|
20
|
+
response.raise_for_status()
|
|
21
|
+
except HTTPError as error:
|
|
22
|
+
if response.text:
|
|
23
|
+
error.add_note(response.text)
|
|
24
|
+
raise
|
|
25
|
+
|
|
26
|
+
def retrieve_published_posts(self, blog_identifier: str, after: int) -> Response:
|
|
27
|
+
return self.get(
|
|
28
|
+
f"https://api.tumblr.com/v2/blog/{blog_identifier}/posts",
|
|
29
|
+
params={
|
|
30
|
+
"after": after,
|
|
31
|
+
"sort": "asc",
|
|
32
|
+
"npf": True,
|
|
33
|
+
},
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def create_post(self, blog_identifier: str, post: Post) -> Response:
|
|
37
|
+
return self.post(
|
|
38
|
+
f"https://api.tumblr.com/v2/blog/{blog_identifier}/posts",
|
|
39
|
+
json=post.model_dump(),
|
|
40
|
+
)
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
from typing import Self
|
|
3
|
-
|
|
4
|
-
from niquests import HTTPError, PreparedRequest, Response, Session
|
|
5
|
-
from requests_oauthlib import OAuth1
|
|
6
|
-
|
|
7
|
-
from tumblrbot.utils.models import Post, Tokens
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@dataclass
|
|
11
|
-
class TumblrSession(Session):
|
|
12
|
-
tokens: Tokens
|
|
13
|
-
|
|
14
|
-
def __post_init__(self) -> None:
|
|
15
|
-
super().__init__(multiplexed=True, happy_eyeballs=True)
|
|
16
|
-
|
|
17
|
-
self.auth = OAuth1(**self.tokens.tumblr.model_dump(mode="json"))
|
|
18
|
-
self.hooks["response"].append(self.response_hook)
|
|
19
|
-
|
|
20
|
-
def __enter__(self) -> Self:
|
|
21
|
-
super().__enter__()
|
|
22
|
-
return self
|
|
23
|
-
|
|
24
|
-
def response_hook(self, response: PreparedRequest | Response) -> None:
|
|
25
|
-
if isinstance(response, Response):
|
|
26
|
-
try:
|
|
27
|
-
response.raise_for_status()
|
|
28
|
-
except HTTPError as error:
|
|
29
|
-
if response.text:
|
|
30
|
-
error.add_note(response.text)
|
|
31
|
-
raise
|
|
32
|
-
|
|
33
|
-
def retrieve_published_posts(self, blog_identifier: str, after: int) -> Response:
|
|
34
|
-
return self.get(
|
|
35
|
-
f"https://api.tumblr.com/v2/blog/{blog_identifier}/posts",
|
|
36
|
-
params={
|
|
37
|
-
"after": str(after),
|
|
38
|
-
"sort": "asc",
|
|
39
|
-
"npf": str(True),
|
|
40
|
-
},
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
def create_post(self, blog_identifier: str, post: Post) -> Response:
|
|
44
|
-
return self.post(
|
|
45
|
-
f"https://api.tumblr.com/v2/blog/{blog_identifier}/posts",
|
|
46
|
-
json=post.model_dump(mode="json"),
|
|
47
|
-
)
|
|
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
|
|
File without changes
|
|
File without changes
|