clan-core/pkgs/clan-cli/clan_cli/clan_uri.py

149 lines
4.8 KiB
Python
Raw Normal View History

2023-12-05 17:08:27 +00:00
# Import the urllib.parse, enum and dataclasses modules
2023-12-05 17:16:51 +00:00
import dataclasses
2023-12-05 15:17:15 +00:00
import urllib.parse
2023-12-19 17:02:06 +00:00
import urllib.request
2023-12-05 17:08:27 +00:00
from dataclasses import dataclass
2023-12-05 17:16:51 +00:00
from enum import Enum, member
2023-12-05 17:08:27 +00:00
from pathlib import Path
2024-03-06 19:24:36 +00:00
from typing import Any
2023-12-05 17:16:51 +00:00
2023-12-05 15:17:15 +00:00
from .errors import ClanError
2023-12-05 17:16:51 +00:00
2023-12-05 15:17:15 +00:00
2023-12-05 17:08:27 +00:00
# Define an enum with different members that have different values
2024-03-06 19:24:36 +00:00
class ClanUrl(Enum):
2023-12-05 17:08:27 +00:00
# Use the dataclass decorator to add fields and methods to the members
@member
@dataclass
class REMOTE:
2023-12-05 17:16:51 +00:00
url: str # The url field holds the HTTP URL
def __str__(self) -> str:
return f"REMOTE({self.url})" # The __str__ method returns a custom string representation
2023-12-05 17:16:51 +00:00
2023-12-05 17:08:27 +00:00
@member
@dataclass
class LOCAL:
2023-12-05 17:16:51 +00:00
path: Path # The path field holds the local path
def __str__(self) -> str:
return f"LOCAL({self.path})" # The __str__ method returns a custom string representation
2023-12-05 17:08:27 +00:00
2023-12-05 15:17:15 +00:00
2023-12-05 17:08:27 +00:00
# Parameters defined here will be DELETED from the nested uri
# so make sure there are no conflicts with other webservices
@dataclass
2024-03-06 19:24:36 +00:00
class MachineParams:
dummy_opt: str = "dummy"
@dataclass
class MachineData:
name: str = "defaultVM"
params: MachineParams = dataclasses.field(default_factory=MachineParams)
2023-12-05 15:17:15 +00:00
2023-12-05 17:16:51 +00:00
2023-12-05 15:17:15 +00:00
# Define the ClanURI class
class ClanURI:
2024-03-06 19:24:36 +00:00
_orig_uri: str
_nested_uri: str
_components: urllib.parse.ParseResult
url: ClanUrl
machines: list[MachineData]
2023-12-05 15:17:15 +00:00
# Initialize the class with a clan:// URI
def __init__(self, uri: str) -> None:
2024-03-06 19:24:36 +00:00
self.machines = []
2024-01-04 15:30:26 +00:00
# users might copy whitespace along with the uri
uri = uri.strip()
2024-03-06 19:24:36 +00:00
self._orig_uri = uri
2024-01-16 16:11:26 +00:00
2023-12-05 17:08:27 +00:00
# Check if the URI starts with clan://
2024-01-16 16:11:26 +00:00
# If it does, remove the clan:// prefix
2023-12-05 15:17:15 +00:00
if uri.startswith("clan://"):
2023-12-05 17:08:27 +00:00
self._nested_uri = uri[7:]
2023-12-05 15:17:15 +00:00
else:
2024-03-06 19:24:36 +00:00
raise ClanError(f"Invalid uri: expected clan://, got {uri}")
2023-12-05 15:17:15 +00:00
# Parse the URI into components
2024-03-06 19:24:36 +00:00
# url://netloc/path;parameters?query#fragment
2023-12-05 17:08:27 +00:00
self._components = urllib.parse.urlparse(self._nested_uri)
2024-03-06 19:24:36 +00:00
# Replace the query string in the components with the new query string
clean_comps = self._components._replace(
query=self._components.query, fragment=""
)
2023-12-05 17:08:27 +00:00
2024-03-06 19:24:36 +00:00
# Parse the URL into a ClanUrl object
self.url = self._parse_url(clean_comps)
2024-01-16 16:11:26 +00:00
2024-03-06 19:24:36 +00:00
# Parse the fragment into a list of machine queries
# Then parse every machine query into a MachineParameters object
machine_frags = list(
filter(lambda x: len(x) > 0, self._components.fragment.split("#"))
)
for machine_frag in machine_frags:
machine = self._parse_machine_query(machine_frag)
self.machines.append(machine)
2024-01-16 16:11:26 +00:00
2024-03-06 19:24:36 +00:00
# If there are no machine fragments, add a default machine
if len(machine_frags) == 0:
self.machines.append(MachineData())
2023-12-05 17:08:27 +00:00
2024-03-06 19:24:36 +00:00
def _parse_url(self, comps: urllib.parse.ParseResult) -> ClanUrl:
comb = (
2024-03-06 19:24:36 +00:00
comps.scheme,
comps.netloc,
comps.path,
comps.params,
comps.query,
comps.fragment,
)
match comb:
2024-03-06 19:24:36 +00:00
case ("file", "", path, "", "", _) | ("", "", path, "", "", _): # type: ignore
url = ClanUrl.LOCAL.value(Path(path).expanduser().resolve()) # type: ignore
2023-12-05 17:08:27 +00:00
case _:
2024-03-06 19:24:36 +00:00
url = ClanUrl.REMOTE.value(comps.geturl()) # type: ignore
2024-03-06 19:24:36 +00:00
return url
def _parse_machine_query(self, machine_frag: str) -> MachineData:
comp = urllib.parse.urlparse(machine_frag)
2023-12-12 20:31:47 +00:00
query = urllib.parse.parse_qs(comp.query)
2024-03-06 19:24:36 +00:00
machine_name = comp.path
2024-01-16 16:11:26 +00:00
2024-03-06 19:24:36 +00:00
machine_params: dict[str, Any] = {}
for dfield in dataclasses.fields(MachineParams):
if dfield.name in query:
values = query[dfield.name]
if len(values) > 1:
raise ClanError(f"Multiple values for parameter: {dfield.name}")
machine_params[dfield.name] = values[0]
# Remove the field from the query dictionary
# clan uri and nested uri share one namespace for query parameters
# we need to make sure there are no conflicts
del query[dfield.name]
params = MachineParams(**machine_params)
machine = MachineData(name=machine_name, params=params)
return machine
def get_orig_uri(self) -> str:
return self._orig_uri
2024-01-16 16:11:26 +00:00
2024-03-06 19:24:36 +00:00
def get_url(self) -> str:
match self.url:
case ClanUrl.LOCAL.value(path):
return str(path)
case ClanUrl.REMOTE.value(url):
return url
case _:
raise ClanError(f"Unsupported uri components: {self.url}")
2023-12-08 11:18:55 +00:00
def __str__(self) -> str:
2024-03-06 19:24:36 +00:00
return self.get_orig_uri()
def __repr__(self) -> str:
2024-03-06 19:24:36 +00:00
return f"ClanURI({self})"