Skip to content

Commit cf26c7d

Browse files
committed
fix a few type errors
1 parent c8038ac commit cf26c7d

8 files changed

Lines changed: 287 additions & 222 deletions

File tree

scratchattach/cloud/_base.py

Lines changed: 81 additions & 55 deletions
Large diffs are not rendered by default.

scratchattach/cloud/cloud.py

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@
77

88
from websocket import WebSocketBadStatusException
99

10-
from ._base import BaseCloud
10+
from ._base import BaseCloud, LogCloud
1111
from scratchattach.utils.requests import requests
1212
from scratchattach.utils import exceptions, commons
1313
from scratchattach.site import cloud_activity
1414

15-
class ScratchCloud(BaseCloud):
15+
16+
class ScratchCloud(LogCloud):
1617
def __init__(self, *, project_id, _session=None):
1718
super().__init__()
18-
19+
1920
self.project_id = project_id
2021

2122
# Configure this object's attributes specifically for being used with Scratch's cloud:
@@ -28,35 +29,37 @@ def __init__(self, *, project_id, _session=None):
2829
self.origin = "https://scratch.mit.edu"
2930

3031
def connect(self):
31-
self._assert_auth() # Connecting to Scratch's cloud websocket requires a login to the Scratch website
32+
self._assert_auth() # Connecting to Scratch's cloud websocket requires a login to the Scratch website
3233
try:
3334
super().connect()
3435
except WebSocketBadStatusException as e:
3536
raise exceptions.CloudConnectionError("Error: Scratch's Cloud system may be down. Please try again later.") from e
3637

37-
def set_var(self, variable, value, *, max_retries : int = 2):
38-
self._assert_auth() # Setting a cloud var requires a login to the Scratch website
38+
def set_var(self, variable, value, *, max_retries: int = 2):
39+
self._assert_auth() # Setting a cloud var requires a login to the Scratch website
3940
super().set_var(variable, value, max_retries=max_retries)
4041

41-
def set_vars(self, var_value_dict, *, intelligent_waits=True, max_retries : int = 2):
42-
self._assert_auth()
42+
def set_vars(self, var_value_dict, *, intelligent_waits=True, max_retries: int = 2):
43+
self._assert_auth()
4344
super().set_vars(var_value_dict, intelligent_waits=intelligent_waits, max_retries=max_retries)
4445

