Add basic at:// URI support

Changed files
+143 -2
src
atpasser
+29 -1
poetry.lock
···
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
+
name = "jsonpath-ng"
+
version = "1.7.0"
+
description = "A final implementation of JSONPath for Python that aims to be standard compliant, including arithmetic and binary comparison operators and providing clear AST for metaprogramming."
+
optional = false
+
python-versions = "*"
+
groups = ["main"]
+
files = [
+
{file = "jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c"},
+
{file = "jsonpath_ng-1.7.0-py2-none-any.whl", hash = "sha256:898c93fc173f0c336784a3fa63d7434297544b7198124a68f9a3ef9597b0ae6e"},
+
{file = "jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6"},
+
]
+
+
[package.dependencies]
+
ply = "*"
+
+
[[package]]
name = "lxml"
version = "6.0.1"
description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
···
groups = ["main"]
files = [
{file = "morphys-1.0-py2.py3-none-any.whl", hash = "sha256:76d6dbaa4d65f597e59d332c81da786d83e4669387b9b2a750cfec74e7beec20"},
+
]
+
+
[[package]]
+
name = "ply"
+
version = "3.11"
+
description = "Python Lex & Yacc"
+
optional = false
+
python-versions = "*"
+
groups = ["main"]
+
files = [
+
{file = "ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"},
+
{file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"},
]
[[package]]
···
[metadata]
lock-version = "2.1"
python-versions = ">=3.13"
-
content-hash = "f1ed7d854143c5a27897e3599cafa301e939d46f52b876acaae299f57fb47950"
+
content-hash = "91faa3780c077d6f2d96269f782e106c25e374a61a51ecf7a8e56bfdf5564c1c"
+1 -1
pyproject.toml
···
authors = [{ name = "diaowinner", email = "diaowinner@qq.com" }]
readme = "README.md"
requires-python = ">=3.13"
-
dependencies = ["py-cid (>=0.3.0,<0.4.0)", "cbor2 (>=5.7.0,<5.8.0)", "dnspython (>=2.7.0,<3.0.0)", "requests (>=2.32.5,<3.0.0)", "pyld[requests] (>=2.0.4,<3.0.0)"]
+
dependencies = ["py-cid (>=0.3.0,<0.4.0)", "cbor2 (>=5.7.0,<5.8.0)", "dnspython (>=2.7.0,<3.0.0)", "requests (>=2.32.5,<3.0.0)", "pyld[requests] (>=2.0.4,<3.0.0)", "jsonpath-ng (>=1.7.0,<2.0.0)"]
license = "MIT OR Apache-2.0"
license-files = ["LICEN[CS]E.*"]
+113
src/atpasser/uri/__init__.py
···
+
import urllib.parse as up
+
from atpasser import handle, did
+
import jsonpath_ng
+
+
+
class URI:
+
"""
+
A class representing an AT URI.
+
+
Attributes:
+
uri (str): The AT URI.
+
"""
+
+
def __init__(self, uri: str) -> None:
+
"""
+
Initalizes an AT URI.
+
+
Attributes:
+
uri (str): The AT URI.
+
"""
+
+
if not set(uri).issubset(set([chr(i) for i in range(0x80)])):
+
raise ValueError("invalid char in uri")
+
+
if len(uri) > 8000:
+
raise ValueError("uri longer than 8000 chars") # the doc says 8 "kilobytes"
+
+
if not uri.startswith("at://"):
+
raise ValueError("not starts with at://")
+
+
burner = uri[5:].split("#")
+
+
try:
+
fragment = up.unquote(burner[1])
+
except:
+
fragment = None
+
+
try:
+
query = up.unquote(burner[0].split("?")[1])
+
except:
+
query = None
+
+
path = [up.unquote(segment) for segment in burner[0].split("/")[1:]]
+
if len(path) > 0 and path[-1] == "":
+
raise ValueError("trailing slash")
+
+
authorityValue = up.unquote(burner[0].split("/")[0])
+
+
p = up.urlparse(up.unquote("//" + authorityValue))
+
if p.username is not None or p.password is not None:
+
raise ValueError("userinfo unsupported")
+
+
# We decided not to detect if it's a handle there
+
#
+
# try:
+
# authority = handle.Handle(authorityValue)
+
# except:
+
# try:
+
# authority = did.DID(authorityValue)
+
# except:
+
# raise ValueError("authority is neither handle nor tid")
+
+
if fragment != None:
+
self.fragment = jsonpath_ng.parse(fragment)
+
self.fragmentAsText = up.quote(fragment)
+
+
if query != None:
+
self.query = up.parse_qs(query)
+
self.queryAsText = up.quote(query)
+
else:
+
self.query, self.queryAsText = None, None
+
+
self.path = path
+
pathAsText = "/".join([up.quote(i) for i in path])
+
self.pathAsText = pathAsText
+
+
self.authorityAsText = authorityValue
+
try:
+
authority = handle.Handle(authorityValue)
+
except:
+
try:
+
authority = did.DID(authorityValue)
+
except:
+
authority = None
+
self.authority = authority
+
+
unescapedAV = up.quote(authorityValue).replace("%3A", ":")
+
self.uri = "at://{}/{}{}{}".format(
+
unescapedAV,
+
pathAsText,
+
"?" + query if query != None else "",
+
"?" + fragment if fragment != None else "",
+
)
+
+
def __str__(self) -> str:
+
"""
+
+
Convert the RecordKey to a string by given the URI.
+
"""
+
return self.uri
+
+
def __eq__(self, value: object, /) -> bool:
+
"""
+
+
Check if the 2 values are exactly the same.
+
"""
+
+
if isinstance(value, URI):
+
+
return str(self) == str(value)
+
else:
+
+
raise TypeError