combile related stuffs

Changed files
+376 -346
src
+103 -4
src/atpasser/uri/__init__.py
···
import urllib.parse as up
-
from . import handle, did
import jsonpath_ng
-
from .exceptions import InvalidURIError, ValidationError, URIError
class URI:
···
self.authorityAsText = authorityValue
try:
-
authority = handle.Handle(authorityValue)
except Exception as e:
try:
-
authority = did.DID(authorityValue)
except Exception as e2:
# Don't throw exception, just set authority to None
authority = None
···
else:
return False
···
import urllib.parse as up
+
from .identifier import Handle, DID
import jsonpath_ng
+
from .exceptions import (
+
InvalidURIError,
+
ValidationError,
+
URIError,
+
InvalidRestrictedURIError,
+
)
class URI:
···
self.authorityAsText = authorityValue
try:
+
authority = Handle(authorityValue)
except Exception as e:
try:
+
authority = DID(authorityValue)
except Exception as e2:
# Don't throw exception, just set authority to None
authority = None
···
else:
return False
+
+
+
from .identifier import Handle
+
from . import nsid
+
from . import rkey as rKey
+
from . import URI
+
+
+
class RestrictedURI(URI):
+
"""A class representing a restricted AT Protocol URI for record access.
+
+
RestrictedURIs provide a specialized form of AT Protocol URIs that specifically
+
address records within repositories. They follow the format 'at://authority/collection/rkey'
+
where collection identifies the record type (via NSID) and rkey identifies the specific
+
record instance. This format is commonly used for referencing specific social records
+
like posts, profiles, and other user-generated content.
+
+
Attributes:
+
collection (atpasser.uri.NSID): The record collection identified by NSID.
+
rkey (atpasser.uri.RKey): The record key identifying a specific record instance.
+
"""
+
+
def __init__(self, uri: str) -> None:
+
"""Initializes a restricted URI with validation.
+
+
Parses and validates an AT Protocol URI specifically for record access.
+
Restricted URIs must have a valid authority (DID or handle), may include
+
a collection (NSID), and optionally a record key (rkey). Query parameters
+
and fragments are not allowed in restricted URIs.
+
+
Args:
+
uri (str): The AT Protocol URI string to parse as a restricted URI.
+
+
Raises:
+
InvalidRestrictedURIError: If the URI has query parameters or fragments, invalid authority,
+
or too many path segments for a restricted URI format.
+
InvalidURIError: If the underlying URI validation fails.
+
"""
+
+
try:
+
super().__init__(uri)
+
except InvalidURIError:
+
raise
+
except Exception as e:
+
raise InvalidURIError(
+
uri, "base URI validation failed", f"Failed to parse base URI: {str(e)}"
+
)
+
+
if self.query is not None:
+
raise InvalidRestrictedURIError(
+
uri,
+
"query parameters not supported",
+
"Restricted URI cannot contain query parameters",
+
)
+
+
if self.fragment is not None:
+
raise InvalidRestrictedURIError(
+
uri,
+
"fragments not supported",
+
"Restricted URI cannot contain fragments",
+
)
+
+
if self.authority is None:
+
raise InvalidRestrictedURIError(
+
uri,
+
"invalid authority",
+
"Restricted URI must contain a valid DID or Handle",
+
)
+
+
try:
+
if len(self.path) == 0:
+
self.collection, self.rkey = None, None
+
+
elif len(self.path) == 1:
+
self.collection = nsid.NSID(self.path[0])
+
self.rkey = None
+
+
elif len(self.path) == 2:
+
self.collection = nsid.NSID(self.path[0])
+
self.rkey = rKey.RKey(self.path[1])
+
else:
+
raise InvalidRestrictedURIError(
+
uri,
+
"too many path segments",
+
f"Restricted URI can have at most 2 path segments, currently has {len(self.path)}",
+
)
+
except Exception as e:
+
if isinstance(e, (InvalidRestrictedURIError, InvalidURIError)):
+
raise
+
raise InvalidRestrictedURIError(
+
uri,
+
"parsing error",
+
f"Error occurred while parsing Restricted URI: {str(e)}",
+
)
-112
src/atpasser/uri/did.py
···
-
import re
-
from pyld import jsonld
-
from .exceptions import InvalidDIDError, ResolutionError
-
-
-
class DID:
-
"""A class representing a DID (Decentralized Identifier) in the AT Protocol.
-
-
Decentralized Identifiers (DIDs) are a key component of the AT Protocol's identity system.
-
They provide cryptographically verifiable, persistent identifiers for users and services
-
in the decentralized network. DIDs follow the format 'did:method:specific-identifier'
-
where method defines the resolution mechanism (e.g., 'plc' for PLC directory, 'web' for web-based).
-
-
Attributes:
-
uri (str): The complete DID URI string.
-
"""
-
-
def __init__(self, uri: str) -> None:
-
"""Initializes a DID object with validation.
-
-
Parses and validates a DID URI string according to the W3C DID specification
-
and AT Protocol requirements. The DID must follow the format 'did:method:identifier'
-
and contain only valid characters.
-
-
Args:
-
uri (str): The DID URI string to validate.
-
-
Raises:
-
InvalidDIDError: If the URI doesn't match the DID format or exceeds maximum length.
-
"""
-
pattern = re.compile("^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$")
-
patternMatch = pattern.match(uri)
-
-
if not patternMatch:
-
raise InvalidDIDError(
-
uri, "invalid format", "DID must follow 'did:method:identifier' format"
-
)
-
-
if len(uri) > 2048:
-
raise InvalidDIDError(
-
uri,
-
"exceeds maximum length",
-
f"DID length {len(uri)} exceeds maximum allowed length of 2048 characters",
-
)
-
-
self.uri = patternMatch[0]
-
-
def __str__(self) -> str:
-
"""Convert the DID object to its string representation.
-
-
Returns:
-
str: The canonical DID URI string.
-
"""
-
return self.uri
-
-
def __eq__(self, value: object, /) -> bool:
-
"""Check if two DID objects represent the same identifier.
-
-
Args:
-
value (object): The object to compare with.
-
-
Returns:
-
bool: True if the objects represent the same DID, False otherwise.
-
"""
-
if isinstance(value, DID):
-
return str(self) == str(value)
-
else:
-
return False
-
-
def fetch(self):
-
"""Fetch the DID document metadata from the appropriate resolver.
-
-
Resolves the DID to its associated DID document by querying the appropriate
-
resolver based on the DID method. Currently supports 'did:plc:' (resolved via
-
PLC directory) and 'did:web:' (resolved via well-known HTTP endpoint).
-
-
Returns:
-
list: The expanded JSON-LD document representing the DID document,
-
compatible with the PyLD library for further processing.
-
-
Raises:
-
ResolutionError: If the DID document cannot be fetched or parsed.
-
InvalidDIDError: If the DID method is not supported.
-
-
Note:
-
- For 'did:plc:' DIDs, queries https://plc.directory/{did}
-
- For 'did:web:' DIDs, queries https://{domain}/.well-known/did.json
-
"""
-
try:
-
if self.uri.startswith("did:plc:"):
-
return jsonld.expand(f"https://plc.directory/{self.uri}")
-
elif self.uri.startswith("did:web:"):
-
domain = self.uri.replace("did:web:", "")
-
if not domain:
-
raise InvalidDIDError(
-
self.uri, "invalid format", "did:web DID must contain a domain"
-
)
-
return jsonld.expand(f"https://{domain}/.well-known/did.json")
-
else:
-
raise InvalidDIDError(
-
self.uri,
-
"unsupported DID method",
-
f"Unsupported DID method: {self.uri.split(':')[1]}",
-
)
-
except Exception as e:
-
if isinstance(e, (InvalidDIDError, ResolutionError)):
-
raise
-
raise ResolutionError(
-
self.uri,
-
"fetch DID document",
-
f"Failed to fetch or parse DID document: {str(e)}",
-
)
···
+116 -2
src/atpasser/uri/handle.py src/atpasser/uri/identifier.py
···
import dns.resolver, requests
-
from .did import DID
-
from .exceptions import InvalidHandleError, ResolutionError, InvalidDIDError
class Handle:
···
+
import re
+
from pyld import jsonld
import dns.resolver, requests
+
from .exceptions import (
+
InvalidDIDError,
+
InvalidHandleError,
+
ResolutionError,
+
InvalidDIDError,
+
)
+
+
class DID:
+
"""A class representing a DID (Decentralized Identifier) in the AT Protocol.
+
+
Decentralized Identifiers (DIDs) are a key component of the AT Protocol's identity system.
+
They provide cryptographically verifiable, persistent identifiers for users and services
+
in the decentralized network. DIDs follow the format 'did:method:specific-identifier'
+
where method defines the resolution mechanism (e.g., 'plc' for PLC directory, 'web' for web-based).
+
+
Attributes:
+
uri (str): The complete DID URI string.
+
"""
+
+
def __init__(self, uri: str) -> None:
+
"""Initializes a DID object with validation.
+
+
Parses and validates a DID URI string according to the W3C DID specification
+
and AT Protocol requirements. The DID must follow the format 'did:method:identifier'
+
and contain only valid characters.
+
+
Args:
+
uri (str): The DID URI string to validate.
+
+
Raises:
+
InvalidDIDError: If the URI doesn't match the DID format or exceeds maximum length.
+
"""
+
pattern = re.compile("^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$")
+
patternMatch = pattern.match(uri)
+
+
if not patternMatch:
+
raise InvalidDIDError(
+
uri, "invalid format", "DID must follow 'did:method:identifier' format"
+
)
+
+
if len(uri) > 2048:
+
raise InvalidDIDError(
+
uri,
+
"exceeds maximum length",
+
f"DID length {len(uri)} exceeds maximum allowed length of 2048 characters",
+
)
+
+
self.uri = patternMatch[0]
+
+
def __str__(self) -> str:
+
"""Convert the DID object to its string representation.
+
+
Returns:
+
str: The canonical DID URI string.
+
"""
+
return self.uri
+
+
def __eq__(self, value: object, /) -> bool:
+
"""Check if two DID objects represent the same identifier.
+
+
Args:
+
value (object): The object to compare with.
+
+
Returns:
+
bool: True if the objects represent the same DID, False otherwise.
+
"""
+
if isinstance(value, DID):
+
return str(self) == str(value)
+
else:
+
return False
+
+
def fetch(self):
+
"""Fetch the DID document metadata from the appropriate resolver.
+
+
Resolves the DID to its associated DID document by querying the appropriate
+
resolver based on the DID method. Currently supports 'did:plc:' (resolved via
+
PLC directory) and 'did:web:' (resolved via well-known HTTP endpoint).
+
+
Returns:
+
list: The expanded JSON-LD document representing the DID document,
+
compatible with the PyLD library for further processing.
+
+
Raises:
+
ResolutionError: If the DID document cannot be fetched or parsed.
+
InvalidDIDError: If the DID method is not supported.
+
+
Note:
+
- For 'did:plc:' DIDs, queries https://plc.directory/{did}
+
- For 'did:web:' DIDs, queries https://{domain}/.well-known/did.json
+
"""
+
try:
+
if self.uri.startswith("did:plc:"):
+
return jsonld.expand(f"https://plc.directory/{self.uri}")
+
elif self.uri.startswith("did:web:"):
+
domain = self.uri.replace("did:web:", "")
+
if not domain:
+
raise InvalidDIDError(
+
self.uri, "invalid format", "did:web DID must contain a domain"
+
)
+
return jsonld.expand(f"https://{domain}/.well-known/did.json")
+
else:
+
raise InvalidDIDError(
+
self.uri,
+
"unsupported DID method",
+
f"Unsupported DID method: {self.uri.split(':')[1]}",
+
)
+
except Exception as e:
+
if isinstance(e, (InvalidDIDError, ResolutionError)):
+
raise
+
raise ResolutionError(
+
self.uri,
+
"fetch DID document",
+
f"Failed to fetch or parse DID document: {str(e)}",
+
)
class Handle:
-92
src/atpasser/uri/restricted.py
···
-
from . import handle, nsid
-
from . import rkey as rKey
-
from . import URI
-
from .exceptions import InvalidRestrictedURIError, InvalidURIError
-
-
-
class RestrictedURI(URI):
-
"""A class representing a restricted AT Protocol URI for record access.
-
-
RestrictedURIs provide a specialized form of AT Protocol URIs that specifically
-
address records within repositories. They follow the format 'at://authority/collection/rkey'
-
where collection identifies the record type (via NSID) and rkey identifies the specific
-
record instance. This format is commonly used for referencing specific social records
-
like posts, profiles, and other user-generated content.
-
-
Attributes:
-
collection (atpasser.uri.NSID): The record collection identified by NSID.
-
rkey (atpasser.uri.RKey): The record key identifying a specific record instance.
-
"""
-
-
def __init__(self, uri: str) -> None:
-
"""Initializes a restricted URI with validation.
-
-
Parses and validates an AT Protocol URI specifically for record access.
-
Restricted URIs must have a valid authority (DID or handle), may include
-
a collection (NSID), and optionally a record key (rkey). Query parameters
-
and fragments are not allowed in restricted URIs.
-
-
Args:
-
uri (str): The AT Protocol URI string to parse as a restricted URI.
-
-
Raises:
-
InvalidRestrictedURIError: If the URI has query parameters or fragments, invalid authority,
-
or too many path segments for a restricted URI format.
-
InvalidURIError: If the underlying URI validation fails.
-
"""
-
-
try:
-
super().__init__(uri)
-
except InvalidURIError:
-
raise
-
except Exception as e:
-
raise InvalidURIError(
-
uri, "base URI validation failed", f"Failed to parse base URI: {str(e)}"
-
)
-
-
if self.query is not None:
-
raise InvalidRestrictedURIError(
-
uri,
-
"query parameters not supported",
-
"Restricted URI cannot contain query parameters",
-
)
-
-
if self.fragment is not None:
-
raise InvalidRestrictedURIError(
-
uri,
-
"fragments not supported",
-
"Restricted URI cannot contain fragments",
-
)
-
-
if self.authority is None:
-
raise InvalidRestrictedURIError(
-
uri,
-
"invalid authority",
-
"Restricted URI must contain a valid DID or Handle",
-
)
-
-
try:
-
if len(self.path) == 0:
-
self.collection, self.rkey = None, None
-
-
elif len(self.path) == 1:
-
self.collection = nsid.NSID(self.path[0])
-
self.rkey = None
-
-
elif len(self.path) == 2:
-
self.collection = nsid.NSID(self.path[0])
-
self.rkey = rKey.RKey(self.path[1])
-
else:
-
raise InvalidRestrictedURIError(
-
uri,
-
"too many path segments",
-
f"Restricted URI can have at most 2 path segments, currently has {len(self.path)}",
-
)
-
except Exception as e:
-
if isinstance(e, (InvalidRestrictedURIError, InvalidURIError)):
-
raise
-
raise InvalidRestrictedURIError(
-
uri,
-
"parsing error",
-
f"Error occurred while parsing Restricted URI: {str(e)}",
-
)
···
+157
src/atpasser/uri/rkey.py
···
return str(self) == str(value)
else:
return False
···
return str(self) == str(value)
else:
return False
+
+
+
import datetime, random
+
+
+
class TID(RKey):
+
"""A class representing a TID (Time-based Identifier) in the AT Protocol.
+
+
TIDs are time-based identifiers used for ordering and uniquely identifying
+
records in the AT Protocol. They combine a microsecond-precision timestamp
+
with a clock identifier to ensure uniqueness even when multiple records
+
are created in the same microsecond. TIDs are sortable and provide both
+
temporal ordering and uniqueness guarantees.
+
+
Attributes:
+
timestamp (datetime.datetime): The timestamp component of the TID.
+
clockIdentifier (int): Clock identifier (0-1023) for disambiguation.
+
recordKey (str): The record key string identifying a specific record.
+
"""
+
+
def __init__(
+
self, time: datetime.datetime | None = None, clockIdentifier: int | None = None
+
) -> None:
+
"""Initializes a TID object with timestamp and clock identifier.
+
+
Creates a new TID with the specified timestamp and clock identifier.
+
If no timestamp is provided, uses the current time. If no clock identifier
+
is provided, generates a random value between 0-1023.
+
+
Args:
+
time (datetime.datetime, optional): The timestamp for the TID.
+
Defaults to current time if not provided.
+
clockIdentifier (int, optional): Clock identifier (0-1023) for
+
disambiguation. Defaults to random value if not provided.
+
"""
+
if time == None:
+
self.timestamp = datetime.datetime.now()
+
else:
+
self.timestamp = time
+
if clockIdentifier == None:
+
self.clockIdentifier = random.randrange(0, 1024)
+
else:
+
self.clockIdentifier = clockIdentifier
+
+
# Generate TID string representation, then call parent class constructor
+
tid_string = self._generate_tid_string()
+
super().__init__(tid_string)
+
+
def _generate_tid_string(self) -> str:
+
"""Generate TID string representation.
+
+
Returns:
+
str: The base32 string representation of the TID.
+
"""
+
integer = (
+
int(self.timestamp.timestamp() * 1000000) * 1024 + self.clockIdentifier
+
)
+
binary = f"{integer:065b}"
+
return "".join(
+
[
+
"234567abcdefghijklmnopqrstuvwxyz"[int(binary[i : i + 5], base=2)]
+
for i in range(0, len(binary), 5)
+
]
+
)
+
+
def __int__(self):
+
"""Convert the TID to its integer representation.
+
+
Combines the timestamp (in microseconds) and clock identifier into
+
a single 64-bit integer where the high bits represent the timestamp
+
and the low 10 bits represent the clock identifier.
+
+
Returns:
+
int: The integer representation of the TID.
+
"""
+
timestamp = int(self.timestamp.timestamp() * 1000000)
+
return timestamp * 1024 + self.clockIdentifier
+
+
def __str__(self):
+
"""Convert the TID to a base32-sortable string representation.
+
+
Encodes the TID as a base32 string using a custom character set that
+
maintains lexicographical sort order corresponding to temporal order.
+
This format is commonly used in the AT Protocol for compact,
+
sortable identifiers.
+
+
Returns:
+
str: The base32 string representation of the TID.
+
"""
+
# Use parent class __str__ method, which returns recordKey
+
return super().__str__()
+
+
def __eq__(self, value: object, /) -> bool:
+
"""Check if two TID objects represent the same identifier.
+
+
Args:
+
value (object): The object to compare with.
+
+
Returns:
+
bool: True if the objects represent the same TID, False otherwise.
+
"""
+
if isinstance(value, TID):
+
return int(self) == int(value)
+
elif isinstance(value, RKey):
+
# If comparing with RKey, use string comparison
+
return str(self) == str(value)
+
else:
+
return False
+
+
+
def importTIDfromInteger(value: int | None = None):
+
"""Create a TID object from an integer representation.
+
+
Converts a 64-bit integer back into a TID object by extracting the
+
timestamp and clock identifier components. If no value is provided,
+
creates a TID for the current time.
+
+
Args:
+
value (int, optional): The integer value to convert to a TID.
+
Defaults to creating a TID for the current time if not provided.
+
+
Returns:
+
TID: The TID object created from the integer value.
+
"""
+
if value == None:
+
value = int(TID())
+
clockIdentifier = value % 1024
+
timestamp = (value >> 10) / 1000000
+
return TID(datetime.datetime.fromtimestamp(timestamp), clockIdentifier)
+
+
+
def importTIDfromBase32(value: str | None = None):
+
"""Create a TID object from a base32 string representation.
+
+
Converts a base32-encoded TID string back into a TID object by decoding
+
the string to its integer representation and then extracting the timestamp
+
and clock identifier components. If no value is provided, creates a TID
+
for the current time.
+
+
Args:
+
value (str, optional): The base32 string to convert to a TID.
+
Defaults to creating a TID for the current time if not provided.
+
+
Returns:
+
TID: The TID object created from the base32 string.
+
"""
+
if value == None:
+
value = str(TID())
+
b32s = "234567abcdefghijklmnopqrstuvwxyz"
+
return importTIDfromInteger(
+
sum(
+
[
+
b32s.find(value[i]) * (32 ** (len(value) - i - 1))
+
for i in range(len(value))
+
]
+
)
+
)
-136
src/atpasser/uri/tid.py
···
-
import datetime, random
-
-
-
class TID:
-
"""A class representing a TID (Time-based Identifier) in the AT Protocol.
-
-
TIDs are time-based identifiers used for ordering and uniquely identifying
-
records in the AT Protocol. They combine a microsecond-precision timestamp
-
with a clock identifier to ensure uniqueness even when multiple records
-
are created in the same microsecond. TIDs are sortable and provide both
-
temporal ordering and uniqueness guarantees.
-
-
Attributes:
-
timestamp (datetime.datetime): The timestamp component of the TID.
-
clockIdentifier (int): Clock identifier (0-1023) for disambiguation.
-
"""
-
-
def __init__(
-
self, time: datetime.datetime | None = None, clockIdentifier: int | None = None
-
) -> None:
-
"""Initializes a TID object with timestamp and clock identifier.
-
-
Creates a new TID with the specified timestamp and clock identifier.
-
If no timestamp is provided, uses the current time. If no clock identifier
-
is provided, generates a random value between 0-1023.
-
-
Args:
-
time (datetime.datetime, optional): The timestamp for the TID.
-
Defaults to current time if not provided.
-
clockIdentifier (int, optional): Clock identifier (0-1023) for
-
disambiguation. Defaults to random value if not provided.
-
"""
-
if time == None:
-
self.timestamp = datetime.datetime.now()
-
else:
-
self.timestamp = time
-
if clockIdentifier == None:
-
self.clockIdentifier = random.randrange(0, 1024)
-
else:
-
self.clockIdentifier = clockIdentifier
-
-
def __int__(self):
-
"""Convert the TID to its integer representation.
-
-
Combines the timestamp (in microseconds) and clock identifier into
-
a single 64-bit integer where the high bits represent the timestamp
-
and the low 10 bits represent the clock identifier.
-
-
Returns:
-
int: The integer representation of the TID.
-
"""
-
timestamp = int(self.timestamp.timestamp() * 1000000)
-
return timestamp * 1024 + self.clockIdentifier
-
-
def __str__(self):
-
"""Convert the TID to a base32-sortable string representation.
-
-
Encodes the TID as a base32 string using a custom character set that
-
maintains lexicographical sort order corresponding to temporal order.
-
This format is commonly used in the AT Protocol for compact,
-
sortable identifiers.
-
-
Returns:
-
str: The base32 string representation of the TID.
-
"""
-
integer = int(self)
-
binary = f"{integer:065b}"
-
return "".join(
-
[
-
"234567abcdefghijklmnopqrstuvwxyz"[int(binary[i : i + 5], base=2)]
-
for i in range(0, len(binary), 5)
-
]
-
)
-
-
def __eq__(self, value: object, /) -> bool:
-
"""Check if two TID objects represent the same identifier.
-
-
Args:
-
value (object): The object to compare with.
-
-
Returns:
-
bool: True if the objects represent the same TID, False otherwise.
-
"""
-
if isinstance(value, TID):
-
return int(self) == int(value)
-
else:
-
return False
-
-
-
def importTIDfromInteger(value: int | None = None):
-
"""Create a TID object from an integer representation.
-
-
Converts a 64-bit integer back into a TID object by extracting the
-
timestamp and clock identifier components. If no value is provided,
-
creates a TID for the current time.
-
-
Args:
-
value (int, optional): The integer value to convert to a TID.
-
Defaults to creating a TID for the current time if not provided.
-
-
Returns:
-
TID: The TID object created from the integer value.
-
"""
-
if value == None:
-
value = int(TID())
-
clockIdentifier = value % 1024
-
timestamp = (value >> 10) / 1000000
-
return TID(datetime.datetime.fromtimestamp(timestamp), clockIdentifier)
-
-
-
def importTIDfromBase32(value: str | None = None):
-
"""Create a TID object from a base32 string representation.
-
-
Converts a base32-encoded TID string back into a TID object by decoding
-
the string to its integer representation and then extracting the timestamp
-
and clock identifier components. If no value is provided, creates a TID
-
for the current time.
-
-
Args:
-
value (str, optional): The base32 string to convert to a TID.
-
Defaults to creating a TID for the current time if not provided.
-
-
Returns:
-
TID: The TID object created from the base32 string.
-
"""
-
if value == None:
-
value = str(TID())
-
b32s = "234567abcdefghijklmnopqrstuvwxyz"
-
return importTIDfromInteger(
-
sum(
-
[
-
b32s.find(value[i]) * (32 ** (len(value) - i - 1))
-
for i in range(len(value))
-
]
-
)
-
)
···