scratchattach 2.1.9__py3-none-any.whl → 2.1.10a1__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 (59) hide show
  1. scratchattach/__init__.py +28 -25
  2. scratchattach/cloud/__init__.py +2 -0
  3. scratchattach/cloud/_base.py +454 -282
  4. scratchattach/cloud/cloud.py +171 -168
  5. scratchattach/editor/__init__.py +21 -0
  6. scratchattach/editor/asset.py +199 -0
  7. scratchattach/editor/backpack_json.py +117 -0
  8. scratchattach/editor/base.py +142 -0
  9. scratchattach/editor/block.py +507 -0
  10. scratchattach/editor/blockshape.py +353 -0
  11. scratchattach/editor/build_defaulting.py +47 -0
  12. scratchattach/editor/comment.py +74 -0
  13. scratchattach/editor/commons.py +243 -0
  14. scratchattach/editor/extension.py +43 -0
  15. scratchattach/editor/field.py +90 -0
  16. scratchattach/editor/inputs.py +132 -0
  17. scratchattach/editor/meta.py +106 -0
  18. scratchattach/editor/monitor.py +175 -0
  19. scratchattach/editor/mutation.py +317 -0
  20. scratchattach/editor/pallete.py +91 -0
  21. scratchattach/editor/prim.py +170 -0
  22. scratchattach/editor/project.py +273 -0
  23. scratchattach/editor/sbuild.py +2837 -0
  24. scratchattach/editor/sprite.py +586 -0
  25. scratchattach/editor/twconfig.py +113 -0
  26. scratchattach/editor/vlb.py +134 -0
  27. scratchattach/eventhandlers/_base.py +99 -92
  28. scratchattach/eventhandlers/cloud_events.py +110 -103
  29. scratchattach/eventhandlers/cloud_recorder.py +26 -21
  30. scratchattach/eventhandlers/cloud_requests.py +460 -452
  31. scratchattach/eventhandlers/cloud_server.py +246 -244
  32. scratchattach/eventhandlers/cloud_storage.py +135 -134
  33. scratchattach/eventhandlers/combine.py +29 -27
  34. scratchattach/eventhandlers/filterbot.py +160 -159
  35. scratchattach/eventhandlers/message_events.py +41 -40
  36. scratchattach/other/other_apis.py +284 -212
  37. scratchattach/other/project_json_capabilities.py +475 -546
  38. scratchattach/site/_base.py +64 -46
  39. scratchattach/site/activity.py +414 -122
  40. scratchattach/site/backpack_asset.py +118 -84
  41. scratchattach/site/classroom.py +430 -142
  42. scratchattach/site/cloud_activity.py +107 -103
  43. scratchattach/site/comment.py +220 -190
  44. scratchattach/site/forum.py +400 -399
  45. scratchattach/site/project.py +806 -787
  46. scratchattach/site/session.py +1134 -867
  47. scratchattach/site/studio.py +611 -609
  48. scratchattach/site/user.py +835 -837
  49. scratchattach/utils/commons.py +243 -148
  50. scratchattach/utils/encoder.py +157 -156
  51. scratchattach/utils/enums.py +197 -190
  52. scratchattach/utils/exceptions.py +233 -206
  53. scratchattach/utils/requests.py +67 -59
  54. {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a1.dist-info}/METADATA +155 -146
  55. scratchattach-2.1.10a1.dist-info/RECORD +62 -0
  56. {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a1.dist-info}/WHEEL +1 -1
  57. {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a1.dist-info/licenses}/LICENSE +21 -21
  58. scratchattach-2.1.9.dist-info/RECORD +0 -40
  59. {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,134 @@
1
+ """
2
+ Variables, lists & broadcasts
3
+ """
4
+ # Perhaps ids should not be stored in these objects, but in the sprite, similarly
5
+ # to how blocks/prims are stored
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Optional, Literal
10
+
11
+ from . import base, sprite, build_defaulting
12
+ from ..utils import exceptions
13
+
14
+
15
+ class Variable(base.NamedIDComponent):
16
+ def __init__(self, _id: str, _name: str, _value: Optional[str | int | float] = None, _is_cloud: bool = False,
17
+ _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT):
18
+ """
19
+ Class representing a variable.
20
+ https://en.scratch-wiki.info/wiki/Scratch_File_Format#Targets:~:text=variables,otherwise%20not%20present
21
+ """
22
+ if _value is None:
23
+ _value = 0
24
+
25
+ self.value = _value
26
+ self.is_cloud = _is_cloud
27
+
28
+ super().__init__(_id, _name, _sprite)
29
+
30
+ @property
31
+ def is_global(self):
32
+ """
33
+ Works out whethere a variable is global based on whether the sprite is a stage
34
+ :return: Whether this variable is a global variable.
35
+ """
36
+ return self.sprite.is_stage
37
+
38
+ @staticmethod
39
+ def from_json(data: tuple[str, tuple[str, str | int | float] | tuple[str, str | int | float, bool]]):
40
+ """
41
+ Read data in format: (variable id, variable JSON)
42
+ """
43
+ assert len(data) == 2
44
+ _id, data = data
45
+
46
+ assert len(data) in (2, 3)
47
+ _name, _value = data[:2]
48
+
49
+ if len(data) == 3:
50
+ _is_cloud = data[2]
51
+ else:
52
+ _is_cloud = False
53
+
54
+ return Variable(_id, _name, _value, _is_cloud)
55
+
56
+ def to_json(self) -> tuple[str, str | int | float, bool] | tuple[str, str | int | float]:
57
+ """
58
+ Returns Variable data as a tuple
59
+ """
60
+ if self.is_cloud:
61
+ _ret = self.name, self.value, True
62
+ else:
63
+ _ret = self.name, self.value
64
+
65
+ return _ret
66
+
67
+
68
+ class List(base.NamedIDComponent):
69
+ def __init__(self, _id: str, _name: str, _value: Optional[list[str | int | float]] = None,
70
+ _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT):
71
+ """
72
+ Class representing a list.
73
+ https://en.scratch-wiki.info/wiki/Scratch_File_Format#Targets:~:text=lists,as%20an%20array
74
+ """
75
+ if _value is None:
76
+ _value = []
77
+
78
+ self.value = _value
79
+ super().__init__(_id, _name, _sprite)
80
+
81
+ @staticmethod
82
+ def from_json(data: tuple[str, tuple[str, str | int | float] | tuple[str, str | int | float, bool]]):
83
+ """
84
+ Read data in format: (variable id, variable JSON)
85
+ """
86
+ assert len(data) == 2
87
+ _id, data = data
88
+
89
+ assert len(data) == 2
90
+ _name, _value = data
91
+
92
+ return List(_id, _name, _value)
93
+
94
+ def to_json(self) -> tuple[str, tuple[str, str | int | float, bool] | tuple[str, str | int | float]]:
95
+ """
96
+ Returns List data as a tuple
97
+ """
98
+ return self.name, self.value
99
+
100
+
101
+ class Broadcast(base.NamedIDComponent):
102
+ def __init__(self, _id: str, _name: str, _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT):
103
+ """
104
+ Class representing a broadcast.
105
+ https://en.scratch-wiki.info/wiki/Scratch_File_Format#Targets:~:text=broadcasts,in%20the%20stage
106
+ """
107
+ super().__init__(_id, _name, _sprite)
108
+
109
+ @staticmethod
110
+ def from_json(data: tuple[str, str]):
111
+ assert len(data) == 2
112
+ _id, _name = data
113
+
114
+ return Broadcast(_id, _name)
115
+
116
+ def to_json(self) -> str:
117
+ """
118
+ :return: Broadcast as JSON (just a string of its name)
119
+ """
120
+ return self.name
121
+
122
+
123
+ def construct(vlb_type: Literal["variable", "list", "broadcast"], _id: Optional[str] = None, _name: Optional[str] = None,
124
+ _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT) -> Variable | List | Broadcast:
125
+ if vlb_type == "variable":
126
+ vlb_type = Variable
127
+ elif vlb_type == "list":
128
+ vlb_type = List
129
+ elif vlb_type == "broadcast":
130
+ vlb_type = Broadcast
131
+ else:
132
+ raise exceptions.InvalidVLBName(f"Bad VLB {vlb_type!r}")
133
+
134
+ return vlb_type(_id, _name, _sprite)
@@ -1,93 +1,100 @@
1
- from abc import ABC, abstractmethod
2
- from ..utils.requests import Requests as requests
3
- from threading import Thread
4
- from ..utils import exceptions
5
- import traceback
6
- from . import cloud_recorder
7
-
8
- class BaseEventHandler(ABC):
9
-
10
- def __init__(self):
11
- self._thread = None
12
- self.running = False
13
- self._events = {}
14
- self._threaded_events = {}
15
-
16
- def start(self, *, thread=True, ignore_exceptions=True):
17
- """
18
- Starts the event handler.
19
-
20
- Keyword Arguments:
21
- thread (bool): Whether the event handler should be run in a thread.
22
- ignore_exceptions (bool): Whether to catch exceptions that happen in individual events
23
- """
24
- if self.running is False:
25
- self.ignore_exceptions = ignore_exceptions
26
- self.running = True
27
- if thread:
28
- self._thread = Thread(target=self._updater, args=())
29
- self._thread.start()
30
- else:
31
- self._thread = None
32
- self._updater()
33
-
34
- def call_event(self, event_name, args=[]):
35
- try:
36
- if event_name in self._threaded_events:
37
- Thread(target=self._threaded_events[event_name], args=args).start()
38
- if event_name in self._events:
39
- self._events[event_name](*args)
40
- except Exception as e:
41
- if self.ignore_exceptions:
42
- print(
43
- f"Warning: Caught error in event '{event_name}' - Full error below"
44
- )
45
- try:
46
- traceback.print_exc()
47
- except Exception:
48
- print(e)
49
- else:
50
- raise(e)
51
-
52
- @abstractmethod
53
- def _updater(self):
54
- pass
55
-
56
- def stop(self):
57
- """
58
- Permanently stops the event handler.
59
- """
60
- self.running = False
61
- if self._thread is not None:
62
- self._thread = None
63
-
64
- def pause(self):
65
- """
66
- Pauses the event handler.
67
- """
68
- self.running = False
69
-
70
- def resume(self):
71
- """
72
- Resumes the event handler.
73
- """
74
- if self.running is False:
75
- self.start()
76
-
77
- def event(self, function=None, *, thread=False):
78
- """
79
- Decorator function. Adds an event.
80
- """
81
- def inner(function):
82
- # called directly if the decorator provides arguments
83
- if thread is True:
84
- self._threaded_events[function.__name__] = function
85
- else:
86
- self._events[function.__name__] = function
87
-
88
- if function is None:
89
- # => the decorator provides arguments
90
- return inner
91
- else:
92
- # => the decorator doesn't provide arguments
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections import defaultdict
5
+ from threading import Thread
6
+ from collections.abc import Callable
7
+ import traceback
8
+ from ..utils.requests import Requests as requests
9
+ from ..utils import exceptions
10
+
11
+ class BaseEventHandler(ABC):
12
+ _events: defaultdict[str, list[Callable]]
13
+ _threaded_events: defaultdict[str, list[Callable]]
14
+
15
+ def __init__(self):
16
+ self._thread = None
17
+ self.running = False
18
+ self._events = defaultdict(list)
19
+ self._threaded_events = defaultdict(list)
20
+
21
+ def start(self, *, thread=True, ignore_exceptions=True):
22
+ """
23
+ Starts the event handler.
24
+
25
+ Keyword Arguments:
26
+ thread (bool): Whether the event handler should be run in a thread.
27
+ ignore_exceptions (bool): Whether to catch exceptions that happen in individual events
28
+ """
29
+ if self.running is False:
30
+ self.ignore_exceptions = ignore_exceptions
31
+ self.running = True
32
+ if thread:
33
+ self._thread = Thread(target=self._updater, args=())
34
+ self._thread.start()
35
+ else:
36
+ self._thread = None
37
+ self._updater()
38
+
39
+ def call_event(self, event_name, args=[]):
40
+ try:
41
+ if event_name in self._threaded_events:
42
+ for func in self._threaded_events[event_name]:
43
+ Thread(target=func, args=args).start()
44
+ if event_name in self._events:
45
+ for func in self._events[event_name]:
46
+ func(*args)
47
+ except Exception as e:
48
+ if self.ignore_exceptions:
49
+ print(
50
+ f"Warning: Caught error in event '{event_name}' - Full error below"
51
+ )
52
+ try:
53
+ traceback.print_exc()
54
+ except Exception:
55
+ print(e)
56
+ else:
57
+ raise(e)
58
+
59
+ @abstractmethod
60
+ def _updater(self):
61
+ pass
62
+
63
+ def stop(self):
64
+ """
65
+ Permanently stops the event handler.
66
+ """
67
+ self.running = False
68
+ if self._thread is not None:
69
+ self._thread = None
70
+
71
+ def pause(self):
72
+ """
73
+ Pauses the event handler.
74
+ """
75
+ self.running = False
76
+
77
+ def resume(self):
78
+ """
79
+ Resumes the event handler.
80
+ """
81
+ if self.running is False:
82
+ self.start()
83
+
84
+ def event(self, function=None, *, thread=False):
85
+ """
86
+ Decorator function. Adds an event.
87
+ """
88
+ def inner(function):
89
+ # called directly if the decorator provides arguments
90
+ if thread is True:
91
+ self._threaded_events[function.__name__].append(function)
92
+ else:
93
+ self._events[function.__name__].append(function)
94
+
95
+ if function is None:
96
+ # => the decorator provides arguments
97
+ return inner
98
+ else:
99
+ # => the decorator doesn't provide arguments
93
100
  inner(function)
@@ -1,103 +1,110 @@
1
- """CloudEvents class"""
2
-
3
- from ..cloud import cloud
4
- from ._base import BaseEventHandler
5
- from ..site import cloud_activity
6
- import time
7
- import json
8
- from threading import Thread
9
-
10
- class CloudEvents(BaseEventHandler):
11
- """
12
- Class that calls events when on cloud updates that are received through a websocket connection.
13
- """
14
- def __init__(self, cloud):
15
- super().__init__()
16
- self.cloud = cloud
17
- self._session = cloud._session
18
- self.source_cloud = type(cloud)(project_id=cloud.project_id)
19
- self.source_cloud._session = cloud._session
20
- self.source_cloud.cookie = cloud.cookie
21
- self.source_cloud.header = cloud.header
22
- self.source_cloud.origin = cloud.origin
23
- self.source_cloud.username = cloud.username
24
- self.source_cloud.ws_timeout = None # No timeout -> allows continous listening
25
- self.startup_time = time.time() * 1000
26
-
27
- def _updater(self):
28
- """
29
- A process that listens for cloud activity and executes events on cloud activity
30
- """
31
- self.source_cloud.connect()
32
-
33
- self.call_event("on_ready")
34
-
35
- if self.running is False:
36
- return
37
- while True:
38
- try:
39
- while True:
40
- data = self.source_cloud.websocket.recv().split('\n')
41
- result = []
42
- for i in data:
43
- try:
44
- _a = cloud_activity.CloudActivity(timestamp=time.time()*1000, _session=self._session, cloud=self.cloud)
45
- if _a.timestamp < self.startup_time + 500: # catch the on_connect message sent by TurboWarp's (and sometimes Scratch's) cloud server
46
- continue
47
- data = json.loads(i)
48
- data["name"] = data["name"].replace("☁ ", "")
49
- _a._update_from_dict(data)
50
- self.call_event("on_"+_a.type, [_a])
51
- except Exception as e:
52
- pass
53
- except Exception:
54
- print("CloudEvents: Disconnected. Reconnecting ...", time.time())
55
- time.sleep(0.1) # cooldown
56
- while True:
57
- try:
58
- self.source_cloud.connect()
59
- except Exception:
60
- print("CloudEvents: Reconnecting failed, trying again in 10 seconds.", time.time())
61
- time.sleep(10)
62
- else:
63
- break
64
-
65
- print("CloudEvents: Reconnected.", time.time())
66
- self.call_event("on_reconnect", [])
67
-
68
-
69
- class CloudLogEvents(BaseEventHandler):
70
- """
71
- Class that calls events on cloud updates that are received from a clouddata log.
72
- """
73
- def __init__(self, cloud, *, update_interval=0.1):
74
- super().__init__()
75
- if not hasattr(cloud, "logs"):
76
- raise ValueError("Cloud log events can't be used with a cloud that has no logs available")
77
- self.cloud = cloud
78
- self.source_cloud = cloud
79
- self.update_interval = update_interval
80
- self._session = cloud._session
81
- self.last_timestamp = 0
82
-
83
- def _updater(self):
84
- logs = self.source_cloud.logs(limit=25)
85
- self.last_timestamp = 0
86
- if len(logs) != 0:
87
- self.last_timestamp = logs[0].timestamp
88
-
89
- self.call_event("on_ready")
90
-
91
- while True:
92
- if self.running is False:
93
- return
94
- try:
95
- data = self.source_cloud.logs(limit=25)
96
- for _a in data[::-1]:
97
- if _a.timestamp <= self.last_timestamp:
98
- continue
99
- self.last_timestamp = _a.timestamp
100
- self.call_event("on_"+_a.type, [_a])
101
- except Exception:
102
- pass
103
- time.sleep(self.update_interval)
1
+ """CloudEvents class"""
2
+ from __future__ import annotations
3
+
4
+ from ..cloud import _base
5
+ from ._base import BaseEventHandler
6
+ from ..site import cloud_activity
7
+ import time
8
+ import json
9
+ from collections.abc import Iterator
10
+
11
+ class CloudEvents(BaseEventHandler):
12
+ """
13
+ Class that calls events when on cloud updates that are received through a websocket connection.
14
+ """
15
+ def __init__(self, cloud: _base.AnyCloud):
16
+ super().__init__()
17
+ self.cloud = cloud
18
+ self._session = cloud._session
19
+ self.source_stream = cloud.create_event_stream()
20
+ self.startup_time = time.time() * 1000
21
+
22
+ def disconnect(self):
23
+ self.source_stream.close()
24
+
25
+ def _updater(self):
26
+ """
27
+ A process that listens for cloud activity and executes events on cloud activity
28
+ """
29
+
30
+ self.call_event("on_ready")
31
+
32
+ if self.running is False:
33
+ return
34
+ while True:
35
+ try:
36
+ while True:
37
+ for data in self.source_stream.read():
38
+ try:
39
+ _a = cloud_activity.CloudActivity(timestamp=time.time()*1000, _session=self._session, cloud=self.cloud)
40
+ if _a.timestamp < self.startup_time + 500: # catch the on_connect message sent by TurboWarp's (and sometimes Scratch's) cloud server
41
+ continue
42
+ data["variable_name"] = data["name"]
43
+ data["name"] = data["variable_name"].replace("☁ ", "")
44
+ _a._update_from_dict(data)
45
+ self.call_event("on_"+_a.type, [_a])
46
+ except Exception as e:
47
+ pass
48
+ except Exception:
49
+ print("CloudEvents: Disconnected. Reconnecting ...", time.time())
50
+ time.sleep(0.1) # cooldown
51
+
52
+ print("CloudEvents: Reconnected.", time.time())
53
+ self.call_event("on_reconnect", [])
54
+
55
+ class ManualCloudLogEvents:
56
+ """
57
+ Class that calls events on cloud updates that are received from a clouddata log.
58
+ """
59
+ def __init__(self, cloud: _base.LogCloud):
60
+ if not isinstance(cloud, _base.LogCloud):
61
+ raise ValueError("Cloud log events can't be used with a cloud that has no logs available")
62
+ self.cloud = cloud
63
+ self.source_cloud = cloud
64
+ self._session = cloud._session
65
+ self.last_timestamp = 0
66
+
67
+ def update(self) -> Iterator[tuple[str, list[cloud_activity.CloudActivity]]]:
68
+ """
69
+ Update once and yield all packets
70
+ """
71
+ try:
72
+ data = self.source_cloud.logs(limit=25)
73
+ for _a in data[::-1]:
74
+ if _a.timestamp <= self.last_timestamp:
75
+ continue
76
+ self.last_timestamp = _a.timestamp
77
+ yield ("on_"+_a.type, [_a])
78
+ except Exception:
79
+ pass
80
+
81
+
82
+ class CloudLogEvents(BaseEventHandler):
83
+ """
84
+ Class that calls events on cloud updates that are received from a clouddata log.
85
+ """
86
+ def __init__(self, cloud: _base.LogCloud, *, update_interval=0.1):
87
+ super().__init__()
88
+ if not isinstance(cloud, _base.LogCloud):
89
+ raise ValueError("Cloud log events can't be used with a cloud that has no logs available")
90
+ self.cloud = cloud
91
+ self.source_cloud = cloud
92
+ self.update_interval = update_interval
93
+ self._session = cloud._session
94
+ self.last_timestamp = 0
95
+ self.manual_cloud_log_events = ManualCloudLogEvents(cloud)
96
+
97
+ def _updater(self):
98
+ logs = self.source_cloud.logs(limit=25)
99
+ self.last_timestamp = 0
100
+ if len(logs) != 0:
101
+ self.last_timestamp = logs[0].timestamp
102
+
103
+ self.call_event("on_ready")
104
+
105
+ while True:
106
+ if self.running is False:
107
+ return
108
+ for event_type, event_data in self.manual_cloud_log_events.update():
109
+ self.call_event(event_type, event_data)
110
+ time.sleep(self.update_interval)
@@ -1,21 +1,26 @@
1
- """CloudRecorder class (used by ScratchCloud, TwCloud and other classes inheriting from BaseCloud to deliver cloud var values)"""
2
-
3
- from .cloud_events import CloudEvents
4
-
5
- class CloudRecorder(CloudEvents):
6
-
7
- def __init__(self, cloud, *, initial_values={}):
8
- super().__init__(cloud)
9
- self.cloud_values = initial_values
10
- self.event(self.on_set)
11
-
12
- def get_var(self, var):
13
- if not var in self.cloud_values:
14
- return None
15
- return self.cloud_values[var]
16
-
17
- def get_all_vars(self):
18
- return self.cloud_values
19
-
20
- def on_set(self, activity):
21
- self.cloud_values[activity.var] = activity.value
1
+ """CloudRecorder class (used by ScratchCloud, TwCloud and other classes inheriting from BaseCloud to deliver cloud var values)"""
2
+ from __future__ import annotations
3
+
4
+ from .cloud_events import CloudEvents
5
+ from typing import Optional
6
+
7
+
8
+ class CloudRecorder(CloudEvents):
9
+ def __init__(self, cloud, *, initial_values: Optional[dict] = None):
10
+ if initial_values is None:
11
+ initial_values = {}
12
+
13
+ super().__init__(cloud)
14
+ self.cloud_values = initial_values
15
+ self.event(self.on_set)
16
+
17
+ def get_var(self, var):
18
+ if var not in self.cloud_values:
19
+ return None
20
+ return self.cloud_values[var]
21
+
22
+ def get_all_vars(self):
23
+ return self.cloud_values
24
+
25
+ def on_set(self, activity):
26
+ self.cloud_values[activity.var] = activity.value