diff --git a/onshape_api/models/geometry.py b/onshape_api/models/geometry.py index b8cf796..7cc1cf5 100644 --- a/onshape_api/models/geometry.py +++ b/onshape_api/models/geometry.py @@ -28,6 +28,10 @@ class BaseGeometry(ABC): @abstractmethod def to_xml(self, root: ET.Element | None = None) -> ET.Element: ... + @classmethod + @abstractmethod + def from_xml(cls, element: ET.Element) -> "BaseGeometry": ... + @dataclass class BoxGeometry(BaseGeometry): @@ -67,6 +71,26 @@ def to_xml(self, root: ET.Element | None = None) -> ET.Element: ET.SubElement(geometry, "box", size=" ".join(format_number(v) for v in self.size)) return geometry + @classmethod + def from_xml(cls, element) -> "BoxGeometry": + """ + Create a box geometry from an XML element. + + Args: + element: The XML element to create the box geometry from. + + Returns: + The box geometry created from the XML element. + + Examples: + >>> element = ET.Element("geometry") + >>> ET.SubElement(element, "box", size="1.0 2.0 3.0") + >>> BoxGeometry.from_xml(element) + BoxGeometry(size=(1.0, 2.0, 3.0)) + """ + size = tuple(float(v) for v in element.find("box").attrib["size"].split()) + return cls(size) + @dataclass class CylinderGeometry(BaseGeometry): @@ -113,6 +137,27 @@ def to_xml(self, root: ET.Element | None = None) -> ET.Element: ) return geometry + @classmethod + def from_xml(cls, element) -> "CylinderGeometry": + """ + Create a cylinder geometry from an XML element. + + Args: + element: The XML element to create the cylinder geometry from. + + Returns: + The cylinder geometry created from the XML element. + + Examples: + >>> element = ET.Element("geometry") + >>> ET.SubElement(element, "cylinder", radius="1.0", length="2.0") + >>> CylinderGeometry.from_xml(element) + CylinderGeometry(radius=1.0, length=2.0) + """ + radius = float(element.find("cylinder").attrib["radius"]) + length = float(element.find("cylinder").attrib["length"]) + return cls(radius, length) + @dataclass class SphereGeometry(BaseGeometry): @@ -152,6 +197,26 @@ def to_xml(self, root: ET.Element | None = None) -> ET.Element: ET.SubElement(geometry, "sphere", radius=format_number(self.radius)) return geometry + @classmethod + def from_xml(cls, element) -> "SphereGeometry": + """ + Create a sphere geometry from an XML element. + + Args: + element: The XML element to create the sphere geometry from. + + Returns: + The sphere geometry created from the XML element. + + Examples: + >>> element = ET.Element("geometry") + >>> ET.SubElement(element, "sphere", radius="1.0") + >>> SphereGeometry.from_xml(element) + SphereGeometry(radius=1.0) + """ + radius = float(element.find("sphere").attrib["radius"]) + return cls(radius) + @dataclass class MeshGeometry(BaseGeometry): @@ -191,5 +256,25 @@ def to_xml(self, root: ET.Element | None = None) -> ET.Element: ET.SubElement(geometry, "mesh", filename=self.filename) return geometry + @classmethod + def from_xml(cls, element) -> "MeshGeometry": + """ + Create a mesh geometry from an XML element. + + Args: + element: The XML element to create the mesh geometry from. + + Returns: + The mesh geometry created from the XML element. + + Examples: + >>> element = ET.Element("geometry") + >>> ET.SubElement(element, "mesh", filename="mesh.stl") + >>> MeshGeometry.from_xml(element) + MeshGeometry(filename="mesh.stl") + """ + filename = element.find("mesh").attrib["filename"] + return cls(filename) + def __post_init__(self) -> None: self.filename = xml_escape(self.filename) diff --git a/onshape_api/models/joint.py b/onshape_api/models/joint.py index 6630535..ef7de91 100644 --- a/onshape_api/models/joint.py +++ b/onshape_api/models/joint.py @@ -154,6 +154,31 @@ def to_xml(self, root: ET.Element | None = None) -> ET.Element: mimic.set("offset", format_number(self.offset)) return mimic + @classmethod + def from_xml(cls, element: ET.Element) -> "JointMimic": + """ + Create a joint mimic from an XML element. + + Args: + element: The XML element to create the joint mimic from. + + Returns: + The joint mimic created from the XML element. + + Examples: + >>> element = ET.Element("mimic") + >>> element.set("joint", "joint1") + >>> element.set("multiplier", "1.0") + >>> element.set("offset", "0.0") + >>> JointMimic.from_xml(element) + JointMimic(joint="joint1", multiplier=1.0, offset=0.0) + """ + + joint = element.attrib["joint"] + multiplier = float(element.attrib.get("multiplier", 1.0)) + offset = float(element.attrib.get("offset", 0.0)) + return cls(joint, multiplier, offset) + @dataclass class JointDynamics: @@ -197,6 +222,28 @@ def to_xml(self, root: ET.Element | None = None) -> ET.Element: joint.set("friction", format_number(self.friction)) return joint + def from_xml(cls, element: ET.Element) -> "JointDynamics": + """ + Create joint dynamics from an XML element. + + Args: + element: The XML element to create the joint dynamics from. + + Returns: + The joint dynamics created from the XML element. + + Examples: + >>> element = ET.Element("dynamics") + >>> element.set("damping", "0.0") + >>> element.set("friction", "0.0") + >>> JointDynamics.from_xml(element) + JointDynamics(damping=0.0, friction=0.0) + """ + + damping = float(element.attrib.get("damping", 0)) + friction = float(element.attrib.get("friction", 0)) + return cls(damping, friction) + @dataclass class BaseJoint(ABC): @@ -252,6 +299,10 @@ def joint_type(self) -> str: pass + @classmethod + @abstractmethod + def from_xml(cls, element: ET.Element) -> "BaseJoint": ... + @dataclass class DummyJoint(BaseJoint): @@ -273,6 +324,35 @@ class DummyJoint(BaseJoint): 'dummy' """ + @classmethod + def from_xml(cls, element: ET.Element) -> "DummyJoint": + """ + Create a dummy joint from an XML element. + + Args: + element: The XML element to create the dummy joint from. + + Returns: + The dummy joint created from the XML element. + + Examples: + >>> element = ET.Element("joint") + >>> element.set("name", "joint1") + >>> element.set("type", "dummy") + >>> origin = Origin(xyz=(0, 0, 0), rpy=(0, 0, 0)) + >>> ET.SubElement(element, "origin", xyz="0 0 0", rpy="0 0 0") + >>> ET.SubElement(element, "parent", link="base_link") + >>> ET.SubElement(element, "child", link="link1") + >>> DummyJoint.from_xml(element) + DummyJoint(name="joint1", parent="base_link", child="link1", origin=Origin(xyz=(0, 0, 0), rpy=(0, 0, 0))) + """ + + name = element.attrib["name"] + parent = element.find("parent").attrib["link"] + child = element.find("child").attrib["link"] + origin = Origin.from_xml(element.find("origin")) + return cls(name, parent, child, origin) + @property def joint_type(self) -> str: """ @@ -358,14 +438,87 @@ def to_xml(self, root: ET.Element | None = None) -> ET.Element: """ joint = super().to_xml(root) - self.limits.to_xml(joint) self.axis.to_xml(joint) + if self.limits is not None: + self.limits.to_xml(joint) if self.dynamics is not None: self.dynamics.to_xml(joint) if self.mimic is not None: self.mimic.to_xml(joint) return joint + @classmethod + def from_xml(cls, element: ET.Element) -> "RevoluteJoint": + """ + Create a revolute joint from an XML element. + + Args: + element: The XML element to create the revolute joint from. + + Returns: + The revolute joint created from the XML element. + + Examples: + >>> element = ET.Element("joint") + >>> element.set("name", "joint1") + >>> element.set("type", "revolute") + >>> origin = Origin(xyz=(0, 0, 0), rpy=(0, 0, 0)) + >>> ET.SubElement(element, "origin", xyz="0 0 0", rpy="0 0 0") + >>> ET.SubElement(element, "parent", link="base_link") + >>> ET.SubElement(element, "child", link="link1") + >>> limits = ET.SubElement(element, "limit", effort="10.0", velocity="1.0", lower="-1.0", upper="1.0") + >>> axis = ET.SubElement(element, "axis", xyz="0 0 1") + >>> dynamics = ET.SubElement(element, "dynamics", damping="0.0", friction="0.0") + >>> mimic = ET.SubElement(element, "mimic", joint="joint1", multiplier="1.0", offset="0.0") + >>> RevoluteJoint.from_xml(element) + + RevoluteJoint( + name="joint1", + parent="base_link", + child="link1", + origin=Origin(xyz=(0, 0, 0), rpy=(0, 0, 0)), + limits=JointLimits(effort=10.0, velocity=1.0, lower=-1.0, upper=1.0), + axis=Axis(xyz=(0, 0, 1)), + dynamics=JointDynamics(damping=0.0, friction=0.0), + mimic=JointMimic(joint="joint1", multiplier=1.0, offset=0.0) + ) + """ + + name = element.attrib["name"] + parent = element.find("parent").attrib["link"] + child = element.find("child").attrib["link"] + origin = Origin.from_xml(element.find("origin")) + # Handle limits + limit_element = element.find("limit") + if limit_element is not None: + limits = JointLimits( + effort=float(limit_element.attrib.get("effort", 0)), + velocity=float(limit_element.attrib.get("velocity", 0)), + lower=float(limit_element.attrib.get("lower", 0)), + upper=float(limit_element.attrib.get("upper", 0)), + ) + else: + limits = None + + # Handle axis + axis = Axis.from_xml(element.find("axis")) + + # Handle dynamics + dynamics_element = element.find("dynamics") + if dynamics_element is not None: + dynamics = JointDynamics( + damping=float(dynamics_element.attrib.get("damping", 0)), + friction=float(dynamics_element.attrib.get("friction", 0)), + ) + else: + dynamics = None + + # Handle mimic + mimic_element = element.find("mimic") + mimic = JointMimic.from_xml(mimic_element) if mimic_element is not None else None + + return cls(name, parent, child, origin, limits, axis, dynamics, mimic) + @property def joint_type(self) -> str: """ @@ -437,6 +590,48 @@ def to_xml(self, root: ET.Element | None = None) -> ET.Element: self.mimic.to_xml(joint) return joint + @classmethod + def from_xml(cls, element: ET.Element) -> "ContinuousJoint": + """ + Create a continuous joint from an XML element. + + Args: + element: The XML element to create the continuous joint from. + + Returns: + The continuous joint created from the XML element. + + Examples: + >>> element = ET.Element("joint") + >>> element.set("name", "joint1") + >>> element.set("type", "continuous") + >>> origin = Origin(xyz=(0, 0, 0), rpy=(0, 0, 0)) + >>> ET.SubElement(element, "origin", xyz="0 0 0", rpy="0 0 0") + >>> ET.SubElement(element, "parent", link="base_link") + >>> ET.SubElement(element, "child", link="link1") + >>> mimic = ET.SubElement(element, "mimic", joint="joint1", multiplier="1.0", offset="0.0") + >>> ContinuousJoint.from_xml(element) + + ContinuousJoint( + name="joint1", + parent="base_link", + child="link1", + origin=Origin(xyz=(0, 0, 0), rpy=(0, 0, 0)), + mimic=JointMimic(joint="joint1", multiplier=1.0, offset=0.0) + ) + """ + + name = element.attrib["name"] + parent = element.find("parent").attrib["link"] + child = element.find("child").attrib["link"] + origin = Origin.from_xml(element.find("origin")) + + # Handle mimic + mimic_element = element.find("mimic") + mimic = JointMimic.from_xml(mimic_element) if mimic_element is not None else None + + return cls(name, parent, child, origin, mimic) + @property def joint_type(self) -> str: """ @@ -522,14 +717,84 @@ def to_xml(self, root: ET.Element | None = None) -> ET.Element: """ joint = super().to_xml(root) - self.limits.to_xml(joint) self.axis.to_xml(joint) + if self.limits is not None: + self.limits.to_xml(joint) if self.dynamics is not None: self.dynamics.to_xml(joint) if self.mimic is not None: self.mimic.to_xml(joint) return joint + @classmethod + def from_xml(cls, element: ET.Element) -> "PrismaticJoint": + """ + Create a prismatic joint from an XML element. + + Args: + element: The XML element to create the prismatic joint from. + + Returns: + The prismatic joint created from the XML element. + + Examples: + >>> element = ET.Element("joint") + >>> element.set("name", "joint1") + >>> element.set("type", "prismatic") + >>> origin = Origin(xyz=(0, 0, 0), rpy=(0, 0, 0)) + >>> ET.SubElement(element, "origin", xyz="0 0 0", rpy="0 0 0") + >>> ET.SubElement(element, "parent", link="base_link") + >>> ET.SubElement(element, "child", link="link1") + >>> limits = ET.SubElement(element, "limit", effort="10.0", velocity="1.0", lower="-1.0", upper="1.0") + >>> axis = ET.SubElement(element, "axis", xyz="0 0 1") + >>> dynamics = ET.SubElement(element, "dynamics", damping="0.0", friction="0.0") + >>> mimic = ET.SubElement(element, "mimic", joint="joint1", multiplier="1.0", offset="0.0") + >>> PrismaticJoint.from_xml(element) + + PrismaticJoint( + name="joint1", + parent="base_link", + child="link1", + origin=Origin(xyz=(0, 0, 0), rpy=(0, 0, 0)), + limits=JointLimits(effort=10.0, velocity=1.0, lower=-1.0, upper=1.0), + axis=Axis(xyz=(0, 0, 1)), + dynamics=JointDynamics(damping=0.0, friction=0.0), + mimic=JointMimic(joint="joint1", multiplier=1.0, offset=0.0) + ) + """ + + name = element.attrib["name"] + parent = element.find("parent").attrib["link"] + child = element.find("child").attrib["link"] + origin = Origin.from_xml(element.find("origin")) + + limit_element = element.find("limit") + if limit_element is not None: + limits = JointLimits( + effort=float(limit_element.attrib.get("effort", 0)), + velocity=float(limit_element.attrib.get("velocity", 0)), + lower=float(limit_element.attrib.get("lower", 0)), + upper=float(limit_element.attrib.get("upper", 0)), + ) + else: + limits = None + + axis = Axis.from_xml(element.find("axis")) + + dynamics_element = element.find("dynamics") + if dynamics_element is not None: + dynamics = JointDynamics( + damping=float(dynamics_element.attrib.get("damping", 0)), + friction=float(dynamics_element.attrib.get("friction", 0)), + ) + else: + dynamics = None + + mimic_element = element.find("mimic") + mimic = JointMimic.from_xml(mimic_element) if mimic_element is not None else None + + return cls(name, parent, child, origin, limits, axis, dynamics, mimic) + @property def joint_type(self) -> str: """ @@ -590,6 +855,36 @@ def to_xml(self, root: ET.Element | None = None) -> ET.Element: joint = super().to_xml(root) return joint + @classmethod + def from_xml(cls, element: ET.Element) -> "FixedJoint": + """ + Create a fixed joint from an XML element. + + Args: + element: The XML element to create the fixed joint from. + + Returns: + The fixed joint created from the XML element. + + Examples: + >>> element = ET.Element("joint") + >>> element.set("name", "joint1") + >>> element.set("type", "fixed") + >>> origin = Origin(xyz=(0, 0, 0), rpy=(0, 0, 0)) + >>> ET.SubElement(element, "origin", xyz="0 0 0", rpy="0 0 0") + >>> ET.SubElement(element, "parent", link="base_link") + >>> ET.SubElement(element, "child", link="link1") + >>> FixedJoint.from_xml(element) + + FixedJoint(name="joint1", parent="base_link", child="link1", origin=Origin(xyz=(0, 0, 0), rpy=(0, 0, 0)) + """ + + name = element.attrib["name"] + parent = element.find("parent").attrib["link"] + child = element.find("child").attrib["link"] + origin = Origin.from_xml(element.find("origin")) + return cls(name, parent, child, origin) + @property def joint_type(self) -> str: """ @@ -661,6 +956,47 @@ def to_xml(self, root: ET.Element | None = None) -> ET.Element: self.mimic.to_xml(joint) return joint + @classmethod + def from_xml(cls, element: ET.Element) -> "FloatingJoint": + """ + Create a floating joint from an XML element. + + Args: + element: The XML element to create the floating joint from. + + Returns: + The floating joint created from the XML element. + + Examples: + >>> element = ET.Element("joint") + >>> element.set("name", "joint1") + >>> element.set("type", "floating") + >>> origin = Origin(xyz=(0, 0, 0), rpy=(0, 0, 0)) + >>> ET.SubElement(element, "origin", xyz="0 0 0", rpy="0 0 0") + >>> ET.SubElement(element, "parent", link="base_link") + >>> ET.SubElement(element, "child", link="link1") + >>> mimic = ET.SubElement(element, "mimic", joint="joint1", multiplier="1.0", offset="0.0") + >>> FloatingJoint.from_xml(element) + + FloatingJoint( + name="joint1", + parent="base_link", + child="link1", + origin=Origin(xyz=(0, 0, 0), rpy=(0, 0, 0)), + mimic=JointMimic(joint="joint1", multiplier=1.0, offset=0.0) + ) + """ + + name = element.attrib["name"] + parent = element.find("parent").attrib["link"] + child = element.find("child").attrib["link"] + origin = Origin.from_xml(element.find("origin")) + + mimic_element = element.find("mimic") + mimic = JointMimic.from_xml(mimic_element) if mimic_element is not None else None + + return cls(name, parent, child, origin, mimic) + @property def joint_type(self) -> str: """ @@ -740,12 +1076,71 @@ def to_xml(self, root: ET.Element | None = None) -> ET.Element: """ joint = super().to_xml(root) - self.limits.to_xml(joint) self.axis.to_xml(joint) + if self.limits is not None: + self.limits.to_xml(joint) if self.mimic is not None: self.mimic.to_xml(joint) return joint + @classmethod + def from_xml(cls, element: ET.Element) -> "PlanarJoint": + """ + Create a planar joint from an XML element. + + Args: + element: The XML element to create the planar joint from. + + Returns: + The planar joint created from the XML element. + + Examples: + >>> element = ET.Element("joint") + >>> element.set("name", "joint1") + >>> element.set("type", "planar") + >>> origin = Origin(xyz=(0, 0, 0), rpy=(0, 0, 0)) + >>> ET.SubElement(element, "origin", xyz="0 0 0", rpy="0 0 0") + >>> ET.SubElement(element, "parent", link="base_link") + >>> ET.SubElement(element, "child", link="link1") + >>> limits = ET.SubElement(element, "limit", effort="10.0", velocity="1.0", lower="-1.0", upper="1.0") + >>> axis = ET.SubElement(element, "axis", xyz="0 0 1") + >>> mimic = ET.SubElement(element, "mimic", joint="joint1", multiplier="1.0", offset="0.0") + >>> PlanarJoint.from_xml(element) + + PlanarJoint( + name="joint1", + parent="base_link", + child="link1", + origin=Origin(xyz=(0, 0, 0), rpy=(0, 0, 0)), + limits=JointLimits(effort=10.0, velocity=1.0, lower=-1.0, upper=1.0), + axis=Axis(xyz=(0, 0, 1)), + mimic=JointMimic(joint="joint1", multiplier=1.0, offset=0.0) + ) + """ + + name = element.attrib["name"] + parent = element.find("parent").attrib["link"] + child = element.find("child").attrib["link"] + origin = Origin.from_xml(element.find("origin")) + + limit_element = element.find("limit") + if limit_element is not None: + limits = JointLimits( + effort=float(limit_element.attrib.get("effort", 0)), + velocity=float(limit_element.attrib.get("velocity", 0)), + lower=float(limit_element.attrib.get("lower", 0)), + upper=float(limit_element.attrib.get("upper", 0)), + ) + else: + limits = None + + axis = Axis.from_xml(element.find("axis")) + + mimic_element = element.find("mimic") + mimic = JointMimic.from_xml(mimic_element) if mimic_element is not None else None + + return cls(name, parent, child, origin, limits, axis, mimic) + @property def joint_type(self) -> str: """ diff --git a/onshape_api/models/link.py b/onshape_api/models/link.py index d3cb714..d272b9e 100644 --- a/onshape_api/models/link.py +++ b/onshape_api/models/link.py @@ -22,8 +22,7 @@ import numpy as np from scipy.spatial.transform import Rotation -from onshape_api.models.assembly import Part -from onshape_api.models.geometry import BaseGeometry +from onshape_api.models.geometry import BaseGeometry, BoxGeometry, CylinderGeometry, MeshGeometry, SphereGeometry from onshape_api.utilities import format_number @@ -118,6 +117,27 @@ def to_xml(self, root: ET.Element | None = None) -> ET.Element: origin.set("rpy", " ".join(format_number(v) for v in self.rpy)) return origin + @classmethod + def from_xml(cls, xml: ET.Element) -> "Origin": + """ + Create an origin from an XML element. + + Args: + xml: The XML element to create the origin from. + + Returns: + The origin created from the XML element. + + Examples: + >>> xml = ET.Element('origin') + >>> Origin.from_xml(xml) + Origin(xyz=(0.0, 0.0, 0.0), rpy=(0.0, 0.0, 0.0)) + """ + + xyz = tuple(map(float, xml.get("xyz").split())) + rpy = tuple(map(float, xml.get("rpy").split())) + return cls(xyz, rpy) + @classmethod def from_matrix(cls, matrix: np.matrix) -> "Origin": """ @@ -201,6 +221,25 @@ def to_xml(self, root: ET.Element | None = None) -> ET.Element: axis.set("xyz", " ".join(format_number(v) for v in self.xyz)) return axis + @classmethod + def from_xml(cls, xml: ET.Element) -> "Axis": + """ + Create an axis from an XML element. + + Args: + xml: The XML element to create the axis from. + + Returns: + The axis created from the XML element. + + Examples: + >>> xml = ET.Element('axis') + >>> Axis.from_xml(xml) + Axis(xyz=(0.0, 0.0, 0.0)) + """ + xyz = tuple(map(float, xml.get("xyz").split())) + return cls(xyz) + @dataclass class Inertia: @@ -256,6 +295,30 @@ def to_xml(self, root: ET.Element | None = None) -> ET.Element: inertia.set("iyz", format_number(self.iyz)) return inertia + @classmethod + def from_xml(cls, xml: ET.Element) -> "Inertia": + """ + Create an inertia tensor from an XML element. + + Args: + xml: The XML element to create the inertia tensor from. + + Returns: + The inertia tensor created from the XML element. + + Examples: + >>> xml = ET.Element('inertia') + >>> Inertia.from_xml(xml) + Inertia(ixx=0.0, iyy=0.0, izz=0.0, ixy=0.0, ixz=0.0, iyz=0.0) + """ + ixx = float(xml.get("ixx")) + iyy = float(xml.get("iyy")) + izz = float(xml.get("izz")) + ixy = float(xml.get("ixy")) + ixz = float(xml.get("ixz")) + iyz = float(xml.get("iyz")) + return cls(ixx, iyy, izz, ixy, ixz, iyz) + @dataclass class Material: @@ -305,6 +368,27 @@ def to_xml(self, root: ET.Element | None = None) -> ET.Element: ET.SubElement(material, "color", rgba=" ".join(format_number(v) for v in self.color)) return material + @classmethod + def from_xml(cls, xml: ET.Element) -> "Material": + """ + Create a material from an XML element. + + Args: + xml: The XML element to create the material from. + + Returns: + The material created from the XML element. + + Examples: + >>> xml = ET.Element('material') + >>> Material.from_xml(xml) + Material(name='material', color=(1.0, 0.0, 0.0, 1.0)) + """ + + name = xml.get("name") + color = tuple(map(float, xml.find("color").get("rgba").split())) + return cls(name, color) + @classmethod def from_color(cls, name: str, color: Colors) -> "Material": """ @@ -368,6 +452,44 @@ def to_xml(self, root: ET.Element | None = None) -> ET.Element: self.origin.to_xml(inertial) return inertial + @classmethod + def from_xml(cls, xml: ET.Element) -> "InertialLink": + """ + Create inertial properties from an XML element. + + Args: + xml: The XML element to create the inertial properties from. + + Returns: + The inertial properties created from the XML element. + + Examples: + >>> xml = ET.Element('inertial') + >>> InertialLink.from_xml(xml) + InertialLink(mass=0.0, inertia=None, origin=None) + """ + mass = float(xml.find("mass").get("value")) + + inertia_element = xml.find("inertia") + inertia = Inertia.from_xml(inertia_element) if inertia_element is not None else None + + origin_element = xml.find("origin") + origin = Origin.from_xml(origin_element) if origin_element is not None else None + + return cls(mass=mass, inertia=inertia, origin=origin) + + +def set_geometry_from_xml(geometry: ET.Element) -> BaseGeometry | None: + if geometry.find("filename"): + return MeshGeometry.from_xml(geometry) + elif geometry.find("box"): + return BoxGeometry.from_xml(geometry) + elif geometry.find("length") and geometry.find("radius"): + return CylinderGeometry.from_xml(geometry) + elif geometry.find("radius"): + return SphereGeometry.from_xml(geometry) + return None + @dataclass class VisualLink: @@ -415,6 +537,34 @@ def to_xml(self, root: ET.Element | None = None) -> ET.Element: self.material.to_xml(visual) return visual + @classmethod + def from_xml(cls, xml: ET.Element) -> "VisualLink": + """ + Create a visual link from an XML element. + + Args: + xml: The XML element to create the visual link from. + + Returns: + The visual link created from the XML element. + + Examples: + >>> xml = ET.Element('visual') + >>> VisualLink.from_xml(xml) + VisualLink(name='visual', origin=None, geometry=None, material=None) + """ + name = xml.get("name") + + origin_element = xml.find("origin") + origin = Origin.from_xml(origin_element) if origin_element is not None else None + + geometry_element = xml.find("geometry") + geometry = set_geometry_from_xml(geometry_element) if geometry_element is not None else None + + material_element = xml.find("material") + material = Material.from_xml(material_element) if material_element is not None else None + return cls(name=name, origin=origin, geometry=geometry, material=material) + @dataclass class CollisionLink: @@ -459,6 +609,32 @@ def to_xml(self, root: ET.Element | None = None) -> ET.Element: self.geometry.to_xml(collision) return collision + @classmethod + def from_xml(cls, xml: ET.Element) -> "CollisionLink": + """ + Create a collision link from an XML element. + + Args: + xml: The XML element to create the collision link from. + + Returns: + The collision link created from the XML element. + + Examples: + >>> xml = ET.Element('collision') + >>> CollisionLink.from_xml(xml) + CollisionLink(name='collision', origin=None, geometry=None) + """ + name = xml.get("name") + + origin_element = xml.find("origin") + origin = Origin.from_xml(origin_element) if origin_element is not None else None + + geometry_element = xml.find("geometry") + geometry = set_geometry_from_xml(geometry_element) if geometry_element is not None else None + + return cls(name=name, origin=origin, geometry=geometry) + @dataclass class Link: @@ -475,7 +651,7 @@ class Link: to_xml: Converts the link to an XML element. Class Methods: - from_part: Creates a link from a part. + from_xml: Creates a link from an XML element. Examples: >>> link = Link(name="link", visual=VisualLink(...), collision=CollisionLink(...), inertial=InertialLink(...)) @@ -523,21 +699,50 @@ def to_xml(self, root: ET.Element | None = None) -> ET.Element: return link @classmethod - def from_part(cls, part: Part) -> "Link": + def from_xml(cls, xml: ET.Element) -> "Link": """ - Create a link from a part. + Create a link from an XML element. Args: - part: The part to create the link from. + xml: The XML element to create the link from. Returns: - The link created from the part. + The link created from the XML element. Examples: - >>> part = Part(...) - >>> Link.from_part(part) - Link(name='partId', visual=None, collision=None, inertial=None) + >>> xml = ET.Element('link') + >>> Link.from_xml(xml) + Link(name='link', visual=None, collision=None, inertial=None) """ - # TODO: Retrieve visual, collision, and inertial properties from the part - _cls = cls(name=part.partId) - return _cls + name = xml.get("name") + + visual_element = xml.find("visual") + visual = VisualLink.from_xml(visual_element) if visual_element is not None else None + + collision_element = xml.find("collision") + collision = CollisionLink.from_xml(collision_element) if collision_element is not None else None + + inertial_element = xml.find("inertial") + inertial = InertialLink.from_xml(inertial_element) if inertial_element is not None else None + + return cls(name=name, visual=visual, collision=collision, inertial=inertial) + + # TODO: Implement from part method + # @classmethod + # def from_part(cls, part: Part) -> "Link": + # """ + # Create a link from a part. + + # Args: + # part: The part to create the link from. + + # Returns: + # The link created from the part. + + # Examples: + # >>> part = Part(...) + # >>> Link.from_part(part) + # Link(name='partId', visual=None, collision=None, inertial=None) + # """ + # _cls = cls(name=part.partId) + # return _cls diff --git a/onshape_api/robot.py b/onshape_api/robot.py index 8577464..d60fee2 100644 --- a/onshape_api/robot.py +++ b/onshape_api/robot.py @@ -12,14 +12,22 @@ from pathlib import Path from typing import Optional -from defusedxml import minidom +from lxml import etree from onshape_api.connect import Asset, Client from onshape_api.graph import create_graph from onshape_api.log import LOGGER from onshape_api.models.document import Document -from onshape_api.models.joint import BaseJoint, FixedJoint -from onshape_api.models.link import Link, Origin +from onshape_api.models.joint import ( + BaseJoint, + ContinuousJoint, + FixedJoint, + FloatingJoint, + JointType, + PrismaticJoint, + RevoluteJoint, +) +from onshape_api.models.link import Link from onshape_api.parse import get_instances, get_mates_and_relations, get_parts, get_subassemblies from onshape_api.urdf import get_urdf_components from onshape_api.utilities.helpers import save_model_as_json @@ -37,6 +45,35 @@ def __str__(self): return self.value +def set_joint_from_xml(element: ET.Element) -> BaseJoint | None: + """ + Set the joint type from an XML element. + + Args: + element (ET.Element): The XML element. + + Returns: + BaseJoint: The joint type. + + Examples: + >>> element = ET.Element("joint", type="fixed") + >>> set_joint_from_xml(element) + + """ + joint_type = element.attrib["type"] + if joint_type == JointType.FIXED: + return FixedJoint.from_xml(element) + elif joint_type == JointType.REVOLUTE: + return RevoluteJoint.from_xml(element) + elif joint_type == JointType.CONTINUOUS: + return ContinuousJoint.from_xml(element) + elif joint_type == JointType.PRISMATIC: + return PrismaticJoint.from_xml(element) + elif joint_type == JointType.FLOATING: + return FloatingJoint.from_xml(element) + return None + + class Robot: """ Represents a robot model in URDF format, containing links and joints. @@ -67,6 +104,8 @@ def __init__( joints: dict[str, BaseJoint], assets: Optional[dict[str, Asset]] = None, robot_type: RobotType = RobotType.URDF, + element: Optional[ET.Element] = None, + tree: Optional[ET.ElementTree] = None, ): self.name = name self.links = links @@ -74,8 +113,8 @@ def __init__( self.assets = assets self.type = robot_type - self.element: ET.Element = self.to_xml(robot_type=self.type) - self.tree: ET.ElementTree = ET.ElementTree(self.element) + self.element: ET.Element = element if element is not None else self.to_xml(robot_type=self.type) + self.tree: ET.ElementTree = tree if tree is not None else ET.ElementTree(self.element) def to_xml(self, robot_type: RobotType) -> ET.Element: """ @@ -109,7 +148,7 @@ def to_xml(self, robot_type: RobotType) -> ET.Element: return robot - def save(self) -> None: + def save(self, file_path: Optional[str] = None, download_assets: bool = True) -> None: """ Save the robot model to a URDF file. @@ -117,13 +156,16 @@ def save(self) -> None: >>> robot = Robot( ... ) >>> robot.save() """ - path = f"{self.name}.{self.type}" - # download assets before saving the URDF file - asyncio.run(self._download_assets()) + path = file_path if file_path else f"{self.name}.{self.type}" + + if download_assets: + asyncio.run(self._download_assets()) if isinstance(path, (str, Path)): xml_str = ET.tostring(self.tree.getroot(), encoding="unicode") - pretty_xml_str = minidom.parseString(xml_str).toprettyxml(indent=" ") + xml_tree = etree.fromstring(xml_str) # noqa: S320 + pretty_xml_str = etree.tostring(xml_tree, pretty_print=True, encoding="unicode") + with open(path, "w", encoding="utf-8") as f: f.write(pretty_xml_str) @@ -177,6 +219,40 @@ async def _download_assets(self) -> None: LOGGER.error(f"Error during asset download: {e}") return + @classmethod + def from_urdf(cls, filename: str) -> "Robot": + """ + Load a robot model from a URDF file. + + Args: + filename (str): The path to the URDF file. + + Returns: + Robot: The robot model loaded from the URDF file. + + Examples: + >>> robot = Robot.from_urdf("robot.urdf") + """ + tree = ET.parse(filename) # noqa: S314 + root = tree.getroot() + + name = root.attrib["name"] + links = {} + joints = {} + + for child in root: + if child.tag == "link": + link = Link.from_xml(child) + links[link.name] = link + elif child.tag == "joint": + joint = set_joint_from_xml(child) + if joint: + joints[joint.name] = joint + + return Robot( + name=name, links=links, joints=joints, assets=None, robot_type=RobotType.URDF, element=root, tree=tree + ) + def get_robot( robot_name: str, @@ -259,40 +335,21 @@ def get_robot( if __name__ == "__main__": LOGGER.set_file_name("robot.log") - robot = Robot( - name="Test", - links={ - "link1": Link(name="link1"), - "link2": Link(name="link2"), - }, - joints={ - "joint1": FixedJoint(name="joint1", parent="link1", child="link2", origin=Origin.zero_origin()), - }, - assets={ - "link1": Asset( - did="1f42f849180e6e5c9abfce52", - wtype="w", - wid="0c00b6520fac5fada24b2104", - eid="c96b40ef586e60c182f41d29", - client=Client(env="E:/onshape-api/tests/.env"), - transform=(0, 0, 0, 0, 0, 0), - file_name="link1.stl", - is_rigid_assembly=False, - partID="KHD", - ), - "link2": Asset( - did="1f42f849180e6e5c9abfce52", - wtype="w", - wid="0c00b6520fac5fada24b2104", - eid="c96b40ef586e60c182f41d29", - client=Client(env="E:/onshape-api/tests/.env"), - transform=(0, 0, 0, 0, 0, 0), - file_name="link2.stl", - is_rigid_assembly=False, - partID="KHD", - ), - }, - robot_type=RobotType.MJCF, - ) + # robot = Robot( + # name="Test", + # links={ + # "link1": Link(name="link1"), + # "link2": Link(name="link2"), + # }, + # joints={ + # "joint1": FixedJoint(name="joint1", parent="link1", child="link2", origin=Origin.zero_origin()), + # }, + # robot_type=RobotType.URDF, + # ) + + # robot.save() + + robot = Robot.from_urdf("E:/onshape-api/playground/Co-Design-Prototype-UMV.urdf") robot.show() + robot.save(file_path="E:/onshape-api/playground/test.urdf", download_assets=False) diff --git a/poetry.lock b/poetry.lock index fcd8dc0..59176bc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -384,17 +384,6 @@ files = [ docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] tests = ["pytest", "pytest-cov", "pytest-xdist"] -[[package]] -name = "defusedxml" -version = "0.7.1" -description = "XML bomb protection for Python stdlib modules" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, - {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, -] - [[package]] name = "deptry" version = "0.16.2" @@ -860,6 +849,160 @@ files = [ {file = "kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60"}, ] +[[package]] +name = "lxml" +version = "5.3.0" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +optional = false +python-versions = ">=3.6" +files = [ + {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656"}, + {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7"}, + {file = "lxml-5.3.0-cp310-cp310-win32.whl", hash = "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80"}, + {file = "lxml-5.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3"}, + {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b"}, + {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be"}, + {file = "lxml-5.3.0-cp311-cp311-win32.whl", hash = "sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9"}, + {file = "lxml-5.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1"}, + {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859"}, + {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d"}, + {file = "lxml-5.3.0-cp312-cp312-win32.whl", hash = "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30"}, + {file = "lxml-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f"}, + {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a"}, + {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b"}, + {file = "lxml-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957"}, + {file = "lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d"}, + {file = "lxml-5.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8f0de2d390af441fe8b2c12626d103540b5d850d585b18fcada58d972b74a74e"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1afe0a8c353746e610bd9031a630a95bcfb1a720684c3f2b36c4710a0a96528f"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56b9861a71575f5795bde89256e7467ece3d339c9b43141dbdd54544566b3b94"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:9fb81d2824dff4f2e297a276297e9031f46d2682cafc484f49de182aa5e5df99"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2c226a06ecb8cdef28845ae976da407917542c5e6e75dcac7cc33eb04aaeb237"}, + {file = "lxml-5.3.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:7d3d1ca42870cdb6d0d29939630dbe48fa511c203724820fc0fd507b2fb46577"}, + {file = "lxml-5.3.0-cp36-cp36m-win32.whl", hash = "sha256:094cb601ba9f55296774c2d57ad68730daa0b13dc260e1f941b4d13678239e70"}, + {file = "lxml-5.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:eafa2c8658f4e560b098fe9fc54539f86528651f61849b22111a9b107d18910c"}, + {file = "lxml-5.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cb83f8a875b3d9b458cada4f880fa498646874ba4011dc974e071a0a84a1b033"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25f1b69d41656b05885aa185f5fdf822cb01a586d1b32739633679699f220391"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23e0553b8055600b3bf4a00b255ec5c92e1e4aebf8c2c09334f8368e8bd174d6"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ada35dd21dc6c039259596b358caab6b13f4db4d4a7f8665764d616daf9cc1d"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:81b4e48da4c69313192d8c8d4311e5d818b8be1afe68ee20f6385d0e96fc9512"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:2bc9fd5ca4729af796f9f59cd8ff160fe06a474da40aca03fcc79655ddee1a8b"}, + {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07da23d7ee08577760f0a71d67a861019103e4812c87e2fab26b039054594cc5"}, + {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ea2e2f6f801696ad7de8aec061044d6c8c0dd4037608c7cab38a9a4d316bfb11"}, + {file = "lxml-5.3.0-cp37-cp37m-win32.whl", hash = "sha256:5c54afdcbb0182d06836cc3d1be921e540be3ebdf8b8a51ee3ef987537455f84"}, + {file = "lxml-5.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f2901429da1e645ce548bf9171784c0f74f0718c3f6150ce166be39e4dd66c3e"}, + {file = "lxml-5.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c56a1d43b2f9ee4786e4658c7903f05da35b923fb53c11025712562d5cc02753"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ee8c39582d2652dcd516d1b879451500f8db3fe3607ce45d7c5957ab2596040"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdf3a3059611f7585a78ee10399a15566356116a4288380921a4b598d807a22"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:146173654d79eb1fc97498b4280c1d3e1e5d58c398fa530905c9ea50ea849b22"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:0a7056921edbdd7560746f4221dca89bb7a3fe457d3d74267995253f46343f15"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:9e4b47ac0f5e749cfc618efdf4726269441014ae1d5583e047b452a32e221920"}, + {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f914c03e6a31deb632e2daa881fe198461f4d06e57ac3d0e05bbcab8eae01945"}, + {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:213261f168c5e1d9b7535a67e68b1f59f92398dd17a56d934550837143f79c42"}, + {file = "lxml-5.3.0-cp38-cp38-win32.whl", hash = "sha256:218c1b2e17a710e363855594230f44060e2025b05c80d1f0661258142b2add2e"}, + {file = "lxml-5.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:315f9542011b2c4e1d280e4a20ddcca1761993dda3afc7a73b01235f8641e903"}, + {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de"}, + {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a"}, + {file = "lxml-5.3.0-cp39-cp39-win32.whl", hash = "sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff"}, + {file = "lxml-5.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:94d6c3782907b5e40e21cadf94b13b0842ac421192f26b84c45f13f3c9d5dc27"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c300306673aa0f3ed5ed9372b21867690a17dba38c68c44b287437c362ce486b"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d9b952e07aed35fe2e1a7ad26e929595412db48535921c5013edc8aa4a35ce"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2d9b8d9177afaef80c53c0a9e30fa252ff3036fb1c6494d427c066a4ce6a282f"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:20094fc3f21ea0a8669dc4c61ed7fa8263bd37d97d93b90f28fc613371e7a875"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ace2c2326a319a0bb8a8b0e5b570c764962e95818de9f259ce814ee666603f19"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e67a0be1639c251d21e35fe74df6bcc40cba445c2cda7c4a967656733249e2"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd5350b55f9fecddc51385463a4f67a5da829bc741e38cf689f38ec9023f54ab"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c1fefd7e3d00921c44dc9ca80a775af49698bbfd92ea84498e56acffd4c5469"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:71a8dd38fbd2f2319136d4ae855a7078c69c9a38ae06e0c17c73fd70fc6caad8"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:97acf1e1fd66ab53dacd2c35b319d7e548380c2e9e8c54525c6e76d21b1ae3b1"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c"}, + {file = "lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f"}, +] + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html-clean = ["lxml-html-clean"] +html5 = ["html5lib"] +htmlsoup = ["BeautifulSoup4"] +source = ["Cython (>=3.0.11)"] + [[package]] name = "markdown" version = "3.7" @@ -2484,4 +2627,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<4.0" -content-hash = "8be374f09d5a0c76bdc9335567299398cf2e9b82ff9b9420e6ae25ef857e04b2" +content-hash = "28c94bad0c0d4a788dc13f4215b4d039d3152bd1b2a682bfa9abaf70fd4b83b6" diff --git a/pyproject.toml b/pyproject.toml index 0339565..a299b78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,8 +22,8 @@ pandas = "^2.2.3" pyarrow = "^18.0.0" regex = "^2024.9.11" tqdm = "^4.67.0" -defusedxml = "^0.7.1" mujoco = "^3.2.6" +lxml = "^5.3.0" [tool.poetry.group.dev.dependencies] pytest = "^7.2.0"