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.
Files changed (80) hide show
  1. scratchattach/cli/__about__.py +1 -0
  2. scratchattach/cli/__init__.py +26 -0
  3. scratchattach/cli/cmd/__init__.py +4 -0
  4. scratchattach/cli/cmd/group.py +127 -0
  5. scratchattach/cli/cmd/login.py +60 -0
  6. scratchattach/cli/cmd/profile.py +7 -0
  7. scratchattach/cli/cmd/sessions.py +5 -0
  8. scratchattach/cli/context.py +142 -0
  9. scratchattach/cli/db.py +66 -0
  10. scratchattach/cli/namespace.py +14 -0
  11. scratchattach/cloud/__init__.py +2 -0
  12. scratchattach/cloud/_base.py +483 -0
  13. scratchattach/cloud/cloud.py +183 -0
  14. scratchattach/editor/__init__.py +22 -0
  15. scratchattach/editor/asset.py +265 -0
  16. scratchattach/editor/backpack_json.py +115 -0
  17. scratchattach/editor/base.py +191 -0
  18. scratchattach/editor/block.py +584 -0
  19. scratchattach/editor/blockshape.py +357 -0
  20. scratchattach/editor/build_defaulting.py +51 -0
  21. scratchattach/editor/code_translation/__init__.py +0 -0
  22. scratchattach/editor/code_translation/parse.py +177 -0
  23. scratchattach/editor/comment.py +80 -0
  24. scratchattach/editor/commons.py +145 -0
  25. scratchattach/editor/extension.py +50 -0
  26. scratchattach/editor/field.py +99 -0
  27. scratchattach/editor/inputs.py +138 -0
  28. scratchattach/editor/meta.py +117 -0
  29. scratchattach/editor/monitor.py +185 -0
  30. scratchattach/editor/mutation.py +381 -0
  31. scratchattach/editor/pallete.py +88 -0
  32. scratchattach/editor/prim.py +174 -0
  33. scratchattach/editor/project.py +381 -0
  34. scratchattach/editor/sprite.py +609 -0
  35. scratchattach/editor/twconfig.py +114 -0
  36. scratchattach/editor/vlb.py +134 -0
  37. scratchattach/eventhandlers/__init__.py +0 -0
  38. scratchattach/eventhandlers/_base.py +101 -0
  39. scratchattach/eventhandlers/cloud_events.py +130 -0
  40. scratchattach/eventhandlers/cloud_recorder.py +26 -0
  41. scratchattach/eventhandlers/cloud_requests.py +544 -0
  42. scratchattach/eventhandlers/cloud_server.py +249 -0
  43. scratchattach/eventhandlers/cloud_storage.py +135 -0
  44. scratchattach/eventhandlers/combine.py +30 -0
  45. scratchattach/eventhandlers/filterbot.py +163 -0
  46. scratchattach/eventhandlers/message_events.py +42 -0
  47. scratchattach/other/__init__.py +0 -0
  48. scratchattach/other/other_apis.py +598 -0
  49. scratchattach/other/project_json_capabilities.py +475 -0
  50. scratchattach/site/__init__.py +0 -0
  51. scratchattach/site/_base.py +93 -0
  52. scratchattach/site/activity.py +426 -0
  53. scratchattach/site/alert.py +226 -0
  54. scratchattach/site/backpack_asset.py +119 -0
  55. scratchattach/site/browser_cookie3_stub.py +17 -0
  56. scratchattach/site/browser_cookies.py +61 -0
  57. scratchattach/site/classroom.py +454 -0
  58. scratchattach/site/cloud_activity.py +121 -0
  59. scratchattach/site/comment.py +228 -0
  60. scratchattach/site/forum.py +436 -0
  61. scratchattach/site/placeholder.py +132 -0
  62. scratchattach/site/project.py +932 -0
  63. scratchattach/site/session.py +1323 -0
  64. scratchattach/site/studio.py +704 -0
  65. scratchattach/site/typed_dicts.py +151 -0
  66. scratchattach/site/user.py +1252 -0
  67. scratchattach/utils/__init__.py +0 -0
  68. scratchattach/utils/commons.py +263 -0
  69. scratchattach/utils/encoder.py +161 -0
  70. scratchattach/utils/enums.py +237 -0
  71. scratchattach/utils/exceptions.py +277 -0
  72. scratchattach/utils/optional_async.py +154 -0
  73. scratchattach/utils/requests.py +306 -0
  74. {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b2.dist-info}/METADATA +1 -1
  75. scratchattach-3.0.0b2.dist-info/RECORD +81 -0
  76. scratchattach-3.0.0b0.dist-info/RECORD +0 -8
  77. {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b2.dist-info}/WHEEL +0 -0
  78. {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b2.dist-info}/entry_points.txt +0 -0
  79. {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b2.dist-info}/licenses/LICENSE +0 -0
  80. {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()