Add proof-measure client + Gitea PyPI publish; bump to 0.4.0
All checks were successful
Publish to PyPI Registry / publish (release) Successful in 1m14s

proof-measure is a separate, public, unauthenticated Dragonchain service. Adds:
- UnauthenticatedClient: HMAC-free transport mirroring Client (session
  injection, allow_redirects=False, from_dict decoding).
- ProofMeasureClient: get_security / report / health; default base URL
  https://proof-measure.dragonchain.com. Standalone (ProofMeasureClient()) and
  via DragonchainSDK.proof_measure.
- Proof-measure dataclass models (decimals as strings, timestamps as int).
- .gitea/workflows/publish.yml: build + twine upload to the Gitea PyPI registry
  on release (the SDK had no publish workflow before).

Also fixes 3 pre-existing failing tests: _FakeSession.request didn't accept the
allow_redirects kwarg the client now passes (added by the prior redirect change),
so the suite was red at HEAD.
This commit is contained in:
2026-06-04 13:53:16 -04:00
parent 5410b283e2
commit 7b1e9f6309
9 changed files with 581 additions and 4 deletions

View File

@@ -492,3 +492,169 @@ class ListResponse:
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "ListResponse":
return cls(items=d.get("items") or [], total_count=d.get("total_count", 0))
# --------------------------------------------------------------------------- #
# Proof Measure (measured immutability / "securedBy")
# --------------------------------------------------------------------------- #
#
# Decimal-valued fields are kept as strings (full precision); timestamps are int
# unix seconds. JSON keys are camelCase, preserved verbatim from the service.
@dataclass
class RawMeasure:
"""A network's native cumulative security measure: cumulative hashes for
PoW, stake-seconds for PoS."""
value: str = "" # human-scaled mantissa, e.g. "484.44"
unit: str = "" # e.g. "Zettahashes", "ETH·s"
base: str = "" # unscaled base amount (hashes or stake-seconds)
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "RawMeasure":
return cls(
value=d.get("value", ""),
unit=d.get("unit", ""),
base=d.get("base", ""),
)
@dataclass
class SecurityResult:
"""Security a single network accrued for a window/anchor, as the raw native
measure AND a USD valuation, plus a normalized 0..1 score."""
network: str = ""
consensus: str = "" # "pow" | "pos"
raw: RawMeasure = field(default_factory=RawMeasure)
value_usd: str = ""
value_usd_formatted: str = ""
label: str = ""
normalized_score: str = ""
since: int = 0 # window/anchor start (unix seconds)
as_of: int = 0 # latest sample time (unix seconds)
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "SecurityResult":
return cls(
network=d.get("network", ""),
consensus=d.get("consensus", ""),
raw=RawMeasure.from_dict(d.get("raw") or {}),
value_usd=d.get("valueUsd", ""),
value_usd_formatted=d.get("valueUsdFormatted", ""),
label=d.get("label", ""),
normalized_score=d.get("normalizedScore", ""),
since=d.get("since", 0),
as_of=d.get("asOf", 0),
)
@dataclass
class ReportAnchorInput:
"""One anchor supplied in a report request. Provide either ``network``
("BTC"/"ETH") or the public-chain numeric ``chain_id``; network wins."""
tx_hash: str
timestamp: int # anchor time, unix seconds
network: Optional[str] = None
chain_id: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
d: Dict[str, Any] = {"txHash": self.tx_hash, "timestamp": self.timestamp}
if self.network:
d["network"] = self.network
if self.chain_id:
d["chainId"] = self.chain_id
return d
@dataclass
class ReportRequest:
"""Body of ``ProofMeasureClient.report``: a transaction's interchain anchors."""
transaction_id: str
prime_id: str
block_id: str
anchors: List[ReportAnchorInput] = field(default_factory=list)
def to_dict(self) -> Dict[str, Any]:
return {
"transactionId": self.transaction_id,
"primeId": self.prime_id,
"blockId": self.block_id,
"anchors": [a.to_dict() for a in self.anchors],
}
@dataclass
class AnchorSecurity:
"""One interchain anchor with the security its public network has accumulated
since the anchor was placed."""
network: str = ""
anchor_timestamp: int = 0 # unix seconds
anchor_tx_hash: str = ""
security: SecurityResult = field(default_factory=SecurityResult)
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "AnchorSecurity":
return cls(
network=d.get("network", ""),
anchor_timestamp=d.get("anchorTimestamp", 0),
anchor_tx_hash=d.get("anchorTxHash", ""),
security=SecurityResult.from_dict(d.get("security") or {}),
)
@dataclass
class HashPower:
"""Combined raw hash power across a report's PoW anchors."""
value: str = ""
units: str = ""
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "HashPower":
return cls(value=d.get("value", ""), units=d.get("units", ""))
@dataclass
class TransactionReport:
"""Per-transaction "securedBy" report: every public-chain anchor covering the
transaction's block with both raw and USD security, plus combined totals.
``hash_power`` is None when there are no PoW anchors."""
transaction_id: str = ""
prime_id: str = ""
block_id: str = ""
anchors: List[AnchorSecurity] = field(default_factory=list)
total_value_usd: str = ""
total_value_usd_formatted: str = ""
hash_power: Optional[HashPower] = None
total_normalized_score: str = ""
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "TransactionReport":
hp = d.get("hashPower")
return cls(
transaction_id=d.get("transactionId", ""),
prime_id=d.get("primeId", ""),
block_id=d.get("blockId", ""),
anchors=[AnchorSecurity.from_dict(a) for a in (d.get("anchors") or [])],
total_value_usd=d.get("totalValueUsd", ""),
total_value_usd_formatted=d.get("totalValueUsdFormatted", ""),
hash_power=HashPower.from_dict(hp) if hp else None,
total_normalized_score=d.get("totalNormalizedScore", ""),
)
@dataclass
class HealthResponse:
"""proof-measure liveness payload."""
status: str = ""
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "HealthResponse":
return cls(status=d.get("status", ""))