mirror of
https://github.com/rio-labs/rio.git
synced 2026-02-11 16:19:32 -06:00
remove unused files
This commit is contained in:
@@ -1,83 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import keyring
|
||||
import platformdirs
|
||||
import tomlkit
|
||||
from revel import fatal
|
||||
|
||||
from . import rio_api
|
||||
|
||||
|
||||
class CliInstance:
|
||||
"""
|
||||
Singleton class which abstracts away the environment for the Rio CLI. It
|
||||
handles access to commonly used functionality such as the configuration file
|
||||
and the API client.
|
||||
"""
|
||||
|
||||
_instance = None
|
||||
_config: tomlkit.TOMLDocument
|
||||
_api_client: rio_api.RioApi | None
|
||||
|
||||
def __new__(cls) -> CliInstance:
|
||||
# Already initialized?
|
||||
if cls._instance is not None:
|
||||
return cls._instance
|
||||
|
||||
# Nope, create a new instance
|
||||
self = cls._instance = super(CliInstance, cls).__new__(cls)
|
||||
|
||||
# Read the config
|
||||
try:
|
||||
config_dir = Path(platformdirs.user_config_dir("rio"))
|
||||
self._config = tomlkit.loads(
|
||||
(config_dir / "config.toml").read_text()
|
||||
)
|
||||
except FileNotFoundError:
|
||||
self._config = tomlkit.document()
|
||||
except OSError as err:
|
||||
fatal(f"Couldn't read Rio's configuration: {err}")
|
||||
|
||||
# Api client
|
||||
self._api_client = None
|
||||
|
||||
return self
|
||||
|
||||
async def __aenter__(self) -> CliInstance:
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *args) -> None:
|
||||
if self._api_client is not None:
|
||||
await self._api_client.close()
|
||||
|
||||
@property
|
||||
def auth_token(self) -> str | None:
|
||||
"""
|
||||
Tries to get the auth token from the keyring. Returns `None` if no token
|
||||
was stored in the keyring yet.
|
||||
"""
|
||||
return keyring.get_password("rio", "rioApiAuthToken")
|
||||
|
||||
@auth_token.setter
|
||||
def auth_token(self, value: str) -> None:
|
||||
"""
|
||||
Stores the given auth token in the keyring.
|
||||
"""
|
||||
keyring.set_password("rio", "rioApiAuthToken", value)
|
||||
|
||||
async def get_api_client(self, *, logged_in: bool = True) -> rio_api.RioApi:
|
||||
"""
|
||||
Return an API client for the Rio API. If `logged_in` is True, the
|
||||
client will be authenticated, prompting the user to login if necessary.
|
||||
"""
|
||||
# If there is no client yet, create one
|
||||
if self._api_client is None:
|
||||
self._api_client = rio_api.RioApi()
|
||||
|
||||
# Authenticate, if necessary
|
||||
if logged_in and not self._api_client.is_logged_in:
|
||||
raise NotImplementedError("TODO: Login")
|
||||
|
||||
return self._api_client
|
||||
@@ -1,21 +0,0 @@
|
||||
import threading
|
||||
import typing as t
|
||||
|
||||
T = t.TypeVar("T")
|
||||
|
||||
|
||||
class ThreadsafeFuture(t.Generic[T]):
|
||||
def __init__(self) -> None:
|
||||
self._result_value: t.Any
|
||||
self._event = threading.Event()
|
||||
|
||||
def set_result(self, result: T) -> None:
|
||||
self._result_value = result
|
||||
self._event.set()
|
||||
|
||||
def result(self) -> T:
|
||||
self._event.wait()
|
||||
return self._result_value
|
||||
|
||||
def done(self) -> bool:
|
||||
return self._event.is_set()
|
||||
@@ -1,21 +0,0 @@
|
||||
import dataclasses
|
||||
import types
|
||||
|
||||
import introspection.typing
|
||||
|
||||
__all__ = ["ScopedAnnotation"]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ScopedAnnotation:
|
||||
annotation: introspection.types.TypeAnnotation
|
||||
module: types.ModuleType
|
||||
|
||||
def evaluate(self) -> introspection.types.TypeAnnotation:
|
||||
return introspection.typing.resolve_forward_refs(
|
||||
self.annotation,
|
||||
self.module,
|
||||
mode="eval",
|
||||
strict=False,
|
||||
treat_name_errors_as_imports=True,
|
||||
)
|
||||
@@ -1,451 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import copy
|
||||
import dataclasses
|
||||
import json
|
||||
import re
|
||||
import typing as t
|
||||
from pathlib import Path
|
||||
|
||||
from uniserde import Jsonable, JsonDoc
|
||||
|
||||
import rio
|
||||
|
||||
from .. import inspection
|
||||
|
||||
__all__ = [
|
||||
"ClientComponent",
|
||||
"ValidationError",
|
||||
"Validator",
|
||||
]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ClientComponent:
|
||||
id: int
|
||||
type: str
|
||||
state: JsonDoc
|
||||
|
||||
@classmethod
|
||||
def from_json(
|
||||
cls,
|
||||
id: int,
|
||||
delta_state: JsonDoc,
|
||||
registered_html_components: set[str],
|
||||
) -> ClientComponent:
|
||||
# Don't modify the original dict
|
||||
delta_state = copy.deepcopy(delta_state)
|
||||
|
||||
# Get the component type
|
||||
try:
|
||||
type = delta_state.pop("_type_")
|
||||
except KeyError:
|
||||
raise ValidationError(
|
||||
f"Component with id `{id}` is missing `_type_` field"
|
||||
)
|
||||
|
||||
if not isinstance(type, str):
|
||||
raise ValidationError(
|
||||
f"Component with id `{id}` has non-string type `{type}`"
|
||||
)
|
||||
|
||||
if (
|
||||
type
|
||||
not in inspection.get_child_component_containing_attribute_names_for_builtin_components()
|
||||
and type not in registered_html_components
|
||||
):
|
||||
raise ValidationError(
|
||||
f"Component with id `{id}` has unknown type `{type}`"
|
||||
)
|
||||
|
||||
# Construct the result
|
||||
return cls(
|
||||
id=id,
|
||||
type=type,
|
||||
state=delta_state,
|
||||
)
|
||||
|
||||
def _get_child_attribute_names(self) -> t.Iterable[str]:
|
||||
child_attr_names = inspection.get_child_component_containing_attribute_names_for_builtin_components()
|
||||
try:
|
||||
return child_attr_names[self.type]
|
||||
except KeyError:
|
||||
return tuple() # TODO: How to get the children of HTML components?
|
||||
|
||||
@property
|
||||
def non_child_containing_properties(
|
||||
self,
|
||||
) -> JsonDoc:
|
||||
child_attribute_names = self._get_child_attribute_names()
|
||||
|
||||
result = {}
|
||||
for name, value in self.state.items():
|
||||
if name in child_attribute_names:
|
||||
continue
|
||||
|
||||
result[name] = value
|
||||
|
||||
return result
|
||||
|
||||
@property
|
||||
def child_containing_properties(
|
||||
self,
|
||||
) -> dict[str, None | int | list[int]]:
|
||||
child_attribute_names = self._get_child_attribute_names()
|
||||
|
||||
result = {}
|
||||
for name, value in self.state.items():
|
||||
if name not in child_attribute_names:
|
||||
continue
|
||||
|
||||
result[name] = value
|
||||
|
||||
return result
|
||||
|
||||
@property
|
||||
def referenced_child_ids(self) -> t.Iterable[int]:
|
||||
for property_value in self.child_containing_properties.values():
|
||||
if property_value is None:
|
||||
continue
|
||||
|
||||
if isinstance(property_value, int):
|
||||
yield property_value
|
||||
continue
|
||||
|
||||
assert isinstance(property_value, list), property_value
|
||||
yield from property_value
|
||||
|
||||
def __str__(self) -> str:
|
||||
# For placeholders, include the python type
|
||||
if self.type == "Placeholder":
|
||||
component_type = f"{self.type} ({self.state['_python_type_']})"
|
||||
else:
|
||||
component_type = self.type
|
||||
|
||||
return f"{component_type} #{self.id}"
|
||||
|
||||
|
||||
class ValidationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Validator:
|
||||
def __init__(
|
||||
self,
|
||||
sess: rio.Session,
|
||||
*,
|
||||
dump_directory_path: Path | None = None,
|
||||
):
|
||||
self.session = sess
|
||||
|
||||
if dump_directory_path is not None:
|
||||
assert dump_directory_path.exists(), dump_directory_path
|
||||
assert dump_directory_path.is_dir(), dump_directory_path
|
||||
|
||||
self.dump_directory_path = dump_directory_path
|
||||
|
||||
self.root_component: ClientComponent | None = None
|
||||
self.components_by_id: dict[int, ClientComponent] = {}
|
||||
|
||||
# HTML components must be registered with the frontend before use. This set
|
||||
# contains the ids (`FundamentalComponent._unique_id_`) of all registered components.
|
||||
self.registered_html_components: set[str] = set(
|
||||
inspection.get_child_component_containing_attribute_names_for_builtin_components().keys()
|
||||
)
|
||||
|
||||
def dump_message(
|
||||
self,
|
||||
msg: Jsonable,
|
||||
*,
|
||||
incoming: bool,
|
||||
) -> None:
|
||||
"""
|
||||
Dump the message to a JSON file.
|
||||
|
||||
If no path is set in the validator, this function does nothing.
|
||||
"""
|
||||
if self.dump_directory_path is None:
|
||||
return
|
||||
|
||||
direction = "incoming" if incoming else "outgoing"
|
||||
path = self.dump_directory_path / f"message-{direction}.json"
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(msg, f, indent=4)
|
||||
|
||||
def dump_client_state(
|
||||
self,
|
||||
component: ClientComponent | None = None,
|
||||
path: Path | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Dump the client state to a JSON file.
|
||||
|
||||
If no component is specified, the root component is used.
|
||||
|
||||
If no path is used the Validator's `dump_client_state_path` is used. If
|
||||
no path is used and no path set in the validator, this function does
|
||||
nothing.
|
||||
"""
|
||||
if path is None and self.dump_directory_path is not None:
|
||||
path = self.dump_directory_path / "client-state.json"
|
||||
|
||||
if path is None:
|
||||
return
|
||||
|
||||
if component is None:
|
||||
assert self.root_component is not None
|
||||
component = self.root_component
|
||||
|
||||
with open(path, "w") as f:
|
||||
json.dump(
|
||||
self.as_json(component),
|
||||
f,
|
||||
indent=4,
|
||||
# The keys are intentionally in a legible order. Don't destroy
|
||||
# that.
|
||||
sort_keys=False,
|
||||
)
|
||||
|
||||
def prune_components(self) -> None:
|
||||
"""
|
||||
Remove all components which are not referenced directly or indirectly by
|
||||
the root component.
|
||||
"""
|
||||
# If there is no root component, everybody is an orphan
|
||||
if self.root_component is None:
|
||||
self.components_by_id.clear()
|
||||
return
|
||||
|
||||
# Find all components which are referenced directly or indirectly by the
|
||||
# root component
|
||||
visited_ids: set[int] = set()
|
||||
|
||||
to_do = [self.root_component]
|
||||
|
||||
while to_do:
|
||||
current = to_do.pop()
|
||||
|
||||
# Use this opportunity to detect cycles
|
||||
if current.id in visited_ids:
|
||||
print(
|
||||
f"Warning: Validator found a cycle in the component tree involving component with id `{current.id}`"
|
||||
)
|
||||
continue
|
||||
|
||||
# Mark the current component as visited
|
||||
visited_ids.add(current.id)
|
||||
|
||||
# Chain to its children
|
||||
for child_id in current.referenced_child_ids:
|
||||
to_do.append(self.components_by_id[child_id])
|
||||
|
||||
# Remove all superfluous components
|
||||
self.components_by_id = {
|
||||
id: component
|
||||
for id, component in self.components_by_id.items()
|
||||
if id in visited_ids
|
||||
}
|
||||
|
||||
def as_json(self, component: ClientComponent | None = None) -> JsonDoc:
|
||||
"""
|
||||
Return a JSON-serializable representation of the client state.
|
||||
"""
|
||||
|
||||
if component is None:
|
||||
assert self.root_component is not None
|
||||
component = self.root_component
|
||||
|
||||
result = {
|
||||
"_type_": component.type,
|
||||
"_id_": component.id,
|
||||
}
|
||||
|
||||
for name, value in component.non_child_containing_properties.items():
|
||||
result[name] = value
|
||||
|
||||
for name, value in component.child_containing_properties.items():
|
||||
if value is None:
|
||||
result[name] = None
|
||||
continue
|
||||
|
||||
if isinstance(value, int):
|
||||
result[name] = self.as_json(self.components_by_id[value])
|
||||
continue
|
||||
|
||||
assert isinstance(value, list), value
|
||||
result[name] = [
|
||||
self.as_json(self.components_by_id[id]) for id in value
|
||||
]
|
||||
|
||||
return result
|
||||
|
||||
def handle_incoming_message(self, msg: t.Any) -> None:
|
||||
"""
|
||||
Process a message passed from Client -> Server.
|
||||
|
||||
This will update the `Validator`'s internal client state and validate
|
||||
the message, raising a `ValidationError` if any issues are detected.
|
||||
"""
|
||||
|
||||
# Delegate to the appropriate handler
|
||||
try:
|
||||
method = msg["method"]
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
handler_name = f"_handle_incoming_{method}"
|
||||
|
||||
try:
|
||||
handler = getattr(self, handler_name)
|
||||
except AttributeError:
|
||||
return
|
||||
|
||||
handler(msg["params"])
|
||||
|
||||
def handle_outgoing_message(self, msg: t.Any) -> None:
|
||||
"""
|
||||
Process a message passed from Server -> Client.
|
||||
|
||||
This will update the `Validator`'s internal client state and validate
|
||||
the message, raising a `ValidationError` if any issues are detected.
|
||||
"""
|
||||
|
||||
# Delegate to the appropriate handler
|
||||
try:
|
||||
method = msg["method"]
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
handler_name = f"_handle_outgoing_{method}"
|
||||
|
||||
try:
|
||||
handler = getattr(self, handler_name)
|
||||
except AttributeError:
|
||||
return
|
||||
|
||||
handler(msg["params"])
|
||||
|
||||
def _handle_outgoing_updateComponentStates(self, msg: t.Any) -> None:
|
||||
# Dump the message, if requested
|
||||
self.dump_message(msg, incoming=False)
|
||||
|
||||
# Update the individual component states
|
||||
for component_id, delta_state in msg["delta_states"].items():
|
||||
# Make sure the delta state isn't empty. While this isn't
|
||||
# technically invalid, the frontend relies on values such as the
|
||||
# margin and alignment to be present. This works, because those
|
||||
# values are generated by python regardless of whether they have
|
||||
# changed.
|
||||
required_keys = {
|
||||
"_type_",
|
||||
"_margin_",
|
||||
"_size_",
|
||||
"_align_",
|
||||
"_grow_",
|
||||
}
|
||||
missing_keys = required_keys - set(delta_state.keys())
|
||||
|
||||
if missing_keys:
|
||||
delta_state_nice = json.dumps(delta_state, indent=4)
|
||||
raise ValidationError(
|
||||
f"Delta state for with id `{component_id}` is missing required keys `{missing_keys}`. The full delta state follows:\n{delta_state_nice}"
|
||||
)
|
||||
|
||||
# Get the component's existing state
|
||||
try:
|
||||
component = self.components_by_id[component_id]
|
||||
except KeyError:
|
||||
component = ClientComponent.from_json(
|
||||
component_id,
|
||||
delta_state,
|
||||
self.registered_html_components,
|
||||
)
|
||||
self.components_by_id[component_id] = component
|
||||
else:
|
||||
delta_state = delta_state.copy()
|
||||
|
||||
# A component's `_type_` cannot be modified. This value is also
|
||||
# stored separately by `ClientComponent`, so make sure it never
|
||||
# makes it into the component's state.
|
||||
try:
|
||||
new_type = delta_state.pop("_type_")
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if new_type != component.type:
|
||||
raise ValidationError(
|
||||
f"Attempted to modify the `_type_` for component with id `{component_id}` from `{component.type}` to `{new_type}`"
|
||||
) from None
|
||||
|
||||
# Update the component's state
|
||||
component.state.update(delta_state)
|
||||
|
||||
# Update the root component if requested
|
||||
if msg["root_component_id"] is not None:
|
||||
try:
|
||||
self.root_component = self.components_by_id[
|
||||
msg["root_component_id"]
|
||||
]
|
||||
except KeyError:
|
||||
raise ValidationError(
|
||||
f"Attempted to set root component to unknown component with id `{msg['root_component_id']}`"
|
||||
) from None
|
||||
|
||||
# If no root component is known yet, this message has to contain one
|
||||
if self.root_component is None:
|
||||
raise ValidationError(
|
||||
"Despite no root component being known yet, an `UpdateComponentStates` message was sent without a `root_component_id`",
|
||||
)
|
||||
|
||||
# Make sure no invalid component references are present
|
||||
invalid_references = collections.defaultdict(list)
|
||||
for component in self.components_by_id.values():
|
||||
for child_id in component.referenced_child_ids:
|
||||
if child_id not in self.components_by_id:
|
||||
invalid_references[component.id].append(child_id)
|
||||
|
||||
if invalid_references:
|
||||
invalid_references = {
|
||||
str(self.components_by_id[component_id]): child_ids
|
||||
for component_id, child_ids in invalid_references.items()
|
||||
}
|
||||
raise ValidationError(
|
||||
f"Invalid component references detected: {invalid_references}"
|
||||
)
|
||||
|
||||
# Prune the component tree
|
||||
self.prune_components()
|
||||
|
||||
# Look for any components which were sent in the message, but are not
|
||||
# actually used in the component tree
|
||||
ids_sent = set(msg["delta_states"].keys())
|
||||
ids_existing = set(self.components_by_id.keys())
|
||||
ids_superfluous = sorted(ids_sent - ids_existing)
|
||||
|
||||
if ids_superfluous:
|
||||
print(
|
||||
"Validator Warning: Message contained superfluous component ids:"
|
||||
)
|
||||
|
||||
for id in ids_superfluous:
|
||||
delta_state = msg["delta_states"][id]
|
||||
print(
|
||||
f"- {delta_state.get('_type_', 'unknown type')} #{id} - {delta_state}"
|
||||
)
|
||||
|
||||
# Dump the client state if requested
|
||||
self.dump_client_state()
|
||||
|
||||
def _handle_outgoing_evaluateJavascript(self, msg: t.Any):
|
||||
# Is this message registering a new component class?
|
||||
match = re.search(
|
||||
r"window.componentClasses\['(.*)'\]", msg["java_script_source"]
|
||||
)
|
||||
|
||||
if match is None:
|
||||
return
|
||||
|
||||
# Remember the component class as registered
|
||||
self.registered_html_components.add(match.group(1))
|
||||
Reference in New Issue
Block a user