scratchattach 2.1.14__py3-none-any.whl → 3.0.0b0__py3-none-any.whl
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.
- scratchattach/__init__.py +14 -6
- scratchattach/__main__.py +93 -0
- {scratchattach-2.1.14.dist-info → scratchattach-3.0.0b0.dist-info}/METADATA +7 -11
- scratchattach-3.0.0b0.dist-info/RECORD +8 -0
- {scratchattach-2.1.14.dist-info → scratchattach-3.0.0b0.dist-info}/WHEEL +1 -1
- scratchattach-3.0.0b0.dist-info/entry_points.txt +2 -0
- scratchattach/cloud/__init__.py +0 -2
- scratchattach/cloud/_base.py +0 -458
- scratchattach/cloud/cloud.py +0 -183
- scratchattach/editor/__init__.py +0 -21
- scratchattach/editor/asset.py +0 -253
- scratchattach/editor/backpack_json.py +0 -117
- scratchattach/editor/base.py +0 -193
- scratchattach/editor/block.py +0 -579
- scratchattach/editor/blockshape.py +0 -357
- scratchattach/editor/build_defaulting.py +0 -51
- scratchattach/editor/code_translation/__init__.py +0 -0
- scratchattach/editor/code_translation/parse.py +0 -177
- scratchattach/editor/comment.py +0 -80
- scratchattach/editor/commons.py +0 -306
- scratchattach/editor/extension.py +0 -50
- scratchattach/editor/field.py +0 -99
- scratchattach/editor/inputs.py +0 -135
- scratchattach/editor/meta.py +0 -114
- scratchattach/editor/monitor.py +0 -183
- scratchattach/editor/mutation.py +0 -324
- scratchattach/editor/pallete.py +0 -90
- scratchattach/editor/prim.py +0 -170
- scratchattach/editor/project.py +0 -279
- scratchattach/editor/sprite.py +0 -599
- scratchattach/editor/twconfig.py +0 -114
- scratchattach/editor/vlb.py +0 -134
- scratchattach/eventhandlers/__init__.py +0 -0
- scratchattach/eventhandlers/_base.py +0 -100
- scratchattach/eventhandlers/cloud_events.py +0 -110
- scratchattach/eventhandlers/cloud_recorder.py +0 -26
- scratchattach/eventhandlers/cloud_requests.py +0 -459
- scratchattach/eventhandlers/cloud_server.py +0 -246
- scratchattach/eventhandlers/cloud_storage.py +0 -136
- scratchattach/eventhandlers/combine.py +0 -30
- scratchattach/eventhandlers/filterbot.py +0 -161
- scratchattach/eventhandlers/message_events.py +0 -42
- scratchattach/other/__init__.py +0 -0
- scratchattach/other/other_apis.py +0 -284
- scratchattach/other/project_json_capabilities.py +0 -475
- scratchattach/site/__init__.py +0 -0
- scratchattach/site/_base.py +0 -66
- scratchattach/site/activity.py +0 -382
- scratchattach/site/alert.py +0 -227
- scratchattach/site/backpack_asset.py +0 -118
- scratchattach/site/browser_cookie3_stub.py +0 -17
- scratchattach/site/browser_cookies.py +0 -61
- scratchattach/site/classroom.py +0 -447
- scratchattach/site/cloud_activity.py +0 -107
- scratchattach/site/comment.py +0 -242
- scratchattach/site/forum.py +0 -432
- scratchattach/site/project.py +0 -825
- scratchattach/site/session.py +0 -1238
- scratchattach/site/studio.py +0 -611
- scratchattach/site/user.py +0 -956
- scratchattach/utils/__init__.py +0 -0
- scratchattach/utils/commons.py +0 -255
- scratchattach/utils/encoder.py +0 -158
- scratchattach/utils/enums.py +0 -236
- scratchattach/utils/exceptions.py +0 -243
- scratchattach/utils/requests.py +0 -93
- scratchattach-2.1.14.dist-info/RECORD +0 -66
- {scratchattach-2.1.14.dist-info → scratchattach-3.0.0b0.dist-info}/licenses/LICENSE +0 -0
- {scratchattach-2.1.14.dist-info → scratchattach-3.0.0b0.dist-info}/top_level.txt +0 -0
scratchattach/__init__.py
CHANGED
|
@@ -8,22 +8,30 @@ from .eventhandlers.cloud_storage import Database
|
|
|
8
8
|
from .eventhandlers.combine import MultiEventHandler
|
|
9
9
|
|
|
10
10
|
from .other.other_apis import *
|
|
11
|
-
from .other.project_json_capabilities import ProjectBody, get_empty_project_pb, get_pb_from_dict, read_sb3_file, download_asset
|
|
11
|
+
# from .other.project_json_capabilities import ProjectBody, get_empty_project_pb, get_pb_from_dict, read_sb3_file, download_asset
|
|
12
12
|
from .utils.encoder import Encoding
|
|
13
13
|
from .utils.enums import Languages, TTSVoices
|
|
14
|
-
from .utils.exceptions import
|
|
14
|
+
from .utils.exceptions import (
|
|
15
|
+
LoginDataWarning,
|
|
16
|
+
GetAuthenticationWarning,
|
|
17
|
+
StudioAuthenticationWarning,
|
|
18
|
+
ClassroomAuthenticationWarning,
|
|
19
|
+
ProjectAuthenticationWarning,
|
|
20
|
+
UserAuthenticationWarning)
|
|
15
21
|
|
|
16
|
-
from .site.activity import Activity
|
|
22
|
+
from .site.activity import Activity, ActivityTypes
|
|
17
23
|
from .site.backpack_asset import BackpackAsset
|
|
18
|
-
from .site.comment import Comment
|
|
24
|
+
from .site.comment import Comment, CommentSource
|
|
19
25
|
from .site.cloud_activity import CloudActivity
|
|
20
26
|
from .site.forum import ForumPost, ForumTopic, get_topic, get_topic_list, youtube_link_to_scratch
|
|
21
27
|
from .site.project import Project, get_project, search_projects, explore_projects
|
|
22
|
-
from .site.session import Session, login, login_by_id, login_by_session_string, login_by_io, login_by_file,
|
|
28
|
+
from .site.session import Session, login, login_by_id, login_by_session_string, login_by_io, login_by_file, \
|
|
29
|
+
login_from_browser
|
|
23
30
|
from .site.studio import Studio, get_studio, search_studios, explore_studios
|
|
24
31
|
from .site.classroom import Classroom, get_classroom
|
|
25
|
-
from .site.user import User, get_user
|
|
32
|
+
from .site.user import User, get_user, Rank
|
|
26
33
|
from .site._base import BaseSiteComponent
|
|
27
34
|
from .site.browser_cookies import Browser, ANY, FIREFOX, CHROME, CHROMIUM, VIVALDI, EDGE, EDGE_DEV, SAFARI
|
|
35
|
+
from .site.placeholder import PlaceholderProject, get_placeholder_project, create_placeholder_project
|
|
28
36
|
|
|
29
37
|
from . import editor
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Scratchattach CLI. Most source code is in the `cli` directory
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from scratchattach import cli
|
|
8
|
+
from scratchattach.cli import db, cmd
|
|
9
|
+
from scratchattach.cli.context import ctx, console
|
|
10
|
+
|
|
11
|
+
import rich.traceback
|
|
12
|
+
|
|
13
|
+
rich.traceback.install()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# noinspection PyUnusedLocal
|
|
17
|
+
def main():
|
|
18
|
+
parser = argparse.ArgumentParser(
|
|
19
|
+
prog="scratch",
|
|
20
|
+
description="Scratchattach CLI",
|
|
21
|
+
epilog=f"Running scratchattach CLI version {cli.VERSION}",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Using walrus operator & ifs for artificial indentation
|
|
25
|
+
if commands := parser.add_subparsers(dest="command"):
|
|
26
|
+
commands.add_parser("profile", help="View your profile")
|
|
27
|
+
commands.add_parser("sessions", help="View session list")
|
|
28
|
+
if login := commands.add_parser("login", help="Login to Scratch"):
|
|
29
|
+
login.add_argument("--sessid", dest="sessid", nargs="?", default=False, const=True,
|
|
30
|
+
help="Login by session ID")
|
|
31
|
+
if group := commands.add_parser("group", help="View current session group"):
|
|
32
|
+
if group_commands := group.add_subparsers(dest="group_command"):
|
|
33
|
+
group_commands.add_parser("list", help="List all session groups")
|
|
34
|
+
group_commands.add_parser("add", help="Add sessions to group")
|
|
35
|
+
group_commands.add_parser("remove", help="Remove sessions from a group")
|
|
36
|
+
group_commands.add_parser("delete", help="Delete current group")
|
|
37
|
+
if group_copy := group_commands.add_parser("copy", help="Copy current group with a new name"):
|
|
38
|
+
group_copy.add_argument("group_name", help="New group name")
|
|
39
|
+
if group_rename := group_commands.add_parser("rename", help="Rename current group"):
|
|
40
|
+
group_rename.add_argument("group_name", help="New group name")
|
|
41
|
+
if group_new := group_commands.add_parser("new", help="Create a new group"):
|
|
42
|
+
group_new.add_argument("group_name")
|
|
43
|
+
if group_switch := group_commands.add_parser("switch", help="Change the current group"):
|
|
44
|
+
group_switch.add_argument("group_name")
|
|
45
|
+
|
|
46
|
+
parser.add_argument("-U", "--username", dest="username", help="Name of user to look at")
|
|
47
|
+
parser.add_argument("-P", "--project", dest="project_id", help="ID of project to look at")
|
|
48
|
+
parser.add_argument("-S", "--studio", dest="studio_id", help="ID of studio to look at")
|
|
49
|
+
parser.add_argument("-L", "--session_name", dest="session_name",
|
|
50
|
+
help="Name of (registered) session/login to look at")
|
|
51
|
+
|
|
52
|
+
args = parser.parse_args(namespace=cli.ArgSpace())
|
|
53
|
+
cli.ctx.args = args
|
|
54
|
+
cli.ctx.parser = parser
|
|
55
|
+
|
|
56
|
+
match args.command:
|
|
57
|
+
case "sessions":
|
|
58
|
+
cmd.sessions()
|
|
59
|
+
case "login":
|
|
60
|
+
cmd.login()
|
|
61
|
+
case "group":
|
|
62
|
+
cmd.group()
|
|
63
|
+
case "profile":
|
|
64
|
+
cmd.profile()
|
|
65
|
+
case None:
|
|
66
|
+
if args.username:
|
|
67
|
+
user = ctx.session.connect_user(args.username)
|
|
68
|
+
console.print(cli.try_get_img(user.icon, (30, 30)))
|
|
69
|
+
console.print(user)
|
|
70
|
+
return
|
|
71
|
+
if args.studio_id:
|
|
72
|
+
studio = ctx.session.connect_studio(args.studio_id)
|
|
73
|
+
console.print(cli.try_get_img(studio.thumbnail, (34, 20)))
|
|
74
|
+
console.print(studio)
|
|
75
|
+
return
|
|
76
|
+
if args.project_id:
|
|
77
|
+
project = ctx.session.connect_project(args.project_id)
|
|
78
|
+
console.print(cli.try_get_img(project.thumbnail, (30, 23)))
|
|
79
|
+
console.print(project)
|
|
80
|
+
return
|
|
81
|
+
if args.session_name:
|
|
82
|
+
if sess := ctx.db_get_sess(args.session_name):
|
|
83
|
+
console.print(sess)
|
|
84
|
+
else:
|
|
85
|
+
raise ValueError(f"No session logged in called {args.session_name!r} "
|
|
86
|
+
f"- try using `scratch sessions` to see available sessions")
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
parser.print_help()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if __name__ == '__main__':
|
|
93
|
+
main()
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scratchattach
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0b0
|
|
4
4
|
Summary: A Scratch API Wrapper
|
|
5
5
|
Author: TimMcCool
|
|
6
|
-
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Source, https://github.com/timmccool/scratchattach
|
|
8
8
|
Project-URL: Homepage, https://scratchattach.tim1de.net
|
|
9
9
|
Keywords: scratch api,scratchattach,scratch api python,scratch python,scratch for python,scratch,scratch cloud,scratch cloud variables,scratch bot
|
|
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
13
13
|
Classifier: Operating System :: Unix
|
|
14
14
|
Classifier: Operating System :: MacOS :: MacOS X
|
|
15
15
|
Classifier: Operating System :: Microsoft :: Windows
|
|
16
|
+
Requires-Python: >=3.12
|
|
16
17
|
Description-Content-Type: text/markdown
|
|
17
18
|
License-File: LICENSE
|
|
18
19
|
Requires-Dist: websocket-client
|
|
@@ -21,18 +22,13 @@ Requires-Dist: bs4
|
|
|
21
22
|
Requires-Dist: SimpleWebSocketServer
|
|
22
23
|
Requires-Dist: typing-extensions
|
|
23
24
|
Requires-Dist: browser_cookie3
|
|
25
|
+
Requires-Dist: aiohttp
|
|
26
|
+
Requires-Dist: rich
|
|
27
|
+
Provides-Extra: cli
|
|
28
|
+
Requires-Dist: rich-pixels; extra == "cli"
|
|
24
29
|
Provides-Extra: lark
|
|
25
30
|
Requires-Dist: lark; extra == "lark"
|
|
26
|
-
Dynamic: author
|
|
27
|
-
Dynamic: classifier
|
|
28
|
-
Dynamic: description
|
|
29
|
-
Dynamic: description-content-type
|
|
30
|
-
Dynamic: keywords
|
|
31
31
|
Dynamic: license-file
|
|
32
|
-
Dynamic: project-url
|
|
33
|
-
Dynamic: provides-extra
|
|
34
|
-
Dynamic: requires-dist
|
|
35
|
-
Dynamic: summary
|
|
36
32
|
|
|
37
33
|
**scratchattach is a Scratch API wrapper with support for almost all site features.** Created by [TimMcCool](https://scratch.mit.edu/users/TimMcCool/).
|
|
38
34
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
scratchattach/__init__.py,sha256=K26DsJHYlj7atN0WVQOmtN7d1rNAnmLcO3ljA1UD1lc,1849
|
|
2
|
+
scratchattach/__main__.py,sha256=K520LnmXe5WAlr8UZQE1Owrm8BDncy19QQ5GJ75V9FM,4054
|
|
3
|
+
scratchattach-3.0.0b0.dist-info/licenses/LICENSE,sha256=1PRKLhZU4wYt5M-C9f7q0W3go3u_ojnZMNOdR3g3J-E,1080
|
|
4
|
+
scratchattach-3.0.0b0.dist-info/METADATA,sha256=qapruF1xW-anLx5HiFgZ8B1k6oH_a1YcV2WHGRsU5PI,5633
|
|
5
|
+
scratchattach-3.0.0b0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
6
|
+
scratchattach-3.0.0b0.dist-info/entry_points.txt,sha256=vNXuP05TQKEoIzmzmUzS7zbtSZx0p3JmeUW3QhNdYfg,56
|
|
7
|
+
scratchattach-3.0.0b0.dist-info/top_level.txt,sha256=gIwCwW39ohXn0JlnvSzAjV7VtL3qPlRnHiRqBbxsEUE,14
|
|
8
|
+
scratchattach-3.0.0b0.dist-info/RECORD,,
|
scratchattach/cloud/__init__.py
DELETED
scratchattach/cloud/_base.py
DELETED
|
@@ -1,458 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import ssl
|
|
5
|
-
import time
|
|
6
|
-
from typing import Optional, Union, TypeVar, Generic, TYPE_CHECKING, Any
|
|
7
|
-
from abc import ABC, abstractmethod, ABCMeta
|
|
8
|
-
from threading import Lock
|
|
9
|
-
from collections.abc import Iterator
|
|
10
|
-
import warnings
|
|
11
|
-
|
|
12
|
-
if TYPE_CHECKING:
|
|
13
|
-
from _typeshed import SupportsRead
|
|
14
|
-
else:
|
|
15
|
-
T = TypeVar("T")
|
|
16
|
-
class SupportsRead(ABC, Generic[T]):
|
|
17
|
-
@abstractmethod
|
|
18
|
-
def read(self) -> T:
|
|
19
|
-
pass
|
|
20
|
-
|
|
21
|
-
class SupportsClose(ABC):
|
|
22
|
-
@abstractmethod
|
|
23
|
-
def close(self) -> None:
|
|
24
|
-
pass
|
|
25
|
-
|
|
26
|
-
import websocket
|
|
27
|
-
|
|
28
|
-
from scratchattach.site import session
|
|
29
|
-
from scratchattach.eventhandlers import cloud_recorder
|
|
30
|
-
from scratchattach.utils import exceptions
|
|
31
|
-
from scratchattach.eventhandlers.cloud_requests import CloudRequests
|
|
32
|
-
from scratchattach.eventhandlers.cloud_events import CloudEvents
|
|
33
|
-
from scratchattach.eventhandlers.cloud_storage import CloudStorage
|
|
34
|
-
from scratchattach.site import cloud_activity
|
|
35
|
-
|
|
36
|
-
T = TypeVar("T")
|
|
37
|
-
|
|
38
|
-
class EventStream(SupportsRead[Iterator[dict[str, Any]]], SupportsClose):
|
|
39
|
-
"""
|
|
40
|
-
Allows you to stream events
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
class AnyCloud(ABC, Generic[T]):
|
|
44
|
-
"""
|
|
45
|
-
Represents a cloud that is not necessarily using a websocket.
|
|
46
|
-
"""
|
|
47
|
-
active_connection: bool
|
|
48
|
-
var_stets_since_first: int
|
|
49
|
-
_session: Optional[session.Session]
|
|
50
|
-
|
|
51
|
-
@abstractmethod
|
|
52
|
-
def connect(self):
|
|
53
|
-
pass
|
|
54
|
-
|
|
55
|
-
@abstractmethod
|
|
56
|
-
def disconnect(self):
|
|
57
|
-
pass
|
|
58
|
-
|
|
59
|
-
def reconnect(self):
|
|
60
|
-
self.disconnect()
|
|
61
|
-
self.connect()
|
|
62
|
-
|
|
63
|
-
@abstractmethod
|
|
64
|
-
def _enforce_ratelimit(self, *, n: int) -> None:
|
|
65
|
-
pass
|
|
66
|
-
|
|
67
|
-
@abstractmethod
|
|
68
|
-
def set_var(self, variable: str, value: T) -> None:
|
|
69
|
-
"""
|
|
70
|
-
Sets a cloud variable.
|
|
71
|
-
|
|
72
|
-
Args:
|
|
73
|
-
variable (str): The name of the cloud variable that should be set (provided without the cloud emoji)
|
|
74
|
-
value (Any): The value the cloud variable should be set to
|
|
75
|
-
"""
|
|
76
|
-
|
|
77
|
-
@abstractmethod
|
|
78
|
-
def set_vars(self, var_value_dict: dict[str, T], *, intelligent_waits: bool = True):
|
|
79
|
-
"""
|
|
80
|
-
Sets multiple cloud variables at once (works for an unlimited amount of variables).
|
|
81
|
-
|
|
82
|
-
Args:
|
|
83
|
-
var_value_dict (dict): variable:value dictionary with the variables / values to set. The dict should like this: {"var1":"value1", "var2":"value2", ...}
|
|
84
|
-
|
|
85
|
-
Kwargs:
|
|
86
|
-
intelligent_waits (boolean): When enabled, the method will automatically decide how long to wait before performing this cloud variable set, to make sure no rate limits are triggered
|
|
87
|
-
"""
|
|
88
|
-
|
|
89
|
-
@abstractmethod
|
|
90
|
-
def get_var(self, var, *, recorder_initial_values={}) -> T:
|
|
91
|
-
pass
|
|
92
|
-
|
|
93
|
-
@abstractmethod
|
|
94
|
-
def get_all_vars(self, *, recorder_initial_values={}) -> dict[str, T]:
|
|
95
|
-
pass
|
|
96
|
-
|
|
97
|
-
def events(self) -> CloudEvents:
|
|
98
|
-
return CloudEvents(self)
|
|
99
|
-
|
|
100
|
-
def requests(self, *, no_packet_loss: bool = False, used_cloud_vars: list[str] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"],
|
|
101
|
-
respond_order="receive", debug: bool = False) -> CloudRequests:
|
|
102
|
-
return CloudRequests(self, used_cloud_vars=used_cloud_vars, no_packet_loss=no_packet_loss,
|
|
103
|
-
respond_order=respond_order, debug=debug)
|
|
104
|
-
|
|
105
|
-
def storage(self, *, no_packet_loss: bool = False, used_cloud_vars: list[str] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]) -> CloudStorage:
|
|
106
|
-
return CloudStorage(self, used_cloud_vars=used_cloud_vars, no_packet_loss=no_packet_loss)
|
|
107
|
-
|
|
108
|
-
@abstractmethod
|
|
109
|
-
def create_event_stream(self) -> EventStream:
|
|
110
|
-
pass
|
|
111
|
-
|
|
112
|
-
class WebSocketEventStream(EventStream):
|
|
113
|
-
packets_left: list[Union[str, bytes]]
|
|
114
|
-
source_cloud: BaseCloud
|
|
115
|
-
reading: Lock
|
|
116
|
-
def __init__(self, cloud: BaseCloud):
|
|
117
|
-
super().__init__()
|
|
118
|
-
self.source_cloud = type(cloud)(project_id=cloud.project_id)
|
|
119
|
-
self.source_cloud._session = cloud._session
|
|
120
|
-
self.source_cloud.cookie = cloud.cookie
|
|
121
|
-
self.source_cloud.header = cloud.header
|
|
122
|
-
self.source_cloud.origin = cloud.origin
|
|
123
|
-
self.source_cloud.username = cloud.username
|
|
124
|
-
self.source_cloud.ws_timeout = None # No timeout -> allows continous listening
|
|
125
|
-
self.reading = Lock()
|
|
126
|
-
try:
|
|
127
|
-
self.source_cloud.connect()
|
|
128
|
-
except exceptions.CloudConnectionError:
|
|
129
|
-
warnings.warn("Initial cloud connection attempt failed, retrying...", exceptions.UnexpectedWebsocketEventWarning)
|
|
130
|
-
self.packets_left = []
|
|
131
|
-
|
|
132
|
-
def receive_new(self, non_blocking: bool = False):
|
|
133
|
-
if non_blocking:
|
|
134
|
-
self.source_cloud.websocket.settimeout(0)
|
|
135
|
-
try:
|
|
136
|
-
received = self.source_cloud.websocket.recv().splitlines()
|
|
137
|
-
self.packets_left.extend(received)
|
|
138
|
-
except Exception:
|
|
139
|
-
pass
|
|
140
|
-
return
|
|
141
|
-
self.source_cloud.websocket.settimeout(None)
|
|
142
|
-
received = self.source_cloud.websocket.recv().splitlines()
|
|
143
|
-
self.packets_left.extend(received)
|
|
144
|
-
|
|
145
|
-
def read(self, amount: int = -1) -> Iterator[dict[str, Any]]:
|
|
146
|
-
i = 0
|
|
147
|
-
with self.reading:
|
|
148
|
-
try:
|
|
149
|
-
self.receive_new(amount != -1)
|
|
150
|
-
while (self.packets_left and amount == -1) or (amount != -1 and i < amount):
|
|
151
|
-
if not self.packets_left and amount != -1:
|
|
152
|
-
self.receive_new()
|
|
153
|
-
yield json.loads(self.packets_left.pop(0))
|
|
154
|
-
i += 1
|
|
155
|
-
except Exception:
|
|
156
|
-
self.source_cloud.reconnect()
|
|
157
|
-
self.receive_new(amount != -1)
|
|
158
|
-
while (self.packets_left and amount == -1) or (amount != -1 and i < amount):
|
|
159
|
-
if not self.packets_left and amount != -1:
|
|
160
|
-
self.receive_new()
|
|
161
|
-
yield json.loads(self.packets_left.pop(0))
|
|
162
|
-
i += 1
|
|
163
|
-
|
|
164
|
-
def close(self) -> None:
|
|
165
|
-
self.source_cloud.disconnect()
|
|
166
|
-
|
|
167
|
-
class BaseCloud(AnyCloud[Union[str, int]]):
|
|
168
|
-
"""
|
|
169
|
-
Base class for a project's cloud variables. Represents a cloud.
|
|
170
|
-
|
|
171
|
-
When inheriting from this class, the __init__ function of the inherited class:
|
|
172
|
-
- must first call the constructor of the super class: super().__init__()
|
|
173
|
-
- must then set some attributes
|
|
174
|
-
|
|
175
|
-
Attributes that must be specified in the __init__ function a class inheriting from this one:
|
|
176
|
-
project_id: Project id of the cloud variables
|
|
177
|
-
|
|
178
|
-
cloud_host: URL of the websocket server ("wss://..." or "ws://...")
|
|
179
|
-
|
|
180
|
-
Attributes that can, but don't have to be specified in the __init__ function:
|
|
181
|
-
|
|
182
|
-
_session: Either None or a scratchattach.site.session.Session object. Defaults to None.
|
|
183
|
-
|
|
184
|
-
ws_shortterm_ratelimit: The wait time between cloud variable sets. Defaults to 0.1
|
|
185
|
-
|
|
186
|
-
ws_longterm_ratelimit: The amount of cloud variable set that can be performed long-term without ever getting ratelimited
|
|
187
|
-
|
|
188
|
-
allow_non_numeric: Whether non-numeric cloud variable values are allowed. Defaults to False
|
|
189
|
-
|
|
190
|
-
length_limit: Length limit for cloud variable values. Defaults to 100000
|
|
191
|
-
|
|
192
|
-
username: The username to send during handshake. Defaults to "scratchattach"
|
|
193
|
-
|
|
194
|
-
header: The header to send. Defaults to None
|
|
195
|
-
|
|
196
|
-
cookie: The cookie to send. Defaults to None
|
|
197
|
-
|
|
198
|
-
origin: The origin to send. Defaults to None
|
|
199
|
-
|
|
200
|
-
print_connect_messages: Whether to print a message on every connect to the cloud server. Defaults to False.
|
|
201
|
-
"""
|
|
202
|
-
project_id: Optional[Union[str, int]]
|
|
203
|
-
cloud_host: str
|
|
204
|
-
ws_shortterm_ratelimit: float
|
|
205
|
-
ws_longterm_ratelimit: float
|
|
206
|
-
allow_non_numeric: bool
|
|
207
|
-
length_limit: int
|
|
208
|
-
username: str
|
|
209
|
-
header: Optional[dict]
|
|
210
|
-
cookie: Optional[dict]
|
|
211
|
-
origin: Optional[str]
|
|
212
|
-
print_connect_message: bool
|
|
213
|
-
ws_timeout: Optional[int]
|
|
214
|
-
websocket: websocket.WebSocket
|
|
215
|
-
event_stream: Optional[EventStream] = None
|
|
216
|
-
|
|
217
|
-
def __init__(self, *, project_id: Optional[Union[int, str]] = None, _session=None):
|
|
218
|
-
|
|
219
|
-
# Required internal attributes that every object representing a cloud needs to have (no matter what cloud is represented):
|
|
220
|
-
self._session = _session
|
|
221
|
-
self.active_connection = False #whether a connection to a cloud variable server is currently established
|
|
222
|
-
|
|
223
|
-
self.websocket = websocket.WebSocket(sslopt={"cert_reqs": ssl.CERT_NONE})
|
|
224
|
-
self.recorder = None # A CloudRecorder object that records cloud activity for the values to be retrieved later,
|
|
225
|
-
# which will be saved in this attribute as soon as .get_var is called
|
|
226
|
-
self.first_var_set = 0
|
|
227
|
-
self.last_var_set = 0
|
|
228
|
-
self.var_stets_since_first = 0
|
|
229
|
-
|
|
230
|
-
# Set default values for attributes that save configurations specific to the represented cloud:
|
|
231
|
-
# (These attributes can be specifically in the constructors of classes inheriting from this base class)
|
|
232
|
-
self.ws_shortterm_ratelimit = 0.06667
|
|
233
|
-
self.ws_longterm_ratelimit = 0.1
|
|
234
|
-
self.ws_timeout = 3 # Timeout for send operations (after the timeout,
|
|
235
|
-
# the connection will be renewed and the operation will be retried 3 times)
|
|
236
|
-
self.allow_non_numeric = False
|
|
237
|
-
self.length_limit = 100000
|
|
238
|
-
self.username = "scratchattach"
|
|
239
|
-
self.header = None
|
|
240
|
-
self.cookie = None
|
|
241
|
-
self.origin = None
|
|
242
|
-
self.print_connect_message = False
|
|
243
|
-
|
|
244
|
-
self.project_id = project_id
|
|
245
|
-
|
|
246
|
-
def _assert_auth(self):
|
|
247
|
-
if self._session is None:
|
|
248
|
-
raise exceptions.Unauthenticated(
|
|
249
|
-
"You need to use session.connect_cloud (NOT get_cloud) in order to perform this operation.")
|
|
250
|
-
|
|
251
|
-
def _send_packet(self, packet):
|
|
252
|
-
try:
|
|
253
|
-
self.websocket.send(json.dumps(packet) + "\n")
|
|
254
|
-
except Exception:
|
|
255
|
-
time.sleep(0.1)
|
|
256
|
-
self.connect()
|
|
257
|
-
time.sleep(0.1)
|
|
258
|
-
try:
|
|
259
|
-
self.websocket.send(json.dumps(packet) + "\n")
|
|
260
|
-
except Exception:
|
|
261
|
-
time.sleep(0.2)
|
|
262
|
-
self.connect()
|
|
263
|
-
time.sleep(0.2)
|
|
264
|
-
try:
|
|
265
|
-
self.websocket.send(json.dumps(packet) + "\n")
|
|
266
|
-
except Exception:
|
|
267
|
-
time.sleep(1.6)
|
|
268
|
-
self.connect()
|
|
269
|
-
time.sleep(1.4)
|
|
270
|
-
try:
|
|
271
|
-
self.websocket.send(json.dumps(packet) + "\n")
|
|
272
|
-
except Exception:
|
|
273
|
-
self.active_connection = False
|
|
274
|
-
raise exceptions.CloudConnectionError(f"Sending packet failed three times in a row: {packet}")
|
|
275
|
-
|
|
276
|
-
def _send_packet_list(self, packet_list):
|
|
277
|
-
packet_string = "".join([json.dumps(packet) + "\n" for packet in packet_list])
|
|
278
|
-
try:
|
|
279
|
-
self.websocket.send(packet_string)
|
|
280
|
-
except Exception:
|
|
281
|
-
time.sleep(0.1)
|
|
282
|
-
self.connect()
|
|
283
|
-
time.sleep(0.1)
|
|
284
|
-
try:
|
|
285
|
-
self.websocket.send(packet_string)
|
|
286
|
-
except Exception:
|
|
287
|
-
time.sleep(0.2)
|
|
288
|
-
self.connect()
|
|
289
|
-
time.sleep(0.2)
|
|
290
|
-
try:
|
|
291
|
-
self.websocket.send(packet_string)
|
|
292
|
-
except Exception:
|
|
293
|
-
time.sleep(1.6)
|
|
294
|
-
self.connect()
|
|
295
|
-
time.sleep(1.4)
|
|
296
|
-
try:
|
|
297
|
-
self.websocket.send(packet_string)
|
|
298
|
-
except Exception:
|
|
299
|
-
self.active_connection = False
|
|
300
|
-
raise exceptions.CloudConnectionError(
|
|
301
|
-
f"Sending packet list failed four times in a row: {packet_list}")
|
|
302
|
-
|
|
303
|
-
def _handshake(self):
|
|
304
|
-
packet = {"method": "handshake", "user": self.username, "project_id": self.project_id}
|
|
305
|
-
self._send_packet(packet)
|
|
306
|
-
|
|
307
|
-
def connect(self):
|
|
308
|
-
self.websocket = websocket.WebSocket(sslopt={"cert_reqs": ssl.CERT_NONE})
|
|
309
|
-
self.websocket.connect(
|
|
310
|
-
self.cloud_host,
|
|
311
|
-
cookie=self.cookie,
|
|
312
|
-
origin=self.origin,
|
|
313
|
-
enable_multithread=True,
|
|
314
|
-
timeout=self.ws_timeout,
|
|
315
|
-
header=self.header
|
|
316
|
-
)
|
|
317
|
-
self._handshake()
|
|
318
|
-
self.active_connection = True
|
|
319
|
-
if self.print_connect_message:
|
|
320
|
-
print("Connected to cloud server ", self.cloud_host)
|
|
321
|
-
|
|
322
|
-
def disconnect(self):
|
|
323
|
-
self.active_connection = False
|
|
324
|
-
if self.recorder is not None:
|
|
325
|
-
self.recorder.stop()
|
|
326
|
-
self.recorder.disconnect()
|
|
327
|
-
self.recorder = None
|
|
328
|
-
try:
|
|
329
|
-
self.websocket.close()
|
|
330
|
-
except Exception:
|
|
331
|
-
pass
|
|
332
|
-
|
|
333
|
-
def _assert_valid_value(self, value):
|
|
334
|
-
if not (value in [True, False, float('inf'), -float('inf')]):
|
|
335
|
-
value = str(value)
|
|
336
|
-
if len(value) > self.length_limit:
|
|
337
|
-
raise (exceptions.InvalidCloudValue(
|
|
338
|
-
f"Value exceeds length limit: {str(value)}"
|
|
339
|
-
))
|
|
340
|
-
if not self.allow_non_numeric:
|
|
341
|
-
x = value.replace(".", "")
|
|
342
|
-
x = x.replace("-", "")
|
|
343
|
-
if not (x.isnumeric() or x == ""):
|
|
344
|
-
raise (exceptions.InvalidCloudValue(
|
|
345
|
-
"Value not numeric"
|
|
346
|
-
))
|
|
347
|
-
|
|
348
|
-
def _enforce_ratelimit(self, *, n):
|
|
349
|
-
# n is the amount of variables being set
|
|
350
|
-
if (time.time() - self.first_var_set) / (
|
|
351
|
-
self.var_stets_since_first + 1) > self.ws_longterm_ratelimit: # if the average delay between cloud variable sets has been bigger than the long-term rate-limit, cloud variables can be set fast (wait time smaller than long-term rate limit) again
|
|
352
|
-
self.var_stets_since_first = 0
|
|
353
|
-
self.first_var_set = time.time()
|
|
354
|
-
|
|
355
|
-
wait_time = self.ws_shortterm_ratelimit * n
|
|
356
|
-
if time.time() - self.first_var_set > 25: # if cloud variables have been continously set fast (wait time smaller than long-term rate limit) for 25 seconds, they should be set slow now (wait time = long-term rate limit) to avoid getting rate-limited
|
|
357
|
-
wait_time = self.ws_longterm_ratelimit * n
|
|
358
|
-
while self.last_var_set + wait_time >= time.time():
|
|
359
|
-
time.sleep(0.001)
|
|
360
|
-
|
|
361
|
-
def set_var(self, variable, value):
|
|
362
|
-
"""
|
|
363
|
-
Sets a cloud variable.
|
|
364
|
-
|
|
365
|
-
Args:
|
|
366
|
-
variable (str): The name of the cloud variable that should be set (provided without the cloud emoji)
|
|
367
|
-
value (str): The value the cloud variable should be set to
|
|
368
|
-
"""
|
|
369
|
-
self._assert_valid_value(value)
|
|
370
|
-
if not isinstance(variable, str):
|
|
371
|
-
raise ValueError("cloud var name must be a string")
|
|
372
|
-
variable = variable.removeprefix("☁ ")
|
|
373
|
-
if not self.active_connection:
|
|
374
|
-
self.connect()
|
|
375
|
-
self._enforce_ratelimit(n=1)
|
|
376
|
-
|
|
377
|
-
self.var_stets_since_first += 1
|
|
378
|
-
|
|
379
|
-
packet = {
|
|
380
|
-
"method": "set",
|
|
381
|
-
"name": "☁ " + variable,
|
|
382
|
-
"value": value,
|
|
383
|
-
"user": self.username,
|
|
384
|
-
"project_id": self.project_id,
|
|
385
|
-
}
|
|
386
|
-
self._send_packet(packet)
|
|
387
|
-
self.last_var_set = time.time()
|
|
388
|
-
|
|
389
|
-
def set_vars(self, var_value_dict, *, intelligent_waits=True):
|
|
390
|
-
"""
|
|
391
|
-
Sets multiple cloud variables at once (works for an unlimited amount of variables).
|
|
392
|
-
|
|
393
|
-
Args:
|
|
394
|
-
var_value_dict (dict): variable:value dictionary with the variables / values to set. The dict should like this: {"var1":"value1", "var2":"value2", ...}
|
|
395
|
-
|
|
396
|
-
Kwargs:
|
|
397
|
-
intelligent_waits (boolean): When enabled, the method will automatically decide how long to wait before performing this cloud variable set, to make sure no rate limits are triggered
|
|
398
|
-
"""
|
|
399
|
-
if not self.active_connection:
|
|
400
|
-
self.connect()
|
|
401
|
-
if intelligent_waits:
|
|
402
|
-
self._enforce_ratelimit(n=len(list(var_value_dict.keys())))
|
|
403
|
-
|
|
404
|
-
self.var_stets_since_first += len(list(var_value_dict.keys()))
|
|
405
|
-
|
|
406
|
-
packet_list = []
|
|
407
|
-
for variable in var_value_dict:
|
|
408
|
-
value = var_value_dict[variable]
|
|
409
|
-
variable = variable.removeprefix("☁ ")
|
|
410
|
-
self._assert_valid_value(value)
|
|
411
|
-
if not isinstance(variable, str):
|
|
412
|
-
raise ValueError("cloud var name must be a string")
|
|
413
|
-
packet = {
|
|
414
|
-
"method": "set",
|
|
415
|
-
"name": "☁ " + variable,
|
|
416
|
-
"value": value,
|
|
417
|
-
"user": self.username,
|
|
418
|
-
"project_id": self.project_id,
|
|
419
|
-
}
|
|
420
|
-
packet_list.append(packet)
|
|
421
|
-
self._send_packet_list(packet_list)
|
|
422
|
-
self.last_var_set = time.time()
|
|
423
|
-
|
|
424
|
-
def get_var(self, var, *, recorder_initial_values={}):
|
|
425
|
-
var = "☁ "+var.removeprefix("☁ ")
|
|
426
|
-
if self.recorder is None:
|
|
427
|
-
self.recorder = cloud_recorder.CloudRecorder(self, initial_values=recorder_initial_values)
|
|
428
|
-
self.recorder.start()
|
|
429
|
-
start_time = time.time()
|
|
430
|
-
while not (self.recorder.cloud_values != {} or start_time < time.time() - 5):
|
|
431
|
-
time.sleep(0.01)
|
|
432
|
-
return self.recorder.get_var(var)
|
|
433
|
-
|
|
434
|
-
def get_all_vars(self, *, recorder_initial_values={}):
|
|
435
|
-
if self.recorder is None:
|
|
436
|
-
self.recorder = cloud_recorder.CloudRecorder(self, initial_values=recorder_initial_values)
|
|
437
|
-
self.recorder.start()
|
|
438
|
-
start_time = time.time()
|
|
439
|
-
while not (self.recorder.cloud_values != {} or start_time < time.time() - 5):
|
|
440
|
-
time.sleep(0.01)
|
|
441
|
-
return self.recorder.get_all_vars()
|
|
442
|
-
|
|
443
|
-
def create_event_stream(self):
|
|
444
|
-
if self.event_stream:
|
|
445
|
-
raise ValueError("Cloud already has an event stream.")
|
|
446
|
-
self.event_stream = WebSocketEventStream(self)
|
|
447
|
-
return self.event_stream
|
|
448
|
-
|
|
449
|
-
class LogCloudMeta(ABCMeta):
|
|
450
|
-
def __instancecheck__(cls, instance) -> bool:
|
|
451
|
-
if hasattr(instance, "logs"):
|
|
452
|
-
return isinstance(instance, BaseCloud)
|
|
453
|
-
return False
|
|
454
|
-
|
|
455
|
-
class LogCloud(BaseCloud, metaclass=LogCloudMeta):
|
|
456
|
-
@abstractmethod
|
|
457
|
-
def logs(self, *, filter_by_var_named: Optional[str] = None, limit: int = 100, offset: int = 0) -> list[cloud_activity.CloudActivity]:
|
|
458
|
-
pass
|