4546
def logs(self, *, filter_by_var_named=None, limit=100, offset=0) -> list[cloud_activity.CloudActivity]:
4647
"""
4748
Gets the data from Scratch's clouddata logs.
48-
49+
4950
Keyword Arguments:
5051
filter_by_var_named (str or None): If you only want to get data for one cloud variable, set this argument to its name.
5152
limit (int): Max. amount of returned activity.
5253
offset (int): Offset of the first activity in the returned list.
5354
log_url (str): If you want to get the clouddata from a cloud log API different to Scratch's normal cloud log API, set this argument to the URL of the API. Only set this argument if you know what you are doing. If you want to get the clouddata from the normal API, don't put this argument.
5455
"""
5556
try:
56-
data = requests.get(f"https://clouddata.scratch.mit.edu/logs?projectid={self.project_id}&limit={limit}&offset={offset}", timeout=10).json()
57+
data = requests.get(
58+
f"https://clouddata.scratch.mit.edu/logs?projectid={self.project_id}&limit={limit}&offset={offset}", timeout=10
59+
).json()
5760
if filter_by_var_named is not None:
5861
filter_by_var_named = filter_by_var_named.removeprefix("☁ ")
59-
data = list(filter(lambda k: k["name"] == "☁ "+filter_by_var_named, data))
62+
data = list(filter(lambda k: k["name"] == "☁ " + filter_by_var_named, data))
6063
for x in data:
6164
x["cloud"] = self
6265
return commons.parse_object_list(data, cloud_activity.CloudActivity, self._session, "name")
@@ -66,16 +69,16 @@ def logs(self, *, filter_by_var_named=None, limit=100, offset=0) -> list[cloud_a
6669
def get_var(self, var, *, recorder_initial_values: Optional[dict[str, Any]] = None, use_logs=False):
6770
var = var.removeprefix("☁ ")
6871
if self._session is None or use_logs:
69-
filtered = self.logs(limit=100, filter_by_var_named="☁ "+var)
72+
filtered = self.logs(limit=100, filter_by_var_named="☁ " + var)
7073
if len(filtered) == 0:
7174
return None
7275
return filtered[0].value
7376
else:
7477
if self.recorder is None:
7578
initial_values = self.get_all_vars(use_logs=True)
76-
return super().get_var("☁ "+var, recorder_initial_values=initial_values)
79+
return super().get_var("☁ " + var, recorder_initial_values=initial_values)
7780
else:
78-
return super().get_var("☁ "+var, recorder_initial_values=recorder_initial_values)
81+
return super().get_var("☁ " + var, recorder_initial_values=recorder_initial_values)
7982

8083
def get_all_vars(self, *, recorder_initial_values: Optional[dict[str, Any]] = None, use_logs=False):
8184
if self._session is None or use_logs:
@@ -95,33 +98,33 @@ def get_all_vars(self, *, recorder_initial_values: Optional[dict[str, Any]] = No
9598
def events(self, *, use_logs=False):
9699
if self._session is None or use_logs:
97100
from scratchattach.eventhandlers.cloud_events import CloudLogEvents
101+
98102
return CloudLogEvents(self)
99103
else:
100104
return super().events()
101105

102106

103107
class TwCloud(BaseCloud):
104-
def __init__(self, *, project_id, cloud_host="wss://clouddata.turbowarp.org", purpose="", contact="",
105-
_session=None):
108+
def __init__(self, *, project_id, cloud_host="wss://clouddata.turbowarp.org", purpose="", contact="", _session=None):
106109
super().__init__()
107-
110+
108111
self.project_id = project_id
109-
112+
110113
# Configure this object's attributes specifically for being used with TurboWarp's cloud:
111114
self.cloud_host = cloud_host
112-
self.ws_shortterm_ratelimit = 0 # TurboWarp doesn't enforce a wait time between cloud variable sets
115+
self.ws_shortterm_ratelimit = 0 # TurboWarp doesn't enforce a wait time between cloud variable sets
113116
self.ws_longterm_ratelimit = 0
114-
self.length_limit = 100000 # TurboWarp doesn't enforce a cloud variable length
117+
self.length_limit = 100000 # TurboWarp doesn't enforce a cloud variable length
115118
purpose_string = ""
116119
if purpose != "" or contact != "":
117120
purpose_string = f" (Purpose:{purpose}; Contact:{contact})"
118-
self.header = {"User-Agent":f"scratchattach/2.0.0{purpose_string}"}
121+
self.header = {"User-Agent": f"scratchattach/2.0.0{purpose_string}"}
119122

120-
class CustomCloud(BaseCloud):
121123

124+
class CustomCloud(BaseCloud):
122125
def __init__(self, *, project_id, cloud_host, **kwargs):
123126
super().__init__()
124-
127+
125128
self.project_id = project_id
126129
self.cloud_host = cloud_host
127130

@@ -145,36 +148,37 @@ def get_cloud(project_id, *, CloudClass: type[BaseCloud] = ScratchCloud) -> Base
145148
146149
Args:
147150
project_id:
148-
151+
149152
Keyword arguments:
150153
CloudClass: The class that the returned object should be of. By default this class is scratchattach.cloud.ScratchCloud.
151154
152155
Returns:
153156
Type[scratchattach.cloud._base.BaseCloud]: An object representing the cloud of a project. Can be of any class inheriting from BaseCloud.
154157
"""
155158
warnings.warn(
156-
"To set Scratch cloud variables, use session.connect_cloud instead of get_cloud",
157-
exceptions.CloudAuthenticationWarning
159+
"To set Scratch cloud variables, use session.connect_cloud instead of get_cloud", exceptions.CloudAuthenticationWarning
158160
)
159161
return CloudClass(project_id=project_id)
160162

163+
161164
def get_scratch_cloud(project_id):
162165
"""
163166
Warning:
164167
Since this method doesn't connect a login / session to the returned object, setting Scratch cloud variables won't be possible with it.
165168
166169
To set Scratch cloud variables, use `scratchattach.Session.connect_scratch_cloud` instead.
167170
168-
171+
169172
Returns:
170173
scratchattach.cloud.ScratchCloud: An object representing the Scratch cloud of a project.
171174
"""
172175
warnings.warn(
173176
"To set Scratch cloud variables, use session.connect_scratch_cloud instead of get_scratch_cloud",
174-
exceptions.CloudAuthenticationWarning
177+
exceptions.CloudAuthenticationWarning,
175178
)
176179
return ScratchCloud(project_id=project_id)
177180

181+
178182
def get_tw_cloud(project_id, *, purpose="", contact="", cloud_host="wss://clouddata.turbowarp.org"):
179183
"""
180184
Returns:

scratchattach/eventhandlers/cloud_events.py

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
"""CloudEvents class"""
2+
23
from __future__ import annotations
4+
from scratchattach.site.typed_dicts import CloudActivityDict
5+
from typing import cast
36

47
import traceback
58

@@ -10,10 +13,14 @@
1013
import time
1114
from collections.abc import Iterator
1215

16+
1317
class CloudEvents(BaseEventHandler):
1418
"""
1519
Class that calls events when on cloud updates that are received through a websocket connection.
1620
"""
21+
22+
source_stream: _base.EventStream
23+
1724
def __init__(self, cloud: _base.AnyCloud):
1825
super().__init__()
1926
self.cloud = cloud
@@ -29,12 +36,12 @@ def _updater(self):
2936
"""
3037
A process that listens for cloud activity and executes events on cloud activity
3138
"""
32-
39+
3340
self.call_event("on_ready")
3441

3542
if not self.running:
3643
return
37-
44+
3845
# TODO: refactor this method. It works, but is hard to read
3946
while self.running:
4047
try:
@@ -46,64 +53,77 @@ def _updater(self):
4653
# print(f"Got event {data}")
4754
self.subsequent_reconnects = 0
4855
try:
49-
_a = cloud_activity.CloudActivity(timestamp=time.time()*1000, _session=self._session, cloud=self.cloud)
56+
_a = cloud_activity.CloudActivity(
57+
timestamp=time.time() * 1000,
58+
_session=self._session,
59+
cloud=self.cloud,
60+
)
5061
# if _a.timestamp < self.startup_time + 500: # catch the on_connect message sent by TurboWarp's (and sometimes Scratch's) cloud server
5162
# # print(f"Skipped as {_a.timestamp} < {self.startup_time + 500}")
5263
# continue
53-
data["variable_name"] = data["name"]
54-
data["name"] = data["variable_name"].replace("☁ ", "")
55-
_a._update_from_dict(data)
64+
cloud_activity_dict = cast(CloudActivityDict, data)
65+
cloud_activity_dict["variable_name"] = cloud_activity_dict["name"]
66+
cloud_activity_dict["name"] = cloud_activity_dict[
67+
"variable_name"
68+
].replace("☁ ", "")
69+
_a._update_from_dict(cloud_activity_dict)
5670
# print(f"sending event {_a}")
5771
self.call_event(f"on_{_a.type}", [_a])
5872
except Exception as e:
5973
print(f"Cloud events _updated ignored: {e} {traceback.format_exc()}")
6074
pass
6175
except Exception:
6276
self.subsequent_reconnects += 1
63-
time.sleep(0.1) # cooldown
77+
time.sleep(0.1) # cooldown
6478

6579
if self.subsequent_reconnects >= 5:
66-
print(f"Warning: {self.subsequent_reconnects} subsequent cloud disconnects. Cloud may be down, causing CloudEvents to not call events.")
80+
print(
81+
f"Warning: {self.subsequent_reconnects} subsequent cloud disconnects. Cloud may be down, causing CloudEvents to not call events."
82+
)
6783
self.call_event("on_reconnect", [])
6884

85+
6986
class ManualCloudLogEvents:
70-
"""
71-
Class that calls events on cloud updates that are received from a clouddata log.
72-
"""
87+
"""Class that calls events on cloud updates that are received from a clouddata log."""
88+
7389
def __init__(self, cloud: _base.LogCloud):
7490
if not isinstance(cloud, _base.LogCloud):
75-
raise ValueError("Cloud log events can't be used with a cloud that has no logs available")
91+
raise ValueError(
92+
"Cloud log events can't be used with a cloud that has no logs available"
93+
)
7694
self.cloud = cloud
7795
self.source_cloud = cloud
7896
self._session = cloud._session
7997
self.last_timestamp = 0
8098
self.subsequent_failed_log_fetches = 0
8199

82100
def update(self) -> Iterator[tuple[str, list[cloud_activity.CloudActivity]]]:
83-
"""
84-
Update once and yield all packets
85-
"""
101+
"""Update once and yield all packets"""
86102
try:
87103
data = self.source_cloud.logs(limit=25)
88104
self.subsequent_failed_log_fetches = 0
89105
for _a in data[::-1]:
90106
if _a.timestamp <= self.last_timestamp:
91107
continue
92108
self.last_timestamp = int(_a.timestamp)
93-
yield ("on_"+_a.type, [_a])
109+
yield ("on_" + _a.type, [_a])
94110
except Exception:
95111
self.subsequent_failed_log_fetches += 1
96112
if self.subsequent_failed_log_fetches == 20:
97-
print("Warning: 20 subsequent clouddata log fetches failed. Scrach's cloud logs may be down, causing CloudLogEvents to not call events.")
98-
113+
print(
114+
"Warning: 20 subsequent clouddata log fetches failed. Scrach's cloud logs may be down, causing CloudLogEvents to not call events."
115+
)
116+
117+
99118
class CloudLogEvents(BaseEventHandler):
100-
"""
101-
Class that calls events on cloud updates that are received from a clouddata log.
102-
"""
119+
"""Class that calls events on cloud updates that are received from a clouddata log."""
120+
103121
def __init__(self, cloud: _base.LogCloud, *, update_interval=0.1):
104122
super().__init__()
105123
if not isinstance(cloud, _base.LogCloud):
106-
raise ValueError("Cloud log events can't be used with a cloud that has no logs available")
124+
raise ValueError(
125+
"Cloud log events can't be used with a cloud that has no logs available"
126+
)
107127
self.cloud = cloud
108128
self.source_cloud = cloud
109129
self.update_interval = update_interval

scratchattach/eventhandlers/cloud_requests.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ class CloudRequests(CloudEvents):
137137
executer_thread: Optional[Thread]
138138
responder_thread: Optional[Thread]
139139
extra_executor_threads: list[Thread]
140+
cloud: _base.AnyCloud
140141

141142
def __init__(
142143
self,
@@ -268,19 +269,19 @@ def _set_FROM_HOST_var(self, value):
268269
self.current_var += 1
269270
if self.current_var == len(self.used_cloud_vars):
270271
self.current_var = 0
271-
time.sleep(self.cloud.ws_shortterm_ratelimit)
272+
time.sleep(getattr(self.cloud, "ws_shortterm_ratelimit", 0.1))
272273

273274
def _respond(self, request_id, response, *, validation=2222):
274275
"""
275276
Sends back the request response to the Scratch project
276277
"""
277-
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
278+
if (getattr(self.cloud, "last_var_set", time.time()) + 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
278279
) or self.no_packet_loss:
279280
self.cloud.reconnect()
280281

281282
memory = ResponseMemory(rid=request_id, packets={})#{"rid":request_id}
282283
remaining_response = str(response)
283-
length_limit = self.cloud.length_limit - (len(str(request_id))+6) # the subtrahend is the worst-case length of the "."+numbers after the "."
284+
length_limit = getattr(self.cloud, "length_limit", 256) - (len(str(request_id))+6) # the subtrahend is the worst-case length of the "."+numbers after the "."
284285

285286
i = 0
286287
while not remaining_response == "":
@@ -457,11 +458,12 @@ def on_reconnect(self):
457458
Called when the underlying cloud events reconnect. Makes sure that no requests are missed in this case.
458459
"""
459460
try:
460-
extradata = self.cloud.logs(limit=35)[::-1] # Reverse result so oldest activity is first
461-
for activity in extradata:
462-
if activity.timestamp < self.startup_time:
463-
continue
464-
self.on_set(activity) # Read in the fetched activity
461+
if isinstance(self.cloud, _base.LogCloud):
462+
extradata = self.cloud.logs(limit=35)[::-1] # Reverse result so oldest activity is first
463+
for activity in extradata:
464+
if activity.timestamp < self.startup_time:
465+
continue
466+
self.on_set(activity) # Read in the fetched activity
465467
except Exception:
466468
pass
467469

@@ -511,7 +513,7 @@ def send(self, data, *, priority=0):
511513
else:
512514
time.sleep(0.07)
513515

514-
def stop(self, wait_extra_threads: bool = True):
516+
def stop(self, wait_extra_threads: bool = True): # ty:ignore[invalid-method-override]
515517
"""
516518
Stops the request handler and all associated threads forever. Lets running response sending processes finish.
517519
"""
@@ -543,6 +545,10 @@ def hard_stop(self):
543545

544546
def credit_check(self):
545547
try:
548+
if not isinstance(self.cloud, _base.BaseCloud):
549+
raise TypeError
550+
if self.cloud.project_id is None:
551+
raise ValueError
546552
p = project.Project(id=self.cloud.project_id)
547553
if not p.update(): # can't get project, probably because it's unshared (no authentication is used for getting it)
548554
print("If you use cloud requests or cloud storages, please credit TimMcCool!")

scratchattach/py.typed

Whitespace-only changes.

0 commit comments

Comments
 (0)