onceonly-sdk 1.2.0__py3-none-any.whl → 2.0.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.
@@ -0,0 +1,168 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ from typing import Any, Dict, Optional, Tuple
6
+
7
+ from ..client import OnceOnly
8
+
9
+
10
+ def _stable_hash_args(args: Tuple[Any, ...], kwargs: Dict[str, Any]) -> str:
11
+ def default_encoder(obj: Any) -> Any:
12
+ # Pydantic v2
13
+ md = getattr(obj, "model_dump", None)
14
+ if callable(md):
15
+ try:
16
+ return md()
17
+ except Exception:
18
+ pass
19
+
20
+ # Pydantic v1
21
+ dct = getattr(obj, "dict", None)
22
+ if callable(dct):
23
+ try:
24
+ return dct()
25
+ except Exception:
26
+ pass
27
+
28
+ # Dataclasses
29
+ if hasattr(obj, "__dataclass_fields__"):
30
+ try:
31
+ import dataclasses
32
+
33
+ return dataclasses.asdict(obj)
34
+ except Exception:
35
+ pass
36
+
37
+ if isinstance(obj, (bytes, bytearray)):
38
+ return obj.hex()
39
+
40
+ return str(obj)
41
+
42
+ payload = {"args": args, "kwargs": {k: v for k, v in sorted(kwargs.items())}}
43
+
44
+ try:
45
+ raw = json.dumps(
46
+ payload,
47
+ ensure_ascii=False,
48
+ default=default_encoder,
49
+ sort_keys=True,
50
+ separators=(",", ":"),
51
+ )
52
+ except Exception:
53
+ raw = str(payload)
54
+
55
+ return hashlib.sha256(raw.encode("utf-8")).hexdigest()
56
+
57
+
58
+ def _hash_tool_input(tool_input: Any) -> str:
59
+ """
60
+ Stable hash for LangChain tool_input across versions.
61
+ """
62
+ if isinstance(tool_input, dict):
63
+ return _stable_hash_args((), tool_input)
64
+ return _stable_hash_args((tool_input,), {})
65
+
66
+
67
+ def make_idempotent_tool(
68
+ tool: Any,
69
+ *,
70
+ client: OnceOnly,
71
+ key_prefix: str = "tool",
72
+ ttl: int = 86400,
73
+ meta: Optional[Dict[str, Any]] = None,
74
+ ) -> Any:
75
+ """
76
+ Optional LangChain integration (no hard dependency).
77
+ Usage: pip install langchain-core
78
+
79
+ Wraps a BaseTool so repeated calls with the same tool_input become idempotent.
80
+
81
+ Implementation detail:
82
+ - We override invoke()/ainvoke() to avoid BaseTool._run signature differences across LC versions
83
+ and to support both single-input Tool and StructuredTool.
84
+ """
85
+ try:
86
+ from langchain_core.tools import BaseTool # type: ignore
87
+ except ImportError as e:
88
+ raise ImportError("LangChain is not installed. Install langchain-core to use this integration.") from e
89
+
90
+ if not isinstance(tool, BaseTool):
91
+ raise TypeError("tool must be an instance of langchain_core.tools.BaseTool")
92
+
93
+ base_meta: Dict[str, Any] = {"tool": getattr(tool, "name", tool.__class__.__name__)}
94
+ if meta:
95
+ base_meta.update(meta)
96
+
97
+ class IdempotentTool(BaseTool): # type: ignore[misc]
98
+ name: str
99
+ description: str
100
+
101
+ _tool: BaseTool
102
+ _client: OnceOnly
103
+ _key_prefix: str
104
+ _ttl: int
105
+ _meta: Dict[str, Any]
106
+
107
+ def __init__(self) -> None:
108
+ super().__init__(name=tool.name, description=tool.description)
109
+ object.__setattr__(self, "_tool", tool)
110
+ object.__setattr__(self, "_client", client)
111
+ object.__setattr__(self, "_key_prefix", key_prefix)
112
+ object.__setattr__(self, "_ttl", int(ttl))
113
+ object.__setattr__(self, "_meta", base_meta)
114
+
115
+ # Preserve schema + a few common attrs
116
+ if hasattr(tool, "args_schema"):
117
+ try:
118
+ object.__setattr__(self, "args_schema", getattr(tool, "args_schema"))
119
+ except Exception:
120
+ pass
121
+
122
+ for attr in ("return_direct", "tags", "metadata", "callbacks", "verbose"):
123
+ if hasattr(tool, attr):
124
+ try:
125
+ setattr(self, attr, getattr(tool, attr))
126
+ except Exception:
127
+ pass
128
+
129
+ def invoke(self, tool_input: Any, config: Any = None, **kwargs: Any) -> Any: # type: ignore[override]
130
+ h = _hash_tool_input(tool_input)
131
+ key = f"{self._key_prefix}:{self.name}:{h}"
132
+
133
+ res = self._client.check_lock(key=key, ttl=self._ttl, meta=self._meta)
134
+ if res.duplicate:
135
+ return f"Action '{self.name}' skipped (idempotency key duplicate)."
136
+
137
+ # Delegate: let LangChain handle parsing/validation/config
138
+ if config is None:
139
+ return self._tool.invoke(tool_input, **kwargs)
140
+ return self._tool.invoke(tool_input, config=config, **kwargs)
141
+
142
+ async def ainvoke(self, tool_input: Any, config: Any = None, **kwargs: Any) -> Any: # type: ignore[override]
143
+ h = _hash_tool_input(tool_input)
144
+ key = f"{self._key_prefix}:{self.name}:{h}"
145
+
146
+ res = await self._client.check_lock_async(key=key, ttl=self._ttl, meta=self._meta)
147
+ if res.duplicate:
148
+ return f"Action '{self.name}' skipped (idempotency key duplicate)."
149
+
150
+ ainvoke = getattr(self._tool, "ainvoke", None)
151
+ if callable(ainvoke):
152
+ if config is None:
153
+ return await ainvoke(tool_input, **kwargs)
154
+ return await ainvoke(tool_input, config=config, **kwargs)
155
+
156
+ # Fallback
157
+ if config is None:
158
+ return self._tool.invoke(tool_input, **kwargs)
159
+ return self._tool.invoke(tool_input, config=config, **kwargs)
160
+
161
+ # Keep BaseTool abstract contract satisfied; not used because we override invoke/ainvoke.
162
+ def _run(self, *args: Any, **kwargs: Any) -> Any:
163
+ raise RuntimeError("IdempotentTool delegates via invoke(); _run() should not be called.")
164
+
165
+ async def _arun(self, *args: Any, **kwargs: Any) -> Any:
166
+ raise RuntimeError("IdempotentTool delegates via ainvoke(); _arun() should not be called.")
167
+
168
+ return IdempotentTool()
onceonly/models.py CHANGED
@@ -14,3 +14,14 @@ class CheckLockResult:
14
14
  request_id: Optional[str]
