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.
Files changed (69) hide show
  1. scratchattach/__init__.py +14 -6
  2. scratchattach/__main__.py +93 -0
  3. {scratchattach-2.1.14.dist-info → scratchattach-3.0.0b0.dist-info}/METADATA +7 -11
  4. scratchattach-3.0.0b0.dist-info/RECORD +8 -0
  5. {scratchattach-2.1.14.dist-info → scratchattach-3.0.0b0.dist-info}/WHEEL +1 -1
  6. scratchattach-3.0.0b0.dist-info/entry_points.txt +2 -0
  7. scratchattach/cloud/__init__.py +0 -2
  8. scratchattach/cloud/_base.py +0 -458
  9. scratchattach/cloud/cloud.py +0 -183
  10. scratchattach/editor/__init__.py +0 -21
  11. scratchattach/editor/asset.py +0 -253
  12. scratchattach/editor/backpack_json.py +0 -117
  13. scratchattach/editor/base.py +0 -193
  14. scratchattach/editor/block.py +0 -579
  15. scratchattach/editor/blockshape.py +0 -357
  16. scratchattach/editor/build_defaulting.py +0 -51
  17. scratchattach/editor/code_translation/__init__.py +0 -0
  18. scratchattach/editor/code_translation/parse.py +0 -177
  19. scratchattach/editor/comment.py +0 -80
  20. scratchattach/editor/commons.py +0 -306
  21. scratchattach/editor/extension.py +0 -50
  22. scratchattach/editor/field.py +0 -99
  23. scratchattach/editor/inputs.py +0 -135
  24. scratchattach/editor/meta.py +0 -114
  25. scratchattach/editor/monitor.py +0 -183
  26. scratchattach/editor/mutation.py +0 -324
  27. scratchattach/editor/pallete.py +0 -90
  28. scratchattach/editor/prim.py +0 -170
  29. scratchattach/editor/project.py +0 -279
  30. scratchattach/editor/sprite.py +0 -599
  31. scratchattach/editor/twconfig.py +0 -114
  32. scratchattach/editor/vlb.py +0 -134
  33. scratchattach/eventhandlers/__init__.py +0 -0
  34. scratchattach/eventhandlers/_base.py +0 -100
  35. scratchattach/eventhandlers/cloud_events.py +0 -110
  36. scratchattach/eventhandlers/cloud_recorder.py +0 -26
  37. scratchattach/eventhandlers/cloud_requests.py +0 -459
  38. scratchattach/eventhandlers/cloud_server.py +0 -246
  39. scratchattach/eventhandlers/cloud_storage.py +0 -136
  40. scratchattach/eventhandlers/combine.py +0 -30
  41. scratchattach/eventhandlers/filterbot.py +0 -161
  42. scratchattach/eventhandlers/message_events.py +0 -42
  43. scratchattach/other/__init__.py +0 -0
  44. scratchattach/other/other_apis.py +0 -284
  45. scratchattach/other/project_json_capabilities.py +0 -475
  46. scratchattach/site/__init__.py +0 -0
  47. scratchattach/site/_base.py +0 -66
  48. scratchattach/site/activity.py +0 -382
  49. scratchattach/site/alert.py +0 -227
  50. scratchattach/site/backpack_asset.py +0 -118
  51. scratchattach/site/browser_cookie3_stub.py +0 -17
  52. scratchattach/site/browser_cookies.py +0 -61
  53. scratchattach/site/classroom.py +0 -447
  54. scratchattach/site/cloud_activity.py +0 -107
  55. scratchattach/site/comment.py +0 -242
  56. scratchattach/site/forum.py +0 -432
  57. scratchattach/site/project.py +0 -825
  58. scratchattach/site/session.py +0 -1238
  59. scratchattach/site/studio.py +0 -611
  60. scratchattach/site/user.py +0 -956
  61. scratchattach/utils/__init__.py +0 -0
  62. scratchattach/utils/commons.py +0 -255
  63. scratchattach/utils/encoder.py +0 -158
  64. scratchattach/utils/enums.py +0 -236
  65. scratchattach/utils/exceptions.py +0 -243
  66. scratchattach/utils/requests.py +0 -93
  67. scratchattach-2.1.14.dist-info/RECORD +0 -66
  68. {scratchattach-2.1.14.dist-info → scratchattach-3.0.0b0.dist-info}/licenses/LICENSE +0 -0
  69. {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 LoginDataWarning
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, login_from_browser
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: 2.1.14
3
+ Version: 3.0.0b0
4
4
  Summary: A Scratch API Wrapper
5
5
  Author: TimMcCool
6
- Author-email:
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ scratch = scratchattach.__main__:main
@@ -1,2 +0,0 @@
1
- from .cloud import *
2
- from ._base import *
@@ -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