tumblrbot 1.9.6__tar.gz → 1.9.7__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.9.6 → tumblrbot-1.9.7}/PKG-INFO +6 -6
- {tumblrbot-1.9.6 → tumblrbot-1.9.7}/README.md +3 -5
- tumblrbot-1.9.7/build.ps1 +8 -0
- {tumblrbot-1.9.6 → tumblrbot-1.9.7}/pyproject.toml +4 -1
- {tumblrbot-1.9.6 → tumblrbot-1.9.7}/src/tumblrbot/__main__.py +4 -4
- {tumblrbot-1.9.6 → tumblrbot-1.9.7}/src/tumblrbot/flow/generate.py +3 -4
- {tumblrbot-1.9.6 → tumblrbot-1.9.7}/src/tumblrbot/utils/common.py +5 -11
- {tumblrbot-1.9.6 → tumblrbot-1.9.7}/src/tumblrbot/utils/models.py +34 -45
- {tumblrbot-1.9.6 → tumblrbot-1.9.7}/src/tumblrbot/utils/tumblr.py +15 -11
- tumblrbot-1.9.6/build.ps1 +0 -1
- {tumblrbot-1.9.6 → tumblrbot-1.9.7}/.github/FUNDING.yml +0 -0
- {tumblrbot-1.9.6 → tumblrbot-1.9.7}/.github/dependabot.yml +0 -0
- {tumblrbot-1.9.6 → tumblrbot-1.9.7}/.gitignore +0 -0
- {tumblrbot-1.9.6 → tumblrbot-1.9.7}/UNLICENSE +0 -0
- {tumblrbot-1.9.6 → tumblrbot-1.9.7}/sample_custom_prompts.jsonl +0 -0
- {tumblrbot-1.9.6 → tumblrbot-1.9.7}/src/tumblrbot/__init__.py +0 -0
- {tumblrbot-1.9.6 → tumblrbot-1.9.7}/src/tumblrbot/flow/__init__.py +0 -0
- {tumblrbot-1.9.6 → tumblrbot-1.9.7}/src/tumblrbot/flow/download.py +0 -0
- {tumblrbot-1.9.6 → tumblrbot-1.9.7}/src/tumblrbot/flow/examples.py +0 -0
- {tumblrbot-1.9.6 → tumblrbot-1.9.7}/src/tumblrbot/flow/fine_tune.py +0 -0
- {tumblrbot-1.9.6 → tumblrbot-1.9.7}/src/tumblrbot/utils/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tumblrbot
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.7
|
|
4
4
|
Summary: An updated bot that posts to Tumblr, based on your very own blog!
|
|
5
5
|
Requires-Python: >= 3.14
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -12,8 +12,10 @@ Requires-Dist: rich
|
|
|
12
12
|
Requires-Dist: tenacity
|
|
13
13
|
Requires-Dist: tiktoken
|
|
14
14
|
Requires-Dist: tomlkit
|
|
15
|
+
Requires-Dist: pyinstaller ; extra == "dev"
|
|
15
16
|
Project-URL: Funding, https://ko-fi.com/maidscientistizutsumimarin
|
|
16
17
|
Project-URL: Source, https://github.com/MaidScientistIzutsumiMarin/tumblrbot
|
|
18
|
+
Provides-Extra: dev
|
|
17
19
|
|
|
18
20
|
# tumblrbot
|
|
19
21
|
|
|
@@ -38,7 +40,6 @@ Project-URL: Source, https://github.com/MaidScientistIzutsumiMarin/tumblrbot
|
|
|
38
40
|
[Tumblr]: https://tumblr.com
|
|
39
41
|
[Tumblr Tokens]: https://tumblr.com/oauth/apps
|
|
40
42
|
[Tumblr API Documentation on Blog Identifiers]: https://tumblr.com/docs/en/api/v2#blog-identifiers
|
|
41
|
-
[Tumblr API Documentation on Rate Limits]: https://tumblr.com/docs/en/api/v2#rate-limits
|
|
42
43
|
|
|
43
44
|
[Format String]: https://docs.python.org/3/library/string.html#format-string-syntax
|
|
44
45
|
|
|
@@ -92,10 +93,9 @@ Features:
|
|
|
92
93
|
- You can use regular expressions to filter out training data in the [config][configurable]. This is more of a brute-force solution, but it can work if the other solutions do not.
|
|
93
94
|
- You can try limiting your dataset by specifying fewer blogs to download from or limiting the number of posts taken from each one in the [config][configurable].
|
|
94
95
|
- If all else fails, you can manually remove data from the examples file until it passes. It is unfortunately not a definitive resource, but it can help to read about what the [OpenAI moderation API flags][Flags].
|
|
95
|
-
- Sometimes, you will get an error about the training file not being found when starting fine-tuning. We do not currently have a fix or workaround for this. You should instead use the online portal for fine-tuning if this continues to happen. Read more in [fine-tuning]
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
- Similar to the above issue, you may sometimes get a message saying your IP is blocked. This block is temporary and probably follows the same rules as previously described.
|
|
96
|
+
- Sometimes, you will get an error about the training file not being found when starting fine-tuning. We do not currently have a fix or workaround for this. You should instead use the online portal for fine-tuning if this continues to happen. Read more in [fine-tuning]
|
|
97
|
+
- *We are unsure if this is still happening.*
|
|
98
|
+
- Post counts are incorrect when downloading posts. Our tests suggest this is a [Tumblr] API problem that is giving inaccurate numbers, so treat them as estimates.
|
|
99
99
|
|
|
100
100
|
**Please submit an issue or contact us for features you want added/reimplemented.**
|
|
101
101
|
|
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
[Tumblr]: https://tumblr.com
|
|
22
22
|
[Tumblr Tokens]: https://tumblr.com/oauth/apps
|
|
23
23
|
[Tumblr API Documentation on Blog Identifiers]: https://tumblr.com/docs/en/api/v2#blog-identifiers
|
|
24
|
-
[Tumblr API Documentation on Rate Limits]: https://tumblr.com/docs/en/api/v2#rate-limits
|
|
25
24
|
|
|
26
25
|
[Format String]: https://docs.python.org/3/library/string.html#format-string-syntax
|
|
27
26
|
|
|
@@ -75,10 +74,9 @@ Features:
|
|
|
75
74
|
- You can use regular expressions to filter out training data in the [config][configurable]. This is more of a brute-force solution, but it can work if the other solutions do not.
|
|
76
75
|
- You can try limiting your dataset by specifying fewer blogs to download from or limiting the number of posts taken from each one in the [config][configurable].
|
|
77
76
|
- If all else fails, you can manually remove data from the examples file until it passes. It is unfortunately not a definitive resource, but it can help to read about what the [OpenAI moderation API flags][Flags].
|
|
78
|
-
- Sometimes, you will get an error about the training file not being found when starting fine-tuning. We do not currently have a fix or workaround for this. You should instead use the online portal for fine-tuning if this continues to happen. Read more in [fine-tuning]
|
|
79
|
-
-
|
|
80
|
-
-
|
|
81
|
-
- Similar to the above issue, you may sometimes get a message saying your IP is blocked. This block is temporary and probably follows the same rules as previously described.
|
|
77
|
+
- Sometimes, you will get an error about the training file not being found when starting fine-tuning. We do not currently have a fix or workaround for this. You should instead use the online portal for fine-tuning if this continues to happen. Read more in [fine-tuning]
|
|
78
|
+
- *We are unsure if this is still happening.*
|
|
79
|
+
- Post counts are incorrect when downloading posts. Our tests suggest this is a [Tumblr] API problem that is giving inaccurate numbers, so treat them as estimates.
|
|
82
80
|
|
|
83
81
|
**Please submit an issue or contact us for features you want added/reimplemented.**
|
|
84
82
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tumblrbot"
|
|
3
|
-
version = "1.9.
|
|
3
|
+
version = "1.9.7"
|
|
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.14"
|
|
@@ -22,6 +22,9 @@ Source = "https://github.com/MaidScientistIzutsumiMarin/tumblrbot"
|
|
|
22
22
|
[project.scripts]
|
|
23
23
|
tumblrbot = "tumblrbot.__main__:main"
|
|
24
24
|
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
dev = ["pyinstaller"]
|
|
27
|
+
|
|
25
28
|
[build-system]
|
|
26
29
|
requires = ["flit_core"]
|
|
27
30
|
build-backend = "flit_core.buildapi"
|
|
@@ -19,16 +19,16 @@ def main() -> None:
|
|
|
19
19
|
tokens = Tokens.load()
|
|
20
20
|
with OpenAI(api_key=tokens.openai_api_key) as openai, TumblrSession(tokens) as tumblr:
|
|
21
21
|
if Confirm.ask("Download latest posts?", default=False):
|
|
22
|
-
PostDownloader(openai
|
|
22
|
+
PostDownloader(openai, tumblr).main()
|
|
23
23
|
|
|
24
|
-
examples_writer = ExamplesWriter(openai
|
|
24
|
+
examples_writer = ExamplesWriter(openai, tumblr)
|
|
25
25
|
if Confirm.ask("Create training data?", default=False):
|
|
26
26
|
examples_writer.main()
|
|
27
27
|
|
|
28
28
|
if Confirm.ask("Remove training data flagged by the OpenAI moderation? [bold]This can sometimes resolve errors with fine-tuning validation, but is slow.", default=False):
|
|
29
29
|
examples_writer.filter_examples()
|
|
30
30
|
|
|
31
|
-
fine_tuner = FineTuner(openai
|
|
31
|
+
fine_tuner = FineTuner(openai, tumblr)
|
|
32
32
|
fine_tuner.print_estimates()
|
|
33
33
|
|
|
34
34
|
message = "Resume monitoring the previous fine-tuning process?" if FlowClass.config.job_id else "Upload data to OpenAI for fine-tuning?"
|
|
@@ -36,7 +36,7 @@ def main() -> None:
|
|
|
36
36
|
fine_tuner.main()
|
|
37
37
|
|
|
38
38
|
if Confirm.ask("Generate drafts?", default=False):
|
|
39
|
-
DraftGenerator(openai
|
|
39
|
+
DraftGenerator(openai, tumblr).main()
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
if __name__ == "__main__":
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
1
2
|
from functools import cache
|
|
2
3
|
from random import choice, random, sample
|
|
3
4
|
from typing import TYPE_CHECKING, override
|
|
4
5
|
|
|
5
|
-
from pydantic import ConfigDict
|
|
6
6
|
from rich import print as rich_print
|
|
7
7
|
from rich.prompt import IntPrompt
|
|
8
8
|
|
|
@@ -13,9 +13,8 @@ if TYPE_CHECKING:
|
|
|
13
13
|
from collections.abc import Iterable
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
@dataclass(frozen=True)
|
|
16
17
|
class DraftGenerator(FlowClass):
|
|
17
|
-
model_config = ConfigDict(frozen=True) # Makes this class hashable.
|
|
18
|
-
|
|
19
18
|
@override
|
|
20
19
|
def main(self) -> None:
|
|
21
20
|
self.config.draft_count = IntPrompt.ask("How many drafts should be generated?", default=self.config.draft_count)
|
|
@@ -48,7 +47,7 @@ class DraftGenerator(FlowClass):
|
|
|
48
47
|
tags = tags.tags
|
|
49
48
|
|
|
50
49
|
return Post(
|
|
51
|
-
content=[Block(
|
|
50
|
+
content=[Block(text=text)],
|
|
52
51
|
tags=tags or [],
|
|
53
52
|
parent_tumblelog_uuid=original.blog.uuid,
|
|
54
53
|
parent_post_id=original.id,
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
from abc import abstractmethod
|
|
2
|
+
from dataclasses import dataclass
|
|
2
3
|
from random import choice
|
|
3
|
-
from typing import TYPE_CHECKING, ClassVar
|
|
4
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
4
5
|
|
|
5
6
|
from openai import OpenAI # noqa: TC002
|
|
6
|
-
from pydantic import ConfigDict
|
|
7
7
|
from rich._spinners import SPINNERS
|
|
8
8
|
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.models import Config
|
|
12
|
+
from tumblrbot.utils.models import Config
|
|
13
13
|
from tumblrbot.utils.tumblr import TumblrSession # noqa: TC001
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
@@ -18,9 +18,8 @@ if TYPE_CHECKING:
|
|
|
18
18
|
from rich.console import RenderableType
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class FlowClass:
|
|
24
23
|
config: ClassVar = Config.load()
|
|
25
24
|
|
|
26
25
|
openai: OpenAI
|
|
@@ -51,11 +50,6 @@ class PreviewLive(Live):
|
|
|
51
50
|
|
|
52
51
|
self.custom_update()
|
|
53
52
|
|
|
54
|
-
@override
|
|
55
|
-
def __enter__(self) -> Self:
|
|
56
|
-
super().__enter__()
|
|
57
|
-
return self
|
|
58
|
-
|
|
59
53
|
def custom_update(self, *renderables: RenderableType | None) -> None:
|
|
60
54
|
table = Table.grid()
|
|
61
55
|
table.add_row(self.progress)
|
|
@@ -112,6 +112,16 @@ class Tokens(FileSyncSettings):
|
|
|
112
112
|
openai_api_key: str = ""
|
|
113
113
|
tumblr: Tumblr = Tumblr()
|
|
114
114
|
|
|
115
|
+
@staticmethod
|
|
116
|
+
def online_token_prompt(url: str, *tokens: str) -> Generator[str]:
|
|
117
|
+
formatted_token_string = " and ".join(f"[cyan]{token}[/]" for token in tokens)
|
|
118
|
+
|
|
119
|
+
rich_print(f"Retrieve your {formatted_token_string} from: {url}")
|
|
120
|
+
for token in tokens:
|
|
121
|
+
yield getpass(f"Enter your {token} (masked): ", echo_char="*").strip()
|
|
122
|
+
|
|
123
|
+
rich_print()
|
|
124
|
+
|
|
115
125
|
@override
|
|
116
126
|
def model_post_init(self, context: object) -> None:
|
|
117
127
|
super().model_post_init(context)
|
|
@@ -126,38 +136,18 @@ class Tokens(FileSyncSettings):
|
|
|
126
136
|
# This is the whole OAuth 1.0 process.
|
|
127
137
|
# https://requests-oauthlib.readthedocs.io/en/latest/examples/tumblr.html
|
|
128
138
|
# We tried setting up OAuth 2.0, but the token refresh process is far too unreliable for this sort of program.
|
|
129
|
-
with OAuth1Session(
|
|
130
|
-
|
|
131
|
-
self.tumblr.client_secret,
|
|
132
|
-
) as oauth_session:
|
|
133
|
-
fetch_response = oauth_session.fetch_request_token("http://tumblr.com/oauth/request_token") # pyright: ignore[reportUnknownMemberType]
|
|
134
|
-
full_authorize_url = oauth_session.authorization_url("http://tumblr.com/oauth/authorize") # pyright: ignore[reportUnknownMemberType]
|
|
135
|
-
(redirect_response,) = self.online_token_prompt(full_authorize_url, "full redirect URL")
|
|
136
|
-
oauth_response = oauth_session.parse_authorization_response(redirect_response)
|
|
137
|
-
|
|
138
|
-
with OAuth1Session(
|
|
139
|
-
self.tumblr.client_key,
|
|
140
|
-
self.tumblr.client_secret,
|
|
141
|
-
*self.get_oauth_tokens(fetch_response),
|
|
142
|
-
verifier=oauth_response["oauth_verifier"],
|
|
143
|
-
) as oauth_session:
|
|
144
|
-
oauth_tokens = oauth_session.fetch_access_token("http://tumblr.com/oauth/access_token") # pyright: ignore[reportUnknownMemberType]
|
|
145
|
-
|
|
146
|
-
self.tumblr.resource_owner_key, self.tumblr.resource_owner_secret = self.get_oauth_tokens(oauth_tokens)
|
|
147
|
-
|
|
148
|
-
@staticmethod
|
|
149
|
-
def online_token_prompt(url: str, *tokens: str) -> Generator[str]:
|
|
150
|
-
formatted_token_string = " and ".join(f"[cyan]{token}[/]" for token in tokens)
|
|
139
|
+
with OAuth1Session(**self.tumblr.model_dump()) as session:
|
|
140
|
+
session.fetch_request_token("http://tumblr.com/oauth/request_token") # pyright: ignore[reportUnknownMemberType]
|
|
151
141
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
142
|
+
rich_print("Open the link below in your browser, and authorize this application.\nAfter authorizing, copy and paste the URL of the page you are redirected to below.")
|
|
143
|
+
authorization_url = session.authorization_url("http://tumblr.com/oauth/authorize") # pyright: ignore[reportUnknownMemberType]
|
|
144
|
+
(authorization_response,) = self.online_token_prompt(authorization_url, "full redirect URL")
|
|
145
|
+
session.parse_authorization_response(authorization_response)
|
|
155
146
|
|
|
156
|
-
|
|
147
|
+
access_token = session.fetch_access_token("http://tumblr.com/oauth/access_token") # pyright: ignore[reportUnknownMemberType]
|
|
157
148
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return token["oauth_token"], token["oauth_token_secret"]
|
|
149
|
+
self.tumblr.resource_owner_key = access_token["oauth_token"]
|
|
150
|
+
self.tumblr.resource_owner_secret = access_token["oauth_token_secret"]
|
|
161
151
|
|
|
162
152
|
|
|
163
153
|
class Blog(FullyValidatedModel):
|
|
@@ -166,35 +156,34 @@ class Blog(FullyValidatedModel):
|
|
|
166
156
|
uuid: str = ""
|
|
167
157
|
|
|
168
158
|
|
|
169
|
-
class Response(FullyValidatedModel):
|
|
170
|
-
blog: Blog = Blog()
|
|
171
|
-
posts: list[Any] = []
|
|
172
|
-
|
|
173
|
-
|
|
174
159
|
class ResponseModel(FullyValidatedModel):
|
|
160
|
+
class Response(FullyValidatedModel):
|
|
161
|
+
blog: Blog = Blog()
|
|
162
|
+
posts: list[Any] = []
|
|
163
|
+
|
|
175
164
|
response: Response
|
|
176
165
|
|
|
177
166
|
|
|
178
167
|
class Block(FullyValidatedModel):
|
|
179
|
-
type: str = ""
|
|
168
|
+
type: str = "text"
|
|
180
169
|
text: str = ""
|
|
181
170
|
blocks: list[int] = []
|
|
182
171
|
|
|
183
172
|
|
|
184
173
|
class Post(FullyValidatedModel):
|
|
185
|
-
blog:
|
|
186
|
-
id:
|
|
187
|
-
parent_tumblelog_uuid:
|
|
188
|
-
parent_post_id:
|
|
189
|
-
reblog_key:
|
|
174
|
+
blog: Blog = Blog()
|
|
175
|
+
id: int = 0
|
|
176
|
+
parent_tumblelog_uuid: str = ""
|
|
177
|
+
parent_post_id: int = 0
|
|
178
|
+
reblog_key: str = ""
|
|
190
179
|
|
|
191
|
-
timestamp:
|
|
180
|
+
timestamp: int = 0
|
|
192
181
|
tags: Annotated[list[str], PlainSerializer(",".join)] = []
|
|
193
|
-
state:
|
|
182
|
+
state: Literal["published", "queued", "draft", "private", "unapproved"] = "draft"
|
|
194
183
|
|
|
195
|
-
content:
|
|
196
|
-
layout:
|
|
197
|
-
trail:
|
|
184
|
+
content: list[Block] = []
|
|
185
|
+
layout: list[Block] = []
|
|
186
|
+
trail: list[Self] = []
|
|
198
187
|
|
|
199
188
|
is_submission: SkipJsonSchema[bool] = False
|
|
200
189
|
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
from requests import HTTPError, Response
|
|
4
|
-
from requests_oauthlib import OAuth1Session
|
|
1
|
+
from requests import HTTPError, Response, Session
|
|
2
|
+
from requests_oauthlib import OAuth1
|
|
5
3
|
from rich import print as rich_print
|
|
6
4
|
from tenacity import retry, retry_if_exception_message, stop_after_attempt, wait_random_exponential
|
|
7
5
|
|
|
@@ -16,25 +14,30 @@ rate_limit_retry = retry(
|
|
|
16
14
|
)
|
|
17
15
|
|
|
18
16
|
|
|
19
|
-
class TumblrSession(
|
|
17
|
+
class TumblrSession(Session):
|
|
20
18
|
def __init__(self, tokens: Tokens) -> None:
|
|
21
|
-
super().__init__(
|
|
19
|
+
super().__init__()
|
|
20
|
+
self.auth = OAuth1(**tokens.tumblr.model_dump())
|
|
22
21
|
self.hooks["response"].append(self.response_hook)
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
super().__enter__()
|
|
26
|
-
return self
|
|
23
|
+
self.api_key = tokens.tumblr.client_key
|
|
27
24
|
|
|
28
25
|
def response_hook(self, response: Response, *_args: object, **_kwargs: object) -> None:
|
|
29
26
|
try:
|
|
30
27
|
response.raise_for_status()
|
|
31
28
|
except HTTPError as error:
|
|
32
|
-
|
|
29
|
+
for error_msg in response.json()["errors"]:
|
|
30
|
+
error.add_note(f"{error_msg['code']}: {error_msg['detail']}")
|
|
33
31
|
raise
|
|
34
32
|
|
|
35
33
|
@rate_limit_retry
|
|
36
34
|
def retrieve_blog_info(self, blog_identifier: str) -> ResponseModel:
|
|
37
|
-
response = self.get(
|
|
35
|
+
response = self.get(
|
|
36
|
+
f"https://api.tumblr.com/v2/blog/{blog_identifier}/info",
|
|
37
|
+
params={
|
|
38
|
+
"api_key": self.api_key,
|
|
39
|
+
},
|
|
40
|
+
)
|
|
38
41
|
return ResponseModel.model_validate_json(response.text)
|
|
39
42
|
|
|
40
43
|
@rate_limit_retry
|
|
@@ -47,6 +50,7 @@ class TumblrSession(OAuth1Session):
|
|
|
47
50
|
response = self.get(
|
|
48
51
|
f"https://api.tumblr.com/v2/blog/{blog_identifier}/posts",
|
|
49
52
|
params={
|
|
53
|
+
"api_key": self.api_key,
|
|
50
54
|
"offset": offset,
|
|
51
55
|
"after": after,
|
|
52
56
|
"sort": "asc",
|
tumblrbot-1.9.6/build.ps1
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
..\..\Powershell\build.ps1 -ExtraArgs '--collect-all tiktoken_ext'
|
|
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
|