scratchattach 3.0.0b0__py3-none-any.whl → 3.0.0b2__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/cli/__about__.py +1 -0
- scratchattach/cli/__init__.py +26 -0
- scratchattach/cli/cmd/__init__.py +4 -0
- scratchattach/cli/cmd/group.py +127 -0
- scratchattach/cli/cmd/login.py +60 -0
- scratchattach/cli/cmd/profile.py +7 -0
- scratchattach/cli/cmd/sessions.py +5 -0
- scratchattach/cli/context.py +142 -0
- scratchattach/cli/db.py +66 -0
- scratchattach/cli/namespace.py +14 -0
- scratchattach/cloud/__init__.py +2 -0
- scratchattach/cloud/_base.py +483 -0
- scratchattach/cloud/cloud.py +183 -0
- scratchattach/editor/__init__.py +22 -0
- scratchattach/editor/asset.py +265 -0
- scratchattach/editor/backpack_json.py +115 -0
- scratchattach/editor/base.py +191 -0
- scratchattach/editor/block.py +584 -0
- scratchattach/editor/blockshape.py +357 -0
- scratchattach/editor/build_defaulting.py +51 -0
- scratchattach/editor/code_translation/__init__.py +0 -0
- scratchattach/editor/code_translation/parse.py +177 -0
- scratchattach/editor/comment.py +80 -0
- scratchattach/editor/commons.py +145 -0
- scratchattach/editor/extension.py +50 -0
- scratchattach/editor/field.py +99 -0
- scratchattach/editor/inputs.py +138 -0
- scratchattach/editor/meta.py +117 -0
- scratchattach/editor/monitor.py +185 -0
- scratchattach/editor/mutation.py +381 -0
- scratchattach/editor/pallete.py +88 -0
- scratchattach/editor/prim.py +174 -0
- scratchattach/editor/project.py +381 -0
- scratchattach/editor/sprite.py +609 -0
- scratchattach/editor/twconfig.py +114 -0
- scratchattach/editor/vlb.py +134 -0
- scratchattach/eventhandlers/__init__.py +0 -0
- scratchattach/eventhandlers/_base.py +101 -0
- scratchattach/eventhandlers/cloud_events.py +130 -0
- scratchattach/eventhandlers/cloud_recorder.py +26 -0
- scratchattach/eventhandlers/cloud_requests.py +544 -0
- scratchattach/eventhandlers/cloud_server.py +249 -0
- scratchattach/eventhandlers/cloud_storage.py +135 -0
- scratchattach/eventhandlers/combine.py +30 -0
- scratchattach/eventhandlers/filterbot.py +163 -0
- scratchattach/eventhandlers/message_events.py +42 -0
- scratchattach/other/__init__.py +0 -0
- scratchattach/other/other_apis.py +598 -0
- scratchattach/other/project_json_capabilities.py +475 -0
- scratchattach/site/__init__.py +0 -0
- scratchattach/site/_base.py +93 -0
- scratchattach/site/activity.py +426 -0
- scratchattach/site/alert.py +226 -0
- scratchattach/site/backpack_asset.py +119 -0
- scratchattach/site/browser_cookie3_stub.py +17 -0
- scratchattach/site/browser_cookies.py +61 -0
- scratchattach/site/classroom.py +454 -0
- scratchattach/site/cloud_activity.py +121 -0
- scratchattach/site/comment.py +228 -0
- scratchattach/site/forum.py +436 -0
- scratchattach/site/placeholder.py +132 -0
- scratchattach/site/project.py +932 -0
- scratchattach/site/session.py +1323 -0
- scratchattach/site/studio.py +704 -0
- scratchattach/site/typed_dicts.py +151 -0
- scratchattach/site/user.py +1252 -0
- scratchattach/utils/__init__.py +0 -0
- scratchattach/utils/commons.py +263 -0
- scratchattach/utils/encoder.py +161 -0
- scratchattach/utils/enums.py +237 -0
- scratchattach/utils/exceptions.py +277 -0
- scratchattach/utils/optional_async.py +154 -0
- scratchattach/utils/requests.py +306 -0
- {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b2.dist-info}/METADATA +1 -1
- scratchattach-3.0.0b2.dist-info/RECORD +81 -0
- scratchattach-3.0.0b0.dist-info/RECORD +0 -8
- {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b2.dist-info}/WHEEL +0 -0
- {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b2.dist-info}/entry_points.txt +0 -0
- {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b2.dist-info}/licenses/LICENSE +0 -0
- {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
"""CloudRequests class (threading.Event version)"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from threading import Thread, Event, local
|
|
5
|
+
import time
|
|
6
|
+
import random
|
|
7
|
+
import traceback
|
|
8
|
+
import warnings
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Protocol, Any, runtime_checkable, TypedDict, Union, Optional
|
|
11
|
+
from enum import Enum, auto
|
|
12
|
+
|
|
13
|
+
from scratchattach.utils.encoder import Encoding
|
|
14
|
+
from scratchattach.utils import exceptions
|
|
15
|
+
from scratchattach.site import project, cloud_activity
|
|
16
|
+
from scratchattach.cloud import _base
|
|
17
|
+
from .cloud_events import CloudEvents
|
|
18
|
+
|
|
19
|
+
class RequestHandlerThreadInfo(local):
|
|
20
|
+
request_id: str = ""
|
|
21
|
+
|
|
22
|
+
request_handler_thread_info = RequestHandlerThreadInfo()
|
|
23
|
+
|
|
24
|
+
class ErrorInRequest(RuntimeWarning):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
class ErrorWithMessage(Exception):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
@runtime_checkable
|
|
31
|
+
class RequestHandler(Protocol):
|
|
32
|
+
def __call__(self, *args: str) -> Any:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
class Request:
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
Saves a request added to the request handler
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
request_name: str,
|
|
44
|
+
*,
|
|
45
|
+
on_call: RequestHandler,
|
|
46
|
+
cloud_requests: CloudRequests,
|
|
47
|
+
thread: bool = True,
|
|
48
|
+
enabled: bool = True,
|
|
49
|
+
response_priority: int = 0,
|
|
50
|
+
debug: bool = False
|
|
51
|
+
):
|
|
52
|
+
self.name = request_name
|
|
53
|
+
self.on_call = on_call
|
|
54
|
+
self.thread = thread
|
|
55
|
+
self.enabled = enabled
|
|
56
|
+
self.response_priority = response_priority
|
|
57
|
+
self.cloud_requests = cloud_requests # the corresponding CloudRequests object
|
|
58
|
+
self.debug = debug or self.cloud_requests.debug
|
|
59
|
+
|
|
60
|
+
def __call__(self, received_request: ReceivedRequest):
|
|
61
|
+
if not self.enabled:
|
|
62
|
+
self.cloud_requests.call_event("on_disabled_request", [received_request])
|
|
63
|
+
return
|
|
64
|
+
try:
|
|
65
|
+
request_handler_thread_info.request_id = received_request.request_id # Used by .get_requester() / .get_timestamp() as lookup key
|
|
66
|
+
output = self.on_call(*received_request.arguments)
|
|
67
|
+
self.cloud_requests.request_outputs.append({"receive":received_request.timestamp, "request_id": received_request.request_id, "output":output, "priority":self.response_priority})
|
|
68
|
+
except ErrorWithMessage as e:
|
|
69
|
+
self.cloud_requests.call_event("on_error", [received_request, e])
|
|
70
|
+
output = [i for arg in e.args for i in str(arg).splitlines()]
|
|
71
|
+
self.cloud_requests.request_outputs.append({"receive": received_request.timestamp, "request_id": received_request.request_id, "output": output, "priority": self.response_priority})
|
|
72
|
+
except Exception as e:
|
|
73
|
+
self.cloud_requests.call_event("on_error", [received_request, e])
|
|
74
|
+
if self.cloud_requests.ignore_exceptions:
|
|
75
|
+
warnings.warn(
|
|
76
|
+
f"Warning: Caught error in request {self.name!r} - Full error below\n{traceback.format_exc()}",
|
|
77
|
+
ErrorInRequest
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
print(f"Exception in request {self.name!r}:")
|
|
81
|
+
raise(e)
|
|
82
|
+
if self.debug:
|
|
83
|
+
traceback_full = traceback.format_exc().splitlines()
|
|
84
|
+
output = [f"Error in request {self.name}", "Traceback: "]
|
|
85
|
+
output.extend(traceback_full)
|
|
86
|
+
self.cloud_requests.request_outputs.append({"receive": received_request.timestamp, "request_id": received_request.request_id, "output": output, "priority": self.response_priority})
|
|
87
|
+
else:
|
|
88
|
+
self.cloud_requests.request_outputs.append({"receive": received_request.timestamp, "request_id": received_request.request_id, "output": [f"Error in request {self.name}", "Check the Python console"], "priority": self.response_priority})
|
|
89
|
+
self.cloud_requests.responder_event.set() # Activate the .cloud_requests._responder process so it sends back the data to Scratch
|
|
90
|
+
|
|
91
|
+
class EmptyRequest(Request):
|
|
92
|
+
def __init__(self):
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
def __call__(self, received_request):
|
|
96
|
+
raise TypeError("Empty request can not be called.")
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class ReceivedRequest:
|
|
100
|
+
request: Request = field(kw_only=True, default_factory=EmptyRequest)
|
|
101
|
+
request_name: str = field(kw_only=True, default="")
|
|
102
|
+
requester: str = field(kw_only=True, default="")
|
|
103
|
+
timestamp: float = field(kw_only=True, default=0.0)
|
|
104
|
+
arguments: list[str] = field(kw_only=True, default_factory=list)
|
|
105
|
+
request_id: str = field(kw_only=True, default="0")
|
|
106
|
+
activity: cloud_activity.CloudActivity = field(kw_only=True)
|
|
107
|
+
|
|
108
|
+
class RespondOrder(Enum):
|
|
109
|
+
FINISH = auto() # {"receive":time.time()*1000, "request_id":"100000000"+str(random.randint(1000, 9999)), "output":data, "priority":priority}
|
|
110
|
+
RECEIVE = auto()
|
|
111
|
+
REQUEST_ID = auto()
|
|
112
|
+
OUTPUT = auto()
|
|
113
|
+
PRIORITY = auto()
|
|
114
|
+
|
|
115
|
+
class RequestOutput(TypedDict):
|
|
116
|
+
receive: float
|
|
117
|
+
request_id: str
|
|
118
|
+
output: Union[str, list[str]]
|
|
119
|
+
priority: int
|
|
120
|
+
|
|
121
|
+
class ResponseMemory(TypedDict):
|
|
122
|
+
rid: str
|
|
123
|
+
packets: dict[int, str]
|
|
124
|
+
|
|
125
|
+
class CloudRequests(CloudEvents):
|
|
126
|
+
|
|
127
|
+
# The CloudRequests class is built upon CloudEvents, similar to how Filterbot is built upon MessageEvents
|
|
128
|
+
|
|
129
|
+
_requests: dict[str, Request]
|
|
130
|
+
request_parts: dict[str, list[str]]
|
|
131
|
+
received_requests: list[ReceivedRequest]
|
|
132
|
+
executed_requests: dict[str, ReceivedRequest]
|
|
133
|
+
request_outputs: list[RequestOutput]
|
|
134
|
+
responded_request_ids: list[str]
|
|
135
|
+
packet_memory: list[ResponseMemory]
|
|
136
|
+
_packets_to_resend: list[str]
|
|
137
|
+
|
|
138
|
+
def __init__(
|
|
139
|
+
self,
|
|
140
|
+
cloud: _base.AnyCloud,
|
|
141
|
+
used_cloud_vars: Optional[list[str]] = None,
|
|
142
|
+
no_packet_loss: bool = False,
|
|
143
|
+
respond_order: RespondOrder = RespondOrder.RECEIVE,
|
|
144
|
+
debug = False
|
|
145
|
+
):
|
|
146
|
+
used_cloud_vars = used_cloud_vars or ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
|
147
|
+
super().__init__(cloud)
|
|
148
|
+
# Setup
|
|
149
|
+
self._requests = {}
|
|
150
|
+
self.event(self.on_set, thread=False)
|
|
151
|
+
self.event(self.on_reconnect, thread=True)
|
|
152
|
+
self.no_packet_loss = no_packet_loss # When enabled, query the clouddata log regularly for missed requests and reconnect after every single request (reduces packet loss a lot, but is spammy and can make response duration longer)
|
|
153
|
+
self.used_cloud_vars = used_cloud_vars
|
|
154
|
+
self.respond_order = respond_order
|
|
155
|
+
self.debug = debug
|
|
156
|
+
|
|
157
|
+
# Lists and dicts for saving request-related stuff
|
|
158
|
+
self.request_parts = {} # Dict (key: request_id) for saving the parts of the requests not fully received yet
|
|
159
|
+
self.received_requests = [] # List saving the requests that have been fully received, but not executed yet (as ReceivedRequest objects). Requests that run in threads will never be put into this list, but are executed directly.
|
|
160
|
+
self.executed_requests = {} # Dict (key: request_id) saving the request that are currently being executed and have not been responded yet (as ReceivedRequest objects)
|
|
161
|
+
self.request_outputs = [] # List for the output data returned by the requests (so the thread sending it back to Scratch can access it)
|
|
162
|
+
self.responded_request_ids = [] # Saves the last 15 request ids that have been responded to. This prevents double responses then using the clouddata logs as 2nd source for preventing packet loss
|
|
163
|
+
self.packet_memory = [] # Saves the last 15 responses so the Scratch project can re-request packets that weren't received
|
|
164
|
+
self._packets_to_resend = []
|
|
165
|
+
|
|
166
|
+
# threading Event objects used to block threads until they are needed (lower CPU usage compared to a busy-sleep event queue)#
|
|
167
|
+
self.executer_event = Event()
|
|
168
|
+
self.responder_event = Event()
|
|
169
|
+
|
|
170
|
+
# Start ._executer and ._responder threads (these threads are remain blocked until cloud activity is received and don't consume any CPU)
|
|
171
|
+
self.executer_thread = Thread(target=self._executer)
|
|
172
|
+
self.responder_thread = Thread(target=self._responder)
|
|
173
|
+
self.executer_thread.start()
|
|
174
|
+
self.responder_thread.start()
|
|
175
|
+
|
|
176
|
+
self.current_var = 0 # ID of the last set FROM_HOST_ variable (when a response is sent back to Scratch, these are set cyclically)
|
|
177
|
+
self.credit_check()
|
|
178
|
+
self.hard_stopped = False # When set to True, all processes will halt immediately without finishing safely (can result in not fully received / responded requests etc.)
|
|
179
|
+
|
|
180
|
+
# -- Adding and removing requests --
|
|
181
|
+
|
|
182
|
+
def request(self, function=None, *, enabled=True, name=None, thread=True, response_priority=0, debug=False):
|
|
183
|
+
"""
|
|
184
|
+
Decorator function. Adds a request to the request handler.
|
|
185
|
+
"""
|
|
186
|
+
def inner(_function):
|
|
187
|
+
_name = _function.__name__ if name is None else name
|
|
188
|
+
# called if the decorator provides arguments
|
|
189
|
+
self._requests[_name] = Request(
|
|
190
|
+
_name,
|
|
191
|
+
enabled=enabled,
|
|
192
|
+
thread=thread,
|
|
193
|
+
response_priority=response_priority,
|
|
194
|
+
on_call=_function,
|
|
195
|
+
cloud_requests=self,
|
|
196
|
+
debug=debug
|
|
197
|
+
)
|
|
198
|
+
return _function
|
|
199
|
+
|
|
200
|
+
if function is None:
|
|
201
|
+
# => the decorator provides arguments
|
|
202
|
+
return inner
|
|
203
|
+
else:
|
|
204
|
+
# => the decorator doesn't provide arguments
|
|
205
|
+
return inner(function)
|
|
206
|
+
|
|
207
|
+
def add_request(self, function, *, enabled=True, name=None):
|
|
208
|
+
self.request(enabled=enabled, name=name)(function)
|
|
209
|
+
|
|
210
|
+
def remove_request(self, name):
|
|
211
|
+
try:
|
|
212
|
+
self._requests.pop(name)
|
|
213
|
+
except KeyError as e:
|
|
214
|
+
raise KeyError(
|
|
215
|
+
f"No request with name {name} found to remove"
|
|
216
|
+
) from e
|
|
217
|
+
|
|
218
|
+
# -- Parse and send back the request output --
|
|
219
|
+
|
|
220
|
+
def _parse_output(self, request_name, output, request_id):
|
|
221
|
+
"""
|
|
222
|
+
Prepares the transmission of the request output to the Scratch project
|
|
223
|
+
"""
|
|
224
|
+
if len(str(output)) > 3000:
|
|
225
|
+
print(
|
|
226
|
+
f"Warning: Output of request '{request_name}' is longer than 3000 characters (length: {len(str(output))} characters). Responding the request will take >4 seconds."
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if str(request_id).endswith("0"):
|
|
230
|
+
try:
|
|
231
|
+
int(output) == output
|
|
232
|
+
except Exception:
|
|
233
|
+
send_as_integer = False
|
|
234
|
+
else:
|
|
235
|
+
send_as_integer = not ("-" in str(output)) and not isinstance(output, bool)
|
|
236
|
+
else:
|
|
237
|
+
send_as_integer = False
|
|
238
|
+
|
|
239
|
+
if output is None:
|
|
240
|
+
print(f"Warning: Request '{request_name}' didn't return anything.")
|
|
241
|
+
return
|
|
242
|
+
elif send_as_integer:
|
|
243
|
+
output = str(output)
|
|
244
|
+
elif not isinstance(output, list):
|
|
245
|
+
if output == "":
|
|
246
|
+
output = "-"
|
|
247
|
+
output = Encoding.encode(output)
|
|
248
|
+
else:
|
|
249
|
+
input = output
|
|
250
|
+
output = ""
|
|
251
|
+
for i in input:
|
|
252
|
+
output += Encoding.encode(i)
|
|
253
|
+
output += "89"
|
|
254
|
+
self._respond(request_id, output, validation=3222 if send_as_integer else 2222)
|
|
255
|
+
|
|
256
|
+
def _set_FROM_HOST_var(self, value):
|
|
257
|
+
try:
|
|
258
|
+
self.cloud.set_var(f"FROM_HOST_{self.used_cloud_vars[self.current_var]}", value)
|
|
259
|
+
except exceptions.CloudConnectionError:
|
|
260
|
+
self.call_event("on_disconnect")
|
|
261
|
+
except Exception as e:
|
|
262
|
+
print("scratchattach: internal error while responding (please submit a bug report on GitHub):", e)
|
|
263
|
+
self.current_var += 1
|
|
264
|
+
if self.current_var == len(self.used_cloud_vars):
|
|
265
|
+
self.current_var = 0
|
|
266
|
+
time.sleep(self.cloud.ws_shortterm_ratelimit)
|
|
267
|
+
|
|
268
|
+
def _respond(self, request_id, response, *, validation=2222):
|
|
269
|
+
"""
|
|
270
|
+
Sends back the request response to the Scratch project
|
|
271
|
+
"""
|
|
272
|
+
if (self.cloud.last_var_set + 8 < time.time() # if the cloud connection has been idle for too long, a reconnect is necessary to make sure the first package will not be lost
|
|
273
|
+
) or self.no_packet_loss:
|
|
274
|
+
self.cloud.reconnect()
|
|
275
|
+
|
|
276
|
+
memory = ResponseMemory(rid=request_id, packets={})#{"rid":request_id}
|
|
277
|
+
remaining_response = str(response)
|
|
278
|
+
length_limit = self.cloud.length_limit - (len(str(request_id))+6) # the subtrahend is the worst-case length of the "."+numbers after the "."
|
|
279
|
+
|
|
280
|
+
i = 0
|
|
281
|
+
while not remaining_response == "":
|
|
282
|
+
if len(remaining_response) > length_limit:
|
|
283
|
+
response_part = remaining_response[:length_limit]
|
|
284
|
+
remaining_response = remaining_response[length_limit:]
|
|
285
|
+
|
|
286
|
+
i += 1
|
|
287
|
+
if i > 99:
|
|
288
|
+
iteration_string = str(i)
|
|
289
|
+
elif i > 9:
|
|
290
|
+
iteration_string = "0" + str(i)
|
|
291
|
+
else:
|
|
292
|
+
iteration_string = "00" + str(i)
|
|
293
|
+
|
|
294
|
+
value_to_send = f"{response_part}.{request_id}{iteration_string}1"
|
|
295
|
+
memory["packets"][i] = value_to_send
|
|
296
|
+
|
|
297
|
+
self._set_FROM_HOST_var(value_to_send)
|
|
298
|
+
|
|
299
|
+
else:
|
|
300
|
+
self._set_FROM_HOST_var(f"{remaining_response}.{request_id}{validation}")
|
|
301
|
+
self.packet_memory.append(memory)
|
|
302
|
+
if len(self.packet_memory) > 15:
|
|
303
|
+
self.packet_memory.pop(0)
|
|
304
|
+
remaining_response = ""
|
|
305
|
+
|
|
306
|
+
if self.hard_stopped: # stop immediately without exiting safely
|
|
307
|
+
break
|
|
308
|
+
|
|
309
|
+
def _request_packet_from_memory(self, request_id: str, packet_id: Union[str, int]):
|
|
310
|
+
memory = list(filter(lambda x : x["rid"] == request_id, self.packet_memory))
|
|
311
|
+
if len(memory) > 0:
|
|
312
|
+
self._packets_to_resend.append(memory[0]["packets"][int(packet_id)])
|
|
313
|
+
self.responder_event.set() # activate _responder process
|
|
314
|
+
|
|
315
|
+
# -- Register and handle incoming requests --
|
|
316
|
+
|
|
317
|
+
def on_set(self, activity: cloud_activity.CloudActivity):
|
|
318
|
+
"""
|
|
319
|
+
This function is automatically called on cloud activites by the underlying cloud events that this CloudRequests class inherits from
|
|
320
|
+
It registers incoming cloud activity and (if request.thread is True) runs them directly or (else) adds detected request to the .received_requests list
|
|
321
|
+
"""
|
|
322
|
+
# Note for contributors: All functions called in this on_set function MUST be executed in threads because this function blocks the cloud events receiver, which is usually not a problem (because of the websocket buffer) but can cause problems in rare cases
|
|
323
|
+
activity_value = str(activity.value)
|
|
324
|
+
if activity.var == "TO_HOST" and "." in activity_value:
|
|
325
|
+
# Parsing the received request
|
|
326
|
+
raw_request, request_id = activity_value.split(".")
|
|
327
|
+
|
|
328
|
+
if len(request_id) == 8 and request_id[-1] == "9":
|
|
329
|
+
# A lost packet was re-requested
|
|
330
|
+
self._request_packet_from_memory(request_id[1:], int(raw_request))
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
if request_id in self.responded_request_ids:
|
|
334
|
+
# => The received request has already been answered, meaning this activity has already been received
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
if activity_value[0] == "-":
|
|
338
|
+
# => The received request is actually part of a bigger request
|
|
339
|
+
if not request_id in self.request_parts:
|
|
340
|
+
self.request_parts[request_id] = []
|
|
341
|
+
self.request_parts[request_id].append(raw_request[1:])
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
self.responded_request_ids.insert(0, request_id)
|
|
345
|
+
self.responded_request_ids = self.responded_request_ids[:35]
|
|
346
|
+
|
|
347
|
+
# If the request consists of multiple parts: Put together the parts to get the whole raw request string
|
|
348
|
+
_raw_request = ""
|
|
349
|
+
if request_id in self.request_parts:
|
|
350
|
+
data = self.request_parts[request_id]
|
|
351
|
+
for i in data:
|
|
352
|
+
_raw_request += i
|
|
353
|
+
self.request_parts.pop(request_id)
|
|
354
|
+
raw_request = _raw_request + raw_request
|
|
355
|
+
|
|
356
|
+
# Decode request and parse arguemtns:
|
|
357
|
+
request = Encoding.decode(raw_request)
|
|
358
|
+
arguments = request.split("&")
|
|
359
|
+
request_name = arguments.pop(0)
|
|
360
|
+
|
|
361
|
+
# Check if the request is unknown:
|
|
362
|
+
if request_name not in self._requests:
|
|
363
|
+
print(
|
|
364
|
+
f"Warning: Client received an unknown request called {request_name!r}"
|
|
365
|
+
)
|
|
366
|
+
self.call_event("on_unknown_request", [
|
|
367
|
+
ReceivedRequest(
|
|
368
|
+
request_name=request,
|
|
369
|
+
requester=activity.user,
|
|
370
|
+
timestamp=activity.timestamp,
|
|
371
|
+
arguments=arguments,
|
|
372
|
+
request_id=request_id,
|
|
373
|
+
activity=activity
|
|
374
|
+
)
|
|
375
|
+
])
|
|
376
|
+
return
|
|
377
|
+
|
|
378
|
+
received_request = ReceivedRequest(
|
|
379
|
+
request = self._requests[request_name],
|
|
380
|
+
request_name=request_name,
|
|
381
|
+
requester=activity.user,
|
|
382
|
+
timestamp=activity.timestamp,
|
|
383
|
+
arguments=arguments,
|
|
384
|
+
request_id=request_id,
|
|
385
|
+
activity=activity
|
|
386
|
+
)
|
|
387
|
+
self.call_event("on_request", [received_request])
|
|
388
|
+
if received_request.request.thread:
|
|
389
|
+
self.executed_requests[request_id] = received_request
|
|
390
|
+
Thread(target=received_request.request, args=[received_request]).start() # Execute the request function directly in a thread
|
|
391
|
+
else:
|
|
392
|
+
self.received_requests.append(received_request)
|
|
393
|
+
self.executer_event.set() # Activate the ._executer process so that it handles the received request
|
|
394
|
+
|
|
395
|
+
def _executer(self):
|
|
396
|
+
"""
|
|
397
|
+
A process that detects new requests in .received_requests, moves them to .executed_requests and executes them. Only requests not running in threads are handled in this process.
|
|
398
|
+
"""
|
|
399
|
+
# If .no_packet_loss is enabled and the cloud provides logs, the logs are used to check whether there are cloud activities that were not received over the cloud connection used by the underlying cloud events
|
|
400
|
+
use_extra_data = (self.no_packet_loss and hasattr(self.cloud, "logs"))
|
|
401
|
+
|
|
402
|
+
self.executer_event.wait() # Wait for requests to be received
|
|
403
|
+
while self.executer_thread is not None: # If self.executer_thread is None, it means cloud requests were stopped using .stop()
|
|
404
|
+
self.executer_event.clear()
|
|
405
|
+
|
|
406
|
+
if self.received_requests == [] and use_extra_data:
|
|
407
|
+
Thread(target=self.on_reconnect).start()
|
|
408
|
+
|
|
409
|
+
while self.received_requests != []:
|
|
410
|
+
received_request = self.received_requests.pop(0)
|
|
411
|
+
self.executed_requests[received_request.request_id] = received_request
|
|
412
|
+
received_request.request(received_request) # Execute the request function
|
|
413
|
+
|
|
414
|
+
if use_extra_data:
|
|
415
|
+
Thread(target=self.on_reconnect).start()
|
|
416
|
+
if self.hard_stopped: # stop immediately without exiting safely
|
|
417
|
+
break
|
|
418
|
+
|
|
419
|
+
self.executer_event.wait(timeout = 2.5 if use_extra_data else None) # Wait for requests to be received
|
|
420
|
+
|
|
421
|
+
def _responder(self):
|
|
422
|
+
"""
|
|
423
|
+
A process that detects incoming request outputs in .request_outputs and handles them by sending them back to the Scratch project, also removes the corresponding ReceivedRequest object from .executed_requests
|
|
424
|
+
"""
|
|
425
|
+
while self.responder_thread is not None: # If self.responder_thread is None, it means cloud requests were stopped using .stop()
|
|
426
|
+
self.responder_event.wait() # Wait for executed requests to respond
|
|
427
|
+
self.responder_event.clear()
|
|
428
|
+
|
|
429
|
+
while self._packets_to_resend != []:
|
|
430
|
+
self._set_FROM_HOST_var(self._packets_to_resend.pop(0))
|
|
431
|
+
if self.hard_stopped: # stop immediately without exiting safely
|
|
432
|
+
break
|
|
433
|
+
|
|
434
|
+
while self.request_outputs:
|
|
435
|
+
if self.respond_order == RespondOrder.FINISH:
|
|
436
|
+
output_obj = self.request_outputs.pop(0)
|
|
437
|
+
else:
|
|
438
|
+
output_obj = min(self.request_outputs, key=lambda x : x[self.respond_order.name.lower()])
|
|
439
|
+
self.request_outputs.remove(output_obj)
|
|
440
|
+
if output_obj["request_id"] in self.executed_requests:
|
|
441
|
+
received_request = self.executed_requests.pop(output_obj["request_id"])
|
|
442
|
+
self._parse_output(received_request.request_name, output_obj["output"], output_obj["request_id"])
|
|
443
|
+
else:
|
|
444
|
+
self._parse_output("[sent from backend]", output_obj["output"], output_obj["request_id"])
|
|
445
|
+
if self.hard_stopped: # stop immediately without exiting safely
|
|
446
|
+
break
|
|
447
|
+
|
|
448
|
+
def on_reconnect(self):
|
|
449
|
+
"""
|
|
450
|
+
Called when the underlying cloud events reconnect. Makes sure that no requests are missed in this case.
|
|
451
|
+
"""
|
|
452
|
+
try:
|
|
453
|
+
extradata = self.cloud.logs(limit=35)[::-1] # Reverse result so oldest activity is first
|
|
454
|
+
for activity in extradata:
|
|
455
|
+
if activity.timestamp < self.startup_time:
|
|
456
|
+
continue
|
|
457
|
+
self.on_set(activity) # Read in the fetched activity
|
|
458
|
+
except Exception:
|
|
459
|
+
pass
|
|
460
|
+
|
|
461
|
+
# -- Functions to be used in requests to get info about the request --
|
|
462
|
+
|
|
463
|
+
def get_requester(self):
|
|
464
|
+
"""
|
|
465
|
+
Can be used inside a request to get the username that performed the request.
|
|
466
|
+
"""
|
|
467
|
+
activity = self.executed_requests[request_handler_thread_info.request_id].activity
|
|
468
|
+
if activity.user is None:
|
|
469
|
+
activity.load_log_data()
|
|
470
|
+
return activity.user
|
|
471
|
+
|
|
472
|
+
def get_timestamp(self):
|
|
473
|
+
"""
|
|
474
|
+
Can be used inside a request to get the timestamp of when the request was received.
|
|
475
|
+
"""
|
|
476
|
+
activity = self.executed_requests[request_handler_thread_info.request_id].activity
|
|
477
|
+
return activity.timestamp
|
|
478
|
+
|
|
479
|
+
def get_exact_timestamp(self):
|
|
480
|
+
"""
|
|
481
|
+
Can be used inside a request to get the exact timestamp of when the request was performed.
|
|
482
|
+
"""
|
|
483
|
+
activity = self.executed_requests[request_handler_thread_info.request_id].activity
|
|
484
|
+
activity.load_log_data()
|
|
485
|
+
return activity.timestamp
|
|
486
|
+
|
|
487
|
+
# -- Other stuff --
|
|
488
|
+
|
|
489
|
+
def send(self, data, *, priority=0):
|
|
490
|
+
"""
|
|
491
|
+
Send data to the Scratch project without a priorly received request. The Scratch project will only receive the data if it's running.
|
|
492
|
+
"""
|
|
493
|
+
self.request_outputs.append({"receive":time.time()*1000, "request_id":"100000000"+str(random.randint(1000, 9999)), "output":data, "priority":priority})
|
|
494
|
+
self.responder_event.set() # activate _responder process
|
|
495
|
+
# Prevent user from breaking cloud requests by sending too fast (automatically increase wait time if the server can't keep up):
|
|
496
|
+
if len(self.request_outputs) > 20:
|
|
497
|
+
time.sleep(0.5)
|
|
498
|
+
if len(self.request_outputs) > 15:
|
|
499
|
+
time.sleep(0.2)
|
|
500
|
+
if len(self.request_outputs) > 10:
|
|
501
|
+
time.sleep(0.13)
|
|
502
|
+
elif len(self.request_outputs) > 3:
|
|
503
|
+
time.sleep(0.1)
|
|
504
|
+
else:
|
|
505
|
+
time.sleep(0.07)
|
|
506
|
+
|
|
507
|
+
def stop(self):
|
|
508
|
+
"""
|
|
509
|
+
Stops the request handler and all associated threads forever. Lets running response sending processes finish.
|
|
510
|
+
"""
|
|
511
|
+
# Override the .stop function from BaseEventHandler to make sure the ._executer and ._responder threads are also terminated
|
|
512
|
+
super().stop()
|
|
513
|
+
self.executer_thread = None
|
|
514
|
+
self.responder_thread = None
|
|
515
|
+
self.executer_event.set()
|
|
516
|
+
self.responder_event.set()
|
|
517
|
+
|
|
518
|
+
def hard_stop(self):
|
|
519
|
+
"""
|
|
520
|
+
Stops the request handler and all associated threads forever. Stops running response sending processes immediately.
|
|
521
|
+
"""
|
|
522
|
+
self.hard_stopped = True
|
|
523
|
+
self.stop()
|
|
524
|
+
time.sleep(0.5)
|
|
525
|
+
self.hard_stopped = False
|
|
526
|
+
|
|
527
|
+
def credit_check(self):
|
|
528
|
+
try:
|
|
529
|
+
p = project.Project(id=self.cloud.project_id)
|
|
530
|
+
if not p.update(): # can't get project, probably because it's unshared (no authentication is used for getting it)
|
|
531
|
+
print("If you use cloud requests or cloud storages, please credit TimMcCool!")
|
|
532
|
+
return
|
|
533
|
+
description = (str(p.instructions) + str(p.notes)).lower()
|
|
534
|
+
if not ("timmccool" in description or "timmcool" in description or "timccool" in description or "timcool" in description):
|
|
535
|
+
print("It was detected that no credit was given in the project description! Please credit TimMcCool when using CloudRequests.")
|
|
536
|
+
else:
|
|
537
|
+
print("Thanks for giving credit for CloudRequests!")
|
|
538
|
+
except Exception:
|
|
539
|
+
print("If you use CloudRequests, please credit TimMcCool!")
|
|
540
|
+
|
|
541
|
+
def run(self):
|
|
542
|
+
# Was changed to .start(), but .run() is kept for backwards compatibility
|
|
543
|
+
print("Warning: requests.run() was changed to requests.start() in v2.0. .run() will be removed in a future version")
|
|
544
|
+
self.start()
|