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
@@ -1,459 +0,0 @@
1
- """CloudRequests class (threading.Event version)"""
2
- from __future__ import annotations
3
-
4
- from .cloud_events import CloudEvents
5
- from scratchattach.site import project
6
- from threading import Thread, Event, current_thread
7
- import time
8
- import random
9
- import traceback
10
- from scratchattach.utils.encoder import Encoding
11
- from scratchattach.utils import exceptions
12
-
13
- class Request:
14
-
15
- """
16
- Saves a request added to the request handler
17
- """
18
-
19
- def __init__(self, request_name, *, on_call, cloud_requests, thread=True, enabled=True, response_priority=0, debug=False):
20
- self.name = request_name
21
- self.on_call = on_call
22
- self.thread = thread
23
- self.enabled = enabled
24
- self.response_priority = response_priority
25
- self.cloud_requests = cloud_requests # the corresponding CloudRequests object
26
- self.debug = debug or self.cloud_requests.debug
27
-
28
- def __call__(self, received_request):
29
- if not self.enabled:
30
- self.cloud_requests.call_event("on_disabled_request", [received_request])
31
- try:
32
- current_thread().setName(received_request.request_id)
33
- output = self.on_call(*received_request.arguments)
34
- self.cloud_requests.request_outputs.append({"receive":received_request.timestamp, "request_id":received_request.request_id, "output":output, "priority":self.response_priority})
35
- except Exception as e:
36
- self.cloud_requests.call_event("on_error", [received_request, e])
37
- if self.cloud_requests.ignore_exceptions:
38
- print(
39
- f"Warning: Caught error in request '{self.name}' - Full error below"
40
- )
41
- try:
42
- traceback.print_exc()
43
- except Exception:
44
- print(e)
45
- else:
46
- print(f"Exception in request '{self.name}':")
47
- raise(e)
48
- if self.debug:
49
- traceback_full = traceback.format_exc().splitlines()
50
- output = [f"Error in request {self.name}", "Traceback: "]
51
- output.extend(traceback_full)
52
- self.cloud_requests.request_outputs.append({"receive":received_request.timestamp, "request_id":received_request.request_id, "output":output, "priority":self.response_priority})
53
- else:
54
- 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})
55
- self.cloud_requests.responder_event.set() # Activate the .cloud_requests._responder process so it sends back the data to Scratch
56
-
57
- class ReceivedRequest:
58
-
59
- def __init__(self, **entries):
60
- self.__dict__.update(entries)
61
-
62
- class CloudRequests(CloudEvents):
63
-
64
- # The CloudRequests class is built upon CloudEvents, similar to how Filterbot is built upon MessageEvents
65
-
66
- def __init__(self, cloud, used_cloud_vars=["1", "2", "3", "4", "5", "6", "7", "8", "9"], no_packet_loss=False, respond_order="receive", debug=False):
67
- super().__init__(cloud)
68
- # Setup
69
- self._requests = {}
70
- self.event(self.on_set, thread=False)
71
- self.event(self.on_reconnect, thread=True)
72
- 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)
73
- self.used_cloud_vars = used_cloud_vars
74
- self.respond_order = respond_order
75
- self.debug = debug
76
-
77
- # Lists and dicts for saving request-related stuff
78
- self.request_parts = {} # Dict (key: request_id) for saving the parts of the requests not fully received yet
79
- 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.
80
- self.executed_requests = {} # Dict (key: request_id) saving the request that are currently being executed and have not been responded yet (as ReceivedRequest objects)
81
- self.request_outputs = [] # List for the output data returned by the requests (so the thread sending it back to Scratch can access it)
82
- 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
83
- self.packet_memory = [] # Saves the last 15 responses so the Scratch project can re-request packets that weren't received
84
- self._packets_to_resend = []
85
-
86
- # threading Event objects used to block threads until they are needed (lower CPU usage compared to a busy-sleep event queue)#
87
- self.executer_event = Event()
88
- self.responder_event = Event()
89
-
90
- # Start ._executer and ._responder threads (these threads are remain blocked until cloud activity is received and don't consume any CPU)
91
- self.executer_thread = Thread(target=self._executer)
92
- self.responder_thread = Thread(target=self._responder)
93
- self.executer_thread.start()
94
- self.responder_thread.start()
95
-
96
- self.current_var = 0 # ID of the last set FROM_HOST_ variable (when a response is sent back to Scratch, these are set cyclically)
97
- self.credit_check()
98
- self.hard_stop = False # When set to True, all processes will halt immediately without finishing safely (can result in not fully received / responded requests etc.)
99
-
100
- # -- Adding and removing requests --
101
-
102
- def request(self, function=None, *, enabled=True, name=None, thread=True, response_priority=0, debug=False):
103
- """
104
- Decorator function. Adds a request to the request handler.
105
- """
106
- def inner(function):
107
- # called if the decorator provides arguments
108
- self._requests[function.__name__ if name is None else name] = Request(
109
- function.__name__ if name is None else name,
110
- enabled = enabled,
111
- thread=thread,
112
- response_priority=response_priority,
113
- on_call=function,
114
- cloud_requests=self,
115
- debug=debug
116
- )
117
-
118
- if function is None:
119
- # => the decorator provides arguments
120
- return inner
121
- else:
122
- # => the decorator doesn't provide arguments
123
- inner(function)
124
-
125
- def add_request(self, function, *, enabled=True, name=None):
126
- self.request(enabled=enabled, name=name)(function)
127
-
128
- def remove_request(self, name):
129
- try:
130
- self._requests.pop(name)
131
- except Exception:
132
- raise ValueError(
133
- f"No request with name {name} found to remove"
134
- )
135
-
136
- # -- Parse and send back the request output --
137
-
138
- def _parse_output(self, request_name, output, request_id):
139
- """
140
- Prepares the transmission of the request output to the Scratch project
141
- """
142
- if len(str(output)) > 3000:
143
- print(
144
- f"Warning: Output of request '{request_name}' is longer than 3000 characters (length: {len(str(output))} characters). Responding the request will take >4 seconds."
145
- )
146
-
147
- if str(request_id).endswith("0"):
148
- try:
149
- int(output) == output
150
- except Exception:
151
- send_as_integer = False
152
- else:
153
- send_as_integer = not ("-" in str(output)) and not isinstance(output, bool)
154
- else:
155
- send_as_integer = False
156
-
157
- if output is None:
158
- print(f"Warning: Request '{request_name}' didn't return anything.")
159
- return
160
- elif send_as_integer:
161
- output = str(output)
162
- elif not isinstance(output, list):
163
- if output == "":
164
- output = "-"
165
- output = Encoding.encode(output)
166
- else:
167
- input = output
168
- output = ""
169
- for i in input:
170
- output += Encoding.encode(i)
171
- output += "89"
172
- self._respond(request_id, output, validation=3222 if send_as_integer else 2222)
173
-
174
- def _set_FROM_HOST_var(self, value):
175
- try:
176
- self.cloud.set_var(f"FROM_HOST_{self.used_cloud_vars[self.current_var]}", value)
177
- except exceptions.CloudConnectionError:
178
- self.call_event("on_disconnect")
179
- except Exception as e:
180
- print("scratchattach: internal error while responding (please submit a bug report on GitHub):", e)
181
- self.current_var += 1
182
- if self.current_var == len(self.used_cloud_vars):
183
- self.current_var = 0
184
- time.sleep(self.cloud.ws_shortterm_ratelimit)
185
-
186
- def _respond(self, request_id, response, *, validation=2222):
187
- """
188
- Sends back the request response to the Scratch project
189
- """
190
- 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
191
- ) or self.no_packet_loss:
192
- self.cloud.reconnect()
193
-
194
- memory = {"rid":request_id}
195
- remaining_response = str(response)
196
- length_limit = self.cloud.length_limit - (len(str(request_id))+6) # the subtrahend is the worst-case length of the "."+numbers after the "."
197
-
198
- i = 0
199
- while not remaining_response == "":
200
- if len(remaining_response) > length_limit:
201
- response_part = remaining_response[:length_limit]
202
- remaining_response = remaining_response[length_limit:]
203
-
204
- i += 1
205
- if i > 99:
206
- iteration_string = str(i)
207
- elif i > 9:
208
- iteration_string = "0" + str(i)
209
- else:
210
- iteration_string = "00" + str(i)
211
-
212
- value_to_send = f"{response_part}.{request_id}{iteration_string}1"
213
- memory[i] = value_to_send
214
-
215
- self._set_FROM_HOST_var(value_to_send)
216
-
217
- else:
218
- self._set_FROM_HOST_var(f"{remaining_response}.{request_id}{validation}")
219
- self.packet_memory.append(memory)
220
- if len(self.packet_memory) > 15:
221
- self.packet_memory.pop(0)
222
- remaining_response = ""
223
-
224
- if self.hard_stop: # stop immediately without exiting safely
225
- break
226
-
227
- def _request_packet_from_memory(self, request_id, packet_id):
228
- memory = list(filter(lambda x : x["rid"] == request_id, self.packet_memory))
229
- if len(memory) > 0:
230
- self._packets_to_resend.append(memory[0][int(packet_id)])
231
- self.responder_event.set() # activate _responder process
232
-
233
- # -- Register and handle incoming requests --
234
-
235
- def on_set(self, activity):
236
- """
237
- This function is automatically called on cloud activites by the underlying cloud events that this CloudRequests class inherits from
238
- It registers incoming cloud activity and (if request.thread is True) runs them directly or (else) adds detected request to the .received_requests list
239
- """
240
- # 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
241
- if activity.var == "TO_HOST" and "." in activity.value:
242
- # Parsing the received request
243
- raw_request, request_id = activity.value.split(".")
244
-
245
- if len(request_id) == 8 and request_id[-1] == "9":
246
- # A lost packet was re-requested
247
- self._request_packet_from_memory(request_id[1:], int(raw_request))
248
- return
249
-
250
- if request_id in self.responded_request_ids:
251
- # => The received request has already been answered, meaning this activity has already been received
252
- return
253
-
254
- if activity.value[0] == "-":
255
- # => The received request is actually part of a bigger request
256
- if not request_id in self.request_parts:
257
- self.request_parts[request_id] = []
258
- self.request_parts[request_id].append(raw_request[1:])
259
- return
260
-
261
- self.responded_request_ids.insert(0, request_id)
262
- self.responded_request_ids = self.responded_request_ids[:35]
263
-
264
- # If the request consists of multiple parts: Put together the parts to get the whole raw request string
265
- _raw_request = ""
266
- if request_id in self.request_parts:
267
- data = self.request_parts[request_id]
268
- for i in data:
269
- _raw_request += i
270
- self.request_parts.pop(request_id)
271
- raw_request = _raw_request + raw_request
272
-
273
- # Decode request and parse arguemtns:
274
- request = Encoding.decode(raw_request)
275
- arguments = request.split("&")
276
- request_name = arguments.pop(0)#
277
-
278
- # Check if the request is unknown:
279
- if request_name not in self._requests:
280
- print(
281
- f"Warning: Client received an unknown request called '{request_name}'"
282
- )
283
- self.call_event("on_unknown_request", [
284
- ReceivedRequest(request_name=request,
285
- requester=activity.user,
286
- timestamp=activity.timestamp,
287
- arguments=arguments,
288
- request_id=request_id,
289
- activity=activity
290
- )
291
- ])
292
- return
293
-
294
- received_request = ReceivedRequest(
295
- request = self._requests[request_name],
296
- requester=activity.user,
297
- timestamp=activity.timestamp,
298
- arguments=arguments,
299
- request_id=request_id,
300
- activity=activity
301
- )
302
- self.call_event("on_request", [received_request])
303
- if received_request.request.thread:
304
- self.executed_requests[request_id] = received_request
305
- Thread(target=received_request.request, args=[received_request]).start() # Execute the request function directly in a thread
306
- else:
307
- self.received_requests.append(received_request)
308
- self.executer_event.set() # Activate the ._executer process so that it handles the received request
309
-
310
- def _executer(self):
311
- """
312
- 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.
313
- """
314
- # 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
315
- use_extra_data = (self.no_packet_loss and hasattr(self.cloud, "logs"))
316
-
317
- self.executer_event.wait() # Wait for requests to be received
318
- while self.executer_thread is not None: # If self.executer_thread is None, it means cloud requests were stopped using .stop()
319
- self.executer_event.clear()
320
-
321
- if self.received_requests == [] and use_extra_data:
322
- Thread(target=self.on_reconnect).start()
323
-
324
- while self.received_requests != []:
325
- received_request = self.received_requests.pop(0)
326
- self.executed_requests[received_request.request_id] = received_request
327
- received_request.request(received_request) # Execute the request function
328
-
329
- if use_extra_data:
330
- Thread(target=self.on_reconnect).start()
331
- if self.hard_stop: # stop immediately without exiting safely
332
- break
333
-
334
- self.executer_event.wait(timeout = 2.5 if use_extra_data else None) # Wait for requests to be received
335
-
336
- def _responder(self):
337
- """
338
- 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
339
- """
340
- while self.responder_thread is not None: # If self.responder_thread is None, it means cloud requests were stopped using .stop()
341
- self.responder_event.wait() # Wait for executed requests to respond
342
- self.responder_event.clear()
343
-
344
- while self._packets_to_resend != []:
345
- self._set_FROM_HOST_var(self._packets_to_resend.pop(0))
346
- if self.hard_stop: # stop immediately without exiting safely
347
- break
348
-
349
- while self.request_outputs != []:
350
- if self.respond_order == "finish":
351
- output_obj = self.request_outputs.pop(0)
352
- else:
353
- output_obj = min(self.request_outputs, key=lambda x : x[self.respond_order])
354
- self.request_outputs.remove(output_obj)
355
- if output_obj["request_id"] in self.executed_requests:
356
- received_request = self.executed_requests.pop(output_obj["request_id"])
357
- self._parse_output(received_request, output_obj["output"], output_obj["request_id"])
358
- else:
359
- self._parse_output("[sent from backend]", output_obj["output"], output_obj["request_id"])
360
- if self.hard_stop: # stop immediately without exiting safely
361
- break
362
-
363
- def on_reconnect(self):
364
- """
365
- Called when the underlying cloud events reconnect. Makes sure that no requests are missed in this case.
366
- """
367
- try:
368
- extradata = self.cloud.logs(limit=35)[::-1] # Reverse result so oldest activity is first
369
- for activity in extradata:
370
- if activity.timestamp < self.startup_time:
371
- continue
372
- self.on_set(activity) # Read in the fetched activity
373
- except Exception:
374
- pass
375
-
376
- # -- Functions to be used in requests to get info about the request --
377
-
378
- def get_requester(self):
379
- """
380
- Can be used inside a request to get the username that performed the request.
381
- """
382
- activity = self.executed_requests[current_thread().name].activity
383
- if activity.user is None:
384
- activity.load_log_data()
385
- return activity.user
386
-
387
- def get_timestamp(self):
388
- """
389
- Can be used inside a request to get the timestamp of when the request was received.
390
- """
391
- activity = self.executed_requests[current_thread().name].activity
392
- return activity.timestamp
393
-
394
- def get_exact_timestamp(self):
395
- """
396
- Can be used inside a request to get the exact timestamp of when the request was performed.
397
- """
398
- activity = self.executed_requests[current_thread().name].activity
399
- activity.load_log_data()
400
- return activity.timestamp
401
-
402
- # -- Other stuff --
403
-
404
- def send(self, data, *, priority=0):
405
- """
406
- Send data to the Scratch project without a priorly received request. The Scratch project will only receive the data if it's running.
407
- """
408
- self.request_outputs.append({"receive":time.time()*1000, "request_id":"100000000"+str(random.randint(1000, 9999)), "output":data, "priority":priority})
409
- self.responder_event.set() # activate _responder process
410
- # Prevent user from breaking cloud requests by sending too fast (automatically increase wait time if the server can't keep up):
411
- if len(self.request_outputs) > 20:
412
- time.sleep(0.5)
413
- if len(self.request_outputs) > 15:
414
- time.sleep(0.2)
415
- if len(self.request_outputs) > 10:
416
- time.sleep(0.13)
417
- elif len(self.request_outputs) > 3:
418
- time.sleep(0.1)
419
- else:
420
- time.sleep(0.07)
421
-
422
- def stop(self):
423
- """
424
- Stops the request handler and all associated threads forever. Lets running response sending processes finish.
425
- """
426
- # Override the .stop function from BaseEventHandler to make sure the ._executer and ._responder threads are also terminated
427
- super().stop()
428
- self.executer_thread = None
429
- self.responder_thread = None
430
- self.executer_event.set()
431
- self.responder_event.set()
432
-
433
- def hard_stop(self):
434
- """
435
- Stops the request handler and all associated threads forever. Stops running response sending processes immediately.
436
- """
437
- self.hard_stop = True
438
- self.stop()
439
- time.sleep(0.5)
440
- self.hard_stop = False
441
-
442
- def credit_check(self):
443
- try:
444
- p = project.Project(id=self.cloud.project_id)
445
- if not p.update(): # can't get project, probably because it's unshared (no authentication is used for getting it)
446
- print("If you use cloud requests or cloud storages, please credit TimMcCool!")
447
- return
448
- description = (str(p.instructions) + str(p.notes)).lower()
449
- if not ("timmccool" in description or "timmcool" in description or "timccool" in description or "timcool" in description):
450
- print("It was detected that no credit was given in the project description! Please credit TimMcCool when using CloudRequests.")
451
- else:
452
- print("Thanks for giving credit for CloudRequests!")
453
- except Exception:
454
- print("If you use CloudRequests, please credit TimMcCool!")
455
-
456
- def run(self):
457
- # Was changed to .start(), but .run() is kept for backwards compatibility
458
- print("Warning: requests.run() was changed to requests.start() in v2.0. .run() will be removed in a future version")
459
- self.start()