15
15
  status_code: int
16
16
  raw: Dict[str, Any]
17
+
18
+ def should_proceed(self) -> bool:
19
+ """
20
+ Helper for agents/tools:
21
+ - True => proceed with the expensive/side-effect operation
22
+ - False => treat as duplicate (or blocked)
23
+ """
24
+ return bool(self.locked) and not bool(self.duplicate)
25
+
26
+ def is_duplicate(self) -> bool:
27
+ return bool(self.duplicate)
onceonly/version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "2.0.0"
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.4
2
+ Name: onceonly-sdk
3
+ Version: 2.0.0
4
+ Summary: Python SDK for OnceOnly idempotency API
5
+ Author-email: OnceOnly <support@onceonly.tech>
6
+ License: MIT
7
+ Project-URL: Homepage, https://onceonly.tech/
8
+ Project-URL: Documentation, https://onceonly.tech/docs/
9
+ Project-URL: Repository, https://github.com/mykolademyanov/onceonly-python
10
+ Keywords: idempotency,automation,zapier,make,ai-agents
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: httpx>=0.25
18
+ Provides-Extra: test
19
+ Requires-Dist: pytest>=7.0; extra == "test"
20
+ Requires-Dist: pytest-asyncio>=0.23; extra == "test"
21
+ Requires-Dist: anyio>=4.0; extra == "test"
22
+ Provides-Extra: langchain
23
+ Requires-Dist: langchain-core>=0.1.0; extra == "langchain"
24
+ Dynamic: license-file
25
+
26
+ # OnceOnly Python SDK
27
+
28
+ **The Idempotency Layer for AI Agents, Webhooks, and Distributed Systems.**
29
+
30
+ OnceOnly is a high-performance Python SDK that ensures **exactly-once execution**.
31
+ It prevents duplicate actions (payments, emails, tool calls) in unstable environments like
32
+ AI agents, webhooks, retries, or background workers.
33
+
34
+ Website: https://onceonly.tech/ai/
35
+ Documentation: https://onceonly.tech/docs/
36
+
37
+ ---
38
+
39
+ ## Features
40
+
41
+ - Sync + Async client (httpx-based)
42
+ - Fail-open mode for production safety
43
+ - Stable idempotency keys (supports Pydantic & dataclasses)
44
+ - Decorator for zero-boilerplate usage
45
+ - Optional AI / LangChain integrations
46
+
47
+ ---
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ pip install onceonly-sdk
53
+ ```
54
+
55
+ ### With LangChain support included:
56
+
57
+ ```bash
58
+ pip install "onceonly-sdk[langchain]"
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Quick Start
64
+
65
+ ```python
66
+ from onceonly import OnceOnly
67
+
68
+ client = OnceOnly(
69
+ api_key="once_live_...",
70
+ fail_open=True # default: continues if API is down
71
+ )
72
+
73
+ res = client.check_lock(key="order:123", ttl=300)
74
+
75
+ if res.duplicate:
76
+ print("Duplicate blocked")
77
+ else:
78
+ print("First execution")
79
+ ```
80
+
81
+ ---
82
+
83
+ ## AI Agents / LangChain Integration 🤖
84
+
85
+ OnceOnly integrates cleanly with AI-agent frameworks like LangChain.
86
+
87
+ ```python
88
+ from onceonly.integrations.langchain import make_idempotent_tool
89
+
90
+ tool = make_idempotent_tool(
91
+ original_tool,
92
+ client=client,
93
+ key_prefix="agent:tool"
94
+ )
95
+ ```
96
+
97
+ Repeated tool calls with the same inputs will execute **exactly once**, even across retries or agent restarts.
98
+
99
+ ---
100
+
101
+ ## Decorator
102
+
103
+ ```python
104
+ from onceonly.decorators import idempotent
105
+
106
+ @idempotent(client, ttl=3600)
107
+ def process_order(order_id):
108
+ ...
109
+ ```
110
+
111
+ Idempotency keys are generated automatically and are stable across restarts.
112
+
113
+ ---
114
+
115
+ ## Fail-Open Mode
116
+
117
+ Fail-open is enabled by default.
118
+
119
+ Network errors, timeouts, or server errors (5xx) will **not break your application**.
120
+ The SDK will allow execution to continue safely.
121
+
122
+ Fail-open never applies to:
123
+ - Auth errors (401 / 403)
124
+ - Plan limits (402)
125
+ - Validation errors (422)
126
+ - Rate limits (429)
127
+
128
+ ---
129
+
130
+ ## Support
131
+
132
+ Need help?
133
+ Email: support@onceonly.tech
134
+ Or open an issue on GitHub.
135
+
136
+ ---
137
+
138
+ ## License
139
+
140
+ MIT
@@ -0,0 +1,17 @@
1
+ onceonly/__init__.py,sha256=KMS6F4DejM5nI5-gw3UC8SvETnK90oUE9V5pskh--Uw,481
2
+ onceonly/_http.py,sha256=bFAgrLv0T7cGFq3LqaQCwEiqx-VfKEiT8jUommmhRws,3240
3
+ onceonly/_util.py,sha256=YVdEWn1bvipAzR3g3oXpHmgLiaODwGRB1IGA3gHZ2PM,1273
4
+ onceonly/ai.py,sha256=NjMHtZgc-a-l1Wr3mTWwL9HnIOLZbVr9gkuMXMHbuqA,7043
5
+ onceonly/ai_models.py,sha256=7bHYnAavdb3c-4nlh9HgRY18949TgmU9XfXfv3PXQEE,2910
6
+ onceonly/client.py,sha256=6DtLdWc-7_bAXsaaewUQUTHVnCkRZGsc-PByMVPRhYY,12838
7
+ onceonly/decorators.py,sha256=nP7Wu-RAQQNaTwyOnibzClEgcBJvYheMrG3_KztdlG8,5171
8
+ onceonly/exceptions.py,sha256=Issh08A4IHSDaysJhVZNRCU9W_9BfiGt65UHaMhDCs4,1156
9
+ onceonly/models.py,sha256=hVEBPgIVZP3ELjWYIFSFCKPzI38t5DA0gio9FvrmHJg,678
10
+ onceonly/version.py,sha256=_7OlQdbVkK4jad0CLdpI0grT-zEAb-qgFmH5mFzDXiA,22
11
+ onceonly/integrations/__init__.py,sha256=0tk-2HTTsmc42NhWuR_G_Afmz5-5WG8NvmlO7iIPkIY,34
12
+ onceonly/integrations/langchain.py,sha256=cdpHIluddX48uYeDeE1cxmn-arruVdE3k6gvZxYC9z4,5821
13
+ onceonly_sdk-2.0.0.dist-info/licenses/LICENSE,sha256=YQQ8IT_P7hcGmmLFFuOy3eKDZ90e1cqef_okg85oAiQ,129
14
+ onceonly_sdk-2.0.0.dist-info/METADATA,sha256=3cso7k9xZoja4JR8VwZuT4QtnclBcR9cdB4zKYnOM1w,3080
15
+ onceonly_sdk-2.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
+ onceonly_sdk-2.0.0.dist-info/top_level.txt,sha256=lvz-sHerZcTwlZW-uYoda_wgx62kY07GdtzIdw89hnU,9
17
+ onceonly_sdk-2.0.0.dist-info/RECORD,,
@@ -1,153 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: onceonly-sdk
3
- Version: 1.2.0
4
- Summary: Python SDK for OnceOnly idempotency API
5
- Author-email: OnceOnly <support@onceonly.tech>
6
- License: MIT
7
- Project-URL: Homepage, https://onceonly.tech/
8
- Project-URL: Documentation, https://onceonly.tech/docs/
9
- Project-URL: Repository, https://github.com/mykolademyanov/onceonly-python
10
- Keywords: idempotency,automation,zapier,make,ai-agents
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Operating System :: OS Independent
14
- Requires-Python: >=3.9
15
- Description-Content-Type: text/markdown
16
- License-File: LICENSE
17
- Requires-Dist: httpx>=0.25
18
- Provides-Extra: test
19
- Requires-Dist: pytest>=7.0; extra == "test"
20
- Dynamic: license-file
21
-
22
- # OnceOnly Python SDK
23
-
24
- **The Idempotency Layer for AI Agents, Webhooks, and Distributed Systems.**
25
-
26
- OnceOnly is a high-performance Python SDK designed to ensure **exactly-once execution**.
27
- It prevents duplicate actions (payments, emails, tool calls) in unstable environments like
28
- AI agents, webhooks, or background workers.
29
-
30
- Website - https://onceonly.tech/ai/
31
-
32
- [![PyPI version](https://img.shields.io/pypi/v/onceonly.svg)](https://pypi.org/project/onceonly-sdk/)
33
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
34
-
35
- ---
36
-
37
- ## Features
38
-
39
- - **Sync + Async Client** — built on httpx for modern Python stacks
40
- - **Connection Pooling** — high performance under heavy load
41
- - **Fail-Open Mode** — business logic keeps running even if API is unreachable
42
- - **Smart Decorator** — automatic idempotency based on function arguments
43
- - **Typed Results & Exceptions**
44
-
45
- ---
46
-
47
- ## Installation
48
-
49
- ```bash
50
- pip install onceonly-sdk
51
- ```
52
-
53
- ---
54
-
55
- ## Quick Start
56
-
57
- ```python
58
- from onceonly import OnceOnly
59
-
60
- client = OnceOnly(api_key="once_live_...")
61
-
62
- result = client.check_lock(
63
- key="order:123",
64
- ttl=300, # 300 seconds = 5 minutes (clamped by your plan)
65
- )
66
-
67
- if result.duplicate:
68
- print("Duplicate blocked")
69
- else:
70
- print("First execution")
71
- ```
72
-
73
- ---
74
-
75
- ## Async Usage
76
-
77
- ```python
78
- async def handler():
79
- result = await client.check_lock_async("order:123")
80
- if result.locked:
81
- print("Locked")
82
- ```
83
-
84
- ---
85
-
86
- ## TTL Behavior
87
-
88
- - TTL is specified in seconds
89
- - If ttl is not provided, the server applies the plan default TTL
90
- - If ttl is provided, it is automatically clamped to your plan limits
91
-
92
- ---
93
-
94
- ## Metadata
95
-
96
- You can optionally attach metadata to each check-lock call.
97
- Metadata is useful for debugging, tracing, and server-side analytics.
98
-
99
- Rules:
100
- - JSON-serializable only
101
- - Size-limited
102
- - Safely logged on the server
103
-
104
- ---
105
-
106
- ## Decorator
107
-
108
- The SDK provides an optional decorator that automatically generates
109
- an idempotency key based on the **function name and arguments**.
110
-
111
- This allows you to add exactly-once guarantees to existing code
112
- with zero manual key management.
113
-
114
- ```python
115
- from onceonly.decorators import idempotent
116
-
117
- @idempotent(client, ttl=3600)
118
- def process_order(order_id):
119
- ...
120
- ```
121
-
122
- ---
123
-
124
- ## Fail-Open Mode
125
-
126
- Enabled by default.
127
-
128
- If a network error, timeout, or server error (5xx) occurs, the SDK returns a locked result
129
- instead of breaking your application.
130
-
131
- Fail-open never triggers for:
132
- - Authentication errors (401 / 403)
133
- - Plan limits (402)
134
- - Validation errors (422)
135
- - Rate limits (429)
136
-
137
- ---
138
-
139
- ## Exceptions
140
-
141
- | Exception | HTTP Status | Description |
142
- |--------------------|------------|------------------------------------------|
143
- | UnauthorizedError | 401 / 403 | Invalid or disabled API key |
144
- | OverLimitError | 402 | Plan limit reached |
145
- | RateLimitError | 429 | Too many requests |
146
- | ValidationError | 422 | Invalid input |
147
- | ApiError | 5xx / other| Server or unexpected API error |
148
-
149
- ---
150
-
151
- ## License
152
-
153
- MIT
@@ -1,10 +0,0 @@
1
- onceonly/__init__.py,sha256=_SwJ4Y2345WJKfpW2IsOVfgNaUzjZqTSSOnPQBroW-M,452
2
- onceonly/client.py,sha256=1RMTkGDpalHfx-xEH8DZsn9ZJ3VeGomG6PKAQQjPdZQ,13361
3
- onceonly/decorators.py,sha256=9eBhRUQDBGTUXVyRYlpYy77y-EHF-udX3ERkrfIt9Kg,2651
4
- onceonly/exceptions.py,sha256=RP556LUtO54TPFTZybF4dVA9n3TByFn35esz7EbcrpY,994
5
- onceonly/models.py,sha256=xhPfP1kpyHYfKJNiac2REeXTpPLzrEWJeqn-p6B6taQ,329
6
- onceonly_sdk-1.2.0.dist-info/licenses/LICENSE,sha256=YQQ8IT_P7hcGmmLFFuOy3eKDZ90e1cqef_okg85oAiQ,129
7
- onceonly_sdk-1.2.0.dist-info/METADATA,sha256=dvml-Mhrd_n0xFEhk7-rvShxiYfaLcl_Pq2vyGD0XRo,3891
8
- onceonly_sdk-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
- onceonly_sdk-1.2.0.dist-info/top_level.txt,sha256=lvz-sHerZcTwlZW-uYoda_wgx62kY07GdtzIdw89hnU,9
10
- onceonly_sdk-1.2.0.dist-info/RECORD,,