spendwall 0.0.6__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.
- spendwall/__init__.py +11 -0
- spendwall/anthropic.py +88 -0
- spendwall/client.py +118 -0
- spendwall/errors.py +37 -0
- spendwall/modes.py +26 -0
- spendwall/openai.py +98 -0
- spendwall-0.0.6.dist-info/METADATA +33 -0
- spendwall-0.0.6.dist-info/RECORD +10 -0
- spendwall-0.0.6.dist-info/WHEEL +5 -0
- spendwall-0.0.6.dist-info/top_level.txt +1 -0
spendwall/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Spendwall — local-first agent spending firewall."""
|
|
2
|
+
__version__ = "0.0.2"
|
|
3
|
+
|
|
4
|
+
from spendwall.client import SpendwallClient # noqa: F401
|
|
5
|
+
from spendwall.errors import ( # noqa: F401
|
|
6
|
+
ApprovalRequiredError,
|
|
7
|
+
BlockedError,
|
|
8
|
+
SpendwallError,
|
|
9
|
+
SpendwallVersionMismatch,
|
|
10
|
+
)
|
|
11
|
+
from spendwall.modes import Mode # noqa: F401
|
spendwall/anthropic.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Wrapper for ``anthropic.Anthropic`` clients.
|
|
2
|
+
|
|
3
|
+
Patches ``client.messages.create`` in-place so every upstream call is
|
|
4
|
+
observed by spendwalld. In ``enforce`` mode, the wrapper consults policy
|
|
5
|
+
*before* dispatching to the upstream and raises
|
|
6
|
+
``BlockedError``/``ApprovalRequiredError`` when the daemon refuses.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json as _json
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
|
|
14
|
+
from spendwall.client import SpendwallClient
|
|
15
|
+
from spendwall.modes import Mode, resolve as resolve_mode
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _estimate_tokens(messages_or_input: Any) -> int:
|
|
19
|
+
try:
|
|
20
|
+
return max(1, len(_json.dumps(messages_or_input)) // 4)
|
|
21
|
+
except (TypeError, ValueError):
|
|
22
|
+
return 1
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def wrap(
|
|
26
|
+
anthropic_client: Any,
|
|
27
|
+
*,
|
|
28
|
+
sw_client: Optional[SpendwallClient] = None,
|
|
29
|
+
agent_id: str = "anonymous",
|
|
30
|
+
mode: Optional[Mode] = None,
|
|
31
|
+
tags: Optional[dict[str, str]] = None,
|
|
32
|
+
) -> Any:
|
|
33
|
+
"""Patch ``anthropic_client.messages.create`` to call into spendwall
|
|
34
|
+
before/after the upstream request.
|
|
35
|
+
|
|
36
|
+
Mutates the supplied client in-place AND returns it for ergonomic chaining:
|
|
37
|
+
|
|
38
|
+
client = wrap(anthropic.Anthropic(...), agent_id="x")
|
|
39
|
+
"""
|
|
40
|
+
sw = sw_client or SpendwallClient()
|
|
41
|
+
configured_mode = mode
|
|
42
|
+
|
|
43
|
+
original = anthropic_client.messages.create
|
|
44
|
+
|
|
45
|
+
def patched(*args: Any, **kwargs: Any) -> Any:
|
|
46
|
+
call_mode = resolve_mode(configured=configured_mode)
|
|
47
|
+
model = kwargs.get("model", "unknown")
|
|
48
|
+
est_input_tokens = _estimate_tokens(kwargs.get("messages"))
|
|
49
|
+
|
|
50
|
+
if call_mode == "enforce":
|
|
51
|
+
sw.policy_check_strict(
|
|
52
|
+
agent_id=agent_id, provider="anthropic", model=model,
|
|
53
|
+
estimated_cost_cents=0, tags=tags or {},
|
|
54
|
+
)
|
|
55
|
+
elif call_mode == "advisory":
|
|
56
|
+
sw.policy_check(
|
|
57
|
+
agent_id=agent_id, provider="anthropic", model=model,
|
|
58
|
+
estimated_cost_cents=0, tags=tags or {},
|
|
59
|
+
)
|
|
60
|
+
# observe-mode skips policy_check entirely.
|
|
61
|
+
try:
|
|
62
|
+
result = original(*args, **kwargs)
|
|
63
|
+
except Exception:
|
|
64
|
+
sw.observe({
|
|
65
|
+
"agent_id": agent_id, "provider": "anthropic", "model": model,
|
|
66
|
+
"source": "sdk_python", "input_tokens": est_input_tokens,
|
|
67
|
+
"output_tokens": None, "estimated_cost_cents": 0,
|
|
68
|
+
"actual_cost_cents": None, "decision_str": "allowed",
|
|
69
|
+
"binding_caps": [], "tags": tags or {},
|
|
70
|
+
"outcome": "upstream_error",
|
|
71
|
+
})
|
|
72
|
+
raise
|
|
73
|
+
|
|
74
|
+
usage = getattr(result, "usage", None)
|
|
75
|
+
input_tokens = getattr(usage, "input_tokens", None) if usage else None
|
|
76
|
+
output_tokens = getattr(usage, "output_tokens", None) if usage else None
|
|
77
|
+
sw.observe({
|
|
78
|
+
"agent_id": agent_id, "provider": "anthropic", "model": model,
|
|
79
|
+
"source": "sdk_python", "input_tokens": input_tokens,
|
|
80
|
+
"output_tokens": output_tokens, "estimated_cost_cents": 0,
|
|
81
|
+
"actual_cost_cents": None, "decision_str": "allowed",
|
|
82
|
+
"binding_caps": [], "tags": tags or {},
|
|
83
|
+
"outcome": "success",
|
|
84
|
+
})
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
anthropic_client.messages.create = patched
|
|
88
|
+
return anthropic_client
|
spendwall/client.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""UDS HTTP client for spendwalld."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
from urllib.parse import quote
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
import requests_unixsocket
|
|
11
|
+
|
|
12
|
+
from spendwall.errors import ApprovalRequiredError, BlockedError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _default_socket_path() -> str:
|
|
16
|
+
return os.environ.get(
|
|
17
|
+
"SPENDWALL_SOCKET",
|
|
18
|
+
os.path.expanduser("~/.spendwall/spendwall.sock"),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _default_bearer() -> Optional[str]:
|
|
23
|
+
return os.environ.get("SPENDWALL_TOKEN")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SpendwallClient:
|
|
27
|
+
"""Talks to the spendwall daemon over its UDS HTTP API.
|
|
28
|
+
|
|
29
|
+
Fail-open: when ``fail_open=True`` and the socket is unreachable, ``observe()``
|
|
30
|
+
is silently dropped and ``policy_check()`` returns a synthetic "allowed"
|
|
31
|
+
decision. Default is ``fail_open=True`` because a spending firewall MUST NOT
|
|
32
|
+
bring down user agents when the daemon is down.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
socket_path: Optional[str] = None,
|
|
38
|
+
bearer: Optional[str] = None,
|
|
39
|
+
timeout_seconds: float = 2.0,
|
|
40
|
+
fail_open: bool = True,
|
|
41
|
+
):
|
|
42
|
+
self.socket_path = socket_path or _default_socket_path()
|
|
43
|
+
self.bearer = bearer or _default_bearer()
|
|
44
|
+
self.timeout_seconds = timeout_seconds
|
|
45
|
+
self.fail_open = fail_open
|
|
46
|
+
self._session = requests_unixsocket.Session()
|
|
47
|
+
self._base = f"http+unix://{quote(self.socket_path, safe='')}"
|
|
48
|
+
|
|
49
|
+
def _headers(self) -> dict[str, str]:
|
|
50
|
+
h = {"content-type": "application/json"}
|
|
51
|
+
if self.bearer:
|
|
52
|
+
h["authorization"] = f"Bearer {self.bearer}"
|
|
53
|
+
return h
|
|
54
|
+
|
|
55
|
+
def policy_check(
|
|
56
|
+
self,
|
|
57
|
+
*,
|
|
58
|
+
agent_id: str,
|
|
59
|
+
provider: str,
|
|
60
|
+
model: str,
|
|
61
|
+
estimated_cost_cents: int,
|
|
62
|
+
tags: Optional[dict[str, str]] = None,
|
|
63
|
+
) -> dict[str, Any]:
|
|
64
|
+
body = {
|
|
65
|
+
"agent_id": agent_id,
|
|
66
|
+
"provider": provider,
|
|
67
|
+
"model": model,
|
|
68
|
+
"estimated_cost_cents": estimated_cost_cents,
|
|
69
|
+
"tags": tags or {},
|
|
70
|
+
}
|
|
71
|
+
try:
|
|
72
|
+
r = self._session.post(
|
|
73
|
+
f"{self._base}/v1/policy/check",
|
|
74
|
+
json=body,
|
|
75
|
+
headers=self._headers(),
|
|
76
|
+
timeout=self.timeout_seconds,
|
|
77
|
+
)
|
|
78
|
+
r.raise_for_status()
|
|
79
|
+
return r.json()
|
|
80
|
+
except (requests.RequestException, ValueError):
|
|
81
|
+
if self.fail_open:
|
|
82
|
+
return {"decision": "allowed", "alert_caps": []}
|
|
83
|
+
raise
|
|
84
|
+
|
|
85
|
+
def policy_check_strict(self, **kwargs: Any) -> dict[str, Any]:
|
|
86
|
+
"""Like ``policy_check`` but raises ``BlockedError`` /
|
|
87
|
+
``ApprovalRequiredError`` on non-allowed decisions."""
|
|
88
|
+
d = self.policy_check(**kwargs)
|
|
89
|
+
if d.get("decision") == "blocked":
|
|
90
|
+
raise BlockedError(
|
|
91
|
+
cap_id=d.get("cap_id", "?"),
|
|
92
|
+
projected_cents=d.get("projected_cents", 0),
|
|
93
|
+
limit_cents=d.get("limit_cents", 0),
|
|
94
|
+
)
|
|
95
|
+
if d.get("decision") == "approval_required":
|
|
96
|
+
raise ApprovalRequiredError(approval_caps=d.get("approval_caps", []))
|
|
97
|
+
return d
|
|
98
|
+
|
|
99
|
+
def observe(self, event: dict[str, Any]) -> None:
|
|
100
|
+
try:
|
|
101
|
+
self._session.post(
|
|
102
|
+
f"{self._base}/v1/spend/observe",
|
|
103
|
+
json=event,
|
|
104
|
+
headers=self._headers(),
|
|
105
|
+
timeout=self.timeout_seconds,
|
|
106
|
+
)
|
|
107
|
+
except requests.RequestException:
|
|
108
|
+
if not self.fail_open:
|
|
109
|
+
raise
|
|
110
|
+
# Fail-open: drop on the floor.
|
|
111
|
+
|
|
112
|
+
def about(self) -> dict[str, Any]:
|
|
113
|
+
r = self._session.get(
|
|
114
|
+
f"{self._base}/v1/about",
|
|
115
|
+
timeout=self.timeout_seconds,
|
|
116
|
+
)
|
|
117
|
+
r.raise_for_status()
|
|
118
|
+
return r.json()
|
spendwall/errors.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Spendwall SDK exceptions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SpendwallError(Exception):
|
|
7
|
+
"""Base for all spendwall-raised exceptions."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BlockedError(SpendwallError):
|
|
11
|
+
"""Raised in `enforce` mode when policy returned Blocked."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, cap_id: str, projected_cents: int, limit_cents: int):
|
|
14
|
+
self.cap_id = cap_id
|
|
15
|
+
self.projected_cents = projected_cents
|
|
16
|
+
self.limit_cents = limit_cents
|
|
17
|
+
super().__init__(
|
|
18
|
+
f"Spendwall blocked by cap '{cap_id}': "
|
|
19
|
+
f"projected {projected_cents}c > limit {limit_cents}c"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ApprovalRequiredError(SpendwallError):
|
|
24
|
+
"""Raised in `enforce` mode when policy returned ApprovalRequired and the
|
|
25
|
+
approval was denied or timed out."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, approval_caps: list[str], reason: str = "denied"):
|
|
28
|
+
self.approval_caps = approval_caps
|
|
29
|
+
self.reason = reason
|
|
30
|
+
super().__init__(
|
|
31
|
+
f"Spendwall approval {reason} for caps={approval_caps}"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SpendwallVersionMismatch(SpendwallError):
|
|
36
|
+
"""Raised once when the daemon's API version does not match the SDK's.
|
|
37
|
+
SDK falls back to fail-open `observe` mode for the rest of the process."""
|
spendwall/modes.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Mode resolution — observe, advisory, enforce."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Literal, Optional
|
|
7
|
+
|
|
8
|
+
Mode = Literal["observe", "advisory", "enforce"]
|
|
9
|
+
_VALID = ("observe", "advisory", "enforce")
|
|
10
|
+
_ENV_VAR = "SPENDWALL_MODE"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def resolve(per_call: Optional[str] = None, configured: Optional[str] = None) -> Mode:
|
|
14
|
+
"""Precedence (most specific wins):
|
|
15
|
+
1. `per_call` kwarg passed by user code at the call site
|
|
16
|
+
2. `configured` value from `wrap(..., mode=...)`
|
|
17
|
+
3. `SPENDWALL_MODE` env var
|
|
18
|
+
4. Default: `observe`
|
|
19
|
+
"""
|
|
20
|
+
for v in (per_call, configured, os.environ.get(_ENV_VAR)):
|
|
21
|
+
if v is None:
|
|
22
|
+
continue
|
|
23
|
+
if v not in _VALID:
|
|
24
|
+
raise ValueError(f"invalid spendwall mode: {v!r}; expected one of {_VALID}")
|
|
25
|
+
return v # type: ignore[return-value]
|
|
26
|
+
return "observe"
|
spendwall/openai.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Wrapper for ``openai.OpenAI`` clients.
|
|
2
|
+
|
|
3
|
+
Patches ``client.chat.completions.create`` and ``client.responses.create``
|
|
4
|
+
in-place so every upstream call is observed by spendwalld. In ``enforce``
|
|
5
|
+
mode, the wrapper consults policy *before* dispatching to the upstream and
|
|
6
|
+
raises ``BlockedError``/``ApprovalRequiredError`` when the daemon refuses.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json as _json
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
|
|
14
|
+
from spendwall.client import SpendwallClient
|
|
15
|
+
from spendwall.modes import Mode, resolve as resolve_mode
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _estimate_tokens(messages_or_input: Any) -> int:
|
|
19
|
+
"""Cheap byte-based estimate; daemon reconciles cost post-flight."""
|
|
20
|
+
try:
|
|
21
|
+
return max(1, len(_json.dumps(messages_or_input)) // 4)
|
|
22
|
+
except (TypeError, ValueError):
|
|
23
|
+
return 1
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def wrap(
|
|
27
|
+
openai_client: Any,
|
|
28
|
+
*,
|
|
29
|
+
sw_client: Optional[SpendwallClient] = None,
|
|
30
|
+
agent_id: str = "anonymous",
|
|
31
|
+
mode: Optional[Mode] = None,
|
|
32
|
+
tags: Optional[dict[str, str]] = None,
|
|
33
|
+
) -> Any:
|
|
34
|
+
"""Patch ``openai_client.chat.completions.create`` (and ``.responses.create``)
|
|
35
|
+
to call into spendwall before/after the upstream request.
|
|
36
|
+
|
|
37
|
+
Mutates the supplied client in-place AND returns it for ergonomic chaining:
|
|
38
|
+
|
|
39
|
+
client = wrap(openai.OpenAI(...), agent_id="x")
|
|
40
|
+
"""
|
|
41
|
+
sw = sw_client or SpendwallClient()
|
|
42
|
+
configured_mode = mode
|
|
43
|
+
|
|
44
|
+
def _patch(parent: Any, name: str) -> None:
|
|
45
|
+
original = getattr(parent, name)
|
|
46
|
+
|
|
47
|
+
def patched(*args: Any, **kwargs: Any) -> Any:
|
|
48
|
+
call_mode = resolve_mode(configured=configured_mode)
|
|
49
|
+
model = kwargs.get("model", "unknown")
|
|
50
|
+
est_input_tokens = _estimate_tokens(
|
|
51
|
+
kwargs.get("messages") or kwargs.get("input")
|
|
52
|
+
)
|
|
53
|
+
# Daemon does pricing lookup; SDK sends estimated_cost_cents=0
|
|
54
|
+
# to defer cost estimation to the daemon side.
|
|
55
|
+
if call_mode == "enforce":
|
|
56
|
+
sw.policy_check_strict(
|
|
57
|
+
agent_id=agent_id, provider="openai", model=model,
|
|
58
|
+
estimated_cost_cents=0, tags=tags or {},
|
|
59
|
+
)
|
|
60
|
+
elif call_mode == "advisory":
|
|
61
|
+
sw.policy_check(
|
|
62
|
+
agent_id=agent_id, provider="openai", model=model,
|
|
63
|
+
estimated_cost_cents=0, tags=tags or {},
|
|
64
|
+
)
|
|
65
|
+
# observe-mode skips policy_check entirely (zero added latency).
|
|
66
|
+
try:
|
|
67
|
+
result = original(*args, **kwargs)
|
|
68
|
+
except Exception:
|
|
69
|
+
sw.observe({
|
|
70
|
+
"agent_id": agent_id, "provider": "openai", "model": model,
|
|
71
|
+
"source": "sdk_python", "input_tokens": est_input_tokens,
|
|
72
|
+
"output_tokens": None, "estimated_cost_cents": 0,
|
|
73
|
+
"actual_cost_cents": None, "decision_str": "allowed",
|
|
74
|
+
"binding_caps": [], "tags": tags or {},
|
|
75
|
+
"outcome": "upstream_error",
|
|
76
|
+
})
|
|
77
|
+
raise
|
|
78
|
+
|
|
79
|
+
usage = getattr(result, "usage", None)
|
|
80
|
+
input_tokens = getattr(usage, "prompt_tokens", None) if usage else None
|
|
81
|
+
output_tokens = getattr(usage, "completion_tokens", None) if usage else None
|
|
82
|
+
sw.observe({
|
|
83
|
+
"agent_id": agent_id, "provider": "openai", "model": model,
|
|
84
|
+
"source": "sdk_python", "input_tokens": input_tokens,
|
|
85
|
+
"output_tokens": output_tokens, "estimated_cost_cents": 0,
|
|
86
|
+
"actual_cost_cents": None, "decision_str": "allowed",
|
|
87
|
+
"binding_caps": [], "tags": tags or {},
|
|
88
|
+
"outcome": "success",
|
|
89
|
+
})
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
setattr(parent, name, patched)
|
|
93
|
+
|
|
94
|
+
if hasattr(openai_client, "chat") and hasattr(openai_client.chat, "completions"):
|
|
95
|
+
_patch(openai_client.chat.completions, "create")
|
|
96
|
+
if hasattr(openai_client, "responses"):
|
|
97
|
+
_patch(openai_client.responses, "create")
|
|
98
|
+
return openai_client
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spendwall
|
|
3
|
+
Version: 0.0.6
|
|
4
|
+
Summary: Spendwall SDK — local-first agent spend firewall
|
|
5
|
+
Author: Spendwall
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: requests-unixsocket>=0.3
|
|
10
|
+
Requires-Dist: requests>=2.28
|
|
11
|
+
Provides-Extra: openai
|
|
12
|
+
Requires-Dist: openai>=1.0; extra == "openai"
|
|
13
|
+
Provides-Extra: anthropic
|
|
14
|
+
Requires-Dist: anthropic>=0.21; extra == "anthropic"
|
|
15
|
+
Provides-Extra: all
|
|
16
|
+
Requires-Dist: openai>=1.0; extra == "all"
|
|
17
|
+
Requires-Dist: anthropic>=0.21; extra == "all"
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
20
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
21
|
+
Requires-Dist: responses>=0.23; extra == "dev"
|
|
22
|
+
|
|
23
|
+
# spendwall (Python)
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import openai
|
|
27
|
+
import spendwall.openai as sw
|
|
28
|
+
|
|
29
|
+
client = openai.OpenAI(api_key="sk-...")
|
|
30
|
+
client = sw.wrap(client, mode="enforce")
|
|
31
|
+
|
|
32
|
+
# Now every client.chat.completions.create(...) is observed by spendwalld.
|
|
33
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
spendwall/__init__.py,sha256=3wFQ_DwCWW1VNLsMKG1Nnnuf4BalHj3CUh1rFdoPgX8,328
|
|
2
|
+
spendwall/anthropic.py,sha256=uHQmwXrSr7VLSIRNdo_GEkQvuJYGuzZbh6rnOa67yf4,3219
|
|
3
|
+
spendwall/client.py,sha256=YfjrgQM1lbuqRLL80Rf2-SjfeSMACxX-JNE35NHDiFo,3756
|
|
4
|
+
spendwall/errors.py,sha256=DuBdVvo8e7tnXim6HVZlHPT-byf_8kerBFL6FNG42TI,1228
|
|
5
|
+
spendwall/modes.py,sha256=-YHg3q90HjQdBqD5i17kyNUeAVXN9aJwuVwuAGdOSsQ,860
|
|
6
|
+
spendwall/openai.py,sha256=0viGVSZ-5fdVDUQBNQXrrhowOmdqyKIZ-rUuePXrn3g,3974
|
|
7
|
+
spendwall-0.0.6.dist-info/METADATA,sha256=z1b3UBj5-K5_yySHhfyeBSbdSk5o2ZCj7FP_xFm0EkA,943
|
|
8
|
+
spendwall-0.0.6.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
9
|
+
spendwall-0.0.6.dist-info/top_level.txt,sha256=Dk33kymC3JHwUjo8dO_m55Qja7dyRtU6Q4AKLwxugdU,10
|
|
10
|
+
spendwall-0.0.6.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
spendwall
|