qerexa 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
qerexa/__init__.py ADDED
@@ -0,0 +1,22 @@
1
+ """Qerexa Python clients (preview).
2
+
3
+ ``QerexaGateMiddleware`` runs a wrapped function only after a direct,
4
+ unexpired Qerexa ``allowed`` decision. ``QerexaVCClient`` manages verifiable
5
+ credentials, federation tokens, and trust relationships.
6
+ """
7
+
8
+ from ._version import __version__
9
+ from .autogen_middleware import QerexaGateMiddleware
10
+ from .gate import ExecutionAuthorizationRequiredError, GateBlockedError, QerexaError
11
+ from .vc_client import QerexaVCClient, VCClientConfig, create_vc_client
12
+
13
+ __all__ = [
14
+ "ExecutionAuthorizationRequiredError",
15
+ "GateBlockedError",
16
+ "QerexaError",
17
+ "QerexaGateMiddleware",
18
+ "QerexaVCClient",
19
+ "VCClientConfig",
20
+ "__version__",
21
+ "create_vc_client",
22
+ ]
qerexa/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,203 @@
1
+ """Fail-closed Qerexa gate decorator for AutoGen (or any Python) functions."""
2
+
3
+ import logging
4
+ from functools import wraps
5
+ from typing import Any, Callable, Dict, Optional, Tuple
6
+
7
+ import requests
8
+
9
+ from .gate import QerexaError, enforce, read_string
10
+
11
+ logger = logging.getLogger("qerexa")
12
+
13
+
14
+ class GateEvaluationRequest:
15
+ """Gate evaluation request payload."""
16
+
17
+ def __init__(
18
+ self,
19
+ action: str,
20
+ actor: str,
21
+ reasoning: str,
22
+ scope: Optional[Dict[str, Any]] = None,
23
+ metadata: Optional[Dict[str, Any]] = None,
24
+ federation_token: Optional[str] = None,
25
+ ):
26
+ self.action = action
27
+ self.actor = actor
28
+ self.reasoning = reasoning
29
+ self.scope = scope
30
+ self.metadata = metadata or {}
31
+ self.federation_token = federation_token
32
+
33
+ def to_dict(self) -> Dict[str, Any]:
34
+ result = {
35
+ "action": self.action,
36
+ "actor": self.actor,
37
+ "reasoning": self.reasoning,
38
+ "scope": self.scope,
39
+ "metadata": self.metadata,
40
+ }
41
+ if self.federation_token:
42
+ result["federation_token"] = self.federation_token
43
+ return result
44
+
45
+
46
+ class QerexaGateMiddleware:
47
+ """
48
+ Wraps functions so they run only after a direct, unexpired Qerexa allow.
49
+
50
+ ``approval_required`` raises ExecutionAuthorizationRequiredError; blocked,
51
+ expired, malformed, and failed requests raise GateBlockedError or
52
+ QerexaError. Function arguments are never sent to Qerexa: the gate sees
53
+ the action, the function name (or your ``reasoning``), and ``metadata``.
54
+
55
+ Usage:
56
+ middleware = QerexaGateMiddleware(api_key=os.environ["QEREXA_API_KEY"])
57
+
58
+ @middleware.gate_check(action="report.export")
59
+ def export_report(report_id: str) -> str:
60
+ ...
61
+
62
+ The decorator wraps the function itself, so it also guards CrewAI or
63
+ LangChain tools whose implementation is a plain Python function.
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ api_key: str,
69
+ api_url: str = "https://api.qerexa.com/api/v1/gates/evaluate",
70
+ actor: str = "autogen-agent",
71
+ federation_token: Optional[str] = None,
72
+ timeout: float = 10.0,
73
+ ):
74
+ """
75
+ Args:
76
+ api_key: Qerexa API key (Bearer token)
77
+ api_url: Qerexa Gate API endpoint
78
+ actor: Actor identifier for gate logs
79
+ federation_token: Optional federation token for cross-org execution
80
+ timeout: Request timeout in seconds
81
+ """
82
+ self.api_key = api_key
83
+ self.api_url = api_url
84
+ self.actor = actor
85
+ self.federation_token = federation_token
86
+ self.timeout = timeout
87
+
88
+ def _evaluate(
89
+ self,
90
+ action: str,
91
+ reasoning: str,
92
+ metadata: Optional[Dict[str, Any]],
93
+ federation_token: Optional[str],
94
+ ) -> Tuple[int, Any]:
95
+ request = GateEvaluationRequest(
96
+ action=action,
97
+ actor=self.actor,
98
+ reasoning=reasoning,
99
+ metadata=metadata or {},
100
+ federation_token=federation_token or self.federation_token,
101
+ )
102
+ try:
103
+ response = requests.post(
104
+ self.api_url,
105
+ json=request.to_dict(),
106
+ headers={
107
+ "Content-Type": "application/json",
108
+ "Authorization": f"Bearer {self.api_key}",
109
+ },
110
+ timeout=self.timeout,
111
+ )
112
+ except requests.RequestException as exc:
113
+ raise QerexaError(f"Failed to reach Qerexa gate for {action}: {exc}") from exc
114
+ try:
115
+ data = response.json()
116
+ except ValueError:
117
+ data = None
118
+ return response.status_code, data
119
+
120
+ def evaluate_gate(
121
+ self,
122
+ action: str,
123
+ reasoning: str,
124
+ metadata: Optional[Dict[str, Any]] = None,
125
+ federation_token: Optional[str] = None,
126
+ ) -> Dict[str, Any]:
127
+ """
128
+ Return the raw gate response body without enforcing it.
129
+
130
+ Use gate_check() or wrap_function() to enforce the decision.
131
+
132
+ Raises:
133
+ QerexaError: If the API cannot be reached or returns a non-object body
134
+ """
135
+ _, data = self._evaluate(action, reasoning, metadata, federation_token)
136
+ if not isinstance(data, dict):
137
+ raise QerexaError(f"Qerexa gate returned an invalid response for {action}")
138
+ return data
139
+
140
+ def gate_check(
141
+ self,
142
+ action: str,
143
+ metadata: Optional[Dict[str, Any]] = None,
144
+ federation_token: Optional[str] = None,
145
+ reasoning: Optional[str] = None,
146
+ ):
147
+ """
148
+ Decorator that runs the function only after a direct, unexpired allow.
149
+
150
+ Args:
151
+ action: Action being gated (e.g., "report.export")
152
+ metadata: Custom metadata for the audit trail; do not include secrets
153
+ federation_token: Optional federation token for cross-org execution
154
+ reasoning: Why the action is needed; defaults to the function name
155
+
156
+ Example:
157
+ @middleware.gate_check(action="report.export")
158
+ def export_report(report_id: str) -> str:
159
+ ...
160
+ """
161
+
162
+ def decorator(func: Callable) -> Callable:
163
+ @wraps(func)
164
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
165
+ status, data = self._evaluate(
166
+ action,
167
+ reasoning or f"Function call: {func.__name__}",
168
+ metadata or {"function": func.__name__},
169
+ federation_token,
170
+ )
171
+ try:
172
+ enforce(action, status, data)
173
+ except QerexaError as error:
174
+ logger.warning("Qerexa gate stopped %s: %s", func.__name__, error)
175
+ raise
176
+ decision_id = read_string(data.get("decision_id"))
177
+ logger.info("Qerexa allowed %s (decision_id: %s)", func.__name__, decision_id)
178
+ return func(*args, **kwargs)
179
+
180
+ return wrapper
181
+
182
+ return decorator
183
+
184
+ def wrap_function(
185
+ self,
186
+ func: Callable,
187
+ action: str,
188
+ metadata: Optional[Dict[str, Any]] = None,
189
+ federation_token: Optional[str] = None,
190
+ reasoning: Optional[str] = None,
191
+ ) -> Callable:
192
+ """
193
+ Wrap a function with a gate check at runtime (same rules as gate_check).
194
+
195
+ Example:
196
+ gated_export = middleware.wrap_function(export_report, action="report.export")
197
+ """
198
+ return self.gate_check(
199
+ action=action,
200
+ metadata=metadata,
201
+ federation_token=federation_token,
202
+ reasoning=reasoning,
203
+ )(func)
qerexa/gate.py ADDED
@@ -0,0 +1,103 @@
1
+ """Gate decision parsing shared by the Qerexa Python helpers.
2
+
3
+ Only a direct, well-formed, unexpired ``allowed`` decision lets an action run.
4
+ Review approval is never execution authorization. This mirrors the decision
5
+ rules of the ``qerexa-langchain`` client.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import time
11
+ from datetime import datetime
12
+ from typing import Any, Optional
13
+
14
+
15
+ class QerexaError(Exception):
16
+ """Base error for Qerexa SDK failures. The guarded action did not run."""
17
+
18
+
19
+ class GateBlockedError(QerexaError):
20
+ """The gate blocked the action, or the response was not a valid allow."""
21
+
22
+ def __init__(self, action: str, reason: str, decision_id: Optional[str] = None):
23
+ super().__init__(f"Qerexa blocked {action}: {reason}")
24
+ self.action = action
25
+ self.reason = reason
26
+ self.decision_id = decision_id
27
+
28
+
29
+ class ExecutionAuthorizationRequiredError(QerexaError):
30
+ """Human review is required. Approval alone does not authorize execution."""
31
+
32
+ def __init__(self, action: str, flow_id: Optional[str], decision_id: Optional[str] = None):
33
+ super().__init__(
34
+ f"Qerexa requires review for {action} (flow {flow_id}); review approval is not "
35
+ "execution authorization, so the action was not run"
36
+ )
37
+ self.action = action
38
+ self.flow_id = flow_id
39
+ self.decision_id = decision_id
40
+
41
+
42
+ def read_string(value: Any) -> Optional[str]:
43
+ return value if isinstance(value, str) and value else None
44
+
45
+
46
+ def _timestamp(value: str) -> float:
47
+ parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
48
+ if parsed.tzinfo is None:
49
+ raise ValueError("timestamp must include a timezone")
50
+ return parsed.timestamp()
51
+
52
+
53
+ def is_direct_allow(status: int, data: Any) -> bool:
54
+ """True only for HTTP 200 with a complete, unexpired ``allowed`` decision."""
55
+ if status != 200 or not isinstance(data, dict):
56
+ return False
57
+ expires_at = read_string(data.get("expires_at"))
58
+ if not (
59
+ data.get("decision") == "allowed"
60
+ and data.get("allowed") is True
61
+ and read_string(data.get("decision_id"))
62
+ and expires_at
63
+ ):
64
+ return False
65
+ try:
66
+ return _timestamp(expires_at) > time.time()
67
+ except ValueError:
68
+ return False
69
+
70
+
71
+ def is_review_required(status: int, data: Any) -> bool:
72
+ return (
73
+ status == 202
74
+ and isinstance(data, dict)
75
+ and data.get("decision") == "approval_required"
76
+ and data.get("allowed") is False
77
+ and read_string(data.get("flow_id")) is not None
78
+ )
79
+
80
+
81
+ def block_reason(data: Any, status: int) -> str:
82
+ if isinstance(data, dict):
83
+ reason = read_string(data.get("reason")) or read_string(data.get("error"))
84
+ if reason:
85
+ return reason
86
+ gates = data.get("gates_checked")
87
+ if isinstance(gates, list):
88
+ for gate in gates:
89
+ if isinstance(gate, dict) and gate.get("status") == "failed":
90
+ detail = read_string(gate.get("reason")) or read_string(gate.get("gate"))
91
+ if detail:
92
+ return detail
93
+ return f"no valid allow decision (HTTP {status})"
94
+
95
+
96
+ def enforce(action: str, status: int, data: Any) -> None:
97
+ """Return normally only for a direct allow; otherwise raise."""
98
+ if is_direct_allow(status, data):
99
+ return
100
+ decision_id = read_string(data.get("decision_id")) if isinstance(data, dict) else None
101
+ if is_review_required(status, data):
102
+ raise ExecutionAuthorizationRequiredError(action, data["flow_id"], decision_id)
103
+ raise GateBlockedError(action, block_reason(data, status), decision_id)
qerexa/py.typed ADDED
File without changes
qerexa/vc_client.py ADDED
@@ -0,0 +1,196 @@
1
+ """
2
+ WS4 Verifiable Credentials SDK (Python)
3
+ Client for issuing and verifying VCs across organizations
4
+ """
5
+
6
+ from typing import Optional, Dict, Any
7
+ from dataclasses import dataclass
8
+ from urllib.parse import quote
9
+ import requests
10
+
11
+ from .gate import QerexaError
12
+
13
+
14
+ @dataclass
15
+ class VCClientConfig:
16
+ """Configuration for VC Client"""
17
+ org_id: str
18
+ api_key: str
19
+ base_url: str = "https://api.qerexa.com"
20
+ timeout: float = 10.0
21
+
22
+
23
+ class QerexaVCClient:
24
+ """SDK client for Qerexa Verifiable Credentials"""
25
+
26
+ def __init__(self, config: VCClientConfig):
27
+ self.org_id = config.org_id
28
+ self.api_key = config.api_key
29
+ self.base_url = config.base_url
30
+ self.timeout = config.timeout
31
+ self.session = requests.Session()
32
+ self.session.headers.update({
33
+ "x-org-id": self.org_id,
34
+ "Authorization": f"Bearer {self.api_key}",
35
+ "Content-Type": "application/json",
36
+ })
37
+
38
+ def issue_competence_vc(
39
+ self,
40
+ member_id: str,
41
+ include_reputation: bool = True,
42
+ include_hardware_key: bool = True,
43
+ ttl_days: Optional[int] = None,
44
+ ) -> Dict[str, Any]:
45
+ """Issue a competence credential for a member"""
46
+ payload = {
47
+ "member_id": member_id,
48
+ "include_reputation": include_reputation,
49
+ "include_hardware_key": include_hardware_key,
50
+ }
51
+ if ttl_days:
52
+ payload["ttl_days"] = ttl_days
53
+
54
+ return self._request("POST", "/api/v1/vc/issue/competence", payload)
55
+
56
+ def issue_risk_assessment_vc(
57
+ self,
58
+ approval_id: str,
59
+ actor_name: str,
60
+ action: str,
61
+ risk_level: str,
62
+ risk_score: int,
63
+ approved_by_member_id: str,
64
+ amount_usd: Optional[int] = None,
65
+ ttl_days: Optional[int] = None,
66
+ ) -> Dict[str, Any]:
67
+ """Issue a risk assessment credential"""
68
+ payload = {
69
+ "approval_id": approval_id,
70
+ "actor_name": actor_name,
71
+ "action": action,
72
+ "risk_level": risk_level,
73
+ "risk_score": risk_score,
74
+ "approved_by_member_id": approved_by_member_id,
75
+ }
76
+ if amount_usd:
77
+ payload["amount_usd"] = amount_usd
78
+ if ttl_days:
79
+ payload["ttl_days"] = ttl_days
80
+
81
+ return self._request("POST", "/api/v1/vc/issue/risk-assessment", payload)
82
+
83
+ def generate_federation_token(
84
+ self,
85
+ recipient_org_id: str,
86
+ member_id: str,
87
+ domains: Optional[list] = None,
88
+ ttl_hours: Optional[int] = None,
89
+ ) -> Dict[str, Any]:
90
+ """Generate a federation token for cross-org approval"""
91
+ payload = {
92
+ "recipient_org_id": recipient_org_id,
93
+ "member_id": member_id,
94
+ }
95
+ if not domains:
96
+ raise ValueError("domains must contain at least one approved domain")
97
+ payload["domains"] = domains
98
+ if ttl_hours:
99
+ payload["ttl_hours"] = ttl_hours
100
+
101
+ return self._request("POST", "/api/v1/federation/tokens", payload)
102
+
103
+ def verify_federation_token(self, token: str) -> Dict[str, Any]:
104
+ """Verify a federation token"""
105
+ payload = {"token": token}
106
+ return self._request("POST", "/api/v1/federation/verify", payload)
107
+
108
+ def create_trust_relationship(
109
+ self,
110
+ trusted_org_id: str,
111
+ trust_level: str,
112
+ approved_domains: Optional[list] = None,
113
+ expires_at: Optional[str] = None,
114
+ ) -> Dict[str, Any]:
115
+ """Create a trust relationship with another organization"""
116
+ payload = {
117
+ "trusted_org_id": trusted_org_id,
118
+ "trust_level": trust_level,
119
+ }
120
+ if not approved_domains:
121
+ raise ValueError("approved_domains must contain at least one domain")
122
+ payload["approved_domains"] = approved_domains
123
+ if expires_at:
124
+ payload["expires_at"] = expires_at
125
+
126
+ return self._request("POST", "/api/v1/trust-relationships", payload)
127
+
128
+ def list_trust_relationships(
129
+ self,
130
+ limit: int = 50,
131
+ offset: int = 0,
132
+ ) -> Dict[str, Any]:
133
+ """List trust relationships"""
134
+ params = {"limit": limit, "offset": offset}
135
+ return self._request("GET", "/api/v1/trust-relationships", params=params)
136
+
137
+ def list_federation_tokens(
138
+ self,
139
+ limit: int = 50,
140
+ offset: int = 0,
141
+ ) -> Dict[str, Any]:
142
+ """List federation tokens received"""
143
+ params = {"limit": limit, "offset": offset}
144
+ return self._request("GET", "/api/v1/federation/tokens", params=params)
145
+
146
+ def revoke_federation_token(
147
+ self, token_jti: str, reason: Optional[str] = None
148
+ ) -> Dict[str, Any]:
149
+ payload = {"reason": reason} if reason else {}
150
+ return self._request(
151
+ "POST", f"/api/v1/federation/tokens/{quote(token_jti, safe='')}/revoke", payload
152
+ )
153
+
154
+ def revoke_credential(
155
+ self, credential_id: str, reason: Optional[str] = None
156
+ ) -> Dict[str, Any]:
157
+ payload = {"reason": reason} if reason else {}
158
+ return self._request(
159
+ "POST", f"/api/v1/vc/{quote(credential_id, safe='')}/revoke", payload
160
+ )
161
+
162
+ def rotate_signing_key(self) -> Dict[str, Any]:
163
+ return self._request("POST", "/api/v1/vc/keys/rotate", {})
164
+
165
+ def revoke_signing_key(self, key_id: str) -> Dict[str, Any]:
166
+ return self._request(
167
+ "POST", f"/api/v1/vc/keys/{quote(key_id, safe='')}/revoke", {}
168
+ )
169
+
170
+ def _request(
171
+ self,
172
+ method: str,
173
+ path: str,
174
+ payload: Optional[Dict[str, Any]] = None,
175
+ params: Optional[Dict[str, Any]] = None,
176
+ ) -> Dict[str, Any]:
177
+ """Make authenticated API request"""
178
+ url = f"{self.base_url}{path}"
179
+
180
+ try:
181
+ if method == "GET":
182
+ response = self.session.get(url, params=params, timeout=self.timeout)
183
+ elif method == "POST":
184
+ response = self.session.post(url, json=payload, timeout=self.timeout)
185
+ else:
186
+ raise ValueError(f"Unsupported HTTP method: {method}")
187
+
188
+ response.raise_for_status()
189
+ return response.json()
190
+ except requests.exceptions.RequestException as e:
191
+ raise QerexaError(f"API Error: {str(e)}") from e
192
+
193
+
194
+ def create_vc_client(config: VCClientConfig) -> QerexaVCClient:
195
+ """Factory function for quick SDK setup"""
196
+ return QerexaVCClient(config)
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: qerexa
3
+ Version: 0.1.0
4
+ Summary: Qerexa clients for verifiable credentials and fail-closed gate checks (preview).
5
+ Project-URL: Homepage, https://qerexa.com
6
+ Project-URL: Documentation, https://qerexa.com/docs
7
+ Project-URL: Repository, https://github.com/rodriguew/Trust-Rails/tree/main/packages/sdk-python
8
+ Project-URL: Issues, https://github.com/rodriguew/Trust-Rails/issues
9
+ Author: Qerexa
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: ai-agents,autogen,governance,trust,verifiable-credentials
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Security
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: requests<3,>=2.28
26
+ Provides-Extra: dev
27
+ Requires-Dist: build<2,>=1; extra == 'dev'
28
+ Requires-Dist: pytest>=7; extra == 'dev'
29
+ Requires-Dist: ruff>=0.1; extra == 'dev'
30
+ Requires-Dist: twine<7,>=5; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # qerexa (Python, preview)
34
+
35
+ Python clients for Qerexa:
36
+
37
+ - `QerexaGateMiddleware` — decorator that runs a function only after a
38
+ direct, unexpired Qerexa `allowed` decision. Works for AutoGen functions and
39
+ any plain Python function, including CrewAI or LangChain tool functions.
40
+ - `QerexaVCClient` — issue and verify verifiable credentials, federation
41
+ tokens, and trust relationships.
42
+
43
+ ## Install
44
+
45
+ ```bash
46
+ pip install qerexa
47
+ ```
48
+
49
+ ## Gate a function
50
+
51
+ ```python
52
+ import os
53
+ from qerexa import (
54
+ ExecutionAuthorizationRequiredError,
55
+ GateBlockedError,
56
+ QerexaGateMiddleware,
57
+ )
58
+
59
+ gate = QerexaGateMiddleware(api_key=os.environ["QEREXA_API_KEY"])
60
+
61
+ @gate.gate_check(action="report.export", reasoning="Monthly compliance export")
62
+ def export_report(report_id: str) -> str:
63
+ ...
64
+
65
+ try:
66
+ export_report("rpt_123")
67
+ except ExecutionAuthorizationRequiredError as e:
68
+ # Human review was requested (flow e.flow_id). Approval of that review does
69
+ # not authorize this call; stop here or use an action-bound execution flow.
70
+ ...
71
+ except GateBlockedError as e:
72
+ ...
73
+ ```
74
+
75
+ The wrapped function runs only for HTTP 200 with `decision: "allowed"`,
76
+ `allowed: true`, a `decision_id`, and a future, timezone-aware `expires_at`.
77
+ Everything else raises and the function does not run: `blocked`,
78
+ `approval_required`, expired or malformed allows, non-JSON responses, and
79
+ network errors (`QerexaError`, the base of both errors above).
80
+
81
+ Function arguments are never sent. Qerexa receives the action, the actor, the
82
+ `reasoning` you pass (default: the function name), and `metadata`. Do not put
83
+ credentials or customer data in either.
84
+
85
+ For CrewAI, decorate the function that implements the tool before handing it
86
+ to CrewAI; the gate then runs on every call regardless of CrewAI callbacks.
87
+
88
+ ## Verifiable credentials
89
+
90
+ ```python
91
+ from qerexa import VCClientConfig, create_vc_client
92
+
93
+ vc = create_vc_client(VCClientConfig(org_id="org_123", api_key=os.environ["QEREXA_API_KEY"]))
94
+ relationships = vc.list_trust_relationships(limit=10)
95
+ ```
96
+
97
+ Requests time out after 10 seconds by default (`VCClientConfig(timeout=...)`).
98
+
99
+ ## Preview limits
100
+
101
+ This package is not a regulated execution adapter. It does not verify
102
+ receipts, poll or resume review flows, or bind an approval to a specific
103
+ execution. Use the documented API proof flow and an independently reviewed
104
+ execution contract for high-impact actions.
105
+
106
+ ## Development
107
+
108
+ ```bash
109
+ python -m pip install -e "./packages/sdk-python[dev]"
110
+ ruff check packages/sdk-python
111
+ pytest packages/sdk-python
112
+ ```
113
+
114
+ Releases follow [the SDK release runbook](../../apps/sdk/SDK_RELEASE.md).
@@ -0,0 +1,10 @@
1
+ qerexa/__init__.py,sha256=imN2Ee8Ds7cb_hSx-uXqABpvITGH6sH8-0IgiRfJBvs,706
2
+ qerexa/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
3
+ qerexa/autogen_middleware.py,sha256=j6Q4SjWIrU2d3SbjQXOPjyT4U6cYqRnTT9jarOedyi8,6759
4
+ qerexa/gate.py,sha256=QDD6vtvb4-mWMXo0l_SelA1H2HdmF7IZxOE_1Uzkex0,3658
5
+ qerexa/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ qerexa/vc_client.py,sha256=6cDxkUgIyIZ8wWhQLRH5DdT5pGSdXNj5EzfNw5yoQtg,6477
7
+ qerexa-0.1.0.dist-info/METADATA,sha256=2hGygqCFiSgbz1XkMhD-KGf_WGsDV9lYFudic539ccE,3942
8
+ qerexa-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ qerexa-0.1.0.dist-info/licenses/LICENSE,sha256=0YvlA1nYl07sp4IyrhL9TGihfDXD8DGDErrrub5ZvZI,1068
10
+ qerexa-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Qerexa Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.