Repository navigation
feat(cloud_server): implement SSL secure websocket from semver2 into semver3 #723
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9863d50
7e8fddd
bab2905
5cd379d
2950297
a1423ce
8d5aa20
70ffb36
92cc748
1924636
2a5dfc9
5b0ff22
b80c260
f81bce7
1d0bf6e
e905eb8
16b58b7
6243190
c476784
65a4ddd
b77df00
3b96888
1b34b7c
b4047e5
14d1074
0f921da
c8a76c9
cb00837
615cb99
cc66264
aa697f9
9964178
c4779a2
d78f40d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,6 @@ | ||
| from __future__ import annotations | ||
| import queue | ||
| import threading | ||
| import traceback | ||
|
|
||
| import json | ||
|
|
@@ -13,6 +15,7 @@ | |
| from scratchattach.cloud import cloud as cloud_module | ||
|
|
||
| if TYPE_CHECKING: | ||
| from scratchattach.eventhandlers import cloud_server | ||
| from _typeshed import SupportsRead | ||
| else: | ||
| T = TypeVar("T") | ||
|
|
@@ -106,7 +109,7 @@ def set_vars( | |
| """ | ||
|
|
||
| @abstractmethod | ||
| def get_var(self, var, *, recorder_initial_values: Optional[dict[str, Any]] = None) -> T: | ||
| def get_var(self, var, *, recorder_initial_values: Optional[dict[str, Any]] = None) -> T | None: | ||
| pass | ||
|
|
||
| @abstractmethod | ||
|
|
@@ -146,7 +149,7 @@ def create_event_stream(self) -> EventStream: | |
| pass | ||
|
|
||
|
|
||
| class DummyCloud(AnyCloud[Any]): | ||
| class DummyCloud(AnyCloud[T]): | ||
| class DummyEventStream(EventStream): | ||
| def read(self, length=...): | ||
| return iter(()) | ||
|
|
@@ -174,12 +177,12 @@ def set_vars( | |
| ): | ||
| pass | ||
|
|
||
| def get_var(self, var, *, recorder_initial_values: Optional[dict[str, Any]] = None) -> Any: | ||
| pass | ||
| def get_var(self, var, *, recorder_initial_values: Optional[dict[str, T]] = None) -> T | None: | ||
| return None | ||
|
|
||
| def get_all_vars( | ||
| self, *, recorder_initial_values: Optional[dict[str, Any]] = None | ||
| ) -> dict[str, Any]: | ||
| self, *, recorder_initial_values: Optional[dict[str, T]] = None | ||
| ) -> dict[str, T]: | ||
| return {} | ||
|
|
||
|
|
||
|
|
@@ -301,7 +304,7 @@ def close(self) -> None: | |
| self.source_cloud.disconnect() | ||
|
|
||
|
|
||
| class BaseCloud(AnyCloud[Union[str, int]]): | ||
| class BaseCloud(AnyCloud[Union[str, int, float]]): | ||
| """ | ||
| Base class for a project's cloud variables. Represents a cloud. | ||
|
|
||
|
|
@@ -557,12 +560,16 @@ def _ensure_recorder_running( | |
| time.sleep(0.01) | ||
| return recorder | ||
|
|
||
| def get_var(self, var, *, recorder_initial_values: Optional[dict[str, Any]] = None): | ||
| def get_var( | ||
| self, var, *, recorder_initial_values: Optional[dict[str, Any]] = None | ||
| ) -> str | int | float | None: | ||
| var = "☁ " + var.removeprefix("☁ ") | ||
| recorder = self._ensure_recorder_running(recorder_initial_values=recorder_initial_values) | ||
| return recorder.get_var(var) | ||
|
|
||
| def get_all_vars(self, *, recorder_initial_values: Optional[dict[str, Any]] = None): | ||
| def get_all_vars( | ||
| self, *, recorder_initial_values: Optional[dict[str, Any]] = None | ||
| ) -> dict[str, str | int | float]: | ||
| recorder = self._ensure_recorder_running(recorder_initial_values=recorder_initial_values) | ||
| return recorder.get_all_vars() | ||
|
|
||
|
|
@@ -623,3 +630,127 @@ def _get_cloud_var_initial_data_or_none(project_id: Union[str, int]) -> Optional | |
| return _get_cloud_var_initial_data(project_id) | ||
| except Exception: | ||
| return None | ||
|
|
||
|
|
||
| class CloudServerAdapter(AnyCloud[str | int | float]): | ||
| server: "cloud_server.BaseCloudServer" | ||
| disconnected: threading.Event | ||
| project_id: str | int | ||
| connected_event_stream_queues: dict[int, queue.Queue[dict[str, Any]]] | ||
|
|
||
| def __init__(self, server: "cloud_server.BaseCloudServer", project_id: str | int): | ||
| self.server = server | ||
| self.disconnected = threading.Event() | ||
| self.project_id = project_id | ||
| self.connected_event_stream_queues = {} | ||
|
|
||
| @self.server.event | ||
| def on_outgoing_packet(event): | ||
| for listener in self.connected_event_stream_queues.values(): | ||
| listener.put(event) | ||
|
|
||
| def connect(self): | ||
| self.active_connection = True | ||
| self.disconnected.clear() | ||
|
|
||
| def disconnect(self): | ||
| self.active_connection = False | ||
| self.disconnected.set() | ||
|
|
||
| def reconnect(self): | ||
| self.disconnect() | ||
| time.sleep(0.1) | ||
| self.connect() | ||
|
|
||
| def _enforce_ratelimit(self, *, n: int) -> None: | ||
| pass | ||
|
|
||
| def set_var(self, variable: str, value: str | int | float, *, max_retries: int = 2) -> None: | ||
| """ | ||
| Sets a cloud variable. | ||
|
|
||
| Args: | ||
| variable (str): The name of the cloud variable that should be set (provided without the cloud emoji) | ||
| value (Any): The value the cloud variable should be set to | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| Kwargs: | ||
| max_retries (int) : Maximum number of times to retry setting the var if setting fails before raising an exception | ||
| """ | ||
| if self.disconnected.is_set(): | ||
| return | ||
| self.server.set_var( | ||
| self.project_id, | ||
| variable, | ||
| value, | ||
| ) | ||
|
|
||
| def set_vars( | ||
| self, | ||
| var_value_dict: dict[str, str | int | float], | ||
| *, | ||
| intelligent_waits: bool = True, | ||
| max_retries: int = 2, | ||
|
Comment on lines
+691
to
+692
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these arguments are unused. if this is intentional, they should also be removed from the docstring |
||
| ): | ||
| """ | ||
| Sets multiple cloud variables at once (works for an unlimited amount of variables). | ||
|
|
||
| Args: | ||
| var_value_dict (dict): variable:value dictionary with the variables / values to set. The dict should like this: {"var1":"value1", "var2":"value2", ...} | ||
|
|
||
| Kwargs: | ||
| intelligent_waits (boolean): When enabled, the method will automatically decide how long to wait before performing this cloud variable set, to make sure no rate limits are triggered | ||
| max_retries (int) : Maximum number of times to retry setting the var if setting fails before raising an exception | ||
| """ | ||
| if self.disconnected.is_set(): | ||
| return | ||
|
Comment on lines
+704
to
+705
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be more appropriate to raise an error if you are trying to set variables if you are disconnected? Also, it is noteworthy that
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that was the thought behind it |
||
| self.server.set_project_vars(self.project_id, var_value_dict) | ||
|
|
||
| def get_var( | ||
| self, var, *, recorder_initial_values: Optional[dict[str, Any]] = None | ||
| ) -> str | int | float | None: | ||
| return self.server.get_var(self.project_id, var) | ||
|
|
||
| def get_all_vars( | ||
| self, *, recorder_initial_values: Optional[dict[str, Any]] = None | ||
| ) -> dict[str, str | int | float]: | ||
| return self.server.get_project_vars(self.project_id).copy() | ||
|
|
||
| def create_event_stream(self) -> CloudServerAdapterEventStream: | ||
| return CloudServerAdapterEventStream(self) | ||
|
|
||
|
|
||
| class CloudServerAdapterEventStream(EventStream): | ||
| adapter: CloudServerAdapter | ||
| disconnected: threading.Event | ||
| _queue: queue.Queue[dict[str, Any]] | ||
|
|
||
| def __init__(self, adapter: CloudServerAdapter): | ||
| self.adapter = adapter | ||
| self.disconnected = threading.Event() | ||
| self._queue = queue.Queue() | ||
| self.adapter.connected_event_stream_queues[id(self)] = self._queue | ||
|
|
||
| def read(self, amount: int = -1) -> Iterator[dict[str, Any]]: | ||
| if self.disconnected.is_set() or self.adapter.disconnected.is_set(): | ||
| return | ||
| end_time = time.time() + self.timeout if self.timeout is not None else None | ||
| progress = 0 | ||
| while (progress < amount if amount >= 0 else progress == 0) and ( | ||
| time.time() < end_time if end_time is not None else True | ||
| ): | ||
|
Comment on lines
+738
to
+740
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is a pretty confusing condition |
||
| try: | ||
| yield self._queue.get( | ||
| self.timeout is None or self.timeout > 0.0, | ||
| max(end_time - time.time(), 0) if end_time is not None else None, | ||
| ) | ||
| progress += 1 | ||
| except queue.Empty: | ||
| pass | ||
|
|
||
| def __del__(self): | ||
| self.close() | ||
|
|
||
| def close(self) -> None: | ||
| if not self.disconnected.is_set(): | ||
| del self.adapter.connected_event_stream_queues[id(self)] | ||
| self.disconnected.set() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
small thing, but
str | int | floatis more conciseThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would apply in a lot of cases in the codebase, e.g. every use of
Optional, so this is more of a separate style issue