pushary 1.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.
pushary/__init__.py ADDED
@@ -0,0 +1,38 @@
1
+ """Pushary Python SDK.
2
+
3
+ Human-in-the-loop decisions for AI products: create a decision, ask a specific
4
+ end-user to approve it, and resume on their answer via webhook or poll. Zero
5
+ runtime dependencies, stdlib only.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from ._types import (
11
+ CancelDecisionResponse,
12
+ CreateDecisionResponse,
13
+ Decision,
14
+ DecisionStatus,
15
+ DecisionType,
16
+ WebhookSecretResponse,
17
+ )
18
+ from .client import PusharyServer
19
+ from .decisions import DecisionsResource
20
+ from .errors import PusharyError
21
+ from .webhook import SIGNATURE_HEADER, verify_webhook_signature
22
+
23
+ __version__ = "1.1.0"
24
+
25
+ __all__ = [
26
+ "PusharyServer",
27
+ "DecisionsResource",
28
+ "PusharyError",
29
+ "verify_webhook_signature",
30
+ "SIGNATURE_HEADER",
31
+ "Decision",
32
+ "DecisionType",
33
+ "DecisionStatus",
34
+ "CreateDecisionResponse",
35
+ "CancelDecisionResponse",
36
+ "WebhookSecretResponse",
37
+ "__version__",
38
+ ]
pushary/_types.py ADDED
@@ -0,0 +1,75 @@
1
+ """Typed shapes for the decision payloads.
2
+
3
+ The client methods return plain ``dict`` objects parsed straight from the API
4
+ response. These ``TypedDict`` definitions describe the keys those dicts carry so
5
+ editors and type checkers can help, without forcing any runtime conversion.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import List, Optional
11
+
12
+ try: # TypedDict and Literal live in typing on Python 3.9+
13
+ from typing import Literal, TypedDict
14
+ except ImportError: # pragma: no cover - defensive for very old runtimes
15
+ from typing_extensions import Literal, TypedDict # type: ignore
16
+
17
+
18
+ DecisionType = Literal["confirm", "select", "input"]
19
+ """How the human answers: a yes/no confirm, a fixed set of options, or free text."""
20
+
21
+ DecisionStatus = Literal["pending", "answered", "expired", "cancelled"]
22
+ """Lifecycle state of a decision."""
23
+
24
+
25
+ class CreateDecisionResponse(TypedDict, total=False):
26
+ """Shape returned by ``decisions.create``.
27
+
28
+ Without ``wait`` the API returns immediately with ``status`` "pending" and
29
+ ``answered`` False. With ``wait`` it may already carry the resolved
30
+ ``value`` when the human answered inside the window, or a ``hint`` telling
31
+ you to poll ``pollUrl`` if it did not.
32
+ """
33
+
34
+ decisionId: str
35
+ question: str
36
+ type: DecisionType
37
+ decisionPageUrl: str
38
+ pollUrl: str
39
+ expiresInSeconds: int
40
+ status: DecisionStatus
41
+ answered: bool
42
+ value: Optional[str]
43
+ hint: str
44
+
45
+
46
+ class Decision(TypedDict, total=False):
47
+ """Shape returned by ``decisions.get``, the durable view of one decision."""
48
+
49
+ decisionId: str
50
+ status: DecisionStatus
51
+ answered: bool
52
+ value: Optional[str]
53
+ type: DecisionType
54
+ question: str
55
+ options: Optional[List[str]]
56
+ externalId: Optional[str]
57
+ createdAt: str
58
+ answeredAt: Optional[str]
59
+ expiresAt: Optional[str]
60
+
61
+
62
+ class CancelDecisionResponse(TypedDict, total=False):
63
+ """Shape returned by ``decisions.cancel``."""
64
+
65
+ decisionId: str
66
+ cancelled: bool
67
+ status: DecisionStatus
68
+
69
+
70
+ class WebhookSecretResponse(TypedDict, total=False):
71
+ """Shape returned by ``decisions.get_webhook_secret`` and
72
+ ``decisions.rotate_webhook_secret``.
73
+ """
74
+
75
+ secret: str
pushary/client.py ADDED
@@ -0,0 +1,116 @@
1
+ """The Pushary server client.
2
+
3
+ Zero dependencies: the transport is stdlib ``urllib.request`` and JSON is parsed
4
+ with stdlib ``json``. Construct once with your full API key and reuse the frozen
5
+ instance across calls.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import urllib.error
12
+ import urllib.request
13
+ from typing import Any, Dict, Optional
14
+ from urllib.parse import urlencode
15
+
16
+ from .decisions import DecisionsResource
17
+ from .errors import PusharyError
18
+
19
+ DEFAULT_BASE_URL = "https://pushary.com/api/v1/server"
20
+
21
+ # The create call and get(wait=N) can hold the connection open while a human
22
+ # answers. The server caps that wait around 55s, so the socket timeout sits a
23
+ # little above it to leave room for the round trip without hanging forever.
24
+ DEFAULT_TIMEOUT_SECONDS = 65.0
25
+
26
+ _SETTINGS_URL = "https://pushary.com/dashboard/settings"
27
+
28
+
29
+ class PusharyServer:
30
+ """Client for the Pushary human-in-the-loop decisions API.
31
+
32
+ Args:
33
+ api_key: The full API key in the form ``pk_xxx.sk_xxx``.
34
+ base_url: Override the API base URL. Defaults to the public endpoint.
35
+
36
+ Raises:
37
+ ValueError: If the API key is empty or is missing the secret half.
38
+ """
39
+
40
+ def __init__(self, api_key: str, base_url: Optional[str] = None) -> None:
41
+ self._validate_api_key(api_key)
42
+
43
+ self._base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
44
+ self._headers: Dict[str, str] = {
45
+ "Content-Type": "application/json",
46
+ "Authorization": f"Bearer {api_key}",
47
+ }
48
+ self._timeout = DEFAULT_TIMEOUT_SECONDS
49
+ self.decisions = DecisionsResource(self._request)
50
+
51
+ @staticmethod
52
+ def _validate_api_key(api_key: str) -> None:
53
+ if not api_key:
54
+ raise ValueError(
55
+ f"API key is required. Get your API key from {_SETTINGS_URL}"
56
+ )
57
+ if "." not in api_key:
58
+ raise ValueError(
59
+ "Invalid API key format. Use the full API key (pk_xxx.sk_xxx). "
60
+ f"Get your API key from {_SETTINGS_URL}"
61
+ )
62
+
63
+ def _request(
64
+ self,
65
+ method: str,
66
+ path: str,
67
+ *,
68
+ body: Optional[Dict[str, Any]] = None,
69
+ params: Optional[Dict[str, Any]] = None,
70
+ ) -> Dict[str, Any]:
71
+ url = self._base_url + path
72
+ if params:
73
+ query = {k: v for k, v in params.items() if v is not None}
74
+ if query:
75
+ url = f"{url}?{urlencode(query)}"
76
+
77
+ data = json.dumps(body).encode("utf-8") if body is not None else None
78
+ request = urllib.request.Request(
79
+ url, data=data, method=method, headers=dict(self._headers)
80
+ )
81
+
82
+ try:
83
+ with urllib.request.urlopen(request, timeout=self._timeout) as response:
84
+ raw = response.read()
85
+ except urllib.error.HTTPError as exc:
86
+ raise self._error_from_http(exc) from None
87
+ except urllib.error.URLError as exc:
88
+ raise PusharyError(f"Request to Pushary failed: {exc.reason}") from exc
89
+
90
+ return self._parse_json(raw)
91
+
92
+ @staticmethod
93
+ def _error_from_http(exc: urllib.error.HTTPError) -> PusharyError:
94
+ status = exc.code
95
+ message: Optional[str] = None
96
+ try:
97
+ parsed = json.loads(exc.read().decode("utf-8"))
98
+ if isinstance(parsed, dict):
99
+ error = parsed.get("error")
100
+ fallback = parsed.get("message")
101
+ message = error if isinstance(error, str) else None
102
+ if message is None and isinstance(fallback, str):
103
+ message = fallback
104
+ except Exception:
105
+ message = None
106
+ return PusharyError(message or f"HTTP {status}", status=status)
107
+
108
+ @staticmethod
109
+ def _parse_json(raw: bytes) -> Dict[str, Any]:
110
+ if not raw:
111
+ return {}
112
+ try:
113
+ parsed = json.loads(raw.decode("utf-8"))
114
+ except (ValueError, UnicodeDecodeError):
115
+ return {}
116
+ return parsed if isinstance(parsed, dict) else {"data": parsed}
pushary/decisions.py ADDED
@@ -0,0 +1,120 @@
1
+ """Decisions resource: create, poll, answer, and cancel human-in-the-loop
2
+ decisions, plus manage the webhook secret.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any, Callable, Dict, List, Optional
8
+ from urllib.parse import quote
9
+
10
+ RequestFn = Callable[..., Dict[str, Any]]
11
+
12
+ class DecisionsResource:
13
+ """Ask a specific end-user to approve something, then resume on their answer.
14
+
15
+ Obtain an instance from ``PusharyServer(...).decisions``. Every method
16
+ returns the parsed JSON body as a ``dict`` and raises ``PusharyError`` on a
17
+ non-2xx response.
18
+ """
19
+
20
+ def __init__(self, request: RequestFn) -> None:
21
+ self._request = request
22
+
23
+ def create(
24
+ self,
25
+ question: str,
26
+ *,
27
+ type: str = "confirm",
28
+ options: Optional[List[str]] = None,
29
+ external_id: Optional[str] = None,
30
+ callback_url: Optional[str] = None,
31
+ agent_name: Optional[str] = None,
32
+ context: Optional[str] = None,
33
+ placeholder: Optional[str] = None,
34
+ expires_in_seconds: Optional[int] = None,
35
+ wait: Optional[bool] = None,
36
+ timeout_seconds: Optional[int] = None,
37
+ idempotency_key: Optional[str] = None,
38
+ powered_by: Optional[bool] = None,
39
+ ) -> Dict[str, Any]:
40
+ """Create a decision and ask one end-user to approve it.
41
+
42
+ Defaults to async (returns immediately with a decisionId); pass
43
+ wait=True to block up to ~55s for a fast answer. Always pass
44
+ idempotency_key so a retried call does not ask the same human twice.
45
+
46
+ ``type`` is "confirm" (yes/no), "select" (needs ``options``), or "input"
47
+ (free text). ``external_id`` targets the specific end-user to notify.
48
+ ``callback_url`` receives the signed webhook when the human answers.
49
+ ``expires_in_seconds`` sets how long the decision stays open.
50
+ ``timeout_seconds`` bounds the server-side wait when ``wait`` is True.
51
+
52
+ Returns a dict with ``decisionId``, ``pollUrl``, ``decisionPageUrl``, and
53
+ the current ``status``.
54
+ """
55
+
56
+ # Wire keys are camelCase even though the arguments are snake_case, and
57
+ # only keys with a value are sent so unset options stay absent.
58
+ body: Dict[str, Any] = {"question": question, "type": type}
59
+ optional = {
60
+ "options": options,
61
+ "externalId": external_id,
62
+ "callbackUrl": callback_url,
63
+ "agentName": agent_name,
64
+ "context": context,
65
+ "placeholder": placeholder,
66
+ "expiresInSeconds": expires_in_seconds,
67
+ "wait": wait,
68
+ "timeoutSeconds": timeout_seconds,
69
+ "idempotencyKey": idempotency_key,
70
+ "poweredBy": powered_by,
71
+ }
72
+ for key, value in optional.items():
73
+ if value is not None:
74
+ body[key] = value
75
+ return self._request("POST", "/decisions", body=body)
76
+
77
+ def get(self, decision_id: str, *, wait: Optional[int] = None) -> Dict[str, Any]:
78
+ """Fetch the current state of a decision.
79
+
80
+ Pass wait=N to long-poll for up to N seconds (capped server-side around
81
+ 55s) so the call returns as soon as the human answers instead of on the
82
+ next poll. Returns a dict with ``status``, ``answered``, and ``value``.
83
+ """
84
+
85
+ params = {"wait": wait} if wait is not None else None
86
+ return self._request("GET", f"/decisions/{_seg(decision_id)}", params=params)
87
+
88
+ def answer(self, decision_id: str, answer: str) -> Dict[str, Any]:
89
+ """Record an answer for a decision on the human's behalf.
90
+
91
+ Useful when your own surface (not the Pushary decision page) collected
92
+ the answer. Any waiting ``get`` or ``create`` call resolves immediately.
93
+ """
94
+
95
+ return self._request("POST", f"/decisions/{_seg(decision_id)}", body={"answer": answer})
96
+
97
+ def cancel(self, decision_id: str) -> Dict[str, Any]:
98
+ """Cancel a still-open decision so it can no longer be answered."""
99
+
100
+ return self._request("DELETE", f"/decisions/{_seg(decision_id)}")
101
+
102
+ def get_webhook_secret(self) -> Dict[str, Any]:
103
+ """Return the current webhook secret used to sign decision callbacks."""
104
+
105
+ return self._request("GET", "/webhook-secret")
106
+
107
+ def rotate_webhook_secret(self) -> Dict[str, Any]:
108
+ """Rotate the webhook secret and return the new value.
109
+
110
+ The previous secret stops verifying once rotated, so update every
111
+ receiver before you rely on the new one.
112
+ """
113
+
114
+ return self._request("POST", "/webhook-secret")
115
+
116
+
117
+ def _seg(value: str) -> str:
118
+ """Percent-encode a single path segment so an id cannot alter the route."""
119
+
120
+ return quote(str(value), safe="")
pushary/errors.py ADDED
@@ -0,0 +1,22 @@
1
+ """Error type raised by the Pushary SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+
8
+ class PusharyError(Exception):
9
+ """Raised when the Pushary API returns a non-2xx response.
10
+
11
+ Carries the HTTP ``status`` and the reason the server reported (its
12
+ ``error`` or ``message`` field), so callers can classify the failure
13
+ instead of parsing a bare status code.
14
+ """
15
+
16
+ def __init__(self, message: str, status: Optional[int] = None) -> None:
17
+ super().__init__(message)
18
+ self.message = message
19
+ self.status = status
20
+
21
+ def __repr__(self) -> str:
22
+ return f"PusharyError(status={self.status!r}, message={self.message!r})"
pushary/py.typed ADDED
File without changes
pushary/webhook.py ADDED
@@ -0,0 +1,53 @@
1
+ """Webhook signature verification.
2
+
3
+ When a human answers a decision, Pushary can POST the result to your
4
+ ``callback_url``. The request carries an ``X-Pushary-Signature`` header, which is
5
+ an HMAC-SHA256 hex digest computed over the raw request body using your webhook
6
+ secret. Verify it before you trust the payload.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import hmac
13
+ from typing import Optional, Union
14
+
15
+ SIGNATURE_HEADER = "X-Pushary-Signature"
16
+ """Name of the header that carries the signature on inbound webhooks."""
17
+
18
+
19
+ def _to_bytes(value: Union[str, bytes]) -> bytes:
20
+ """Encode a value to bytes with utf-8, leaving bytes untouched."""
21
+
22
+ if isinstance(value, bytes):
23
+ return value
24
+ return value.encode("utf-8")
25
+
26
+
27
+ def verify_webhook_signature(
28
+ raw_body: Union[str, bytes],
29
+ signature: Optional[str],
30
+ secret: str,
31
+ ) -> bool:
32
+ """Return True only if ``signature`` is a valid signature for ``raw_body``.
33
+
34
+ Compute ``hmac.new(secret, raw_body, sha256).hexdigest()`` and compare it to
35
+ the ``X-Pushary-Signature`` header in constant time. ``raw_body`` must be the
36
+ exact bytes you received, not a re-serialized copy, because any change to the
37
+ JSON (key order, spacing) changes the digest.
38
+
39
+ Returns False when either ``signature`` or ``secret`` is empty, or when the
40
+ lengths do not match, before doing the constant-time compare.
41
+ """
42
+
43
+ if not signature or not secret:
44
+ return False
45
+
46
+ expected = hmac.new(_to_bytes(secret), _to_bytes(raw_body), hashlib.sha256).hexdigest()
47
+ expected_bytes = expected.encode("ascii")
48
+ provided_bytes = _to_bytes(signature)
49
+
50
+ if len(expected_bytes) != len(provided_bytes):
51
+ return False
52
+
53
+ return hmac.compare_digest(expected_bytes, provided_bytes)
@@ -0,0 +1,201 @@
1
+ Metadata-Version: 2.4
2
+ Name: pushary
3
+ Version: 1.1.0
4
+ Summary: Human-in-the-loop decisions SDK for Pushary: create a decision, ask a specific end-user to approve, and resume on their answer via webhook or poll.
5
+ Project-URL: Homepage, https://pushary.com
6
+ Project-URL: Documentation, https://pushary.com/docs
7
+ Author-email: Pushary <aadil@pushary.com>
8
+ Maintainer-email: Pushary <aadil@pushary.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai-agents,approvals,human-in-the-loop,notifications,webhooks
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+
27
+ # pushary
28
+
29
+ Human-in-the-loop decisions for AI products, in Python. Create a decision, ask a
30
+ specific end-user to approve it, and resume on their answer via webhook or poll.
31
+
32
+ This is the Python counterpart of the `@pushary/server` SDK. It is zero
33
+ dependency (Python standard library only) and targets Python 3.9 and newer.
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install pushary
39
+ ```
40
+
41
+ ## API key
42
+
43
+ The SDK needs your full API key (`pk_xxx.sk_xxx`), which includes the secret
44
+ half. Never expose it in client-side code. Get your key from your
45
+ [Pushary dashboard](https://pushary.com/dashboard/settings).
46
+
47
+ ```python
48
+ import os
49
+ from pushary import PusharyServer
50
+
51
+ pushary = PusharyServer(api_key=os.environ["PUSHARY_API_KEY"])
52
+ ```
53
+
54
+ ## Human-in-the-loop decisions
55
+
56
+ A decision asks one of your end-users to approve something and then lets your
57
+ product resume once they answer. Use it for the moments where a human should be
58
+ in the loop: releasing funds, sending an outbound message, running a
59
+ destructive action, or confirming an AI-proposed change.
60
+
61
+ The flow has three parts:
62
+
63
+ 1. Create a decision and notify the end-user.
64
+ 2. Resume when they answer, either by handling the webhook or by polling.
65
+ 3. Optionally answer or cancel on their behalf from your own surface.
66
+
67
+ ### Create a decision
68
+
69
+ By default `create` is async: it returns right away with a `decisionId` and a
70
+ `pollUrl`, which suits serverless functions that cannot hold a request open
71
+ while a human decides. Pass `wait=True` to block for up to about 55 seconds in
72
+ case the human answers quickly.
73
+
74
+ Always pass `idempotency_key` so a retried call does not ask the same human
75
+ twice.
76
+
77
+ ```python
78
+ decision = pushary.decisions.create(
79
+ "Approve the $4,200 payout to Acme Corp?",
80
+ type="confirm",
81
+ external_id="user_123",
82
+ agent_name="Billing Agent",
83
+ context="Invoice INV-8842, net-30, first payout to this vendor.",
84
+ callback_url="https://your-app.com/webhooks/pushary",
85
+ expires_in_seconds=3600,
86
+ wait=True,
87
+ timeout_seconds=50,
88
+ idempotency_key="payout-INV-8842",
89
+ )
90
+
91
+ if decision["answered"]:
92
+ print("Resolved fast:", decision["value"])
93
+ else:
94
+ print("Still pending, poll:", decision["pollUrl"])
95
+ ```
96
+
97
+ For a multiple choice decision, pass `type="select"` with at least two
98
+ `options`. For free text, pass `type="input"` and an optional `placeholder`.
99
+
100
+ ```python
101
+ decision = pushary.decisions.create(
102
+ "Which shipping speed should we book?",
103
+ type="select",
104
+ options=["Standard", "Express", "Overnight"],
105
+ external_id="user_123",
106
+ )
107
+ ```
108
+
109
+ ### Poll for the answer
110
+
111
+ If you did not wait, or the wait window closed before the human answered, poll
112
+ for the outcome. Pass `wait=N` to long-poll for up to N seconds so the call
113
+ returns as soon as they answer rather than on your next loop.
114
+
115
+ ```python
116
+ result = pushary.decisions.get(decision["decisionId"], wait=50)
117
+
118
+ if result["status"] == "answered":
119
+ print("Answer:", result["value"])
120
+ elif result["status"] == "expired":
121
+ print("The decision expired before anyone answered.")
122
+ ```
123
+
124
+ ### Answer or cancel on their behalf
125
+
126
+ If your own interface collected the answer, record it so any waiting call
127
+ resolves. Cancel a decision to close it when it is no longer needed.
128
+
129
+ ```python
130
+ pushary.decisions.answer(decision["decisionId"], "yes")
131
+
132
+ pushary.decisions.cancel(decision["decisionId"])
133
+ ```
134
+
135
+ ## Webhooks
136
+
137
+ When the end-user answers, Pushary POSTs the result to your `callback_url`. The
138
+ request carries an `X-Pushary-Signature` header, an HMAC-SHA256 hex digest of
139
+ the raw request body signed with your webhook secret. Verify it against the raw
140
+ bytes you received, before parsing the JSON, so a change to spacing or key order
141
+ cannot slip past the check.
142
+
143
+ Fetch or rotate the secret from the SDK:
144
+
145
+ ```python
146
+ secret = pushary.decisions.get_webhook_secret()
147
+ rotated = pushary.decisions.rotate_webhook_secret()
148
+ ```
149
+
150
+ A Flask handler that verifies the signature and resumes your work:
151
+
152
+ ```python
153
+ import os
154
+ from flask import Flask, request, abort
155
+ from pushary import verify_webhook_signature
156
+
157
+ app = Flask(__name__)
158
+ WEBHOOK_SECRET = os.environ["PUSHARY_WEBHOOK_SECRET"]
159
+
160
+ @app.post("/webhooks/pushary")
161
+ def pushary_webhook():
162
+ signature = request.headers.get("X-Pushary-Signature")
163
+ if not verify_webhook_signature(request.data, signature, WEBHOOK_SECRET):
164
+ abort(401)
165
+
166
+ payload = request.get_json()
167
+ decision_id = payload["decisionId"]
168
+ answer = payload.get("value")
169
+ # Resume your work now that the human has answered.
170
+ return "", 204
171
+ ```
172
+
173
+ `request.data` is the raw request body Flask captured, which is exactly what the
174
+ signature was computed over. Do not re-serialize the parsed JSON before
175
+ verifying.
176
+
177
+ ## Errors
178
+
179
+ Every method returns the parsed JSON body as a `dict`. A non-2xx response raises
180
+ `PusharyError`, which carries the HTTP `status` and the reason the server
181
+ reported.
182
+
183
+ ```python
184
+ from pushary import PusharyError
185
+
186
+ try:
187
+ pushary.decisions.get("does-not-exist")
188
+ except PusharyError as error:
189
+ print(error.status, error.message)
190
+ ```
191
+
192
+ ## Security
193
+
194
+ - Keep your API key and webhook secret in environment variables.
195
+ - Rotate the webhook secret from the dashboard or via
196
+ `rotate_webhook_secret()` if it is ever exposed.
197
+ - API keys are site-scoped, so a key can only reach its own decisions.
198
+
199
+ ## License
200
+
201
+ MIT. See `LICENSE`.
@@ -0,0 +1,11 @@
1
+ pushary/__init__.py,sha256=LcMqZXHB9q_B011jz-iwzg9K_ciaHBA8P6VA5qIOHOU,913
2
+ pushary/_types.py,sha256=bmFxikuPUWGWK1hyjj7dGeS5IPYQ1OxNC35KQxJP2zM,2204
3
+ pushary/client.py,sha256=qH5B2Idm4IJpxwyVLhp0DI8zUM733d-frWLCysmEl1k,4034
4
+ pushary/decisions.py,sha256=Qk1YFjn9iI31FHkMwxTOt9Sr-1hN03RazstVlnAaMGM,4730
5
+ pushary/errors.py,sha256=WrZFxTfYmt8c3v9cibkyyCaytAJ_L9x5oAS2lJT3T1w,686
6
+ pushary/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ pushary/webhook.py,sha256=1BSTQG4RMNwyWMyafg0B3Q85ONtnmHd9YuWMEmK9R1s,1774
8
+ pushary-1.1.0.dist-info/METADATA,sha256=0gmzxs3-bOIe_rET8p-9M5-_HztJanp_pMrnyDoTL9c,6434
9
+ pushary-1.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
+ pushary-1.1.0.dist-info/licenses/LICENSE,sha256=dbAOXev8njZg4qMsS4LS9tZq8cVgKYPZ1iTeOux9UBE,1064
11
+ pushary-1.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 Pushary
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.