Implement Phase 9: client libraries (Go, Rust, Lisp, Python)
- clients/README.md: canonical API surface and error type reference - clients/testdata/: shared JSON response fixtures - clients/go/: mciasgoclient package; net/http + TLS 1.2+; sync.RWMutex token state; DisallowUnknownFields on all decoders; 25 tests pass - clients/rust/: async mcias-client crate; reqwest+rustls (no OpenSSL); thiserror MciasError enum; Arc<RwLock> token state; 22+1 tests pass; cargo clippy -D warnings clean - clients/lisp/: ASDF mcias-client; dexador HTTP, yason JSON; mcias-error condition hierarchy; Hunchentoot mock-dispatcher; 37 fiveam checks pass on SBCL 2.6.1; yason boolean normalisation in validate-token - clients/python/: mcias_client package (Python 3.11+); httpx sync; py.typed; dataclasses; 32 pytest tests; mypy --strict + ruff clean - test/mock/mockserver.go: in-memory mock server for Go client tests - ARCHITECTURE.md §19: updated per-language notes to match implementation - PROGRESS.md: Phase 9 marked complete - .gitignore: exclude clients/rust/target/, python .venv, .pytest_cache, .fasl files Security: token never logged or exposed in error messages in any library; TLS enforced in all four languages; token stored under lock/mutex/RwLock
This commit is contained in:
76
clients/python/mcias_client/_models.py
Normal file
76
clients/python/mcias_client/_models.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""Data models for MCIAS API responses."""
|
||||
from dataclasses import dataclass, field
|
||||
from typing import cast
|
||||
|
||||
|
||||
@dataclass
|
||||
class Account:
|
||||
"""A user or service account."""
|
||||
id: str
|
||||
username: str
|
||||
account_type: str
|
||||
status: str
|
||||
created_at: str
|
||||
updated_at: str
|
||||
totp_enabled: bool = False
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict[str, object]) -> "Account":
|
||||
return cls(
|
||||
id=str(d["id"]),
|
||||
username=str(d["username"]),
|
||||
account_type=str(d["account_type"]),
|
||||
status=str(d["status"]),
|
||||
created_at=str(d["created_at"]),
|
||||
updated_at=str(d["updated_at"]),
|
||||
totp_enabled=bool(d.get("totp_enabled", False)),
|
||||
)
|
||||
@dataclass
|
||||
class PublicKey:
|
||||
"""Ed25519 public key in JWK format."""
|
||||
kty: str
|
||||
crv: str
|
||||
x: str
|
||||
use: str = ""
|
||||
alg: str = ""
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict[str, object]) -> "PublicKey":
|
||||
return cls(
|
||||
kty=str(d["kty"]),
|
||||
crv=str(d["crv"]),
|
||||
x=str(d["x"]),
|
||||
use=str(d.get("use", "")),
|
||||
alg=str(d.get("alg", "")),
|
||||
)
|
||||
@dataclass
|
||||
class TokenClaims:
|
||||
"""Claims from a validated token."""
|
||||
valid: bool
|
||||
sub: str = ""
|
||||
roles: list[str] = field(default_factory=list)
|
||||
expires_at: str = ""
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict[str, object]) -> "TokenClaims":
|
||||
roles_raw = cast(list[object], d.get("roles") or [])
|
||||
return cls(
|
||||
valid=bool(d.get("valid", False)),
|
||||
sub=str(d.get("sub", "")),
|
||||
roles=[str(r) for r in roles_raw],
|
||||
expires_at=str(d.get("expires_at", "")),
|
||||
)
|
||||
@dataclass
|
||||
class PGCreds:
|
||||
"""Postgres connection credentials."""
|
||||
host: str
|
||||
port: int
|
||||
database: str
|
||||
username: str
|
||||
password: str
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict[str, object]) -> "PGCreds":
|
||||
return cls(
|
||||
host=str(d["host"]),
|
||||
port=int(cast(int, d["port"])),
|
||||
database=str(d["database"]),
|
||||
username=str(d["username"]),
|
||||
password=str(d["password"]),
|
||||
)
|
||||
Reference in New Issue
Block a user