-
Notifications
You must be signed in to change notification settings - Fork 0
Add Python SDK core with wrap and signed outbox #51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
9df7aaa
Add Python SDK core with wrap and signed outbox
23eefa8
fix(sdk): atomic outbox write and preserve wrapped errors
bf5724a
fix(sdk): start export thread before releasing enqueue lock
7c5187b
fix(sdk): prune export threads and close outbox on reconfigure
ed8d91b
fix(sdk): lazy default data dir and secure keypair create
36b96be
fix(sdk): allow outbox use from worker threads
2daeabc
fix(sdk): serialize chain reserve, sign, and persist
932b68a
fix(sdk): safe reconfigure, async cancel, drop dead code
764c90d
fix(keys): load existing keypair on O_EXCL race
c0cf402
fix(keys): retry load while peer finishes keypair write
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,23 @@ | ||
| """IntentProof Python SDK.""" | ||
|
|
||
| from intentproof import client | ||
| from intentproof.exporter import ingest_request_headers, post_execution_event | ||
| from intentproof.instrumentation import ( | ||
| push_subject_mapping, | ||
| run_with_correlation_id, | ||
| wrap, | ||
| ) | ||
| from intentproof.client import flush | ||
|
|
||
| __all__ = ["ingest_request_headers", "post_execution_event"] | ||
| configure = client.configure | ||
|
|
||
| __all__ = [ | ||
| "client", | ||
| "configure", | ||
| "flush", | ||
| "ingest_request_headers", | ||
| "post_execution_event", | ||
| "push_subject_mapping", | ||
| "run_with_correlation_id", | ||
| "wrap", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| """SDK configuration and shared runtime state.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import os | ||
| from pathlib import Path | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey | ||
|
|
||
| from intentproof.http_exporter import HttpExporter, resolve_ingest_url | ||
| from intentproof.keys import ensure_dir, load_or_create_keypair | ||
| from intentproof.outbox import Outbox | ||
| from intentproof.signing import load_private_key | ||
|
|
||
| if TYPE_CHECKING: | ||
| from intentproof.signing import Ed25519PublicKey | ||
|
|
||
| SDK_VERSION = "python@0.1.0" | ||
|
|
||
|
|
||
| def default_data_dir() -> Path: | ||
| """Default SDK data directory (resolved lazily for container imports).""" | ||
| return Path.home() / ".intentproof" / "sdk-python" | ||
|
|
||
| _instance_private_key: Ed25519PrivateKey | None = None | ||
| _instance_id: str | None = None | ||
| _tenant_id: str = "tnt_default" | ||
| _outbox: Outbox | None = None | ||
| _exporter: HttpExporter | None = None | ||
| _data_dir: Path | None = None | ||
|
|
||
|
|
||
| def configure( | ||
| *, | ||
| db_path: str | None = None, | ||
| tenant_id: str | None = None, | ||
| data_dir: str | Path | None = None, | ||
| ingest_url: str | None = None, | ||
| ) -> None: | ||
| global _instance_private_key, _instance_id, _tenant_id, _outbox, _exporter, _data_dir | ||
|
|
||
| prev_exporter = _exporter | ||
| prev_outbox = _outbox | ||
|
|
||
| new_data_dir = Path(data_dir) if data_dir else default_data_dir() | ||
| ensure_dir(new_data_dir) | ||
|
|
||
| kp = load_or_create_keypair(new_data_dir) | ||
| new_private_key = load_private_key(kp.private_key) | ||
| new_instance_id = kp.instance_id | ||
| new_tenant_id = ( | ||
| tenant_id | ||
| or os.environ.get("INTENTPROOF_TENANT_ID", "").strip() | ||
| or "tnt_default" | ||
| ) | ||
|
|
||
| resolved_db = db_path or os.environ.get("INTENTPROOF_OUTBOX_PATH", "").strip() | ||
| if not resolved_db: | ||
| resolved_db = str(new_data_dir / "outbox.db") | ||
| new_outbox = Outbox(resolved_db) | ||
|
|
||
| ingest = resolve_ingest_url(ingest_url) | ||
| new_exporter = HttpExporter(ingest) if ingest else None | ||
|
|
||
| if prev_exporter is not None: | ||
| prev_exporter.flush() | ||
| if prev_outbox is not None: | ||
| prev_outbox.close() | ||
|
|
||
| _data_dir = new_data_dir | ||
| _instance_private_key = new_private_key | ||
| _instance_id = new_instance_id | ||
| _tenant_id = new_tenant_id | ||
| _outbox = new_outbox | ||
| _exporter = new_exporter | ||
|
|
||
|
|
||
| def flush() -> None: | ||
| if _exporter is not None: | ||
| _exporter.flush() | ||
|
|
||
|
|
||
| def get_outbox() -> Outbox: | ||
| if _outbox is None: | ||
| raise RuntimeError("SDK not configured: call configure() before use") | ||
| return _outbox | ||
|
|
||
|
|
||
| def get_instance_id() -> str: | ||
| if _instance_id is None: | ||
| raise RuntimeError("SDK not configured: call configure() before get_instance_id()") | ||
| return _instance_id | ||
|
|
||
|
|
||
| def get_private_key() -> Ed25519PrivateKey: | ||
| if _instance_private_key is None: | ||
| raise RuntimeError("SDK not configured: call configure() before signing") | ||
| return _instance_private_key | ||
|
|
||
|
|
||
| def get_tenant_id() -> str: | ||
| return _tenant_id | ||
|
|
||
|
|
||
| def get_exporter() -> HttpExporter | None: | ||
| return _exporter | ||
|
|
||
|
|
||
| def get_public_key() -> "Ed25519PublicKey": | ||
| return get_private_key().public_key() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| """Background HTTP export of signed events to ingest.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| import os | ||
| import threading | ||
| from typing import Any, Mapping | ||
|
|
||
| from intentproof.exporter import ingest_request_headers, post_execution_event | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| DEFAULT_LOCAL_INGEST_URL = "http://127.0.0.1:9787/v1/events" | ||
|
|
||
|
|
||
| def resolve_ingest_url(explicit: str | None = None) -> str | None: | ||
| raw = (explicit or os.environ.get("INTENTPROOF_INGEST_URL", "")).strip() | ||
| if raw: | ||
| return _normalize_ingest_url(raw) | ||
| if os.environ.get("INTENTPROOF_USE_LOCAL_INGEST", "").strip() == "1": | ||
| return DEFAULT_LOCAL_INGEST_URL | ||
| return None | ||
|
|
||
|
|
||
| def _normalize_ingest_url(raw: str) -> str: | ||
| trimmed = raw.strip().rstrip("/") | ||
| if trimmed.endswith("/v1/events"): | ||
| return trimmed | ||
| return f"{trimmed}/v1/events" | ||
|
|
||
|
|
||
| class HttpExporter: | ||
| def __init__(self, ingest_url: str) -> None: | ||
| self._ingest_url = ingest_url | ||
| self._lock = threading.Lock() | ||
| self._pending: list[threading.Thread] = [] | ||
|
|
||
| @property | ||
| def ingest_url(self) -> str: | ||
| return self._ingest_url | ||
|
|
||
| def _prune_finished_threads(self) -> None: | ||
| self._pending = [t for t in self._pending if t.is_alive()] | ||
|
|
||
| def enqueue(self, event: Mapping[str, Any]) -> None: | ||
| thread = threading.Thread( | ||
| target=self._export_one, | ||
| args=(dict(event),), | ||
| daemon=True, | ||
| ) | ||
| with self._lock: | ||
| self._prune_finished_threads() | ||
| self._pending.append(thread) | ||
| thread.start() | ||
|
|
||
| def _export_one(self, event: dict[str, Any]) -> None: | ||
| try: | ||
| post_execution_event(self._ingest_url, event) | ||
| except Exception as exc: | ||
| logger.warning("[intentproof] ingest export failed: %s", exc) | ||
|
|
||
| def flush(self) -> None: | ||
| with self._lock: | ||
| threads = list(self._pending) | ||
| self._pending.clear() | ||
| for thread in threads: | ||
| if thread.is_alive(): | ||
| thread.join() | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.