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,114 @@
1
+ """
2
+ Parser for TurboWarp settings configuration
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import math
9
+ from dataclasses import dataclass
10
+ from typing import Any
11
+
12
+ from . import commons, base
13
+
14
+ _START = """Configuration for https://turbowarp.org/
15
+ You can move, resize, and minimize this comment, but don't edit it by hand. This comment can be deleted to remove the stored settings.
16
+ """
17
+ _END = " // _twconfig_"
18
+
19
+
20
+ @dataclass
21
+ class TWConfig(base.JSONSerializable):
22
+ framerate: int = None,
23
+ interpolation: bool = False,
24
+ hq_pen: bool = False,
25
+ max_clones: float | int | None = None,
26
+ misc_limits: bool = True,
27
+ fencing: bool = True
28
+ width: int = None
29
+ height: int = None
30
+
31
+ @staticmethod
32
+ def from_json(data: dict) -> TWConfig:
33
+ # Non-runtime options
34
+ _framerate = data.get("framerate")
35
+ _interpolation = data.get("interpolation", False)
36
+ _hq_pen = data.get("hq", False)
37
+
38
+ # Runtime options
39
+ _runtime_options = data.get("runtimeOptions", {})
40
+
41
+ # Luckily for us, the JSON module actually accepts the 'Infinity' literal. Otherwise, it would be a right pain
42
+ _max_clones = _runtime_options.get("maxClones")
43
+ _misc_limits = _runtime_options.get("miscLimits", True)
44
+ _fencing = _runtime_options.get("fencing", True)
45
+
46
+ # Custom stage size
47
+ _width = data.get("width")
48
+ _height = data.get("height")
49
+
50
+ return TWConfig(_framerate, _interpolation, _hq_pen, _max_clones, _misc_limits, _fencing, _width, _height)
51
+
52
+ def to_json(self) -> dict:
53
+ runtime_options = {}
54
+ commons.noneless_update(
55
+ runtime_options,
56
+ {
57
+ "maxClones": self.max_clones,
58
+ "miscLimits": none_if_eq(self.misc_limits, True),
59
+ "fencing": none_if_eq(self.fencing, True)
60
+ })
61
+
62
+ data = {}
63
+ commons.noneless_update(data, {
64
+ "framerate": self.framerate,
65
+ "runtimeOptions": runtime_options,
66
+ "interpolation": none_if_eq(self.interpolation, False),
67
+ "hq": none_if_eq(self.hq_pen, False),
68
+ "width": self.width,
69
+ "height": self.height
70
+ })
71
+ return data
72
+
73
+ @property
74
+ def infinite_clones(self):
75
+ return self.max_clones == math.inf
76
+
77
+ @staticmethod
78
+ def from_str(string: str):
79
+ return TWConfig.from_json(get_twconfig_data(string))
80
+
81
+
82
+ def is_valid_twconfig(string: str) -> bool:
83
+ """
84
+ Checks if some text is TWConfig (does not check the JSON itself)
85
+ :param string: text (from a comment)
86
+ :return: Boolean whether it is TWConfig
87
+ """
88
+
89
+ if string.startswith(_START) and string.endswith(_END):
90
+ json_part = string[len(_START):-len(_END)]
91
+ if commons.is_valid_json(json_part):
92
+ return True
93
+ return False
94
+
95
+
96
+ def get_twconfig_data(string: str) -> dict | None:
97
+ try:
98
+ return json.loads(string[len(_START):-len(_END)])
99
+ except ValueError:
100
+ return None
101
+
102
+
103
+ # todo: move this to commons.py?
104
+ def none_if_eq(data, compare) -> Any | None:
105
+ """
106
+ Returns None if data and compare are the same
107
+ :param data: Original data
108
+ :param compare: Data to compare
109
+ :return: Either the original data or None
110
+ """
111
+ if data == compare:
112
+ return None
113
+ else:
114
+ return data
@@ -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 scratchattach.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: list[str | int | float] = []
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, list[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)
File without changes
@@ -0,0 +1,101 @@
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 scratchattach.utils.requests import requests
9
+ from scratchattach.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
+ print(f"{self._threaded_events=}")
21
+
22
+ def start(self, *, thread=True, ignore_exceptions=True):
23
+ """
24
+ Starts the event handler.
25
+
26
+ Keyword Arguments:
27
+ thread (bool): Whether the event handler should be run in a thread.
28
+ ignore_exceptions (bool): Whether to catch exceptions that happen in individual events
29
+ """
30
+ if self.running is False:
31
+ self.ignore_exceptions = ignore_exceptions
32
+ self.running = True
33
+ if thread:
34
+ self._thread = Thread(target=self._updater, args=())
35
+ self._thread.start()
36
+ else:
37
+ self._thread = None
38
+ self._updater()
39
+
40
+ def call_event(self, event_name, args : list = []):
41
+ try:
42
+ if event_name in self._threaded_events:
43
+ for func in self._threaded_events[event_name]:
44
+ Thread(target=func, args=args).start()
45
+ if event_name in self._events:
46
+ for func in self._events[event_name]:
47
+ func(*args)
48
+ except Exception as e:
49
+ if self.ignore_exceptions:
50
+ print(
51
+ f"Warning: Caught error in event '{event_name}' - Full error below"
52
+ )
53
+ try:
54
+ traceback.print_exc()
55
+ except Exception:
56
+ print(e)
57
+ else:
58
+ raise(e)
59
+
60
+ @abstractmethod
61
+ def _updater(self):
62
+ pass
63
+
64
+ def stop(self):
65
+ """
66
+ Permanently stops the event handler.
67
+ """
68
+ self.running = False
69
+ if self._thread is not None:
70
+ self._thread = None
71
+
72
+ def pause(self):
73
+ """
74
+ Pauses the event handler.
75
+ """
76
+ self.running = False
77
+
78
+ def resume(self):
79
+ """
80
+ Resumes the event handler.
81
+ """
82
+ if self.running is False:
83
+ self.start()
84
+
85
+ def event(self, function=None, *, thread=False):
86
+ """
87
+ Decorator function. Adds an event.
88
+ """
89
+ def inner(function):
90
+ # called directly if the decorator provides arguments
91
+ if thread is True:
92
+ self._threaded_events[function.__name__].append(function)
93
+ else:
94
+ self._events[function.__name__].append(function)
95
+
96
+ if function is None:
97
+ # => the decorator provides arguments
98
+ return inner
99
+ else:
100
+ # => the decorator doesn't provide arguments
101
+ inner(function)
@@ -0,0 +1,130 @@
1
+ """CloudEvents class"""
2
+ from __future__ import annotations
3
+
4
+ import traceback
5
+
6
+ from scratchattach.cloud import _base
7
+ from ._base import BaseEventHandler
8
+ from scratchattach.utils import exceptions
9
+ from scratchattach.site import cloud_activity
10
+ import time
11
+ import json
12
+ from collections.abc import Iterator
13
+
14
+ class CloudEvents(BaseEventHandler):
15
+ """
16
+ Class that calls events when on cloud updates that are received through a websocket connection.
17
+ """
18
+ def __init__(self, cloud: _base.AnyCloud):
19
+ super().__init__()
20
+ self.cloud = cloud
21
+ self._session = cloud._session
22
+ self.source_stream = cloud.create_event_stream()
23
+ self.startup_time = time.time() * 1000
24
+ self.subsequent_reconnects = 0
25
+
26
+ def disconnect(self):
27
+ self.source_stream.close()
28
+
29
+ def _updater(self):
30
+ """
31
+ A process that listens for cloud activity and executes events on cloud activity
32
+ """
33
+
34
+ self.call_event("on_ready")
35
+
36
+ if self.running is False:
37
+ return
38
+
39
+ # TODO: refactor this method. It works, but is hard to read
40
+ while True:
41
+ try:
42
+ while True:
43
+ # print("Checking for more events")
44
+ for data in self.source_stream.read():
45
+ # print(f"Got event {data}")
46
+ self.subsequent_reconnects = 0
47
+ try:
48
+ _a = cloud_activity.CloudActivity(timestamp=time.time()*1000, _session=self._session, cloud=self.cloud)
49
+ if _a.timestamp < self.startup_time + 500: # catch the on_connect message sent by TurboWarp's (and sometimes Scratch's) cloud server
50
+ # print(f"Skipped as {_a.timestamp} < {self.startup_time + 500}")
51
+ continue
52
+ data["variable_name"] = data["name"]
53
+ data["name"] = data["variable_name"].replace("☁ ", "")
54
+ _a._update_from_dict(data)
55
+ # print(f"sending event {_a}")
56
+ self.call_event("on_"+_a.type, [_a])
57
+ except Exception as e:
58
+ print(f"Cloud events _updated ignored: {e} {traceback.format_exc()}")
59
+ pass
60
+ except Exception:
61
+ self.subsequent_reconnects += 1
62
+ time.sleep(0.1) # cooldown
63
+
64
+ if self.subsequent_reconnects >= 5:
65
+ print(f"Warning: {self.subsequent_reconnects} subsequent cloud disconnects. Cloud may be down, causing CloudEvents to not call events.")
66
+ self.call_event("on_reconnect", [])
67
+
68
+ class ManualCloudLogEvents:
69
+ """
70
+ Class that calls events on cloud updates that are received from a clouddata log.
71
+ """
72
+ def __init__(self, cloud: _base.LogCloud):
73
+ if not isinstance(cloud, _base.LogCloud):
74
+ raise ValueError("Cloud log events can't be used with a cloud that has no logs available")
75
+ self.cloud = cloud
76
+ self.source_cloud = cloud
77
+ self._session = cloud._session
78
+ self.last_timestamp = 0
79
+ self.subsequent_failed_log_fetches = 0
80
+
81
+ def update(self) -> Iterator[tuple[str, list[cloud_activity.CloudActivity]]]:
82
+ """
83
+ Update once and yield all packets
84
+ """
85
+ try:
86
+ data = self.source_cloud.logs(limit=25)
87
+ self.subsequent_failed_log_fetches = 0
88
+ for _a in data[::-1]:
89
+ if _a.timestamp <= self.last_timestamp:
90
+ continue
91
+ self.last_timestamp = _a.timestamp
92
+ yield ("on_"+_a.type, [_a])
93
+ except Exception:
94
+ self.subsequent_failed_log_fetches += 1
95
+ if self.subsequent_failed_log_fetches == 20:
96
+ print("Warning: 20 subsequent clouddata log fetches failed. Scrach's cloud logs may be down, causing CloudLogEvents to not call events.")
97
+
98
+ class CloudLogEvents(BaseEventHandler):
99
+ """
100
+ Class that calls events on cloud updates that are received from a clouddata log.
101
+ """
102
+ def __init__(self, cloud: _base.LogCloud, *, update_interval=0.1):
103
+ super().__init__()
104
+ if not isinstance(cloud, _base.LogCloud):
105
+ raise ValueError("Cloud log events can't be used with a cloud that has no logs available")
106
+ self.cloud = cloud
107
+ self.source_cloud = cloud
108
+ self.update_interval = update_interval
109
+ self._session = cloud._session
110
+ self.last_timestamp = 0
111
+ self.manual_cloud_log_events = ManualCloudLogEvents(cloud)
112
+
113
+ def _updater(self):
114
+ try:
115
+ logs = self.source_cloud.logs(limit=25)
116
+ except exceptions.FetchError:
117
+ logs = []
118
+
119
+ self.last_timestamp = 0
120
+ if len(logs) != 0:
121
+ self.last_timestamp = logs[0].timestamp
122
+
123
+ self.call_event("on_ready")
124
+
125
+ while True:
126
+ if self.running is False:
127
+ return
128
+ for event_type, event_data in self.manual_cloud_log_events.update():
129
+ self.call_event(event_type, event_data)
130
+ time.sleep(self.update_interval)
@@ -0,0 +1,26 @@
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 typing import Optional, Any
5
+
6
+ from .cloud_events import CloudEvents
7
+
8
+
9
+ class CloudRecorder(CloudEvents):
10
+ def __init__(self, cloud, *, initial_values: Optional[dict[str, Any]] = None):
11
+ initial_values = initial_values or {}
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